<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xml" href="https://www.hardscrabble.net/feed.xslt.xml"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.hardscrabble.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.hardscrabble.net/" rel="alternate" type="text/html" /><updated>2026-05-08T13:50:53-04:00</updated><id>https://www.hardscrabble.net/feed.xml</id><title type="html">Hardscrabble</title><subtitle>the personal website of Maxwell Jacobson, a programmer in New York</subtitle><author><name>Maxwell Jacobson</name></author><entry><title type="html">Automatically building assets when using the RSpec CLI</title><link href="https://www.hardscrabble.net/2026/automatically-building-assets-for-rspec-rails-specs/" rel="alternate" type="text/html" title="Automatically building assets when using the RSpec CLI" /><published>2026-04-26T15:13:00-04:00</published><updated>2026-04-26T15:13:00-04:00</updated><id>https://www.hardscrabble.net/2026/automatically-building-assets-for-rspec-rails-specs</id><content type="html" xml:base="https://www.hardscrabble.net/2026/automatically-building-assets-for-rspec-rails-specs/"><![CDATA[<p>Let’s say your Rails app is using <a href="https://github.com/rails/propshaft">propshaft</a>, <a href="https://github.com/rails/jsbundling-rails">jsbundling-rails</a>, and <a href="https://github.com/rails/cssbundling-rails">cssbundling-rails</a> to build assets.</p>

<p>And let’s say you’re using <a href="https://github.com/rspec/rspec-rails">rspec-rails</a> as your testing library.</p>

<p>And let’s say you’re writing <a href="https://rspec.info/features/8-0/rspec-rails/system-specs/system-specs/">system specs</a> (RSpec’s wrapper around <a href="https://guides.rubyonrails.org/testing.html#system-testing">Rails system tests</a>) to test how your Rails app works when you visit it in a browser and click around.</p>

<p>It’s important that, when those system specs run, your front-end assets have already been built, or the test will fail hard, with errors indicating that the assets are unavailable.</p>

<p>It’s also important that, when those system specs run, those front-end assets are <em>up-to-date</em>. propshaft will be looking for assets in <code class="language-plaintext highlighter-rouge">app/assets/builds</code>. If you haven’t built your front-end assets in a few days, maybe some of your source code has changed, and those built assets don’t include the latest changes. In this case, your tests may fail in confusing ways. propshaft is able to find and serve <code class="language-plaintext highlighter-rouge">app/assets/builds/application.js</code> but your test fails because the script is missing some critical new feature that was added this morning.</p>

<p>So how to avoid these annoying and confusing situations?</p>

<p>One factor is to consider <em>how</em> you’re running RSpec. Let’s think about the difference between these different ways to run an RSpec test suite:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ bin/rspec
$ bin/rails spec
</code></pre></div></div>

<p>In the former, you’re using the <a href="https://rspec.info/features/3-13/rspec-core/command-line/">RSpec CLI</a> directly<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> and in the latter you’re using a <a href="https://github.com/rspec/rspec-rails/blob/v8.0.4/lib/rspec/rails/tasks/rspec.rake#L12-L13">rake task defined by the rspec-rails gem</a><sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<p>When you use the rake task (<code class="language-plaintext highlighter-rouge">bin/rails spec</code>), everything Just Works. You’ll notice that assets build automatically every single time you run it, so they’re always present and they’re always up-to-date. Here’s what’s happening:</p>

<ol>
  <li>The task is <a href="https://github.com/rspec/rspec-rails/blob/v8.0.4/lib/rspec/rails/tasks/rspec.rake#L12-L13">defined</a> like this:
    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="no">RSpec</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">RakeTask</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">spec: </span><span class="s2">"spec:prepare"</span><span class="p">)</span>
</code></pre></div>    </div>

    <p>That is, the <code class="language-plaintext highlighter-rouge">spec</code> task (which is defined in rspec-core and which runs all the specs) depends on the <code class="language-plaintext highlighter-rouge">spec:prepare</code> task, which is defined just below.</p>
  </li>
  <li>The <code class="language-plaintext highlighter-rouge">spec:prepare</code> task <a href="https://github.com/rspec/rspec-rails/blob/v8.0.4/lib/rspec/rails/tasks/rspec.rake#L24-L31">invokes</a> the <code class="language-plaintext highlighter-rouge">test:prepare</code> task，which is defined all the way back in Rails</li>
  <li>The <code class="language-plaintext highlighter-rouge">test:prepare</code> task does… <a href="https://github.com/rails/rails/blob/v8.1.3/railties/lib/rails/test_unit/testing.rake#L13-L17">nothing at all</a>! Per the source code, it is a “Placeholder task for other Railtie and plugins to enhance.”</li>
  <li><a href="https://github.com/rails/jsbundling-rails/blob/v1.3.1/lib/tasks/jsbundling/build.rake#L69-L75">jsbundling-rails</a> and <a href="https://github.com/rails/cssbundling-rails/blob/main/lib/tasks/cssbundling/build.rake#L68-L74">cssbundling-rails</a> do exactly that. They enhance the <code class="language-plaintext highlighter-rouge">test:prepare</code> task so that whenever <code class="language-plaintext highlighter-rouge">test:prepare</code> runs, rake will also run the <code class="language-plaintext highlighter-rouge">javascript:build</code> and <code class="language-plaintext highlighter-rouge">css:build</code> tasks.</li>
</ol>

<p>So that’s nice: everything is automatic and works as long as you run your RSpec test suite using the <code class="language-plaintext highlighter-rouge">bin/rails spec</code> task.</p>

<p>It kind of sucks that you need to rebuild <em>all</em> of your assets every time you run <em>any</em> test, but it works.</p>

<p>Now, what if you want to use the <code class="language-plaintext highlighter-rouge">bin/rspec</code> CLI to run your tests? There are a lot of reasons that you might want to do that:</p>

<ol>
  <li>It has a lot of useful flags for customizing its behavior and its output</li>
  <li>You can run specific spec files by passing them as arguments</li>
</ol>

<p>One option you have is to just remember to run <code class="language-plaintext highlighter-rouge">bin/rails spec:prepare</code> or <code class="language-plaintext highlighter-rouge">bin/rails test:prepare</code> before you run your tests, which will take care of it. Try very hard not to forget, or you may end up with confusing failures. Make sure to document this well for everyone else who works on this project to make sure other people know to do this too.</p>

<p>Another option is to make some kind of wrapper script. We can imagine something like a <code class="language-plaintext highlighter-rouge">bin/prepare-and-rspec</code> which looks like</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nb">set</span> <span class="nt">-e</span>

bin/rails <span class="nb">test</span>:prepare
bin/rspec <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div></div>

<p>That way you can just run <code class="language-plaintext highlighter-rouge">bin/prepare-and-rspec --format documentation spec/models/user_spec.rb</code> and everything should Just Work. Of course, it still kind of sucks that you have to rebuild your assets every time you run a spec, even if nothing changed. And it’s still possible that teammates might not discover this script, and they’ll just keep using <code class="language-plaintext highlighter-rouge">bin/rspec</code> and hitting mysterious failures here and there.</p>

<p>Another option is to add some kind of an <a href="https://rspec.info/features/3-13/rspec-core/hooks/before-and-after-hooks/">RSpec hook</a> which takes care of building assets before the test suite:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">before</span><span class="p">(</span><span class="ss">:suite</span><span class="p">)</span> <span class="k">do</span>
    <span class="nb">system</span><span class="p">(</span><span class="s2">"bin/rails javascript:build"</span><span class="p">,</span> <span class="ss">exception: </span><span class="kp">true</span><span class="p">)</span>
    <span class="nb">system</span><span class="p">(</span><span class="s2">"bin/rails css:build"</span><span class="p">,</span> <span class="ss">exception: </span><span class="kp">true</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is a pretty good option! Now you can just use <code class="language-plaintext highlighter-rouge">bin/rspec</code> and trust that it will automatically build assets whenever you run specs.</p>

<p>Of course, it still sucks that you need to rebuild <em>all</em> of your assets every time you run <em>any</em> test.</p>

<p>And even worse: if you do happen to run <code class="language-plaintext highlighter-rouge">bin/rails spec</code>, now you’re rebuilding <em>all</em> of your assets <em>twice</em>, once from your custom hook and once from the <code class="language-plaintext highlighter-rouge">test:prepare</code> enhancements.</p>

<p>This is actually pretty close to where I’ve landed in my project. I’ve zhuzhed it up a little by adding some logic which tries to determine whether re-running is actually necessary:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AutoCompileInTests</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">command</span><span class="p">:,</span> <span class="n">inputs</span><span class="p">:,</span> <span class="nb">name</span><span class="p">:)</span>
    <span class="vi">@command</span> <span class="o">=</span> <span class="n">command</span>
    <span class="vi">@inputs</span> <span class="o">=</span> <span class="n">inputs</span>
    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">run</span>
    <span class="k">return</span> <span class="k">if</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI"</span><span class="p">]</span>
    <span class="k">return</span> <span class="k">if</span> <span class="n">already_compiled?</span>

    <span class="nb">system</span><span class="p">(</span><span class="vi">@command</span><span class="p">,</span> <span class="ss">exception: </span><span class="kp">true</span><span class="p">)</span>

    <span class="n">marker_file</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">wait</span>
    <span class="k">return</span> <span class="k">if</span> <span class="no">ENV</span><span class="p">[</span><span class="s2">"CI"</span><span class="p">]</span>

    <span class="kp">loop</span> <span class="k">do</span>
      <span class="k">return</span> <span class="k">if</span> <span class="n">already_compiled?</span>

      <span class="nb">sleep</span> <span class="mi">1</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">already_compiled?</span>
    <span class="n">marker_file</span><span class="p">.</span><span class="nf">exist?</span> <span class="p">?</span> <span class="n">marker_file</span><span class="p">.</span><span class="nf">read</span> <span class="o">==</span> <span class="n">cache_key</span> <span class="p">:</span> <span class="kp">false</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">marker_file</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"tmp/cache/auto-compile-</span><span class="si">#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">cache_key</span>
    <span class="n">md5</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">MD5</span><span class="p">.</span><span class="nf">new</span>
    <span class="vi">@inputs</span>
      <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">input</span><span class="o">|</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> <span class="p">}</span>
      <span class="p">.</span><span class="nf">flatten</span>
      <span class="p">.</span><span class="nf">uniq</span>
      <span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
        <span class="n">pathname</span> <span class="o">=</span> <span class="no">Pathname</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
        <span class="k">next</span> <span class="k">if</span> <span class="n">pathname</span><span class="p">.</span><span class="nf">directory?</span>

        <span class="n">md5</span><span class="p">.</span><span class="nf">update</span> <span class="n">pathname</span><span class="p">.</span><span class="nf">read</span>
      <span class="k">end</span>

    <span class="n">md5</span><span class="p">.</span><span class="nf">hexdigest</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">before</span><span class="p">(</span><span class="ss">:suite</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">tasks</span> <span class="o">=</span> <span class="p">[</span>
      <span class="no">AutoCompileInTests</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
        <span class="ss">command: </span><span class="s2">"bin/rails css:build"</span><span class="p">,</span>
        <span class="ss">inputs: </span><span class="p">[</span>
          <span class="s2">".node-version"</span><span class="p">,</span>
          <span class="s2">"Gemfile.lock"</span><span class="p">,</span>
          <span class="s2">"package-lock.json"</span><span class="p">,</span>
          <span class="s2">"app/assets/stylesheets/**/*"</span><span class="p">,</span>
        <span class="p">],</span>
        <span class="ss">name: </span><span class="s2">"css"</span>
      <span class="p">),</span>
      <span class="no">AutoCompileInTests</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
        <span class="ss">command: </span><span class="s2">"bin/rails javascript:build"</span><span class="p">,</span>
        <span class="ss">inputs: </span><span class="p">[</span>
          <span class="s2">".node-version"</span><span class="p">,</span>
          <span class="s2">"Gemfile.lock"</span><span class="p">,</span>
          <span class="s2">"package-lock.json"</span><span class="p">,</span>
          <span class="s2">"app/javascript/**/*"</span>
        <span class="p">],</span>
        <span class="ss">name: </span><span class="s2">"javascript"</span>
      <span class="p">)</span>
    <span class="p">]</span>

    <span class="c1"># if you aren't using parallel_tests, this can simply be</span>
    <span class="c1"># tasks.each(&amp;:run)</span>
    <span class="no">ParallelTests</span><span class="p">.</span><span class="nf">first_process?</span> <span class="p">?</span> <span class="n">tasks</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:run</span><span class="p">)</span> <span class="p">:</span> <span class="n">tasks</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:wait</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The goal with this code is to list out the tasks that need to run at the top of the test suite. For each task, we give it a name, a command, and inputs. The inputs are all of the files that, if any of them change, we should rerun the task just to be safe. Then, we’ll make a hash of all of those inputs and write it to a temp file (<code class="language-plaintext highlighter-rouge">./tmp/cache/auto-compile-css</code> or <code class="language-plaintext highlighter-rouge">./tmp/cache/auto-compile-js</code>) and we’ll run the command. The next time, we’ll recompute the hash and compare it to the tempfile; if it matches, no need to rebuild the assets. If you ever want to force the assets to rebuild, you can just remove those tempfiles by running <code class="language-plaintext highlighter-rouge">rm tmp/cache/auto-compile-*</code>.</p>

<p>If you’re using the terrific <a href="https://github.com/grosser/parallel_tests">parallel_tests gem</a> to run your tests in parallel, it’s important to only automatically build your assets in one of the parallel processes and not all of them. To achieve that, we can have the first process take on the job of building the assets, and have the other processes spin their wheels and wait for the build to finish before proceeding to running specs.</p>

<p>This is a bit of extra complexity, but I think it’s worth it if your assets are a bit slow to build.</p>

<p>Of course, it would be nice if all of this Just Worked out of the box, but I’m not sure exactly what that would look like.</p>

<p>Feel free to get in touch if you have ideas to improve this setup.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>I’m presuming that you’ve run <code class="language-plaintext highlighter-rouge">bundle binstubs rspec-core</code> to generate the <code class="language-plaintext highlighter-rouge">bin/rspec</code> file; you could also invoke it with <code class="language-plaintext highlighter-rouge">bundle exec rspec</code> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>You could just as well run <code class="language-plaintext highlighter-rouge">bin/rake spec</code> – same diff. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Maxwell Jacobson</name></author><category term="programming" /><summary type="html"><![CDATA[Let’s say your Rails app is using propshaft, jsbundling-rails, and cssbundling-rails to build assets.]]></summary></entry><entry><title type="html">I keep getting locked out of my phone at the theater</title><link href="https://www.hardscrabble.net/2026/activation-lock-sucks/" rel="alternate" type="text/html" title="I keep getting locked out of my phone at the theater" /><published>2026-04-24T14:19:00-04:00</published><updated>2026-04-24T14:19:00-04:00</updated><id>https://www.hardscrabble.net/2026/activation-lock-sucks</id><content type="html" xml:base="https://www.hardscrabble.net/2026/activation-lock-sucks/"><![CDATA[<p>This has happened twice now.</p>

<p>In March, I went to see <a href="https://www.nytimes.com/2025/10/16/theater/ragtime-review-henry-debessonet.html">Ragtime</a>. Of course I turned off my iPhone at the start of the show. After, when I turned it back on, I found that it had decided to apply a software update. This happened to be the same day that iOS 26.4 was released, which <a href="https://support.apple.com/en-us/126792">included some security fixes</a>.</p>

<p>To my surprise, in order to unlock my phone, I needed to connect to wi-fi, so it could “activate”. Uhh. Okay. Lincoln Center happened to have a public wi-fi network, so I tried connecting to that.</p>

<p>Then it told me that this iPhone is “Locked to Owner”, and that “Activation Lock prevents anyone who is not the owner from using this iPhone. To unlock this iPhone, enter the Apple Account and password that were used during setup.”</p>

<p>This is a reference to a feature called <a href="https://support.apple.com/en-us/108794">Activation Lock</a> that I apparently have enabled.</p>

<p>In theory this is cool – if my phone is stolen, I like the idea of making it harder for the thief to get in to my phone. But why does it think my phone was stolen? I was just trying to be a conscientious theater-goer.</p>

<p>I have a problem now – I don’t have my Apple Account password memorized. I use <a href="https://1password.com/">a password manager</a> to store all of my super strong, randomly-generated passwords. And my password manager is on my phone, which is locked.</p>

<p>Then I notice that there is an option to use a “passcode” instead of my Apple Account password. Oh okay great, I know that. I have a passphrase, not a passcode, but okay, let’s try it.</p>

<p>On that screen, I see “To unlock this iPhone, enter the passcode that was previously used on this device.”</p>

<p>What does <em>that</em> mean?</p>

<p>I try my current passphrase. Nope.</p>

<p>I try the passphrase I’m pretty sure I used before my current passphrase. Nope.</p>

<p>I try a passcode I think I might have used a few years ago before switching to a passphrase. Nope.</p>

<p>At this, I see a dialog indicating “Incorrect Passcode” and “A device passcode can no longer be used to activatet his device. The passcode was entered incorrectly too many times”. Oh okay.</p>

<p>So I turned my phone off and went home.</p>

<p>Thankfully I know how to get home from Manhattan to Brooklyn. Thankfully I wasn’t dependent on Apple Pay to tap in to the subway. Thankfully I didn’t need to call a taxi. Thankfully I didn’t need to contact anyone or be contacted by anyone urgently. Thankfully I had a paperback with me.</p>

<p>At home, I pulled up 1Password on my computer, looked up my Apple Account password, and unlocked my iPhone. Then I forgot all about it.</p>

<p>Until it happened again last night while I was seeing <a href="https://www.nytimes.com/2026/04/20/theater/schmigadoon-review-brightman-broadway.html">Schmigadoon</a>!!</p>

<p>After the show when I turned my phone back on I saw that I had updated to 26.4.1 and I was locked out again.</p>

<p>This experience sucks.</p>

<p>The funny thing is that I <em>love</em> installing updates. There’s no need for them to wait to spring it on me unexpectedly when my phone is off. Just put a little red dot on the Settings app and I’ll happily install the update within hours. 26.4.1 was released like two weeks ago. Why wait until the worst possible moment?</p>

<p>The other funny thing is that 26.4.2 was released yesterday and it <a href="https://support.apple.com/en-us/127002">has security fixes</a>. But they didn’t update me to that one, for reasons that are not at all clear to me.</p>

<p>I’m seeing another show in the next few weeks – feel free to place bets on whether I learn any lesson from this.</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="personal computing" /><summary type="html"><![CDATA[This has happened twice now.]]></summary></entry><entry><title type="html">Don’t forget to add an RSS feed to your blog</title><link href="https://www.hardscrabble.net/2026/dont-forget-to-add-an-rss-feed-to-your-blog/" rel="alternate" type="text/html" title="Don’t forget to add an RSS feed to your blog" /><published>2026-04-19T17:46:00-04:00</published><updated>2026-04-19T17:46:00-04:00</updated><id>https://www.hardscrabble.net/2026/dont-forget-to-add-an-rss-feed-to-your-blog</id><content type="html" xml:base="https://www.hardscrabble.net/2026/dont-forget-to-add-an-rss-feed-to-your-blog/"><![CDATA[<p>It keeps happening. I come across a blog which has written something interesting enough and I try to subscribe to it in my <a href="https://en.wikipedia.org/wiki/RSS">RSS</a> Reader app<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> and the app tells me there’s no feed to subscribe to.</p>

<p>Maybe I should have compiled a few examples before writing this. I have one at the moment. Consider <a href="https://cursor.com/blog">https://cursor.com/blog</a>.</p>

<p>This is the blog for Cursor, which I understand to be a popular and influential fork of Visual Studio Code that adds lots of AI code generation features. I haven’t used it but I’ve heard that people like it.</p>

<p>They’re blogging about where they think things are going in the world of programming, which I’m interested in. But there’s no feed to subscribe to, so I’ll probably miss their future posts.</p>

<p>OK here’s one more: <a href="https://fabro.sh/blog">https://fabro.sh/blog</a> – this is another AI tool, created by my old boss from Code Climate, Bryan Helmkamp. I’m curious about what he’s up to and where this will go, so I’d happily subscribe and hear future updates. But there’s no feed.</p>

<p>I have two theories on what might be happening here:</p>

<ol>
  <li>Skipping the RSS feed is a status symbol. A flex. They’re saying, “we don’t need one. We know that what we have to say is so obviously compelling that whenever we speak, our words will echo across the land across social media, coverage on other blogs and news aggregators, etc. We just can’t be bothered.”</li>
  <li>They’re using AI to build their websites, and whatever prompt they’re using doesn’t mention an RSS feed, so the generated website doesn’t have one. Back when people were using off the shelf software like Wordpress to power their blogs, they’d have RSS feeds because all Wordpress websites automatically get one.</li>
</ol>

<p>Whatever the reason: it sucks. Add an RSS feed.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><a href="https://reederapp.com/classic/">Reeder Classic</a> on macOS; <a href="https://apps.gnome.org/NewsFlash/">Newsflash</a> on Linux <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Maxwell Jacobson</name></author><category term="blogging" /><summary type="html"><![CDATA[It keeps happening. I come across a blog which has written something interesting enough and I try to subscribe to it in my RSS Reader app1 and the app tells me there’s no feed to subscribe to. Reeder Classic on macOS; Newsflash on Linux &#8617;]]></summary></entry><entry><title type="html">Uses page, version one</title><link href="https://www.hardscrabble.net/2026/uses-page-version-one/" rel="alternate" type="text/html" title="Uses page, version one" /><published>2026-04-14T18:16:00-04:00</published><updated>2026-04-14T18:16:00-04:00</updated><id>https://www.hardscrabble.net/2026/uses-page-version-one</id><content type="html" xml:base="https://www.hardscrabble.net/2026/uses-page-version-one/"><![CDATA[<p>I’ve added a “Uses” page to this site at <a href="/uses">/uses</a>. This shares out the stuff that I use, specifically computer stuff. Of course I use all kinds of other stuff.</p>

<p>This is a thing that other blogs have, and I thought it would be fun to participate. You can see a list of all kinds of “slash pages” here: <a href="https://slashpages.net/#uses">https://slashpages.net/#uses</a>. I’m not making all of those but I’ll make this one.</p>

<p>I previously blogged about this once before, in 2017: <a href="/2017/the-hardware-and-software-i-use/">the hardware and software I use (2017)</a>. It’s been long enough that plenty has changed, but it’s fun to see what hasn’t. I’m still using the same monitor, Fastmail, NearlyFreeSpeech.net. This site is still built with Jekyll and hosted on GitHub pages.</p>

<p>I’ll try to remember to update my uses page once or twice a year going forward.</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="personal computing" /><summary type="html"><![CDATA[I’ve added a “Uses” page to this site at /uses. This shares out the stuff that I use, specifically computer stuff. Of course I use all kinds of other stuff.]]></summary></entry><entry><title type="html">The actions/checkout action has this one weird gotcha</title><link href="https://www.hardscrabble.net/2026/actions-checkout-one-weird-gotcha/" rel="alternate" type="text/html" title="The actions/checkout action has this one weird gotcha" /><published>2026-04-13T17:05:00-04:00</published><updated>2026-04-13T17:05:00-04:00</updated><id>https://www.hardscrabble.net/2026/actions-checkout-one-weird-gotcha</id><content type="html" xml:base="https://www.hardscrabble.net/2026/actions-checkout-one-weird-gotcha/"><![CDATA[<p>If you use GitHub Actions, you probably use the <a href="https://github.com/actions/checkout">actions/checkout</a> action to check out your repository.</p>

<p>Let’s say you use it to run tests on pull requests before merging them. The action will check oout the code from your branch, and then run the tests.</p>

<p>What exactly is it checking out?</p>

<p>You might assume that it basically just checks out your branch. Or maybe it checks out the head commit on your branch?</p>

<p>Nope.</p>

<p>It checks out our pull request’s “merge ref”.</p>

<p>The pull request merge ref is sort of like a branch which GitHub creates automatically for all of your pull requests. It represents what you’d get if you were to merge your PR into the target branch.</p>

<p>For example if we consider this diagram<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  main    my-great-feature-branch
          *
          *
      *   *
      *   *
      * *
      *
      *
      *
      *
      * oldest commit
</code></pre></div></div>

<p>This shows a feature branch with a handful of new commits that are not represented on main. It also shows that, since branching off of main, a few other commits have been made to the main branch which are not represented in the feature branch.</p>

<p>When I open a pull request to merge my great feature branch into main, GitHub automatically creates a <em>third</em> branch which has <em>all</em> of the commits from both branches. And <em>that’s</em> what the actions/checkout action checks out.</p>

<p>This is kind of helpful: if your tests pass, you can feel pretty confident that you aren’t going to break the main branch if you merge it in.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>This is also kind of confusing: if your tests fail, and you try to run the tests locally to see exactly why they’re failing, you might find that they pass locally, and now you need to figure out why exactly things are behaving differently locally and in CI. If you find yourself in this scenario, the answer is simple: just rebase your branch on top of main (or merge main into your branch) and then the test should start failing locally too, and you can debug it.</p>

<p>It is possible to configure actions/checkout to just check out the head commit on your pull request’s branch. This is a scenario they document in their README: <a href="https://github.com/actions/checkout/tree/v6.0.2?tab=readme-ov-file#checkout-pull-request-head-commit-instead-of-merge-commit">https://github.com/actions/checkout/tree/v6.0.2?tab=readme-ov-file#checkout-pull-request-head-commit-instead-of-merge-commit</a>. Doing that would be less surprising, perhaps.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Somehow I’ve made it this far in my blogging life without ever manually creating this classic ASCII diagram showing git branches… <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>That is, unless main has changed <em>again</em> since your tests passed. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Maxwell Jacobson</name></author><category term="programming" /><summary type="html"><![CDATA[If you use GitHub Actions, you probably use the actions/checkout action to check out your repository.]]></summary></entry><entry><title type="html">My favorite music of 2025</title><link href="https://www.hardscrabble.net/2025/my-favorite-music-of-2025/" rel="alternate" type="text/html" title="My favorite music of 2025" /><published>2025-12-26T01:11:00-05:00</published><updated>2025-12-26T01:11:00-05:00</updated><id>https://www.hardscrabble.net/2025/my-favorite-music-of-2025</id><content type="html" xml:base="https://www.hardscrabble.net/2025/my-favorite-music-of-2025/"><![CDATA[<p>After reading <a href="https://www.hearingthings.co/how-to-quit-streaming/">How to Quit Streaming</a> in May, I canceled my Apple
Music account. I found the conclusion compelling:</p>

<blockquote>
  <p>I’ll close by saying this: Music just sounds better when you’re not streaming it. Not only because the audio quality
is often literally higher, but because you’re forging a connection with what you’re hearing that’s strengthened by
your choices, your commitment, your active participation—and, if you bought it at a shop or the merch table at a show,
by the lasting imprint of those in-person interactions, however brief they might have been. Spotify can do a lot of
things, but it can’t compete with that.</p>
</blockquote>

<p>I switched to using <a href="https://brushedtype.co/doppler/">Doppler</a> and started buying mp3s again. Mostly on Bandcamp,
occasionally on iTunes. I used <a href="https://syncthing.net/">Syncthing</a> to keep my library in sync across my personal Mac
Studio, my personal Thinkpad running Linux, and my work MacBook Pro. I tried at times to listen to radio, but there are
so many ads that it’s not really possible to have on while working without constantly feeling annoyed, so I mostly gave
up on that. Instead I mostly stuck to my mp3s and YouTube.</p>

<p>(On Linux, which I used a bit this year, I used the <a href="https://apps.gnome.org/Music/">GNOME Music app</a> although to be
honest I didn’t listen to music on my Linux laptop much this year.)</p>

<p>Being off streaming, my listening habits changed. I did end up spending more time with fewer artists, and I did
appreciate that.</p>

<p>So here’s what I listened to this year, in no particular order. Mostly 2025 releases but not all.</p>

<ul>
  <li><a href="https://oklou.bandcamp.com/album/choke-enough"><strong>Oklou - choke enough</strong></a> – my introduction was <a href="https://www.youtube.com/watch?v=Jg9uwVvOy0E">this spellbinding
Tiny Desk performance</a> which I watched many times. The record is more
electronic. I like these songs in either mode.</li>
  <li><a href="https://chloeqisha.bandcamp.com/album/modern-romance-ep"><strong>Chloe Qisha - Modern Romance EP</strong></a> – and also <a href="https://chloeqisha.bandcamp.com/album/chloe-qisha-self-titled-ep">her other
EP</a>, although that was released in 2024. Witty,
droll pop music to listen to on a loop. When I said I didn’t miss you, <a href="https://www.youtube.com/watch?v=8bGIKVbe_Cg">I lied, I’m
sorry</a>.</li>
  <li><a href="https://www.youtube.com/watch?v=aUzBgeI5dpc"><strong>The Clash - Train in Vain</strong></a> – this was on the soundtrack of Halt and
Catch Fire, which I rewatched a good chunk of this year, and it’s just a perfect song</li>
  <li><a href="https://www.youtube.com/watch?v=z0pzzkp85-Q"><strong>Mk.gee - Are You Looking Up (Live)</strong></a> – a magical performance I first
heard of him maybe this year or last year, who knows. This guy is so cool. It’s fun to watch him jam on his own, <a href="https://www.youtube.com/watch?v=TiGhRcruIJY">with
Dijon</a>, or <a href="https://www.youtube.com/watch?v=QyvREl7epGY">with Bieber</a>.</li>
  <li><a href="https://www.youtube.com/watch?v=SrPN_eK8RKc"><strong>Lady Gaga - Killah (Saturday Night Live/2025)</strong></a> – This whole Lady
Gaga performance is so cool. The opening dance. The drumming. The scream. The costume change. Sick.</li>
  <li><a href="https://www.youtube.com/watch?v=seARsMwjbEU"><strong>JENNIE &amp; Dua Lipa - Handlebars</strong></a> – this song has a very nice groove
that I enjoy and the music video is worth watching jjust so you can try to help me understand what they were going for
there. There’s just so much going on. The surreal disconnection between all of the images makes me wonder (like so
many things did this year) if this was AI’s doing. I feel a little bit like the guy in Pete Holmes’s bit about the guy
in the audience of the magic show confidently saying, <a href="https://www.youtube.com/watch?v=-rZRHPSmZSs">“This is
magnets”</a>.</li>
  <li><a href="https://www.youtube.com/watch?v=4w8t8ZBOPxY"><strong>Lizzy McAlpine - Staying (Live from The Greek Theatre, Los Angeles)</strong></a>
– I found this performance (from an artist I’m not terribly familiar with) so lovely and <em>confident</em>. It begins with
a lengthy piano prelude before layering in the vocals and the other instruments… And what a cool set! I wish I’d
seen that show.</li>
  <li><a href="https://www.youtube.com/watch?v=AiX4CIgKMEs"><strong>Gregory Alan Isakov - The Trapeze Swinger (Iron &amp; Wine cover)</strong></a> – an
interpretation of an old favorite Iron &amp; Wine song which feels to me like a whole new song. This one draws my ear to
the rhythm of the vocal melody.</li>
  <li><a href="https://www.youtube.com/watch?v=XIKz-AyYPCw"><strong>ROSÉ &amp; PSY - APT. (live from 싸이흠뻑쇼 SUMMERSWAG 2025)</strong></a> – I think
history has vindicated Psy as the coolest guy in the world. The arc of history is long but it bends toward justice.</li>
  <li><a href="https://mewithoutyou.bandcamp.com/album/live-vol-two"><strong>mewithoutYou - Live (vol. Two)</strong></a> – despite ending their run
a few years ago, mewithoutYou (who are <a href="https://www.hardscrabble.net/2017/mewithoutyou-and-me/">very special to me</a>)
are still producing some aftershocks, in the form of a planned trilogy of live albums, the second volume of which came
out this year. They’re very cool artifacts – they insist on not labeling the tracks in the mp3 metadata because they
want the experience to be like attending a live show, where you don’t know which song is coming up next. The
recordings all come from different shows and are mixed together seamlessly. They sound so good. A bit of on-stage
patter is preserved. I was very happy to see Stereogum write <a href="https://stereogum.com/2311888/mewithoutyou-deserve-their-flowers-as-one-of-the-21st-centurys-greatest-rock-bands-and-the-underrated-pale-horses-is-a-big-reason-why/columns/sounding-board">a love letter to
them</a>
this year, and also see <a href="https://www.instagram.com/reels/DNKNNjyRlEo/">Lucy Dacus bring Aaron out for a cameo</a> on her
tour. He may be a retired rock star who seems pretty happy working as a <a href="https://collegeofidaho.edu/people/aaron-weiss/">professor of
anthropology</a> these days<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup><sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> but I can still daydream that he’ll
pop back up with a solo album at some point…</li>
  <li><a href="https://www.youtube.com/watch?v=scxEEqX-hXo"><strong>Tim Armstrong - Take This City</strong></a> – This song is from 2007 and I
listened to it then and somehow stumbled back upon it this year. What a gem. This is the frontman from Rancid and he
is chilling out here. This is the mood I am trying to inhabit.</li>
  <li><a href="https://movingmountains.bandcamp.com/album/pruning-of-the-lower-limbs"><strong>Moving Mountains - Pruning of the Lower
Limbs</strong></a> kind of out of nowhere, hometown
heroes Moving Mountains came back online with a smooth, moody album that feels more grown up<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> and laid back than some
of their earlier, angstier stuff. This is the album I put on in the subway with my Apple earpods to set the mood for
getting around, unhurried, thoughtful, while reading a book.</li>
  <li><a href="https://www.youtube.com/watch?v=JAZ1nfuluz0"><strong>Sara Bareilles and Rufus Wainwright Perform “She Used to Be Mine”</strong></a>
– this song, from the musical Waitress, was written by Bareilles, who originated the part. Here she is performing it
as a duet with a male vocalist, which seems counter-intuitive and very unexpected to me. But Christ is it pretty! What
a wonderful interpretation. Bareilles has sung this song so many times, but it feels like Wainwright pushes her to an
even more tender place here somehow…</li>
  <li><a href="https://www.youtube.com/watch?v=N3eOQJU0Ruc"><strong>Bear vs. Shark - Out Loud Hey Hey</strong></a> – BVS has been “my favorite
band” for twenty years now and I still listen to them regularly. They went on tour this year, and I saw them twice in
the UK in March. This track has emerged as a new favorite this year. I just love how it has <em>two</em> moments where the
guitars drop out, seemingly to give the drummer a moment to strut his stuff, and then when they do come back it’s in
this very percussive mode, as if the guitars are inspired to be more like drums. Such a potent dose of mania in less
than two minutes. Perfect song. <a href="https://www.youtube.com/watch?v=x_NIz2tAKCA">Perfect band, still</a>. I should’ve
traveled to see more of their shows.</li>
  <li><a href="https://www.youtube.com/watch?v=mprkxTmMeAo"><strong>Jane Wickline Gives Dating Advice</strong></a> – Comedy music lives. I am
rooting for Wickline to thrive on SNL, I think she’s very funny in an internet sort of way.</li>
  <li><a href="https://www.youtube.com/watch?v=qjEyZw6aImA"><strong>Wet Leg - mangetout in the Live Lounge</strong></a> – I should probably
actually buy this album because the YouTube algorithm knows I’m going to click on the thumbnails for this band.
They’re so strange and fun. They feel like they exist in some other world of their own creation.</li>
  <li><a href="https://www.youtube.com/watch?v=RGE-JRsJ2uo"><strong>Geese - Au Pays du Cocaine</strong></a> – The song of the year, easily. This
band got a lot of buzz and at first I thought they were weird and pretentious and dumb and I got fully converted,
they’re actually weird and pretentious and smart. This is the kind of song that makes me want to go watch all of the
amateur covers from all the weirdo sweeties who also felt deeply stirred by this perfect gem of a tune. It’s the kind
of song that makes me want to record my own amateur cover.</li>
  <li><a href="https://www.youtube.com/watch?v=P6LhDUM02zM"><strong>The Weakerthans - Sun In An Empty Room</strong></a> – this is the <a href="https://www.hearingthings.co/in-praise-of-the-most-perfect-podcast-theme-song-ever/">perfect
theme song</a> to Heavyweight, which
came back from hiatus this year. It’s so imbued with that association that I don’t know that I would have even noticed
this song if I’d heard it out of context, but it’s Pavlovian for me now. I hear this and I feel wistful.</li>
  <li><a href="https://www.youtube.com/watch?v=bd7iC_kQiv8"><strong>Christopher Owens - This Is My Guitar (Live Acoustic Performance)</strong></a>
– I hadn’t thought of this guy for many years, but this crossed my transom and hit me like a hammer. That guy! What
happened to him? I don’t really know. I haven’t looked into it and won’t speculate. But this is a really beautiful
tune that suggests it hasn’t always been easy.</li>
  <li><a href="https://defector.com/kiiikiii-and-the-dawn-of-k-pops-process-era?giftLink=f3c285182004768dbac47afabc7a34d8"><strong>KiiiKiii - Debut
Song</strong></a> –
I am not the biggest kpop fan but I loved this article about what the industry is churning out these days and how it
compares to the recent past. I watched a bunch of the linked songs, and my favorite was easily <a href="https://www.youtube.com/watch?v=tna90t2je-4">Crayon Pop’s
FM</a> which is a crazy contrast with this KiiiKiii tune.</li>
  <li><a href="https://www.youtube.com/watch?v=P9XbwCBTdbE"><strong>McKenzie Kurtz - It Hit Me Like a Hammer</strong></a> – This, from the
short-lived Huey Lewis and the News jukebox musical, is Kurtz’s big number. I love McKenzie Kurtz. She’s great. She can
do anything, including make me like Huey Lewis and the News.</li>
  <li><a href="https://www.youtube.com/watch?v=nD3G3j0VEwM"><strong>Maggie Rogers - In The Living Room</strong></a> – This single came out after
her 2024 album. Maybe it didn’t fit there, because it’s this exuberant, bombastic thing, while somehow also a wistful,
domestic lament. I love it. She almost sounds out of breath because she’s giving it her all, which is saying a lot.</li>
  <li><a href="https://www.youtube.com/watch?v=ZqgKKbg2Ja8"><strong>Audrey Hobert - Who’s the Clown?</strong></a> – This is another pop album I
listened to on a loop this year. She’s got such a gift for understated comedy and overstated dancing. Small phrases
will catch me by surprise, like in <a href="https://www.youtube.com/watch?v=qbA_YEOqWNY">Shooting Star</a> when she sings in her
friends voice, telling her “Girl, that’s not a shooting star” and then replies in her own, “I’m sorry, my bad, I
thought that it was”. I like songwriters who are conversational and wordy and somehow rigorous all at once.</li>
  <li><a href="https://www.youtube.com/watch?v=TURkB9zqxa0"><strong>Jacob Collier Improvises the National Symphony Orchestra</strong></a> – I
usually find Collier fairly mechanical and dull, but seeing this former child prodigy be basically a kid in a candy
store really enchanted and impressed me. I’m of course jealous.</li>
  <li><a href="https://www.youtube.com/watch?v=AnbEBT1xUgg"><strong>Frank Watkinson - I Miss You</strong></a> – An old many
mournfully playing Blink-182’s loveliest tune. Achingly beautiful stuff.</li>
  <li><a href="https://www.youtube.com/watch?v=0MawIv5pDFE"><strong>Wednesday - Bull Believer</strong></a> – Seeing them play this live a few
months ago boiled my brain and restarted it. It’s so loud and so committed and so powerful. I also loved the lead
single from their new album from this year, <a href="https://www.youtube.com/watch?v=uE0waEdE2Pw">Elderberry Wine</a>, which
hardly sounds like the same band, but they’ve got range, and which Wednesday you’re gonna get depends on the day of
the week.</li>
  <li><a href="https://www.youtube.com/watch?v=AMnDzN1mIjE"><strong>Dan Hardin - Romeo and Juliet</strong></a> – This was uploaded to YouTube in
2007 and it looks like it. This is a Dire Straits cover and it’s one of my favorite covers. A few years ago I thought
of it and tried to find it and was sad to learn that Hardin had made all of his videos private to protest YouTube
running ads on them (I think?). I happened to think of it again today and was delighted to see that he’d made them
public again. What a perfect song. A Christmas miracle. This time I <a href="https://github.com/yt-dlp/yt-dlp">downloaded</a> it.
It’s only 10mb.</li>
</ul>

<p>This is a very random assortment of recommendations but I hope you found something to like here. Those are the
highlights. Bon voyage, 2025.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>“My previous career was as vocalist for the indie/punk me with out You. Over two decades and 1,400+ shows, I made my living flailing around shouting about things I found important. It was an extremely cool job, but I traded it for the only cooler job I can imagine: teaching anthropology. In that role I continue–a bit more quietly–doing what I did as a lyricist, i.e., to explore big questions in small ways: What has it meant, across time and space, to be human? Where does nature end and culture begin? What is ‘the self,’ and how does it shape our view of reality? What for our species makes life worth living? What sets us apart from the non-human world, from each other? What binds us together?” <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>I did just go on a tangent and spend $60.97 to buy a 20 page pdf of his first published paper. I read the first
few sections before coming up for breath and resolving to finish this blog post first. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>Frontman Dunn, who went to my high school, has had such an interesting career! Indie rock to web dev to <a href="https://billypenn.com/2023/11/01/eeva-philadelphia-bakery-pizzeria-closing-reanimator-local80/">pizza
chef</a> back to indoe
rock!? <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Maxwell Jacobson</name></author><category term="culture" /><summary type="html"><![CDATA[After reading How to Quit Streaming in May, I canceled my Apple Music account. I found the conclusion compelling:]]></summary></entry><entry><title type="html">Life without search engines</title><link href="https://www.hardscrabble.net/2025/life-without-search-engines/" rel="alternate" type="text/html" title="Life without search engines" /><published>2025-05-28T18:52:00-04:00</published><updated>2025-05-28T18:52:00-04:00</updated><id>https://www.hardscrabble.net/2025/life-without-search-engines</id><content type="html" xml:base="https://www.hardscrabble.net/2025/life-without-search-engines/"><![CDATA[<p>I thought it would be fun to try taking a break from using search engines. Is this possible?</p>

<p>Like everyone else, I’ve used Google a ton and it’s been very useful. In recent years, I’ve felt kind of like Google isn’t serving my needs as much. I’m not a big fan of the AI widgets at the top of the search results (see this recent article, <a href="https://nymag.com/intelligencer/article/google-ai-mode-search-results-bury-the-web.html">Google Is Burying the Web Alive</a> from John Herrman). And I find that when I visit the Google homepage on my phone, it’s popping up clickbait articles under the search bar that are uncannily good at grabbing my attention, and suddenly I’m forgetting what I was going to search for in the first place.</p>

<p>I tried out DuckDuckGo for a few months and it’s a fine replacement, but they’re also popping up a little AI widget all the time and you can ask it to go away but then it just comes back after a while and that kind of rubs me the wrong way too.</p>

<p>I just tried out Kagi for a month. It’s a paid search engine, and it’s $10 per month, but you only get 300 searches per month, and I apparently use more than that, and I can’t quite bring myself to pay $25 a month for this, so I think I’m out.</p>

<p>If there’s no search engine that I’m excited to use, maybe I should use <em>no</em> search engine? It’s a bit of a hard sell, given that I clearly love to search (I used up my 300 Kagi searches in 20 days!). It’s muscle memory: just write something and hit enter, over and over.</p>

<p>But I know how to break muscle memory. Recall my <a href="/2022/steam-locomotive/">steam locomotive post</a> about how to break command line habits. All I need to do is find a way to see a six second animation of a train whenever I try to google something, and then I’ll stop googling things.</p>

<p>Thankfully, most browsers have a way to specify a custom search engine, and I know how to make websites, so I can just <em>make my own</em> search engine, and configure my browser to use that. And I can make that search engine display a six second animation of a train, or whatever else I would like.</p>

<p>Despite being motivated here by some antipathy for AI insofar as it is not what I am looking for when I am just trying to Google something, and even though I do worry that it is fairly likely to put me out of work in the next few years or at the very least make my job <a href="https://www.nytimes.com/2025/05/25/business/amazon-ai-coders.html">different in ways that mostly suck</a>, I can’t help but be curious about how other people are using it, and so I decided to try to build this search engine site by <a href="https://en.wikipedia.org/wiki/Vibe_coding">vibe coding</a>. I’ve had fairly limited success with vibe coding but this seemed pretty doable. I was inspired by <a href="https://maryrosecook.com/blog/post/using-ai-to-build-a-tactical-shooter">a recent blog post and video from Mary Rose Cook</a> demonstrating how she approaches “AI-Augmented Programming” which gave me some insight into <em>how</em> to actually use these things effectively, which doesn’t come super naturally to me.</p>

<p>Here’s what I came up with:</p>

<p><a href="https://maxjacobson.github.io/search-engine/">https://maxjacobson.github.io/search-engine/</a> (<a href="https://github.com/maxjacobson/search-engine/tree/v0.1.0">source</a>) (GitHub Copilot wrote 100% of this code and I didn’t read any of it)</p>

<p><a href="/img/2025-05-mj-search.gif"><img src="/img/2025-05-mj-search.gif" alt="gif of searching in MJ Search and getting no results" /></a></p>

<p>No matter what you search, you see a spinner for two seconds and then it says “There are zero results”. Lol.</p>

<p>I think this should be basically fine. I can bookmark websites that I know are useful, and I can go to them, and I can use the search functions built in to those sites. That’s the plan.</p>

<p>This feels likely to flop, but I think it’ll be interesting. Will report back</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="personal computing" /><summary type="html"><![CDATA[I thought it would be fun to try taking a break from using search engines. Is this possible?]]></summary></entry><entry><title type="html">Using fish shell</title><link href="https://www.hardscrabble.net/2025/fish-shell/" rel="alternate" type="text/html" title="Using fish shell" /><published>2025-01-21T00:43:00-05:00</published><updated>2025-01-21T00:43:00-05:00</updated><id>https://www.hardscrabble.net/2025/fish-shell</id><content type="html" xml:base="https://www.hardscrabble.net/2025/fish-shell/"><![CDATA[<h3 id="introduction">Introduction</h3>

<p>After ten years of using <a href="https://www.zsh.org/">zsh</a> as my shell, I’ve recently switched to <a href="https://fishshell.com/">fish</a>.</p>

<p>I’d been aware of fish for a while, and had even kicked its tires once or twice before, but the impression I’d gotten was that it was super opinionated and “quirky”, and that rubbed me the wrong way. It turns out that’s sort of true, and nowadays I think that’s actually a great thing to be.</p>

<p>I was inspired by these recent blog posts to take another look:</p>

<ul>
  <li><a href="https://jvns.ca/blog/2024/09/12/reasons-i--still--love-fish/">Reasons I still love the fish shell</a> by Julia Evans</li>
  <li><a href="https://fishshell.com/blog/rustport/">Fish 4.0: The Fish Of Theseus</a> from the fish blog</li>
</ul>

<p>Evans’ post is a good introduction to what makes fish an appealing shell from a longtime user. It covers some of the great functionality that exists outside of the box. Some of it has been ported to zsh, like <a href="https://github.com/zsh-users/zsh-syntax-highlighting">zsh-users/zsh-syntax-highlighting</a> which adds “Fish shell like syntax highlighting for Zsh” and <a href="https://github.com/zsh-users/zsh-autosuggestions">zsh-users/zsh-autosuggestions</a> which adds “Fish-like autosuggestions for zsh”. Much of my zsh configuration, it turns out, was making zsh work more like fish works out of the box. Hmm!</p>

<p>The announcement post details their ambitious Rust rewrite which was recently completed. The writing in that post is kind of great. It’s funny and clear. There’s compelling storytelling alongside a bunch of technical context. Reading it, I felt like I could happily read changelog updates from these maintainers. It also made me realize that I’ve never read a single piece of writing from the zsh maintainers and don’t even know where I’d go to find it.</p>

<p>Upon a bit of digging, I did find the <a href="https://zsh.sourceforge.io/News/">zsh news</a> page, which has not been updated since May 2022 and the <a href="https://zsh.sourceforge.io/releases.html">zsh release notes page</a> page which has plenty of detail about each release, but does not feel like it was written for a general audience. Overall the zsh web presence is fairly confusing. There is a <a href="https://zsh.org/">zsh.org</a> but it hardly has any information on it. Instead it refers the hapless reader to <a href="https://zsh.sourceforge.io/">zsh.sourceforge.io/</a>. Plenty of information exists there, but the whole thing feels to me like it’s covered in cobwebs. Some ambitious web designer looking for a cool project should absolutely volunteer to do some spring cleaning on that site.</p>

<h3 id="switching">Switching</h3>

<p>I didn’t think I was in the market for a new shell, but I got such a good vibe from the writing in that announcement post that I decided to try it out.</p>

<p>Actually switching to fish is fairly straightforward on macOS:</p>

<ol>
  <li>Install the shell <code class="language-plaintext highlighter-rouge">brew install fish</code>
    <ul>
      <li>As of this writing, that Rust rewrite is still in beta, so this will install the 3.x release. Honestly, don’t worry about it. I’ve tried both and can’t tell the difference. If you do want to join me in running the beta you can instead run <code class="language-plaintext highlighter-rouge">brew install fish-shell/fish-beta-4/fish</code></li>
    </ul>
  </li>
  <li>Confirm where the fish binary is on the system: <code class="language-plaintext highlighter-rouge">which fish</code>, which prints out <code class="language-plaintext highlighter-rouge">/opt/homebrew/bin/fish</code> for me</li>
  <li>Add that binary to the list of allowed shells: <code class="language-plaintext highlighter-rouge">sudo vim /etc/shells</code> and then add <code class="language-plaintext highlighter-rouge">/opt/homebrew/bin/fish</code> to the list</li>
  <li>Change the default shell to fish: <code class="language-plaintext highlighter-rouge">chsh -s /opt/homebrew/bin/fish</code>
    <ul>
      <li>Note: no <code class="language-plaintext highlighter-rouge">sudo</code> needed on this one. If you do use sudo, you’ll change the default shell for your sudo user (super user?), not your regular user</li>
    </ul>
  </li>
  <li>Open a new terminal, and hopefully you’ll see that your shell is now fish. It prints a greeting by default.
    <ul>
      <li>Note: sometimes I need to log out and log back in after changing shells with <code class="language-plaintext highlighter-rouge">chsh</code>, and other times not. I’m not sure why. If the change doesn’t take effect, you can try that.</li>
    </ul>
  </li>
</ol>

<p>Next up was porting my zsh shell configuration to fish configuration. I was starting with a few files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">~/.zshrc</code> – 197 lines (my general user configuration)</li>
  <li><code class="language-plaintext highlighter-rouge">~/.zshenv</code> – 1 line (setting up <a href="https://doc.rust-lang.org/cargo/index.html">cargo</a>)</li>
  <li><code class="language-plaintext highlighter-rouge">~/.zprofile</code> – 1 line (setting up <a href="https://brew.sh/">homebrew</a>)</li>
</ul>

<p>Porting this code meant answering a few questions:</p>

<ol>
  <li>where does this go?</li>
  <li>how do I rewrite this from the zsh scripting language to the fish scripting language?</li>
</ol>

<p>Thankfully, <a href="https://fishshell.com/docs/current/index.html">fish’s docs</a> are fantastic. Let me count the ways:</p>

<ol>
  <li>there is a search function</li>
  <li>each page has a table of contents to make it easily navigable</li>
  <li>there are lots of examples</li>
</ol>

<h3 id="configuration">Configuration</h3>

<p>I found this doc that answered the question of “where does this configuration go?”: <a href="https://fishshell.com/docs/current/index.html#configuration">https://fishshell.com/docs/current/index.html#configuration</a>.</p>

<p>Some things I learned about where configuration goes:</p>

<ul>
  <li>I can put my general user configuration in <code class="language-plaintext highlighter-rouge">~/.config/fish/config.fish</code></li>
  <li>I can also, optionally, organize my configuration into various files in <code class="language-plaintext highlighter-rouge">~/.config/fish/conf.d/</code> which fish will also load</li>
  <li>The <code class="language-plaintext highlighter-rouge">~/.config/fish/conf.d</code> files get loaded first, and they get loaded in alphabetical order, so if I want homebrew’s setup to happen earlier (so later configuration scripts can assume that homebrew-installed programs are available to use) I might want to put that in a file like <code class="language-plaintext highlighter-rouge">~/.config/fish/conf.d/0_homebrew.fish</code>.</li>
  <li>In zsh I needed to worry about what was the difference between <code class="language-plaintext highlighter-rouge">~/.zshrc</code> and <code class="language-plaintext highlighter-rouge">~/.zprofile</code> and <code class="language-plaintext highlighter-rouge">~/.zshenv</code> and never quite could remember. Similarly when I used bash and couldn’t remember what was the difference between <code class="language-plaintext highlighter-rouge">~/.bashrc</code> and <code class="language-plaintext highlighter-rouge">~/.bash_profile</code>. With fish I don’t need to worry about this. All of the config files are always laoded. Within those files, I am <a href="https://fishshell.com/docs/current/index.html#configuration">encouraged</a> to define conditional logic if something only pertains to login shells or interactive shells.</li>
  <li>There is a way to manage configuration in a web UI but you don’t have to use that if you don’t want to</li>
  <li>Instead of putting helper functions into <code class="language-plaintext highlighter-rouge">~/.config/fish/config.fish</code> (which does work) you are encouraged to instead define them in <code class="language-plaintext highlighter-rouge">~/.config/fish/name_of_function.fish</code>. That way, the function doesn’t get loaded until you actually call it the first time, which speeds up your shell initialization. It also means you can add a new function there and it’s immediately available to already-running fish sessions without needing to reload any configuration.</li>
  <li>prompt configuration goes in <code class="language-plaintext highlighter-rouge">~/.config/fish/functions/fish_prompt.fish</code>. To define a prompt you just define that function. A little more on this <a href="#fish_prompt">below</a>.</li>
  <li>Because I’m a very cool minimalist, I can turn off the fish greeting by running <code class="language-plaintext highlighter-rouge">set --universal fish_greeting</code>.</li>
</ul>

<h3 id="syntax">Syntax</h3>

<p>And I found this doc that answered the question of “how do I do all the basic programming stuff in fish?”: <a href="https://fishshell.com/docs/current/language.html">https://fishshell.com/docs/current/language.html</a></p>

<p>Some things I learned about the language:</p>

<ul>
  <li>the syntax feels more modern than zsh or bash. For example, the keyword to end a conditional, loop, function, or block is <code class="language-plaintext highlighter-rouge">end</code>, just like in Ruby. No need to remember if it’s <code class="language-plaintext highlighter-rouge">done</code> or <code class="language-plaintext highlighter-rouge">fi</code> or <code class="language-plaintext highlighter-rouge">esac</code> or <code class="language-plaintext highlighter-rouge">}</code>.</li>
  <li>fish has a standard library of commands like <a href="https://fishshell.com/docs/current/cmds/path.html">path</a>, <a href="https://fishshell.com/docs/current/cmds/math.html">math</a>, <a href="https://fishshell.com/docs/current/cmds/string.html">string</a>, and <a href="https://fishshell.com/docs/current/commands.html#tools">others</a>. Their docs are <em>great</em>. It felt like they’d thought of everything and had elegant, non-hacky solutions to whatever I might need. And I can always access a local version by running, say <code class="language-plaintext highlighter-rouge">help math</code> to pop open the docs</li>
  <li>fish comes with a fish code formatter tool called <a href="https://fishshell.com/docs/current/cmds/fish_indent.html">fish_indent</a>, which you can use to auto-format your fish code. It mostly does indenting, but it seems to do a little bit more than that, too. I enabled it as an <a href="https://github.com/dense-analysis/ale/blob/master/autoload/ale/fixers/fish_indent.vim">ale fixer</a> so it auto-formats on save when I edit fish files in vim. There’s also an ale <a href="https://github.com/dense-analysis/ale/blob/master/ale_linters/fish/fish.vim">fish linter</a> which just checks for syntax errors as you go. Both very useful.</li>
  <li>the only way to create variables is with the <a href="https://fishshell.com/docs/current/cmds/set.html">set command</a>. You can’t just write <code class="language-plaintext highlighter-rouge">foo=bar</code> or <code class="language-plaintext highlighter-rouge">FOO=bar</code> or <code class="language-plaintext highlighter-rouge">export FOO=bar</code>. This felt weird at first, but I quickly got used to it.</li>
</ul>

<h3 id="fish_prompt">fish_prompt</h3>

<p>I’m not a big fan of any of the prompts that fish offers out of the box. Since February 2021, I’ve been <a href="/2021/pure-prompt/">happily using pure</a> and I was a little bit loathe to lose it. I decided to configure a fish prompt that looks a lot like pure, but which doesn’t have all of pure’s cool functionality. Here’s what I came up with in <code class="language-plaintext highlighter-rouge">~/.config/fish/functions/fish_prompt.fish</code>:</p>

<pre><code class="language-fish">function fish_prompt
    # Check if the last command succeeded so we can color the prompt red when it
    # did not succeed
    set --local last_status $status

    # Configure fish_git_prompt to show some more info
    set --global __fish_git_prompt_show_informative_status true
    set --global __fish_git_prompt_showdirtystate true
    set --global __fish_git_prompt_showuntrackedfiles true
    set --global __fish_git_prompt_showstashstate true
    set --global __fish_git_prompt_showcolorhints true

    # Configure fish_git_prompt symbols
    set --global __fish_git_prompt_char_stashstate "≡"
    set --global __fish_git_prompt_char_cleanstate ""

    # blank line before each prompt
    echo ""

    # print some info
    string join '' -- \
        (set_color blue) (prompt_pwd --full-length-dirs 2) (set_color normal) \
        (fish_git_prompt)

    # print the actual prompt
    if test "$last_status" -ne 0
        string join '' -- (set_color red) '❯ '
    else
        string join '' -- (set_color magenta) '❯ '
    end
end
</code></pre>

<p>This looks a lot like pure, but it doesn’t try to match it 100%. It’s just nice for it to feel a bit familiar.</p>

<p>The main “cool functionality” that pure has, which this doesn’t, is that pure will auto-fetch git repositories for you. I have become somewhat dependent on this. I often hit enter a few times in a git repo just to see if the prompt changes, which might suggest that I need to pull down the latest changes. I considered updating this function to run <code class="language-plaintext highlighter-rouge">git fetch</code> when it sees a git repository. Once I started thinking about actually writing that, I felt kind of like that’s actually a crazy thing for a prompt to do, and maybe it’s <em>been</em> crazy that my prompt has been doing that for the last four years. I couldn’t bring myself to do it.</p>

<p>However… I learned that it’s possible to define fish functions which respond to events, and that fish emits an event whenever it is about to call the fish_prompt function. I added this function to <code class="language-plaintext highlighter-rouge">~/.config/fish/conf.d/autofetch.fish</code>:</p>

<pre><code class="language-fish">function autofetch --on-event fish_prompt --description 'Fetch on prompt'
    if test -f ".git/FETCH_HEAD"
        set --local mtime (path mtime --relative .git/FETCH_HEAD)
        set --local duration (math '10 * 60')

        if test "$mtime" -gt "$duration"
            git fetch --prune --prune-tags --quiet
        end
    end
end
</code></pre>

<p>This will autofetch, synchronously, if it determines that we haven’t fetched the repo in the last ten minutes. That’s not exactly what pure did, but in practice it’s good enough. I no longer hit enter a few times as a weird way to say “please fetch for me”. Occasionally my prompt hangs for a sec before rendering. Occasionally my prompt indicates that the remote has new commits I might want to pull down. When I’m not connected to the internet, this autofetch fails. It’s not perfect, but I think I overall do like it. And I like that it’s decoupled from the actual rendering of the prompt, so the prompt function can focus on the single responsibility of rendering a useful prompt.</p>

<p>And I love that this kind of thing is even possible and <em>easy</em> in fish to do. Callback functions! What is this, JavaScript?</p>

<p>(Additionally, pure achieves an impressive performance by rendering its git details asynchronously. In theory that’s very cool but in practice it hasn’t felt like an issue for me to have a synchronous prompt.)</p>

<h3 id="other-events">Other events</h3>

<p>If you want to write some function which runs each time you change directories, that’s also easy to do with events. In fact that’s how tools like <a href="https://github.com/Schniz/fnm/">fnm</a> implement their functionality to switch node version when changing directories into a project that uses a particular version of node (<a href="https://github.com/Schniz/fnm/blob/v1.38.1/src/shell/fish.rs#L45-L55">via</a>):</p>

<pre><code class="language-fish">function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change'
    status --is-command-substitution; and return
    # command that activates the correct version of node
end

_fnm_autoload_hook
</code></pre>

<p>This defines a function, tells fish to call it whenever <code class="language-plaintext highlighter-rouge">$PWD</code> changes, and then calls the function once, to ensure it runs on shell initialization too.</p>

<p>That’s so elegant. I don’t know if I have a need for doing something like that just yet, but I like knowing I can if I need to.</p>

<h3 id="universal-variables">Universal variables</h3>

<p>One gotcha that really confused me was <a href="https://fishshell.com/docs/current/language.html#variable-scope">universal variables</a>, a feature I haven’t seen in other shells before.</p>

<p>If you run:</p>

<pre><code class="language-fish">set --universal foo bar
</code></pre>

<p>Then the <code class="language-plaintext highlighter-rouge">$foo</code> variable is sort of sticky. It exists in every active fish session, including future fish sessions. The value is persisted on disk, and fish references that file on disk to get the value. It’s possible to update the variable (<code class="language-plaintext highlighter-rouge">set --universal foo baz</code>) or remove it (<code class="language-plaintext highlighter-rouge">set --erase foo</code>). But just be aware, because it can be really confusing if you had a line like <code class="language-plaintext highlighter-rouge">set -U foo bar</code> in your <code class="language-plaintext highlighter-rouge">~/.config/fish/config.fish</code>, and then you deleted that line, and the dang variable is still defined???</p>

<h3 id="abbreviations">abbreviations</h3>

<p>fish supports aliases just like zsh does. At first I ported over my various aliases and carried on happily. After continuing to poke around the fish docs, I came upon <a href="https://fishshell.com/docs/current/cmds/abbr.html">abbr</a>, which manages fish abbreviations.</p>

<p>Previously I had this in my <code class="language-plaintext highlighter-rouge">~/.zshrc</code>:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias</span> <span class="s1">'cat'</span><span class="o">=</span><span class="s1">'bat'</span>
</code></pre></div></div>

<p>This is because I wanted to use the very nice <a href="https://github.com/sharkdp/bat">sharkdp/bat</a> instead of cat, but my muscle memory continued to keep typing cat anyway.</p>

<p>Now I have:</p>

<pre><code class="language-fish">abbr --add cat bat
</code></pre>

<p>The behavior is pretty similar, in the sense that I type cat, and bat runs. The difference is that as soon as I type bat, and then hit space or enter, cat gets replaced with bat in my interactive shell. So I get to see what <em>actually</em> ran, instead of what I originally typed. And that’s what goes into your command history, too. It’s a subtle difference, but I appreciate it.</p>

<p>It’s also possible to define <em>command</em> abbreviations. So, for example, I have a handful of git aliases like <code class="language-plaintext highlighter-rouge">git co</code> being an alias for <code class="language-plaintext highlighter-rouge">git checkout</code>. Up until now, I’ve defined those aliases in my <code class="language-plaintext highlighter-rouge">~/.gitconfig</code>. But now I’ve migrated all of those to be fish command abbreviations. For example:</p>

<pre><code class="language-fish">abbr --command git co checkout
</code></pre>

<p>Now when I type <code class="language-plaintext highlighter-rouge">git co</code>, that automatically expands to <code class="language-plaintext highlighter-rouge">git checkout</code>.</p>

<p>It’s even possible to create abbreviations that call a function to programmatically determine what the abbreviation should expand into. For example, I have this:</p>

<pre><code class="language-fish">function __fish_t_command
    set --local name (path basename $PWD)
    echo "tmux new-session -A -s $name"
end
abbr --add --function __fish_t_command t
</code></pre>

<p>Now when I type <code class="language-plaintext highlighter-rouge">t</code> in a project repo, it expands to <code class="language-plaintext highlighter-rouge">tmux new-session -A -s seasoning</code>, presuming I’m in a directory called “seasoning”.</p>

<p>(Astute Hardscrabble readers might notice that I’ve given up <a href="/2023/my-tmux-aliases/">the over-engineered tmux helpers described in October 2023</a>)</p>

<p>I think this expansion is just better than aliases. Once the command has expanded, tab completion works perfectly, for example.</p>

<h3 id="custom-completions">custom completions</h3>

<p>OK speaking of tab completions, one last cool thing before I ship this brain dump of a post.</p>

<p>One of the helper functions I ported from my old zsh configuration was this helper script:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone repos from GitHub.</span>
<span class="c">#</span>
<span class="c"># Usage: clone maxjacobson/film_snob</span>
<span class="c">#</span>
<span class="c"># Inspired by https://github.com/pbrisbin/dotfiles/blob/632ab65643eac277c77c18a2587fec17fd1acac3/zshrc#L19-L28</span>
<span class="k">function </span>clone <span class="o">()</span> <span class="o">{</span>
  <span class="k">case</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">in</span>
    <span class="k">*</span>/<span class="k">*</span><span class="p">)</span>
      <span class="nv">target</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/src/gh/</span><span class="nv">$1</span><span class="s2">"</span>

      <span class="k">if</span> <span class="o">[</span> <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"already exists"</span>
        <span class="nb">cd</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>
      <span class="k">else
        </span><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>
        gh repo clone <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>
        <span class="nb">cd</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>
      <span class="k">fi</span>

      <span class="p">;;</span>
    <span class="k">*</span><span class="p">)</span>
      <span class="nb">echo</span> <span class="s2">"Bad input"</span>
      <span class="p">;;</span>
  <span class="k">esac</span>
<span class="o">}</span>
</code></pre></div></div>

<p>That became this fish function in <code class="language-plaintext highlighter-rouge">~/.config/fish/functions/clone.fish</code>:</p>

<pre><code class="language-fish"># Clone repos from GitHub.
#
# Usage: clone maxjacobson/film_snob
#
# Inspired by https://github.com/pbrisbin/dotfiles/blob/632ab65643eac277c77c18a2587fec17fd1acac3/zshrc#L19-L28
function clone --description "Clone a repository from GitHub"
    switch "$argv"
        case "*/*"
            set --local target "$HOME/src/gh/$argv"
            if test -d "$target"
                echo "already exists"
                cd "$target"
            else
                mkdir -p "$target"
                gh repo clone "$argv" "$target"
                cd "$target"
            end
        case '*'
            echo "Bad input"
    end
end
</code></pre>

<p>Very similar, although there’s definitely less syntactical cruft.</p>

<p>I thought it might be fun to try adding a tab completion to this function that will tab complete the repositories that are available to clone. Everything in fish has great tab completion, but my helper function did not. By default it suggested the files in the current working directory as tab completion candidates, which was annoying.</p>

<p>I learned that it’s possible to configure a command’s tab completions by adding a file in <code class="language-plaintext highlighter-rouge">~/.config/fish/completions/clone.fish</code></p>

<p>For example you can add this just to tell it not to tab complete filenames:</p>

<pre><code class="language-fish">complete --command clone --no-files
</code></pre>

<p>The docs for the <a href="https://fishshell.com/docs/current/cmds/complete.html">complete</a> command have tons of examples. They also encourage you to poke around the completions that come with fish. I found a big trove of them in <code class="language-plaintext highlighter-rouge">/opt/homebrew/Cellar/fish/4.0b1/share/fish/completions</code> after checking the directoreis in <a href="https://fishshell.com/docs/current/language.html#envvar-fish_complete_path">fish_complete_path</a>. There’s lots of inspiration there.</p>

<p>Ultimately here’s what I came up with:</p>

<pre><code class="language-fish">complete --command clone --no-files

function __fish_clone_repo_pattern
    set --local subpattern "[a-zA-Z0-9\-\_\.]+"
    echo "($subpattern)/?($subpattern)?"
end

# clone foo/&lt;tab&gt;
# clone foo/bar&lt;tab&gt;
# clone foo&lt;tab&gt;
function __fish_clone_should_search
    string match \
        --quiet \
        --regex (__fish_clone_repo_pattern) \
        (commandline --current-token)
end

# look up a particular owner's repos
function __fish_clone_search_results
    set --local groups (
      string match \
          --groups-only \
          --regex (__fish_clone_repo_pattern) \
          (commandline --current-token)
    )
    gh search repos \
        --owner="$groups[1]" \
        --json fullName \
        --jq '.[].fullName' \
        "$groups[2]"
end

# clone foo/&lt;tab&gt;
# will search GitHub for foo's repos and offer them as tab completions
complete \
    --command clone \
    --condition __fish_clone_should_search \
    --arguments '(__fish_clone_search_results)'
</code></pre>

<p>There’s a lot going on there, but the upshot is that when I type <code class="language-plaintext highlighter-rouge">clone maxjacobson&lt;tab&gt;</code>, fish will ask GitHub what repos are owned by maxjacobson and offer them as tab-completion suggestions.And if I type <code class="language-plaintext highlighter-rouge">rails/action&lt;tab&gt;</code>, it tab completes just the repos owned by rails that have action in the name.</p>

<p>Programming in fish is fun. I know it’s possible to do this kind of thing in zsh too, but I never would have attempted it.</p>

<h3 id="conclusion">Conclusion</h3>

<p>I have been having a lot of fun with fish! Perhaps more posts to come as I continue poking at things.</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="personal computing" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">generating opengraph social preview images for blog posts automatically in jekyll</title><link href="https://www.hardscrabble.net/2024/generating-automatic-opengraph-images-in-jekyll/" rel="alternate" type="text/html" title="generating opengraph social preview images for blog posts automatically in jekyll" /><published>2024-12-03T21:13:00-05:00</published><updated>2024-12-03T21:13:00-05:00</updated><id>https://www.hardscrabble.net/2024/generating-automatic-opengraph-images-in-jekyll</id><content type="html" xml:base="https://www.hardscrabble.net/2024/generating-automatic-opengraph-images-in-jekyll/"><![CDATA[<p>Some time last year I thought it might be nice to have <a href="https://ogp.me">opengraph</a> preview images on my blog posts, so if I posted them to Mastodon they’d show a cool little preview image and more people would click on them.</p>

<p>It’s easy enough, you just need something like this in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image"</span> <span class="na">content=</span><span class="s">"https://www.hardscrabble.net/img/preview/2024-10-03-colors.png"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Because I use <a href="https://jekyllrb.com">Jekyll</a> to generate my blog, it wasn’t too hard to add <a href="https://github.com/maxjacobson/hardscrabble.net/blob/10561fb935b1a98c96d8abb85a165079fe3c0b5e/_layouts/default.html#L19-L20">a little conditional logic</a> that includes that meta element if a post specifies a preview image filename in its <a href="https://jekyllrb.com/docs/front-matter/">front matter</a>.</p>

<p>But it’s sort of annoying to have to prepare an image whenever you want to blog something. I don’t necessarily have time for all that.</p>

<p>Then, yesterday, I was poking around on Bluesky and saw this post:</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:bhdap3w2bseikypfnjmaskzf/app.bsky.feed.post/3lcbjdiahas2l" data-bluesky-cid="bafyreiauldlnvw5i7inskcwxnaw2h3yjaesxdhbzstzvtkzwv6gxpnygji"><p lang="en">I&#x27;m starting my 2024 #blogvent series where I post a blog a day in December!

Blogvent day 1 is about fighting spam in your open source repos:
cassidoo.co/post/oss-int...<br /><br /><a href="https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf/post/3lcbjdiahas2l?ref_src=embed">[image or embed]</a></p>&mdash; Cassidy (<a href="https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf?ref_src=embed">@cassidoo.co</a>) <a href="https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf/post/3lcbjdiahas2l?ref_src=embed">December 1, 2024 at 4:07 PM</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>And I got so jealous of how nice and clean that preview image is, and I had a few thoughts:</p>

<ul>
  <li>Sigh I should blog more</li>
  <li>Dang can you imagine blogging that much?</li>
  <li>No, I mean, it would be so annoying to generate all those preview images</li>
  <li>Wait, how did she manage to auto-generate the preview images with text for each blog post??? That’s so cool!!!</li>
</ul>

<p>I saw that her blog post is open source and I tracked down <a href="https://github.com/cassidoo/blahg/blob/c7ca18f2ee53cdb781b25b39105a9aec30ea0bcd/src/components/BaseHead.astro#L7-L27">the lines of code</a>, which look sort of like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
const { title, description, image = "/home-blog-card.png" } = Astro.props;
---

&lt;head&gt;
  &lt;meta property="og:image" content={new URL(image, Astro.url)} /&gt;
&lt;/head&gt;
</code></pre></div></div>

<p>I sat and stared at that for several minutes, trying to puzzle out how it works. I’ve never used Astro, but it seems very powerful. I look at the docs.</p>

<p>Eventually I realized that actually it’s just a static image, and it isn’t dynamically changing the text for each blog post. I went back to her page and scrolled down past several links, and I saw that they all have the same text.</p>

<p>Lol.</p>

<p>But hey, I thought, maybe it is possible to generate custom images with text in them? And if it is, maybe it is possible to hook that into the jekyll build process?</p>

<p>And indeed it is. And I’ve done a somewhat basic version of that. And here’s how you can do that too.</p>

<p>First, create a plugin by creating a file called <code class="language-plaintext highlighter-rouge">_plugins/opengraph.rb</code>. Ruby files in the <code class="language-plaintext highlighter-rouge">_plugins</code> folder are loaded during the build process.</p>

<p>Within that plugin, register some jekyll <a href="https://jekyllrb.com/docs/plugins/hooks/">hooks</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">:pre_render</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
  <span class="k">unless</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s2">"preview_image"</span><span class="p">]</span>
    <span class="n">preview</span> <span class="o">=</span> <span class="no">GeneratePreview</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">post</span><span class="p">)</span>
    <span class="n">preview</span><span class="p">.</span><span class="nf">write</span>
    <span class="n">post</span><span class="p">.</span><span class="nf">merge_data!</span><span class="p">({</span><span class="s2">"preview_image"</span> <span class="o">=&gt;</span> <span class="n">preview</span><span class="p">.</span><span class="nf">path</span> <span class="p">},</span> <span class="ss">source: </span><span class="s2">"opengraph plugin"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:after_init</span> <span class="k">do</span>
  <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span> <span class="s2">"tmp/img/preview"</span>
<span class="k">end</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_write</span> <span class="k">do</span>
  <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="s2">"tmp/img/preview/*"</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
    <span class="no">FileUtils</span><span class="p">.</span><span class="nf">cp</span> <span class="n">path</span><span class="p">,</span> <span class="s2">"_site/img/preview"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The idea here is that for any post that doesn’t already have a preview image specified in the front matter, we’ll</p>

<ul>
  <li>generate a preview image and write it to a folder</li>
  <li>modify the in-memory post object to know that it has a preview image with a particular filename. Because this is a <code class="language-plaintext highlighter-rouge">:pre_render</code> hook, the HTML for that post has not yet been written to disk, so it’s not too late to modify its metadata</li>
</ul>

<p>And then, after the whole site has been written, copy all of those generated image files into place.</p>

<p>In this design, I don’t need to commit all of the generated files to the repository, I just generate them at build time. I need to regenerate them each time I deploy. This is a bit slow and ineffecient. I might decide to just commit them down the line.</p>

<p>The actual image generation code is sort of hacky. It’s a bit of ruby glue code that shells out to the venerable <a href="https://imagemagick.org/script/index.php">imagemagick</a> CLI tool. It executes commands like this:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>magick <span class="se">\</span>
    <span class="nt">-background</span> <span class="s2">"#f2d8b2"</span> <span class="se">\</span>
    <span class="nt">-font</span> <span class="s2">"Helvetica"</span> <span class="se">\</span>
    <span class="nt">-fill</span> <span class="s2">"#248165"</span> <span class="se">\</span>
    <span class="nt">-size</span> 1200x630 <span class="se">\</span>
    <span class="nt">-gravity</span> SouthWest <span class="se">\</span>
    <span class="s2">"caption:The</span><span class="se">\ </span><span class="s2">easiest</span><span class="se">\ </span><span class="s2">way</span><span class="se">\ </span><span class="s2">to</span><span class="se">\ </span><span class="s2">indent</span><span class="se">\ </span><span class="s2">paragraphs</span><span class="se">\ </span><span class="s2">online,</span><span class="se">\ </span><span class="s2">not</span><span class="se">\ </span><span class="s2">that</span><span class="se">\ </span><span class="s2">you</span><span class="se">\ </span><span class="s2">necessarily</span><span class="se">\ </span><span class="s2">should"</span> <span class="se">\</span>
    tmp/img/preview/2012-03-21-indenting-paragraphs-online.png
</code></pre></div></div>

<p>Which produces images like this:</p>

<p><a href="/img/2024-12-03-example-preview.png"><img src="/img/2024-12-03-example-preview.png" alt="example preview image with green text on beige background saying &quot;The easiest way to indent paragraphs online, not that you necessarily should&quot;" /></a></p>

<p>Kind of obnoxious right? A little brat maybe? You tell me.</p>

<p>Getting this to run in GitHub Actions presented a few challenges. The <code class="language-plaintext highlighter-rouge">ubuntu-latest</code> machine where the builds run had imagemagick installed, but it was imagemagick 6, not 7. <code class="language-plaintext highlighter-rouge">sudo apt-get install -y imagemagick</code> just complained that hey, it’s already installed baby, nothing for me to do. But I had gotten all of this working with imagemagick 7 and I wasn’t about to redo anything.</p>

<p>Once I figured out how to install imagemagick 7, I had the next issue, which was that no fonts were installed on the machine, at least as far as imagemagick could tell. After installing the <a href="https://packages.ubuntu.com/jammy/gsfonts">gsfonts</a> package, it could see some fonts, but only the off brand, Linux versions of them. By running <code class="language-plaintext highlighter-rouge">magick -list font</code> in the CI context, I could see what those were. Eventually I worked out that something called Nimbus-Sans-L is basically knockoff Helvetica, which was what I had randomly picked locally. Great. Close enough. Perhaps you can tell but I have never really been a Font Guy. But I wanted this to work in both contexts, locally and in GitHub Actions. To work around that, I added some conditional logic to the ruby script to default the font to Helvetica, but allow overriding it via an environment variable.</p>

<p>And with that… everything was working, and I went to bed, criminally late, having forgotten what I originally had been planning to blog about. I remembered over lunch today and finished <a href="/2024/waitress-colorblind/">that post</a>. And now here’s this.</p>

<p>Here’s <a href="https://github.com/maxjacobson/hardscrabble.net/commit/019fded4d2627c8ea20c634486df235b5a5b7253">the commit</a> with all of the code, if you’re curious. It kind of works.</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="blogging" /><summary type="html"><![CDATA[Some time last year I thought it might be nice to have opengraph preview images on my blog posts, so if I posted them to Mastodon they’d show a cool little preview image and more people would click on them.]]></summary></entry><entry><title type="html">How untrustworthy is that?</title><link href="https://www.hardscrabble.net/2024/waitress-colorblind/" rel="alternate" type="text/html" title="How untrustworthy is that?" /><published>2024-12-03T12:30:00-05:00</published><updated>2024-12-03T12:30:00-05:00</updated><id>https://www.hardscrabble.net/2024/waitress-colorblind</id><content type="html" xml:base="https://www.hardscrabble.net/2024/waitress-colorblind/"><![CDATA[<p>In <a href="https://en.wikipedia.org/wiki/Waitress_(musical)">Waitress</a>, a musical I love, there’s a song called “When He Sees Me” that I love. But there’s one line that gives me pause.</p>

<div class="embed-container">
  <iframe src="https://www.youtube.com/embed/TYPbR8yydJw" width="700" height="480" frameborder="0" allowfullscreen="true">
  </iframe>
</div>

<p>Here’s the verse (emphasis mine):</p>

<blockquote>
  <p>Sorry girls</p>

  <p>But he could be criminal, some sort of psychopath</p>

  <p>Who escaped from an institution</p>

  <p>Somewhere where they don’t have girls</p>

  <p>He could have masterminded some way to find me</p>

  <p><strong>He could be colorblind</strong></p>

  <p><strong>How untrustworthy is that?</strong></p>

  <p>He could be less than kind</p>
</blockquote>

<p>The context here is that Dawn is single and hesitant about putting herself out there. Most of all she’s afraid of being hurt, and the joke of the song is that she can’t quite admit that fact, and she chooses instead to enumerate all of her other excuses for not going on a date: minor infractions like calling the waiter by his first name or eating Oreos wrong, for example. It’s a breathless and very funny stream of consciousness. Ha ha, right?</p>

<p>But hold on, what’s her problem with colorblind people? How is it untrustworthy to be colorblind? I love this song, but I always trip over that line. In addition to being a bit ableist, it’s also such a non sequitor.</p>

<p>I’m reminded of the classic 2008 parody of Deep Blue Something’s Breakfast at Tiffany’s from the sketch comedy group Olde English:</p>

<div class="embed-container">
  <iframe src="https://www.youtube.com/embed/3RGiepIoOt8" width="700" height="480" frameborder="0" allowfullscreen="true">
  </iframe>
</div>

<p>That’s me right now.</p>

<p>Dawn’s characterization is probably strengthened by her being a little bit shitty about this. She’s such a sweet character that for her to be randomly biased against a protected group is funny, in a dark way. In the chorus, she asks, “What if when he knows me, he’s only disappointed?” The audience is inclined to love and root for Dawn, but in order for the song to resonate, she needs to show some human flaws.</p>]]></content><author><name>Maxwell Jacobson</name></author><category term="culture" /><summary type="html"><![CDATA[In Waitress, a musical I love, there’s a song called “When He Sees Me” that I love. But there’s one line that gives me pause.]]></summary></entry></feed>