<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[naildrivin5.com - David Bryant Copeland's Website]]></title>
  <link href="https://naildrivin5.com/atom.xml" rel="self"/>
  <link href="https://naildrivin5.com/"/>
  <updated>2026-02-26T14:18:00+00:00</updated>
  <id>https://naildrivin5.com/</id>
  <author>
    <name><![CDATA[David Bryant Copeland]]></name>
    <email><![CDATA[davec@naildrivin5.com]]></email>
  </author>

  
  <entry>
    <title type="html"><![CDATA[The Death of the Software Craftsman]]></title>
    <link href="https://naildrivin5.com/blog/2026/02/23/the-death-of-the-software-craftsman.html"/>
    <updated>2026-02-23T12:30:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2026/02/23/the-death-of-the-software-craftsman.html</id>
    <content type="html"><![CDATA[<blockquote>
  <p>The death of a software craftsman<br />
Well, it happens a lot ‘round here<br />
You think quality is a common goal<br />
That goes to show how little you know</p>
</blockquote>

<p>Developers work hard over the years to cultivate tools and techniques to improve the quality of the construction of their software.  These tools and techniques are slowly becoming even more useless than they may already be. We must adapt by either going all-in, totally opting-out, or finding a middle ground by tracking the AI code-generation craze to fill the gap as a hand-coding artisan.</p>

<!-- more -->

<aside style="padding: 0.75rem; background-color: #eee; font-style: italic; font-size: 80%; margin-bottom: 1rem; border: solid thin black; border-radius: 0.25rem;">
All content on this page was <a href="https://declare-ai.org/1.0.0/none.html">created without any assistance from a Generative AI</a>
(including the Graphviz diagrams!). My
em-dashes are my own. I would also respectfully ask that you read it and not have an LLM summarize it.
</aside>

<h2 id="software-quality-problems-are-people-problems">Software Quality Problems are People Problems</h2>

<p>All techniques for improving the quality of software construction attempt to solve one problem: allow a person to understand the system they are changing so they can safely and correctly make a change to it.  Techniques either prevent certain classes of bugs (e.g. static typing, code review, web frameworks), or make it easier to manage complexity (e.g. object-orientation, microservices, function currying)<sup id="back-1"><a href="#1">1</a></sup>.</p>

<figure>
  <a href="/images/tradprog.png">
    <img src="/images/tradprog.png" srcset="/images/tradprog.png 629w,
                 /images/tradprog-320.png 320w,
                 /images/tradprog-500.png 500w" sizes="(max-width: 320px) 320px,
                (max-width: 500px) 500px,
                629px" alt="A flow chart showing a software development process that starts with three inputs: 'Any System', 'Description of Change', and 'Tools and Techniques for Quality Software Construction'. These feed into 'Developer', which outputs to 'Proposed Change'. This then feeds to a choice of 'Review'. One path out of 'Review' is 'needs revision', which goes back to 'Developer'. The other path out of 'Review' is to 'Updated System with Change'. This then leads to 'Observable Outcome'. There is a note pointing to 'Observable Outcome' that says 'This is all that really matters'" />
  </a>
  <figcaption class="">
    The Traditional Software Development Process (<a target="_new" href="/images/tradprog.png">Open bigger version in new window</a>)
  </figcaption>
</figure>

<p>Although we debate and discuss various techniques amongst ourselves, no non-programmer cares about them. They can’t even conceptualize how software is made, so they have no way to know what quality construction is, how to value it, or even how to identify it. They just care about outcomes.</p>

<blockquote class="pullquote">We tell ourselves that it's these skills that deliver those desired outcomes</blockquote>

<p>Despite all that, we tell ourselves that these skills are critical in delivering those desired outcomes. I myself have a very particular set of skills.  Skills I’ve acquired over a very long career.  I write about them, I use them, and I am truly convinced that careful construction of software is the best way to reliably create systems that can be easily changed over their lifetimes. Even if no one knows or cares.</p>

<p>However.</p>

<h2 id="best-block-no-be-there">Best Block No Be There</h2>

<p>I may relish the solutions to building quality software, but the best solution to any problem is to eliminate the problem. <a href="https://en.wikipedia.org/wiki/The_Karate_Kid">Don’t bet against Mr Miyagai’s wisdom</a>.</p>

<p>Let’s imagine a hypothetical tool that could take, as input, any software system and a description of a change. The tool produces, as output, an updated system with that change incorporated.  This hypothetical system would not rely on any particular software construction technique—it works with any system you give it.</p>

<figure>
  <a href="/images/aiblackbox.png">
    <img src="/images/aiblackbox.png" srcset="/images/aiblackbox.png 629w,
                 /images/aiblackbox-320.png 320w,
                 /images/aiblackbox-500.png 500w" sizes="(max-width: 320px) 320px,
                (max-width: 500px) 500px,
                629px" alt="A flow chart showing a software development process that starts with 'Any System' and 'Description of Change'. These feed into 'Black Box', which then leads to 'Updated System with Change'. This then leads to 'Observable Outcome'. There is a note pointing to 'Observable Outcome' that says 'This is all that really matters'" />
  </a>
  <figcaption class="">
    A Hypothetical Brave New Way to Make Software (<a target="_new" href="/images/aiblackbox.png">Open bigger version in new window</a>)
  </figcaption>
</figure>

<p>If software was created this way, would anyone know what, say, dependency injection was?  Would anyone go to a conference to learn about the latest features of Ruby on Rails?  Would anyone buy a book about carefully designing database schemas?</p>

<p>They would not. There would be no reason to.  It literally would not matter what the code was like. Our hypothetical system could handle whatever it’s given and produce the requested changes.  Sure, a small few would need to understand how code works, but the group would be quite small.</p>

<p>This hypothetical tool isn’t intended to be magic. Like any tool, there would be a skill to it, perhaps a difficult-to-master skill.  And
while there might be <em>some</em> overlap with the skills we use today to create quality software, most of <em>those</em> skills would become obsolete.</p>

<p>This system is not so hypothetical.</p>

<blockquote class="pullquote">We know that in at least some cases, they do exactly what I described above.</blockquote>

<p>We’ve seen what AI code generation systems are capable of.  We know that in at least some cases, they do exactly what I described above.  In some cases, they can take an existing system, a description of a change, and produce a system with that change.  And they seem to be improving quickly, handling more and more cases.</p>

<p>And yet.</p>

<h2 id="soylent-green-is-people">Soylent Green is People</h2>

<p>My visceral reaction to these tools has been a combination of disgust and boredom. Here are the things I have told myself about why this technology can or should be ignored:</p>

<ul>
  <li>It was created unethically.</li>
  <li>It consumes an unreasonable amount of resources (such as electricity and hard drives).</li>
  <li>It is owned and sold by some of the worst people in the world.</li>
  <li>Its true cost is hidden by investor money. Once its pricing is set in line with its true cost, no one could afford it.</li>
  <li>Being non-deterministic, it can never be as a good as a person.</li>
  <li>There’s no way to hold anyone accountable for its output.</li>
  <li>A real programmer will still need to go into the code. Practices around software construction will always matter.</li>
  <li>It’s shit at anything that’s not a popular technology applied to a common use case.</li>
</ul>

<p>As of this writing, these are all true.  But are they intrinsic problems? <em>Must</em> they be true, always?</p>

<blockquote class="pullquote">AI code generation's problems aren't actually as intrinsic as they may seem</blockquote>

<p>Unlike crypto, which is literally made of intrinsic problems preventing it from widespread adoption (please don’t email me), AI code generation’s problems aren’t actually as intrinsic as they may seem.</p>

<ul>
  <li>AI models <em>could</em> be created ethically. There could be (and perhaps are?) systems created that comply with the licenses of their training materials.</li>
  <li>Systems that use AI models <em>could</em> be done with less power and resources.  DeepSeek demonstrated that even minimal performance tweaking can give real benefits. Not enough to fully ameliorate the issues, but enough that I cannot say with certainty that these resource problems are unsolvable.</li>
  <li>AI code generation tools could be required to operate within a system of accountability.  We’ve all seen that CEOs will comply with the government when threatened.  And someday, we all might live under a functioning government that works for its people.</li>
  <li>Their price <em>may</em> be bearable when investor money runs out. Uber still exists as a going concern, despite being more expensive than ever.</li>
  <li>People are non-deterministic, too, so it’s not clear me that AI coding agents are necessarily worse than people at producing results. Coding agents are already better at programming than the vast majority of the population. I can’t say with certainty that every living programmer is the embodiment of <a href="https://en.wikipedia.org/wiki/John_Henry_(folklore)">John Henry</a>.</li>
  <li>While AI coding agents may never be able to handle every imaginable task, it’s not clear to me that there is a limit on what they can do or that any given limit is unreasonable. Most of us are putting spreadsheets in a web browser or wrapping a database in a front-end, so if all that work is automated, that doesn’t seem necessarily bad to me.</li>
</ul>

<p>In other words, all the problems of this technology <em>could</em> be addressed.  I’m not saying they will be, or that it will happen on any particular timeline.  But, it seems entirely possible to have a tool that produces software by taking in an existing system and written request as input, all without producing the externalities they currently do.</p>

<p>Not inevitable, but <em>possible</em>.</p>

<p>This means it’s worth considering a world where AI code generation is commonplace. In fact, professional software developers <em>must</em> consider such a world, and think deeply about their place in it.</p>

<p>As a thought experiment, imagine if all the issues above were addressed.  We have AI code generation tools are ethical, use appropriate resources, etc.  Who <em>wouldn’t</em> use these systems to produce software? At least in the context where the quality of the output was sufficient (a low bar if we are being honest), why would anyone <em>not</em> use these tools?</p>

<p>As much as I love coding, it would be really nice to produce results without fretting over variable names, modularity, OO purity, monads, or
any of that stuff.  Having a system produce reliable code just seems better. And it’s not like these tools are the first examples of code
generation—we use code generation all the time.</p>

<blockquote class="pullquote">Almost everyone in the orbit of software development only cares about outcomes</blockquote>

<p>Remember, almost everyone in the orbit of software development only cares about outcomes and results, not the process by which they were achieved.  It doesn’t mean <em>you</em> are wrong to care, but most people don’t.</p>

<p>So what’s a lonely programmer to do?</p>

<h2 id="ce-nest-pas-un-griefpost">Ce n’est Pas un Griefpost</h2>

<p>I’ve been a professional software developer for 30 years, and these AI code generation tools basically obviates a big chunk the skills I’ve developed. But I <em>like</em> using those skills. I like writing code. I like the process of building software. How do I navigate a world that no longer values that?</p>

<p>I’m results-oriented and am good at communicating with non-programmers. I can manage teams and projects, and keep people focused on business outcomes.  These are all skills you need no matter how code is written. But my least happy professional eras were when I wasn’t involved with code.</p>

<p>Up until now, the never-ending need for programmers has given me a nice career where I can balance all of this stuff.  Thankfully, I’m on the tail end of that career.  But I’m not retired yet.</p>

<p>I see three paths in front of me: Hard Pass, All In, and Embrace Tradition.</p>

<h3 id="hard-pass">Hard Pass</h3>

<p>The problems I listed above are real.  And while they could be solved, there’s no guarantee they will be solved in any given timeframe, or even at all.  The problems compound and create what many believe to be an easy choice: don’t support unethical systems created by terrible people that quickly exhaust our resources.</p>

<p>This is how I felt initially. It’s just easy to write this entire thing off as awful, useless, evil, of poor quality, and not something I want to be involved with (like crypto!). Unlike ignoring crypto, the Hard Pass has consequences to the career of a
software professional.</p>

<p>In the short term, it’s still possible to be gainfully employed in software while abstaining from AI.  There’ll be fewer and fewer such jobs as time goes by, but there should be at least a few years of generally available jobs writing code by hand.</p>

<p>But these positions will become fewer and far between.  Choosing AI abstinence means ultimately exiting professional software development, at least at some point.</p>

<blockquote class="pullquote">Choosing AI abstinence means exiting professional software development</blockquote>

<p>This might seem extreme, but be honest: these tools will become far more prolific over the short term.  Not enough people are going to come around on the ethical issues to slow or stop their adoption. The country I live in is 350+ million people who tolerate the sexual exploitation and murder of children (as just one example).  I say this not to encourage you to give in or sell out, but just to understand that the world of software development as you know it is going to become very small very fast.</p>

<p>This is the consequence of the moral dilemma.  You <em>can</em> opt-out. Almost every job that ever was or ever will be is something other than writing software. Most people make a living without writing software.  But if you want to give AI a Hard Pass, you will eventually be giving your career as a programmer a hard pass, too (though please stick around for option three, below).</p>

<p>If this is you, start figuring out how you’re going to make a living otherwise.</p>

<p>Or, you could go all-in.</p>

<h3 id="all-in">All In</h3>

<p>Going all-in is to accept that the profession is changing so radically that you’ll
need to retrain yourself.  You’ll leverage your experience and incorporate these new
tools in the same way as you would any other advance in the field. Except you may
leave a lot of your existing skills behind.</p>

<blockquote class="pullquote">It's hard to live a life free of compromise</blockquote>

<p>It’s hard to live a life free of compromise. Going All In is to compromise. It means you must tolerate the downsides of this technology (assuming you believe there <em>are</em> at least some downsides). Of course, tolerance is not the same thing as support. Every one, every where, every day must weigh their needs against what their conscience can bear.</p>

<p>If your priority is to stay working in software development, especially if you have a long career ahead of you, going All In is the safest, simplest, most practical option. You just have to be able to live with yourself.</p>

<p>There are two reasons I find this option depressing, beyond having to tolerate the ethical problems.</p>

<figure class="small-figure left">
  <a href="/images/prettyplease.jpg">
    <img src="/images/prettyplease.jpg" alt="A picture of Harvey Keitel's character 'The Wolf' from Pulp Fiction with the text 'Pretty Please with Sugar On Top Validate the Fucking Email' overlayed" />
  </a>
</figure>

<p>One is as I mentioned above: I <em>like</em> coding. I like writing code and everything about it.  I like the control it gives me, and I do not enjoy “coding” by writing Markdown and feeding it to a compiler that sometimes works and sometimes doesn’t until I ask it nicely to do what I need.</p>

<div class="cf"></div>

<figure class="small-figure right">
  <a href="/images/i-like-coding.jpg">
    <img src="/images/i-like-coding.jpg" alt="A four-panel meme comic showing me in panel one saing 'I like coding'. Panel 2 shows someone else saying 'Here's some React and Tailwind'. The third panel just shows me with no text. The fourth panel is me looking angry" />
  </a>
</figure>

<p>Two is related to the “common technology” problem.  These tools produce code using the popular frameworks, techniques, and libraries.  While it’s possible I won’t have to review or modify the code these tools produce some day, today is not that day. And I just really, really don’t want to write React or Tailwind.  I don’t really want to learn Python.  And the creator of Rails can go fuck himself.</p>

<div class="cf"></div>

<p>But that’s me.  If I were younger, smarter, and less concerned with the ethical
issues, I’d embrace this new world, and not worry so much about what code was being
produced. The whole point is that it won’t matter in the end.</p>

<p>All this being said, flat-pack furniture made of fiberboard did not eliminate the skill of putting a chisel to hard wood. The time of the software craftsman could be coming.</p>

<h2 id="reject-modernity-embrace-tradition">Reject Modernity, Embrace Tradition</h2>

<p>I always thought the “software craftsman” movement was dumb. Uncle Bob is a terrible person with bad ideas, and Agile Thought Leaders seem more focused on their billable rate than improving outcomes for users.  Their collective disrepsect for anyone not a programmer is galling.  But most of all, it just seemed like navel-gazing.  Results and outcomes <em>really do</em> matter!</p>

<figure class="small-figure left image-border">
  <a href="/images/tradition.jpg">
    <img src="/images/tradition.jpg" alt="A two panel meme with the top panel titled 'Reject Modernity' and contents of the text '&gt; npm install -g typescript-language-server typescript'. The bottom panel is titled 'Embrace Tradition' and as the text 'hjkl' in it" />
  </a>
</figure>

<p>But what is a craftsman, really? Someone with deep skills honed over many years, who can produce amazing results using a process that’s almost as engaging to observe as the results themselves.  There are certain outcomes that only a highly trained human hand can deliver.</p>

<div class="cf"></div>

<p>There are still craftsmen being trained and employed to this day, across a wide variety of industries.  Although few people have hand-made furniture, you can still commission it. And don’t forget that even uninspiring chain restaurants like The Cheesecake Factory still make almost all their food from scratch.</p>

<p>Thus, it’s not unreasonable to think that such an industry will exist for writing software.  In the short term, there’s still a ton that AI coding agents simply can’t do very well.  And in the long term, there will be at least some demand for software written to a higher standard than what AI is producing. Of course, there will always be the need to pay a high price to have real programmer pop the hood on your vibe-coded mess to figure out what’s actually wrong.</p>

<p>However, to live in this world as a Software Crafter<sup id="back-2"><a href="#2">2</a></sup> is to understand AI code generation…at least until it plateaus in capabilities. To be marketable and make a living, you have to know what gap you are filling. And that gap is changing often.  Thus, you cannot abstain entirely from AI if this is the way you go.  You may not use it to produce your results, but you will need to use it—not just read about it—to understand it.</p>

<p>We don’t get to live our lives free of compromise.</p>

<p>Maybe in the next world.</p>

<hr />

<footer class="footnotes">
<ol>
<li id="1">
<sup>1</sup>I'm not saying stuff like static typing and object-orientation achieve the results they think promise they do, especially in a general sense, but they intend to prevent bugs, manage complexity, and allow easier changes to software.<a href="#back-1">↩</a>
</li>
<li id="2">
<sup>2</sup>In  this new era, we can ditch the gendered language. "Craftsperson" is cumbersome, but would also work. "Maker" can go straight to hell.<a href="#back-2">↩</a>
</li>
</ol>
</footer>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Discussing Brut on Dead Code Podcast]]></title>
    <link href="https://naildrivin5.com/blog/2025/11/05/discussing-brut-on-dead-code-podcast.html"/>
    <updated>2025-11-05T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/11/05/discussing-brut-on-dead-code-podcast.html</id>
    <content type="html"><![CDATA[<p>I recently got to chat with <a href="https://ruby.social/@jardo">Jared Norman</a> on the <a href="https://shows.acast.com/dead-code/episodes/brut-al-death-with-david-bryant-copeland">Dead Code Podcast</a>. We talked mostly about Brut, but also a bit about hardware synthesizers and <a href="https://bandwagon.fm/68e6bf0df1f8302aa576c863">looptober</a>.</p>

<p>If you want to know about more about why Brut exists or its philisophical underpinnings, check it out!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Building a Sub-command Ruby CLI with just OptionParser]]></title>
    <link href="https://naildrivin5.com/blog/2025/10/07/building-a-sub-command-ruby-cli-with-just-optionparser.html"/>
    <updated>2025-10-07T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/10/07/building-a-sub-command-ruby-cli-with-just-optionparser.html</id>
    <content type="html"><![CDATA[<p>I’ve <a href="https://www.amazon.com/Build-Awesome-Command-Line-Applications-Ruby/dp/1934356913">thought deeply about building CLIs</a> and built a
<em>lot</em> of them over the years.  I’ve used Rake, Thor, my own gem GLI and many others.  After all that, the venerable <code class="language-plaintext highlighter-rouge">OptionParser</code>—part of
Ruby’s standard library—is the best choice for scripting and sub-command (git-like) CLIs.  I want to show you how.</p>

<!-- more -->

<h2 id="what-is-a-sub-command-cli">What is a Sub-Command CLI?</h2>

<p>At first glance, <code class="language-plaintext highlighter-rouge">OptionParser</code> doesn’t seem to support a sub-command CLI, like so (I’ll explain what each part is below):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; bin/test --verbose audit --type Component specs/front_end
</code></pre></div></div>

<p>Yes, you could configure <code class="language-plaintext highlighter-rouge">--verbose</code> and <code class="language-plaintext highlighter-rouge">--type TYPE</code>, then figure out that the first thing left over in <code class="language-plaintext highlighter-rouge">ARGV</code> was a command, but it gets very cumbersome when things get beyond trivial, especially when you want to show help.</p>

<p>Fortunately, <code class="language-plaintext highlighter-rouge">OptionParser's</code> lesser-known (and oddly-named) method
<a href="https://docs.ruby-lang.org/en/3.4/OptionParser.html#method-i-order-21"><code class="language-plaintext highlighter-rouge">order!</code></a> parses the command-line up to the first argument
it doesn’t understand.  How does this help?</p>

<p>Consider the command above.  It’s made up of five parts: the app name (<code class="language-plaintext highlighter-rouge">bin/test</code>), the globally-applicable options (<code class="language-plaintext highlighter-rouge">--verbose</code>), the sub
command (<code class="language-plaintext highlighter-rouge">audit</code>), command-scoped options (<code class="language-plaintext highlighter-rouge">--type Component</code>) and the arguments (<code class="language-plaintext highlighter-rouge">specs/front_end</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; bin/test --verbose audit --type Component specs/front_end
   ---+--     ---+-- --+--   ----------+---  ----+------
      |          |     |               |         |
App---+          |     |               |         |
Global Options---+     |               |         |
Sub Command------------+               |         |
Command Options------------------------+         |
Arguments----------------------------------------+
</code></pre></div></div>

<p>You’d design a CLI like this if the various sub-commands had shared code or behavior. It also helps to avoid having a zillion different
scripts and provides a namespace, especially if you can provide good help. Fortunately, <code class="language-plaintext highlighter-rouge">OptionParse</code> <em>does</em> provide good help and can parse this.</p>

<h2 id="two-option-parsers-divide-up-the-work">Two Option Parsers Divide Up the Work</h2>

<p>The key is to use <em>two</em> <code class="language-plaintext highlighter-rouge">OptionParsers</code>:</p>

<ul>
  <li>The first parses the global options. It uses <code class="language-plaintext highlighter-rouge">order!</code> (instead of <code class="language-plaintext highlighter-rouge">parse!</code>) so that it only parses options up to the first argument it
doesn’t understand (<code class="language-plaintext highlighter-rouge">audit</code>) in our case.</li>
  <li>The second uses <code class="language-plaintext highlighter-rouge">parse!</code>, which consumes the entire rest of the command line, leaving <code class="language-plaintext highlighter-rouge">ARGV</code> with whatever wasn’t parsed.</li>
</ul>

<p>Here’s a basic sketch.  First, we’ll create the global <code class="language-plaintext highlighter-rouge">OptionParser</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"optparse"</span>

<span class="n">global_parser</span> <span class="o">=</span> <span class="no">OptionParser</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">opts</span><span class="o">|</span>
  <span class="n">opts</span><span class="p">.</span><span class="nf">banner</span> <span class="o">=</span> <span class="s2">"bin/test [global options] command [command options] [command args...]"</span>
  <span class="n">opts</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="s2">"--verbose"</span><span class="p">,</span> <span class="s2">"Show additional logging/debug information"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Next, we’ll need the second <code class="language-plaintext highlighter-rouge">OptionParser</code> for the <code class="language-plaintext highlighter-rouge">audit</code> subcommand.  You’d need one <code class="language-plaintext highlighter-rouge">OptionParser</code> for each subcommand you want to
support.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">commands</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">commands</span><span class="p">[</span><span class="s2">"audit"</span><span class="p">]</span> <span class="o">=</span> <span class="no">OptionParser</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">opts</span><span class="o">|</span>
  <span class="n">opts</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="s2">"--type TYPE"</span><span class="p">,</span> <span class="s2">"Set the type of test to audit. Omit to audit all types"</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Add more OptionParsers for more commands as needed</span>
</code></pre></div></div>

<p>Now, when the app runs, we parse the global options first using <code class="language-plaintext highlighter-rouge">order!</code>. This means that <code class="language-plaintext highlighter-rouge">ARGV[0]</code> (i.e. the first part of the command line that didn’t match anything in the global <code class="language-plaintext highlighter-rouge">OptionParsers</code>) is the command name. We use that to locate the <code class="language-plaintext highlighter-rouge">OptionParser</code> to use, then call <code class="language-plaintext highlighter-rouge">parse!</code> on that.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">global_options</span>  <span class="o">=</span> <span class="p">{}</span>
<span class="n">command_options</span> <span class="o">=</span> <span class="p">{}</span>

<span class="n">global_parser</span><span class="p">.</span><span class="nf">order!</span><span class="p">(</span><span class="ss">into: </span><span class="n">global_options</span><span class="p">)</span>
<span class="n">command</span> <span class="o">=</span> <span class="no">ARGV</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">command_parser</span> <span class="o">=</span> <span class="n">commands</span><span class="p">[</span><span class="n">command</span><span class="p">]</span>
<span class="n">command_parser</span><span class="p">.</span><span class="nf">parse!</span><span class="p">(</span><span class="ss">into: </span><span class="n">command_options</span><span class="p">)</span>

<span class="c1"># Now, based on the value of command, do whatever needs doing</span>
</code></pre></div></div>

<p>What <code class="language-plaintext highlighter-rouge">OptionParser</code> doesn’t give you is a way to manage the code to run for the e.g. <code class="language-plaintext highlighter-rouge">audit</code> command, but you have all the object-oriented
facilities of Ruby available to do that.  In <a href="https://brutrb.com">Brut</a>, the way I did this was to create a class with an <code class="language-plaintext highlighter-rouge">execute</code> method
that maps to its name and exposes it’s <code class="language-plaintext highlighter-rouge">OptionParser</code>.  Roughly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">class</span> <span class="nc">AuditCommand</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">option_parser</span>
    <span class="no">OptionParser</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">opts</span><span class="o">|</span>
      <span class="n">opts</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="s2">"--type TYPE"</span><span class="p">,</span> <span class="s2">"Set the type of test to audit. Omit to audit all types"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">command_options</span><span class="p">:,</span> <span class="n">args</span><span class="p">:)</span>
    <span class="vi">@command_options</span> <span class="o">=</span> <span class="n">command_options</span>
    <span class="vi">@args</span>            <span class="o">=</span> <span class="n">args</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">execute</span>
    <span class="c1"># whatever</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="n">commands</span><span class="p">[</span><span class="s2">"audit"</span><span class="p">]</span> <span class="o">=</span> <span class="no">AuditCommand</span>

<span class="c1"># ...</span>
<span class="n">command</span> <span class="o">=</span> <span class="no">ARGV</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">command_klass</span> <span class="o">=</span> <span class="n">commands</span><span class="p">[</span><span class="n">command</span><span class="p">]</span>
<span class="n">command_parser</span> <span class="o">=</span> <span class="n">command_klass</span><span class="p">.</span><span class="nf">option_parser</span>
<span class="n">command_parser</span><span class="p">.</span><span class="nf">parse!</span><span class="p">(</span><span class="ss">into: </span><span class="n">command_options</span><span class="p">)</span>
<span class="n">command_klass</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">command_options</span><span class="p">:,</span> <span class="ss">args: </span><span class="no">ARGV</span><span class="p">).</span><span class="nf">execute</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">OptionParser</code> also provides sophisticated type coercion via <a href="https://docs.ruby-lang.org/en/3.4/OptionParser.html#method-i-accept"><code class="language-plaintext highlighter-rouge">accept</code></a>.
<a href="https://docs.ruby-lang.org/en/3.4/OptionParser.html#class-OptionParser-label-Type+Coercion">Many built-in conversions are available</a> and you
can <a href="https://docs.ruby-lang.org/en/3.4/OptionParser.html#class-OptionParser-label-Creating+Custom+Conversions">create your own</a>.</p>

<p>This code gets more complex when you want to show help or handle errors</p>

<h2 id="showing-help-and-handling-errors">Showing Help and Handling Errors</h2>

<p><code class="language-plaintext highlighter-rouge">OptionParser</code> can produce a decent help message:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">puts</span> <span class="n">global_parser</span>
<span class="c1"># or</span>
<span class="nb">puts</span> <span class="n">command_parser</span>
</code></pre></div></div>

<p>You can do much fancier stuff if needed by using <a href="https://docs.ruby-lang.org/en/3.4/OptionParser.html#method-i-summarize"><code class="language-plaintext highlighter-rouge">summarize</code></a>.</p>

<p>For handling errors, <code class="language-plaintext highlighter-rouge">OptionParser</code> will raise an error if options were provided that aren’t valid, and you can check whatever you need and
call <code class="language-plaintext highlighter-rouge">exit</code>.</p>

<p>Now, there is a lot of “do whatever you want” here, as well as potentially verbose code.  Why not use a gem that does this for you?</p>

<h2 id="dont-use-gems-if-your-needs-are-typical">Don’t Use Gems if Your Needs are Typical</h2>

<div data-ad=""></div>

<p>Code that relies only on the standard library is stable code.  The standard library rarely breaks things and is maintained.  <code class="language-plaintext highlighter-rouge">OptionParser</code>
is designed to parse a UNIX-like command line, which is usually  what you want.</p>

<p>Even though <code class="language-plaintext highlighter-rouge">OptionParser</code> is a bit verbose, you likely aren’t writing command-line code frequently, so the verbosity—and reliance on the
standard library—is a bonus.  DSLs, at least in my experience, tend to have a half-life and can be hard to pickup, so you re-learn them over
and over, unless you are working in them every day.</p>

<p>I built <a href="https://github.com/davetron5000/gli">GLI</a> to make this easier, but in practice, it’s a somewhat wide DSL that you have to re-learn
when editing your CLI.</p>

<p><a href="https://github.com/rails/thor">Thor</a> is very popular, included with Rails, and mostly supports this kind of UI, but it is an even denser DSL that I don’t think rewards you for learning it.  And, because it does not use <code class="language-plaintext highlighter-rouge">OptionParser</code>, it’s very sensitive to command and argument ordering in a way that seasoned UNIX people would find surprising and annoying.  It also includes a ton of other code you likely don’t need, such as the ability copy files and templates around.</p>

<p><a href="https://github.com/ruby/rake">Rake</a> <em>is</em> part of the standard library, but the CLIs it produces are not very ergonomic. You must use a
sequence of square brackets and quotes to pass arguments, and there is no facility for options like <code class="language-plaintext highlighter-rouge">--verbose</code>.  Rake is designed as a
dependency manager, e.g. build my <code class="language-plaintext highlighter-rouge">favicon.ico</code> whenever my <code class="language-plaintext highlighter-rouge">favicon.png</code> changes. It’s not a general-purpose way to make command line apps.</p>

<p>So, embrace the standard library, and embrace <code class="language-plaintext highlighter-rouge">OptionParser</code>!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Confirmation Dialog with BrutRB, Web Components, and no JS]]></title>
    <link href="https://naildrivin5.com/blog/2025/08/18/confirmation-dialog-with-brutrb-web-components-and-no-js.html"/>
    <updated>2025-08-18T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/08/18/confirmation-dialog-with-brutrb-web-components-and-no-js.html</id>
    <content type="html"><![CDATA[<p>I created <a href="https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj">a short (8 minute) screencast</a> on adding a confirmation dialog to form submissions using <a href="https://brutrb.com">BrutRB</a>’s bundled Web Components. You don’t have to write any JavaScript, and you can completely control the look and feel with CSS.</p>

<iframe title="Add A Confirmation Dialog in Brut with Zero JS in like 8 Minutes" width="560" height="315" src="https://video.hardlimit.com/videos/embed/4y8Pjd8VVPDK372mozCUdj" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>

<p>There’s also a <a href="https://brutrb.com/tutorials/02-dialog.html">tutorial</a> that does the same thing or, if you are super pressed for time:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;form&gt;</span>
  <span class="nt">&lt;brut-confirm-submit</span> <span class="na">message=</span><span class="s">"Are you sure?"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;button&gt;</span>Save<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;/brut-confirm-submit&gt;</span>
<span class="nt">&lt;/form&gt;</span>

<span class="nt">&lt;brut-confirmation-dialog&gt;</span>
  <span class="nt">&lt;dialog&gt;</span>
    <span class="nt">&lt;h1&gt;&lt;/h1&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">value=</span><span class="s">"ok"</span><span class="nt">&gt;&lt;/button&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">value=</span><span class="s">"cancel"</span><span class="nt">&gt;</span>Nevermind<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;/dialog&gt;</span>
<span class="nt">&lt;/brut-confirmation-dialog&gt;</span>
</code></pre></div></div>

<p>Progressive enhancement, and no magic attributes on existing elements.</p>

<ul>
  <li><a href="https://brutrb.com/brut-js/api/ConfirmSubmit.html"><code class="language-plaintext highlighter-rouge">&lt;brut-confirm-submit&gt;</code> docs</a></li>
  <li><a href="https://brutrb.com/brut-js/api/ConfirmationDialog.html"><code class="language-plaintext highlighter-rouge">&lt;brut-confirmation-dialog&gt;</code> docs</a></li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Please Create Debuggable Systems]]></title>
    <link href="https://naildrivin5.com/blog/2025/08/06/please-create-debuggable-systems.html"/>
    <updated>2025-08-06T14:55:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/08/06/please-create-debuggable-systems.html</id>
    <content type="html"><![CDATA[<p>When a system isn’t working, it’s far easier to debug the problem when that system produces good error messages <em>as well as useful diagnostics</em>. Silent failures are sadly the norm, because they are just easier to implement.  Systems based on conventions or automatic configuration exacerbate this problem, as they tend to just do nothing and produce no error message. Let’s see how to fix this.</p>

<!-- more -->

<p>Rails popularized “convention over configuration”, but it often fails to help when conventions aren’t aligned, often silently failing with no help for debugging. This cultural norm has proliferated to many Ruby tools, like Shopify’s ruby-lsp, and pretty much all of Apple’s software design.</p>

<ul>
  <li>I asked my editor to jump to a definition and the LSP didn’t do it and there is no error message.</li>
  <li>I took a picture on my phone, it’s connected to WiFi, as is my computer, and it’s not synced to my photos. There is no “sync” button, nor
any sort of logging telling me if it tried to sync and failed or didn’t try and why not.</li>
  <li>I’m creating my dev and test databases and <a href="https://stackoverflow.com/questions/50720730/rails-env-development-rake-dbcreate-is-not-creating-development-database">it doesn’t create my dev database, but creates my test database twice</a>. (I hope this poor guy figured it out…it’s been seven years!)</li>
</ul>

<p>We all experience these failures where we get an error message that’s not helpful and then no real way to get more information about the problem.</p>

<p>Creating a debuggable system is critical for managing software, especially now that more and more code is not written by a real person.  To create such a system, it must provide two capabilities:</p>

<ul>
  <li>Helpful and descriptive error messages</li>
  <li>The ability to ask the system for much more detailed information</li>
</ul>

<p><em>Both</em> of these capabilities must be pre-built into the system. They cannot be provided only in some interactive debugging session or only in a development environment.  You want these capabilities in your <em>production</em> system.</p>

<h2 id="write-helpful-and-descriptive-error-messages">Write Helpful and Descriptive Error Messages</h2>

<p>There is always a tension between an error message that is so full of information as to be useless and one so vacant that it, too, is useless.  Designers never want users to see error messages. The security team never wants to allow error messages to provide hackers with information.  And programmers often write errors in their own language, which no one else understands.</p>

<p>Ideally, each error message the system produces is both <em>unique</em> and is written in a way you can <em>reference</em> more detailed information about what to do.</p>

<p>Consider what happens when using NeoVim and Shopify’s ruby-lsp is asked to go to the definition of a Ruby class and, for whatever reason, it can’t:</p>

<blockquote>
  <p>No Location Found</p>
</blockquote>

<p>This absolutely sucks:</p>

<ul>
  <li>It doesn’t explain what went wrong</li>
  <li>It doesn’t provide any pointers for further investigation</li>
  <li>It’s not clear what is producing this message: ruby-lsp, the Neovim plugin, or Neovim itself</li>
  <li>It doesn’t even say what operation it was trying to perform!</li>
</ul>

<p>Here are some better options:</p>

<ul>
  <li>“Could not find definition of ‘FooComponent’”</li>
  <li>“Could not find definition of ‘FooComponent’, ruby-lsp returned empty array”</li>
  <li>“Could not find definition of ‘FooComponent’, restart ruby-lsp server with --debug to debug”</li>
  <li>“Could not find definition of ‘FooComponent’, see NeoVim’s log at ~/cache/logs/neovim.log for details”</li>
  <li>“Could not find definition of ‘FooComponent’, searched 1,234 defined classes from 564 folders”</li>
</ul>

<p>These messages each have attributes of a useful error:</p>

<ul>
  <li>The operation that caused the issue (“Could not find definition”)</li>
  <li>The specific inputs to that operation (<code class="language-plaintext highlighter-rouge">FooComponent</code>)</li>
  <li>Observed behavior of dependent systems (“ruby-lsp returned empty array”)</li>
  <li>Options to get more information (“restart ruby-lsp…” and “see NeoVim’s log”)</li>
  <li>Metadata about the request (“searched 1,234 classes…”)</li>
</ul>

<p>These can all help you try to figure out the problem.  Even if you can’t provide <em>all</em> diagnostics, you should always consider including in your error message:</p>

<ul>
  <li>What operation you tried to perform</li>
  <li>What result you got (<em>summarized</em>, not <em>analyzed</em>)</li>
  <li>What systems are involved:
    <ul>
      <li>attach your system’s name to messages you create</li>
      <li>attach the subsystem’s name to message you receive and pass along</li>
    </ul>
  </li>
</ul>

<p>Aside from this, creating a way to get more information is also extremely helpful.</p>

<h2 id="create-a-debug-or-diagnostic-mode">Create a Debug or Diagnostic Mode</h2>

<p>The volume of information required to fully debug a problem can be quite large.  It can be costly to produce and difficult to analyze. This can be a worthwhile tradeoff if something isn’t working and you don’t have any other options.  This means your system needds a <em>debug</em> or <em>diagnostic</em> mode.</p>

<p>A diagnostic mode should produce the inputs and outputs as well as intermediate values relevant to producing the outputs. Let’s imagine how finding the definition of a class in Ruby works in the ruby-lsp.</p>

<p>At a high level, the inputs are the symbol being looked-up and the outputs are a list of files and locations where that sybmol is defined.  LSP is more low level, however, as it will actually accept as input a line/column of a file where a symbol is referenced, and expect a list similar locations in return.</p>

<p>This means there are a few ways this can fail:</p>

<ul>
  <li>The file doesn’t exist</li>
  <li>There is no symbol at the location of the file</li>
  <li>The symbol’s definition can’t be found</li>
  <li>The symbol was found, but the file isn’t accessible to the caller</li>
</ul>

<p>The most common case is a symbol being correctly identified in the file, but not found. This is where intermediate values can help.</p>

<div data-ad=""></div>

<p>Presumably, a bunch of files were searched for the symbol that can’t be found.  Knowing those files would be useful!  But, presumably, those files were found by searching some list of folders for Ruby files. <em>That</em> list of folders would be nice to know as well!</p>

<p>This is obviously a massive amount of information for a single-line error message, however the information could be stored.  The entire operation could be given a unique ID, which is then included in the error message <em>and</em> included in a log file that produces all of this information.  Given the volume of information, you’d probably want the LSP to only produce this when asked, either with a per-request flag or a flag at startup (e.g. <code class="language-plaintext highlighter-rouge">--diagnostic</code> or <code class="language-plaintext highlighter-rouge">--debug</code>).</p>

<p>Making all this avaiable requires extra effort on the part of the programmer. Sometimes, it could be quite a bit of effort!  For example, there may not be an easy way to generate a unique ID and ensure it’s available to everywhere in the code with access to the diagnostic information.  And, of course, all this diagnostic code can itself fail, creating <em>more</em> intermediate values needed to diagnose problems.  We’ve probably all written something like this before:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">begin</span>
  <span class="n">some_operation</span>
<span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">ex</span>
  <span class="k">begin</span>
    <span class="n">report_error</span><span class="p">(</span><span class="n">ex</span><span class="p">)</span>
  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">ex2</span>
    <span class="vg">$stderr</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"Encountered </span><span class="si">#{</span><span class="n">ex2</span><span class="si">}</span><span class="s2"> while reporting error </span><span class="si">#{</span><span class="n">ex</span><span class="si">}</span><span class="s2"> - something is seriously wrong"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In addition to just culling the data, you have to log it, or not.  Ruby’s <code class="language-plaintext highlighter-rouge">Logger</code> provides a decent solution using blocks:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">logger</span><span class="p">.</span><span class="nf">debug</span> <span class="p">{</span>
  <span class="c1"># expensive calculation</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The block only executes if the logger is set to debug level.  Of course, you may not like the all-or-nothing approach.  The venerable log4j used in almost every Java app allows you to configure the log level <em>per class</em> and even dynamically change it at runtime. You can do this in Ruby with <a href="https://logger.rocketjob.io/">SemanticLogger</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"semantic_logger"</span>

<span class="no">SemanticLogger</span><span class="p">.</span><span class="nf">appenders</span> <span class="o">&lt;&lt;</span> <span class="no">SemanticLogger</span><span class="o">::</span><span class="no">Appender</span><span class="o">::</span><span class="no">IO</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">STDOUT</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">Foo</span>
  <span class="kp">include</span> <span class="no">SemanticLogger</span><span class="o">::</span><span class="no">Loggable</span>
  <span class="k">def</span> <span class="nf">doit</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"FOO!"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Bar</span>
  <span class="kp">include</span> <span class="no">SemanticLogger</span><span class="o">::</span><span class="no">Loggable</span>
  <span class="k">def</span> <span class="nf">doit</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"BAR!"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="n">foo</span> <span class="o">=</span> <span class="no">Foo</span><span class="p">.</span><span class="nf">new</span>
<span class="n">bar</span> <span class="o">=</span> <span class="no">Bar</span><span class="p">.</span><span class="nf">new</span>

<span class="n">foo</span><span class="p">.</span><span class="nf">debug</span> <span class="c1"># =&gt; nothing</span>
<span class="n">bar</span><span class="p">.</span><span class="nf">debug</span> <span class="c1"># =&gt; nothing</span>
<span class="no">Foo</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">level</span> <span class="o">=</span> <span class="ss">:debug</span>
<span class="n">foo</span><span class="p">.</span><span class="nf">debug</span> <span class="c1"># =&gt; 2025-08-06 18:35:59.138433 D [2082290:54184] Foo -- FOO!</span>
<span class="n">bar</span><span class="p">.</span><span class="nf">debug</span> <span class="c1"># =&gt; nothing</span>
</code></pre></div></div>

<p>While SemanticLogger only allows runtime changes of the global log level, you could likely write something yourself to change it per class.</p>

<h2 id="please-create-debuggable-systems">Please Create Debuggable Systems</h2>

<p>While you could consider everything above as a part of <em>observability</em>, to me this is distinct.  Debuggable systems don’t have to have OTel or other fancy stuff—they can write logs or write to standard output.  Debuggable systems show useful error messages that explain (or lead to an explanation of) the problem, and can be configured to produce diagnostic information that tells you what they are doing and why.</p>

<p>You can get started by creating better error messages in your tests!  Instead of writing <code class="language-plaintext highlighter-rouge">assert list.include?("value")</code>, try this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">assert</span> <span class="n">list</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"value"</span><span class="p">),</span>
       <span class="s2">"Checking list '</span><span class="si">#{</span><span class="n">list</span><span class="si">}</span><span class="s2">' for 'value'"</span>
</code></pre></div></div>

<p>Try to make sure when any test fails, the messaging you get is everything you need to understand the problem.  Then proliferate this to the rest of your system.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Build a blog in 15ish Minutes with BrutRB]]></title>
    <link href="https://naildrivin5.com/blog/2025/07/23/build-a-blog-in-15ish-minutes-with-brutrb.html"/>
    <updated>2025-07-23T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/07/23/build-a-blog-in-15ish-minutes-with-brutrb.html</id>
    <content type="html"><![CDATA[<iframe title="Make a Blog App in 15(ish) minutes With BrutRB - A New Ruby Web Framework" width="560" height="315" src="https://video.hardlimit.com/videos/embed/ae7EMhwjDq9kSH5dqQ9swV" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>

<p>This is a whirlwind tour of the basics of <a href="https://brutrb.com">Brut</a>, where I build a blog from scratch in a bit over 15 minutes.  The app is fully tested and even has basic observability as a bonus.  The only software you need to install is Docker.</p>

<!-- more -->

<ul>
  <li><a href="https://youtu.be/hQSSy3AVB28">Watch on YouTube</a>, if PeerTube isn’t working for you for some reason</li>
  <li><a href="https://github.com/thirdtank/blog-demo">Check out the source code</a>, if you don’t want to watch a video</li>
</ul>

<div data-ad=""></div>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Brut: A New Web Framework for Ruby]]></title>
    <link href="https://naildrivin5.com/blog/2025/07/08/brut-a-new-web-framework-for-ruby.html"/>
    <updated>2025-07-08T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/07/08/brut-a-new-web-framework-for-ruby.html</id>
    <content type="html"><![CDATA[<div style="display: flex; align-items: center; gap: 0.5rem;">
<figure style="float: left">
  <a href="/images/BrutLogoTall.png">
    <img src="/images/BrutLogoTall.png" alt="A brown rectangle with a large capital 'B'. Underneathe is 'brut'" />
  </a>
</figure>
<p class="p">
<a href="https://brutrb.com">Brut</a> aims to be a simple, yet fully-featured web framework for Ruby. It's different than other Ruby web frameworks.  Brut has no controllers, verbs, or resources. You build pages, forms, and single-action handlers. You write HTML, which is generated on the server. You can write all the JavaScript and CSS you want.
</p>
</div>
<!-- more -->

<p>Here’s a web page that tells you what time it is:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TimePage</span> <span class="o">&lt;</span> <span class="no">AppPage</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">clock</span><span class="p">:)</span>
    <span class="vi">@clock</span> <span class="o">=</span> <span class="n">clock</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">page_template</span>
    <span class="n">header</span> <span class="k">do</span>
      <span class="n">h1</span> <span class="p">{</span> <span class="s2">"Welcome to the Time Page!"</span> <span class="p">}</span>
      <span class="no">TimeTag</span><span class="p">(</span><span class="ss">timestamp: </span><span class="vi">@clock</span><span class="p">.</span><span class="nf">now</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="k">end</span>
</code></pre></div></div>

<p>Brut is built around low-abstraction and low-ceremony, but is not low-level like Sinatra.  It’s a web framework. Your Brut apps have builtin OpenTelemetry-based instrumentation, a <a href="https://sequel.jeremyevans.net/">Sequel</a>-powered data access layer, and developer automation based on <code class="language-plaintext highlighter-rouge">OptionParser</code>-powered command-line apps.</p>

<p><a href="https://brutrb.com">Brut</a> can be installed right now, and you can build and run an
app in minutes. You don’t even have to install Ruby.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; docker run \
        -v "$PWD":"$PWD" \
        -w "$PWD" \
        -it \
        thirdtank/mkbrut \
        mkbrut my-new-app
&gt; cd my-new-app
&gt; dx/build &amp;&amp; dx/start
&gt; dx/exec bin/setup
&gt; dx/exec bin/dev
# =&gt; localhost:6502 is waiting
</code></pre></div></div>

<p>There’s a full-fledged example app called <a href="https://github.com/thirdtank/adrs.cloud">ADRs.cloud</a> you can run right now and see how it works.</p>

<h2 id="what-you-get">What You Get</h2>

<p><a href="https://brutrb.com">Brut has extensive documentation</a>, however these are some highlights:</p>

<h3 id="bruts-core-design-is-around-classes-that-are-instantiated-into-objects-upon-which-methods-are-called">Brut’s core design is around classes that are instantiated into objects, upon which methods are called.</h3>

<ul>
  <li>No excessive <code class="language-plaintext highlighter-rouge">include</code> calls to create a massive blob of functions.</li>
  <li>No Hashes of Whatever. Your session, flash, and form parameters are all actual classes and defined data types.</li>
  <li>Minimal reliance of dynamically-defined methods or <code class="language-plaintext highlighter-rouge">method_missing</code>.  Almost every
method <a href="https://brutrb.com/api/index.html">has documentation</a>.</li>
</ul>

<h3 id="brut-leverages-the-modern-web-platform">Brut leverages the modern Web Platform.</h3>

<ul>
  <li>Client-side and server-side form validation is unified into one user experience.</li>
  <li><a href="https://brutrb.com/brut-js/api/index.html">BrutJS</a> is an ever-evolving <em>library</em>
of autonomous custom elements AKA web components to progressively enhance your
HTML.</li>
  <li>With <a href="https://esbuild.github.io/">esbuild</a>, you can write regular CSS and have
it instantly packaged, minified, and hashed. No PostCSS, No SASS.</li>
</ul>

<h3 id="brut-sets-up-good-practices-by-default">Brut sets up good practices by default.</h3>

<ul>
  <li>Your app will have a reasonable content security policy.</li>
  <li>Your database columns aren’t null by default.</li>
  <li>Your foreign keys will a) exist, b) be indexed, and c) not be nullable by
default.</li>
  <li>Time, available through Brut’s <code class="language-plaintext highlighter-rouge">Clock</code>, is always timezone-aware.</li>
  <li>Localization is there and is as easy as we can make it. We hope to make it easier.</li>
</ul>

<h3 id="brut-uses-awesome-ruby-gems">Brut uses awesome Ruby gems</h3>

<ul>
  <li>RSpec is how you write your tests. Brut includes custom matchers to make it
easier to focus on what your code should do.</li>
  <li>Faker and FactoryBot will set up your test and dev data</li>
  <li>Phlex generates your HTML. No, we won’t be supporting HAML.</li>
</ul>

<h3 id="brut-doesnt-recreate-configuration-with-yaml">Brut doesn’t recreate configuration with YAML.</h3>

<ul>
  <li>I18n uses the <a href="https://github.com/ruby-i18n/i18n">i18n gem</a>, with translations <em>in a Ruby Hash</em>. No YAML.</li>
  <li>Dynamic configuration is in the environment, managed in dev and test by <a href="https://github.com/bkeepers/dotenv">the
dotenv gem</a>. No YAML.</li>
  <li>OK, the dev environment’s <code class="language-plaintext highlighter-rouge">docker-compose.dx.yml</code> is YAML. But that’s <strong>it</strong>.</li>
  <li>YAML, not even <del>once</del>twice.</li>
</ul>

<h3 id="brut-doesnt-create-abstractions-where-none-were-needed">Brut doesn’t create abstractions where none were needed.</h3>

<ul>
  <li><em>Is this the index action of the widgets resource or the show action of the widget-list resource?</em> is a question you will never have to ask yourself or your team. The widgets page is called <code class="language-plaintext highlighter-rouge">WidgetsPage</code> and available at <code class="language-plaintext highlighter-rouge">/widgets</code>.</li>
  <li><em>My <code class="language-plaintext highlighter-rouge">Widgets</code> class accesses the <code class="language-plaintext highlighter-rouge">WIDGETS</code> table, but it also has all the domain logic of a widget!</em> No it doesn’t. <code class="language-plaintext highlighter-rouge">DB::Widget</code> gets your data.  You can make <code class="language-plaintext highlighter-rouge">Widget</code> do whatever you want. Heck, make a <code class="language-plaintext highlighter-rouge">WidgetService</code> for all we care!</li>
  <li><em>What if our HTML had controllers but they were not the same as the controllers in our back-end?</em> There aren’t any controllers. You don’t want them, you don’t have to make them.</li>
  <li><em>What about monads or algebraic data types or currying or maybe having everything
be a <code class="language-plaintext highlighter-rouge">Proc</code> because <code class="language-plaintext highlighter-rouge">call</code>?!</em> You don’t have to understand any part of that question.  But if you want your business logic to use functors, go for it. We won’t stop you.</li>
</ul>

<div data-ad=""></div>

<h2 id="why">WHY?!?!?!</h2>

<p>I know, we can vibe away all the boilerplate required for Rails apps.  But how much fun is that?  How much do you enjoy setting up RSpec, again, in your new Rails app? How tired are you of changing the “front end solution” every few years?  And aren’t you just <em>tired</em> of debating where your business logic goes or if it’s OK to use HTTP <code class="language-plaintext highlighter-rouge">DELETE</code> (tunneled over a <code class="language-plaintext highlighter-rouge">_method</code> param in a <code class="language-plaintext highlighter-rouge">POST</code>) to archive a widget?</p>

<p>I know I am.</p>

<p>I want to have fun building web apps, which means I want write Ruby, use HTML, and leverage the browser. Do you know how awesome browsers are now?  Also, Ruby 3.4 is pretty great as well. I’d like to use it.</p>

<p>What I <em>don’t</em> want is endless flexibility, constant architectural decision-making, or pointless debates about stuff that doesn’t matter.</p>

<p>I just want to have fun building web apps.</p>

<h2 id="next-steps">Next Steps</h2>

<p>I will continue working <a href="https://brutrb.com/roadmap.html">toward a 1.0 of Brut</a>, building web apps and enjoying the process.  I hope you will, too!</p>

<figure style="float: left">
  <a href="/images/BrutLogoStop.png">
    <img src="/images/BrutLogoStop.png" alt="A brown rectangle with 'BrutRB' in large letters centered.  It is in the style of the Washington, DC metro. Below BrutRB are four colored dots that each have a label. They are in the style of a metro line. The red dot has 'RB' in it and is labeled 'Ruby'. The orange dot has 'WP' in it and is labeled HTML/CSS/JS. The blue dot has 'PL' in it, and is labeled 'Phlex'. The green dot has 'RS' in it and is labeled 'Rspec'" />
  </a>
</figure>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Neovim and LSP Servers Working with Docker-based Development]]></title>
    <link href="https://naildrivin5.com/blog/2025/06/12/neovim-and-lsp-servers-working-with-docker-based-development.html"/>
    <updated>2025-06-12T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/06/12/neovim-and-lsp-servers-working-with-docker-based-development.html</id>
    <content type="html"><![CDATA[<p>Working on an update to <a href="https://devbox.computer">my Docker-based Dev Environment Book</a>, I realized it would be important to show how to get an LSP server worker inside Docker.  And I have! And it’s not that easy, but wasn’t that hard, either.  It hits a lot of my limits of Neovim knowledge, but hopefully fellow Vim users will find this helpful.</p>

<!-- more -->

<h2 id="the-problem">The Problem</h2>

<p>Microsoft created the Language Server Protocol (LSP), and so it’s baked into VSCode pretty well.  If you require a more sophisticated and powerful editing experience, however, you are using Vim and it turns out, Neovim (a Vim fork) can interact with an LSP via the lsp-config plugin.</p>

<p>Getting this all to work requires solving several problems:</p>

<ol>
  <li>Why do this at all?</li>
  <li>How Can Neovim talk to an LSP server?</li>
  <li>Which LSP Servers do I need and how do I set them up?</li>
  <li>But how  do I do that inside a Docker development container?</li>
  <li>What can I do with this power?</li>
</ol>

<h2 id="what-does-an-lsp-server-do">What Does an LSP Server Do?</h2>

<p>I had never previously pursued setting up an LSP server because I never felt the need for it.  To be honest, I couldn’t even tell you what it did or what it was for.  I’ve used vi (and Vim and Neovim) for my entire career never using an IDE beyond poking at a few and deciding they were not for me.</p>

<p>The cornerstone of an LSP server is that it can understand your code at a semantic level, and not just as a series of strings. It can know precisely where a class named <code class="language-plaintext highlighter-rouge">HTMLGenerator</code> is defined and, whenever you cursor onto that class name, jump to that location, even if it doesn’t conform to Ruby’s conventions.</p>

<p>Traditional Vim plugins don’t do this. They’d mangle <code class="language-plaintext highlighter-rouge">HTMLGenerator</code> to come up with <code class="language-plaintext highlighter-rouge">html_generator.rb</code> or <code class="language-plaintext highlighter-rouge">h_t_m_l_generator.rb</code> and then find that filename, hoping that’s where the class was defined.</p>

<p>With this core feature, an LSP server can allow features not possible in Neovim (or at least not possible beyond string manipulation):</p>

<ul>
  <li>Completion based on language and project.  When you enter a variable name and <code class="language-plaintext highlighter-rouge">.</code>, it will show a list
of possible methods to call.  This works in non-dot-calls-a-method languages, too.</li>
  <li>Jump to the definition of a symbol, regardless of the filename where it’s defined.</li>
  <li>List all symbols in the current file to e.g. quickly jump to a method or variable.</li>
  <li>Pop up a window inside Neovim to show the documentation for a symbol, based on the documentation in the
source file you are using (i.e. and not fetching it from a website).</li>
  <li>See where the given symbol is being referenced.</li>
  <li>Perform a project-wide rename of a symbol.</li>
  <li>Show the method signature of a method you are calling</li>
  <li>Show the type hierarchy of the class you are in.</li>
  <li>Syntax highlighting based on language constructs and not regular expressions. For example, an LSP-based
syntax highlight could show local variables and method calls differently, even though in Ruby they look the same.</li>
  <li>
    <p>“Inlay hints” which add context to the code where something implicit is going on.  The simplest example
for Ruby is the somewhat new Hash syntax:</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foo</span> <span class="o">=</span> <span class="s2">"bar"</span>
<span class="n">blah</span> <span class="o">=</span> <span class="mi">42</span>

<span class="n">doit</span><span class="p">({</span> <span class="n">foo</span><span class="p">:,</span> <span class="ss">blah: </span><span class="p">})</span>
</code></pre></div>    </div>

    <p>Since the keys are the same as local symbols, this is the same as <code class="language-plaintext highlighter-rouge">{ foo: foo, blah: blah }</code>.  With inlay hints, the editor would show that:</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foo</span> <span class="o">=</span> <span class="s2">"bar"</span>
<span class="n">blah</span> <span class="o">=</span> <span class="mi">42</span>

<span class="n">doit</span><span class="p">({</span> <span class="ss">foo: </span><span class="n">foo</span><span class="p">,</span> <span class="ss">blah: </span><span class="n">blah</span><span class="p">})</span>
</code></pre></div>    </div>

    <p>This added information is not editable, and if your cursor is on the space after <code class="language-plaintext highlighter-rouge">foo:</code>, moving to the right skips over the “inlayed” <code class="language-plaintext highlighter-rouge">foo</code>, right to the next comma.</p>
  </li>
  <li>Realtime compiler errors and warnings.  This shows a marker in the column of a line with an error,
along with potentially red squiggles, and an ability to open a pop-up window showing the error message.</li>
</ul>

<p>There is more that can be done per language and tons of extensions.  I have had a successful programming career of many years without these features, but they do seem useful.  They’ve just never seemed worth it to give up Vim and use an IDE, which usually provides a terrible text editing experience.</p>

<h2 id="getting-an-lsp-server-to-work-with-neovim">Getting an LSP Server To Work with Neovim</h2>

<p>Getting an LSP server to work requires figuring out how install the server, then configuring Neovim to use it via the <a href="https://github.com/neovim/nvim-lspconfig">lsp-config plugin</a>.  This creates the meta-problem of how to set up that plugin, because Neovim has a lot of plugin management systems.</p>

<p>I use a system that allows me to clone plugins from a Git repo inside <code class="language-plaintext highlighter-rouge">~/.vim</code> (for Vim) and <code class="language-plaintext highlighter-rouge">~/.local/share/nvim/site</code> (for Neovim) and restart Vim and stuff works. It’s been so long I don’t know what this is called.</p>

<p>After you’ve installed <code class="language-plaintext highlighter-rouge">lsp-config</code> however you install Neovim plugins, the next issue is that most of the configuration is documented in Lua. Because I am old, all my configuration is in VimScript.  Getting some Lua configuration is a single line of code, inside <code class="language-plaintext highlighter-rouge">~/.config/nvim/init.vim</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lua require('config')
</code></pre></div></div>

<p>This assumes that <code class="language-plaintext highlighter-rouge">~/.config/nvim/lua/config.lua</code> exists, and then runs that configuration as normal.  With that in place, here’s an outline of the configuration needed to use Shopify’s Ruby LSP server and Microsoft’s CSS and Typescript LSP servers.  These aren’t complete, yet, but this gives you an idea:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">lspconfig</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s1">'lspconfig'</span><span class="p">)</span>

<span class="c1">-- Set up Shopify's LSP server. The string</span>
<span class="c1">-- "ruby_lsp" is magic and you must consult lsp-config's</span>
<span class="c1">-- documentation to figure out what string to use for</span>
<span class="c1">-- what LSP server.</span>
<span class="n">lspconfig</span><span class="p">.</span><span class="n">ruby_lsp</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="c1">-- To be filled in</span>
<span class="p">})</span>

<span class="c1">-- Set up Microsoft's CSS LSP server (again, "cssls" is magic)</span>
<span class="n">lspconfig</span><span class="p">.</span><span class="n">cssls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="c1">-- To be filled in</span>
<span class="p">})</span>

<span class="c1">-- Set up Microsoft's TypeScript/JavaScript</span>
<span class="c1">-- server, "ts_ls" being magic.</span>
<span class="n">lspconfig</span><span class="p">.</span><span class="n">ts_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="c1">-- To be filled in</span>
<span class="p">})</span>
</code></pre></div></div>

<p>With this configuration, Neovim will attempt to use these LSP servers for Ruby, CSS, TypeScript, and JavaScript files.  Without those servers installed, you will get an error each time you load a file.</p>

<h2 id="installing-and-configuring-lsp-servers">Installing and Configuring LSP Servers</h2>

<p>In  most cases, installing LSP servers can be done by installing a package with e.g. RubyGems or NPM.</p>

<ul>
  <li>Ruby: <code class="language-plaintext highlighter-rouge">gem install ruby-lsp</code> (or put in <code class="language-plaintext highlighter-rouge">Gemfile</code>)</li>
  <li>CSS: <code class="language-plaintext highlighter-rouge">npm install --save-dev vscode-langservers-extracted</code></li>
  <li>TypeScript/JavaScript: <code class="language-plaintext highlighter-rouge">npm install --save-dev typescript typescript-language-server</code> (Note: you may have <code class="language-plaintext highlighter-rouge">typescript</code> installed already if you are using it elsewhere in your project)</li>
</ul>

<p>The lsp-config plugin assumes that the servers can be run as bare commands, e.g. <code class="language-plaintext highlighter-rouge">ruby-lsp</code> or <code class="language-plaintext highlighter-rouge">typescript-language-server</code>. In most cases, these don’t work this way (e.g. you must use <code class="language-plaintext highlighter-rouge">npx</code> or <code class="language-plaintext highlighter-rouge">bundle exec</code>). When running them in Docker, they <em>definitely</em> won’t work from the perspective of Neovim running outside Docker.</p>

<p>When the LSP Server and Neovim are running on the same machine, you can get it working easily by tweaking the <code class="language-plaintext highlighter-rouge">cmd</code> configuration option:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lspconfig</span><span class="p">.</span><span class="n">ts_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'npx'</span><span class="p">,</span> <span class="s1">'typescript-language-server'</span><span class="p">,</span> <span class="s1">'--stdio'</span> <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>If we want the servers to be run inside a Docker development container, we’ll need to do a bit more tweaking of the configuration.</p>

<h2 id="configuring-lsp-servers-to-run-inside-docker">Configuring LSP Servers to Run Inside Docker</h2>

<p>Since the LSP servers will be installed inside the Docker container, but will need to be executed from your computer (AKA the host), you’ll need to tell Neovim to basically use <code class="language-plaintext highlighter-rouge">docker compose exec</code> before running the LSP server’s command.</p>

<figure>
  <a href="/images/lsp-docker-exec.png">
    <img src="/images/lsp-docker-exec.png" srcset="/images/lsp-docker-exec.png 629w,
                 /images/lsp-docker-exec-320.png 320w,
                 /images/lsp-docker-exec-500.png 500w" sizes="(max-width: 320px) 320px,
                (max-width: 500px) 500px,
                629px" alt="Diagram showing your computer and a docker container. Inside your computer is Neovim. There's an arrow from it labeled 'docker compose exec' that is connected to a box inside the docker container. The box is labeled 'ruby-lsp # e.g.' in a code font." />
  </a>
  <figcaption class="">
    <a target="_new" href="/images/lsp-docker-exec.png">Open bigger version in new window</a>
  </figcaption>
</figure>

<p>The way I set up my projects, I have a script called <code class="language-plaintext highlighter-rouge">dx/exec</code> that does just this.  <code class="language-plaintext highlighter-rouge">dx/exec bash</code> will run Bash, <code class="language-plaintext highlighter-rouge">dx/exec bin/setup</code> will run the setup script, etc.</p>

<p>The command you ultimately want to run isn’t just the LSP server command. You need to run Bash and have Bash run that command.  This is so your LSP server can access whatever environment set up you have.</p>

<p>To do this, you want Neovim to run <code class="language-plaintext highlighter-rouge">docker compose exec bash -lc «LSP Server command»</code>.  <code class="language-plaintext highlighter-rouge">-l</code> tells Bash to run it as a login shell. You need this to simulate logging in and running the LSP server, which is what is expected outside Docker.  <code class="language-plaintext highlighter-rouge">-c</code> specified the command for bash to run.</p>

<p>Given that I have <code class="language-plaintext highlighter-rouge">dx/exec</code> to wrap <code class="language-plaintext highlighter-rouge">docker compose exec</code>, here is what my configuration looks like:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">lspconfig</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s1">'lspconfig'</span><span class="p">)</span>

<span class="n">lspconfig</span><span class="p">.</span><span class="n">ruby_lsp</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span> <span class="s1">'bash'</span><span class="p">,</span> <span class="s1">'-lc'</span><span class="p">,</span> <span class="s1">'ruby-lsp'</span><span class="p">,</span> <span class="p">},</span>
  <span class="c1">-- More to come</span>
<span class="p">})</span>

<span class="n">lspconfig</span><span class="p">.</span><span class="n">cssls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span>
          <span class="s1">'bash'</span><span class="p">,</span>
          <span class="s1">'-lc'</span><span class="p">,</span>
          <span class="s1">'npx vscode-css-language-server --stdio'</span> <span class="p">},</span>
  <span class="c1">-- More to come</span>
<span class="p">})</span>

<span class="n">lspconfig</span><span class="p">.</span><span class="n">ts_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
  <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span>
          <span class="s1">'bash'</span><span class="p">,</span>
          <span class="s1">'-lc'</span><span class="p">,</span>
          <span class="s1">'npx typescript-language-server --stdio'</span> <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>Note that this is somewhat meta.  <code class="language-plaintext highlighter-rouge">cmd</code> expects a list of command line tokens.  Normally, <code class="language-plaintext highlighter-rouge">npx typescript-language-server --stdio</code> would be considered three tokens.  In this case, it’s a single token being passed to bash, so you do not break it up like you would if running everything locally.</p>

<p>Once they are running, you’ll need to make further tweaks to get them to talk to Neovim in a way that will work.</p>

<h2 id="making-lsp-servers-inside-docker-work-with-neovim">Making LSP Servers Inside Docker Work with Neovim</h2>

<div data-ad=""></div>

<p>The “protocol” in LSP is based around paths to files and locations in those files. This means that both Neovim and the LSP server must view the same files as having the same path.  When they both run on the same  computer, this is how it is.</p>

<p>In a Docker-based dev environment, the container is typically configured to mount your computer’s files
inside the container, so that changes on your computer are seen inside the Docker container and
vice-versa.  If the filenames and paths aren’t identical, the LSP servers won’t work.</p>

<p>Consider a setup where <code class="language-plaintext highlighter-rouge">/home/davec/Projects/my-awesome-app</code> is the path to the code is on my computer, but I’ve mounted it inside my development container at <code class="language-plaintext highlighter-rouge">/home/appuser/app</code>:</p>

<figure>
  <a href="/images/lsp-docker-mounts.png">
    <img src="/images/lsp-docker-mounts.png" srcset="/images/lsp-docker-mounts.png 629w,
                 /images/lsp-docker-mounts-320.png 320w,
                 /images/lsp-docker-mounts-500.png 500w" sizes="(max-width: 320px) 320px,
                (max-width: 500px) 500px,
                629px" alt="A diagram showing your computer and a Docker container. Inside your computer is a folder labeled /home/davec/Projects/my-awesome-app. It has a bi-directional line to a folder inside the Docker container labeled /home/appuser/app." />
  </a>
  <figcaption class="">
    <a target="_new" href="/images/lsp-docker-mounts.png">Open bigger version in new window</a>
  </figcaption>
</figure>

<p>When the LSP Server tells NeoVim that a symbol is defined in <code class="language-plaintext highlighter-rouge">/home/appuser/app/foo.br</code>, Neovim won’t find it, because that file is really in <code class="language-plaintext highlighter-rouge">/home/davec/Projects/my-awesome-app/foo.rb</code>.</p>

<h3 id="ensuring-the-lsp-server-and-neovim-use-the-same-paths">Ensuring the LSP Server and NeoVim Use the Same Paths</h3>

<p>What you want is for them to be mounted in the same location.</p>

<figure>
  <a href="/images/lsp-docker-mounts-same.png">
    <img src="/images/lsp-docker-mounts-same.png" srcset="/images/lsp-docker-mounts-same.png 629w,
                 /images/lsp-docker-mounts-same-320.png 320w,
                 /images/lsp-docker-mounts-same-500.png 500w" sizes="(max-width: 320px) 320px,
                (max-width: 500px) 500px,
                629px" alt="A diagram showing your computer and a Docker container. Inside your computer is a folder labeled /home/davec/Projects/my-awesome-app. It has a bi-directional line to a folder inside the Docker container also labeled /home/davec/Projects/my-awesome-app." />
  </a>
  <figcaption class="">
    <a target="_new" href="/images/lsp-docker-mounts-same.png">Open bigger version in new window</a>
  </figcaption>
</figure>

<p>In my case, I use Docker Compose to configure the volume mapping, so here’s what it should look like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">«image name»</span>
    <span class="na">init</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">bind</span>
        <span class="na">source</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/home/davec/Projects/my-awesome-project"</span>
        <span class="na">target</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/home/davec/Projects/my-awesome-project"</span>
        <span class="na">consistency</span><span class="pi">:</span> <span class="s2">"</span><span class="s">consistent"</span>
    <span class="na">working_dir</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/home/davec/Projects/my-awesome-project"</span>
</code></pre></div></div>

<p>Note that because <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> can interpret environment variables, you can replace the hard-coded paths with <code class="language-plaintext highlighter-rouge">${PWD}</code> so it can work for everyone on your team (assuming you run <code class="language-plaintext highlighter-rouge">docker compose up</code> from <code class="language-plaintext highlighter-rouge">/home/davec/Projects/my-awesome-project</code>).</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">«image name»</span>
    <span class="na">init</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">bind</span>
        <span class="na">source</span><span class="pi">:</span> <span class="s">${PWD}</span>
        <span class="na">target</span><span class="pi">:</span> <span class="s">${PWD}</span>
        <span class="na">consistency</span><span class="pi">:</span> <span class="s2">"</span><span class="s">consistent"</span>
    <span class="na">working_dir</span><span class="pi">:</span> <span class="s">${PWD}</span>
</code></pre></div></div>

<p>This works great…for files in your project.  For files outside your project, it depends.</p>

<h3 id="files-outside-your-project-must-have-the-same-paths-too">Files Outside Your Project Must Have the Same Paths, Too</h3>

<p>For JavaScript or TypeScript third party modules, those are presumably stored in <code class="language-plaintext highlighter-rouge">node_modules</code>, so the paths will be the same for the LSP server inside the Docker container and to Neovim.  Ruby gems, however, will not be, at least by default.</p>

<p>The reason this is important is that you may want to jump to the definition of a class that exists in a gem, or view its method signature or see its documentation.  To do this, because the LSP server uses file paths, the paths to e.g. <code class="language-plaintext highlighter-rouge">HTTParty</code>’s definition must be the same inside the Docker container as they are to Neovim running on your computer.</p>

<p>The solution is to set <code class="language-plaintext highlighter-rouge">GEM_HOME</code> so that Ruby will install gems inside your project root, just as NPM does for JavaScript modules.</p>

<p>This configuration must be done in <strong>both</strong> <code class="language-plaintext highlighter-rouge">~/.profile</code> and <code class="language-plaintext highlighter-rouge">~/.bashrc</code> inside the Docker container, since there is not a normal invocation of Bash that would source both files. I have this as <code class="language-plaintext highlighter-rouge">bash_customizations</code> which is sourced in both files. <code class="language-plaintext highlighter-rouge">bash_customizations</code> looks like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export GEM_HOME=/home/davec/Projects/my-awesome-app/local-gems/gem-home
export PATH=${PATH}:${GEM_HOME}/bin
</code></pre></div></div>

<p>You’ll want to ignore <code class="language-plaintext highlighter-rouge">local-gems</code> in your version control system, the same as you would <code class="language-plaintext highlighter-rouge">node_modules</code>.</p>

<p><em>Now</em>, re-install your gems and jumping to definitions will work great.</p>

<p>This leads to an obvious question: how <strong>do you</strong> jump to a definition?!</p>

<h2 id="configuring-neovim-to-use-lsp-commands">Configuring Neovim to use LSP Commands</h2>

<p>lsp-config does set up a few shortcuts, which you can read <a href="https://neovim.io/doc/user/lsp.html#_defaults">in their docs</a>. This isn’t sufficient to take advantage of all the features.  You also can’t access all the features simply by creating keymappings. Some features must be explicitly enabled or started up.</p>

<p>Of course, you don’t want to set any of this up if you aren’t using an LSP server. This can be addressed by putting all setup code in a Lua function that is called when the LSP “attaches”.  This function will be called <code class="language-plaintext highlighter-rouge">on_attach</code> and we’ll see it in a minute (note that I’m adding some configuration for Ruby LSP to make inlay hints work, as I couldn’t find a better place to do that in this blog post :).</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">local</span> <span class="n">lspconfig</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s1">'lspconfig'</span><span class="p">)</span>

  <span class="n">lspconfig</span><span class="p">.</span><span class="n">ruby_lsp</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span> <span class="s1">'bash'</span><span class="p">,</span> <span class="s1">'-lc'</span><span class="p">,</span> <span class="s1">'ruby-lsp'</span><span class="p">,</span> <span class="p">},</span>
<span class="err">→</span>   <span class="n">on_attach</span> <span class="o">=</span> <span class="n">on_attach</span><span class="p">,</span>
<span class="err">→</span>   <span class="n">init_options</span> <span class="o">=</span> <span class="p">{</span>
<span class="err">→</span>     <span class="n">featuresConfiguration</span> <span class="o">=</span> <span class="p">{</span>
<span class="err">→</span>       <span class="n">inlayHint</span> <span class="o">=</span> <span class="p">{</span>
<span class="err">→</span>         <span class="n">enableAll</span> <span class="o">=</span> <span class="kc">true</span>
<span class="err">→</span>       <span class="p">}</span>
<span class="err">→</span>     <span class="p">},</span>
    <span class="p">}</span>
  <span class="p">})</span>

  <span class="n">lspconfig</span><span class="p">.</span><span class="n">cssls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span> <span class="s1">'bash'</span><span class="p">,</span> <span class="s1">'-lc'</span><span class="p">,</span> <span class="s1">'npx vscode-css-language-server --stdio'</span> <span class="p">},</span>
<span class="err">→</span>   <span class="n">on_attach</span> <span class="o">=</span> <span class="n">on_attach</span><span class="p">,</span>
    <span class="c1">-- More to come</span>
  <span class="p">})</span>

  <span class="n">lspconfig</span><span class="p">.</span><span class="n">ts_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span> <span class="s1">'bash'</span><span class="p">,</span> <span class="s1">'-lc'</span><span class="p">,</span> <span class="s1">'npx typescript-language-server --stdio'</span> <span class="p">},</span>
<span class="err">→</span>   <span class="n">on_attach</span> <span class="o">=</span> <span class="n">on_attach</span><span class="p">,</span>
    <span class="c1">-- More to come</span>

  <span class="p">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">on_attach</code> will do two things: 1) set up keybindings to call the Lua functions exposed by lsp-config (which will then make the right calls to the right server), and 2) enable various LSP features that are off by default.</p>

<p>Here’s how I have mine set up (you may want different keybindings). I’ve commented what each does:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">on_attach</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">bufnr</span><span class="p">)</span>
  <span class="kd">local</span> <span class="n">opts</span> <span class="o">=</span> <span class="p">{</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">bufnr</span><span class="p">,</span> <span class="n">noremap</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span> <span class="n">silent</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span>

  <span class="c1">-- When on a symbol, go to the file that defines it</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'gd'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">buf</span><span class="p">.</span><span class="n">definition</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="c1">-- When on a symbol, open up a split showing files referencing </span>
  <span class="c1">-- this symbol. You can hit enter on any file and that file</span>
  <span class="c1">-- and location of the reference open.</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'gr'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">buf</span><span class="p">.</span><span class="n">references</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="c1">-- Open up a split and show all symbols defined in the current</span>
  <span class="c1">-- file. Hitting enter on any symbol jumps to that location</span>
  <span class="c1">-- in the file</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'gs'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">buf</span><span class="p">.</span><span class="n">document_symbol</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="c1">-- Open a popup window showing any help available for the </span>
  <span class="c1">-- method signature you are on</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'gK'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">buf</span><span class="p">.</span><span class="n">signature_help</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="c1">-- If there are errors or warnings, go to the next one</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'dn'</span><span class="p">,</span> <span class="k">function</span><span class="p">()</span> <span class="n">vim</span><span class="p">.</span><span class="n">diagnostic</span><span class="p">.</span><span class="n">jump</span><span class="p">({</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">float</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">})</span> <span class="k">end</span><span class="p">)</span>

  <span class="c1">-- If there are errors or warnings, go to the previous one</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'dp'</span><span class="p">,</span> <span class="k">function</span><span class="p">()</span> <span class="n">vim</span><span class="p">.</span><span class="n">diagnostic</span><span class="p">.</span><span class="n">jump</span><span class="p">({</span> <span class="n">count</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">float</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">})</span> <span class="k">end</span><span class="p">)</span>

  <span class="c1">-- If you are on a line with an error or warning, open a </span>
  <span class="c1">-- popup showing the error/warning message</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'do'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">diagnostic</span><span class="p">.</span><span class="n">open_float</span><span class="p">)</span>

  <span class="c1">-- Open the "hover" window on a symbol, which tends to show</span>
  <span class="c1">-- documentation on that symbol inline</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">keymap</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'n'</span><span class="p">,</span> <span class="s1">'K'</span><span class="p">,</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">buf</span><span class="p">.</span><span class="n">hover</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="c1">-- While in insert mode, Ctrl-Space will invoke Ctrl-X Ctrl-o </span>
  <span class="c1">-- which initiates completion to show a list of symbols that</span>
  <span class="c1">-- make sense for autocomplete</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">api</span><span class="p">.</span><span class="n">nvim_set_keymap</span><span class="p">(</span><span class="s1">'i'</span><span class="p">,</span> <span class="s1">'&lt;C-Space&gt;'</span><span class="p">,</span> <span class="s1">'&lt;C-x&gt;&lt;C-o&gt;'</span><span class="p">,</span> <span class="p">{</span> <span class="n">noremap</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span> <span class="n">silent</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">})</span>

  <span class="c1">-- Enable "inlay hints"</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">inlay_hint</span><span class="p">.</span><span class="n">enable</span><span class="p">()</span>

  <span class="c1">-- Enable completion</span>
  <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">completion</span><span class="p">.</span><span class="n">enable</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="n">client</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="n">bufnr</span><span class="p">,</span> <span class="p">{</span>
    <span class="n">autotrigger</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">-- automatically pop up when e.g.  you type '.' after a variable</span>
    <span class="n">convert</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
      <span class="k">return</span> <span class="p">{</span> <span class="n">abbr</span> <span class="o">=</span> <span class="n">item</span><span class="p">.</span><span class="n">label</span><span class="p">:</span><span class="nb">gsub</span><span class="p">(</span><span class="err">'%b</span><span class="p">()</span><span class="s1">', '') } -- NGL, no clue what this is for but it'</span><span class="n">s</span> <span class="n">needed</span>
    <span class="k">end</span><span class="p">,</span>
  <span class="p">})</span>

  <span class="c1">-- If the LSP server supports semantic tokens to be used for highlighting</span>
  <span class="c1">-- enable that.</span>
  <span class="k">if</span> <span class="n">client</span> <span class="ow">and</span> <span class="n">client</span><span class="p">.</span><span class="n">server_capabilities</span><span class="p">.</span><span class="n">semanticTokensProvider</span> <span class="k">then</span>
    <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">semantic_tokens</span><span class="p">.</span><span class="n">start</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">buf</span><span class="p">,</span><span class="n">args</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">client_id</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1">-- The documentation said to set this for completion</span>
<span class="c1">-- to work properly and/or well. I'm not sure what happens</span>
<span class="c1">-- if you omit this</span>
<span class="n">vim</span><span class="p">.</span><span class="n">cmd</span><span class="s">[[set completeopt+=menuone,noselect,popup]]</span>
</code></pre></div></div>

<p>Whew!  The lsp-config documentation can help you know what other functions might exist, but the setup above seems to use most of them, at least the ones for Ruby that I think are useful.</p>

<p>Once this is all set up, you will find that the CSS and JavaScript LSP Servers still don’t work.</p>

<h2 id="getting-microsofts-lsp-servers-to-work-because-they-crash-by-default">Getting Microsoft’s LSP Servers to Work Because They Crash By Default</h2>

<p>Once I had Ruby working, I installed CSS and TypeScript and found that they would happily complete any single request and then crash.  Apparently, they assume the editor and server are running on the same computer and use a process identifier to know if everything is running normally.</p>

<p>Since this would not work with Docker (the process IDs would be different or not available), you need to configure both LSP servers in lsp-config to essentially not care about process IDs.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="n">lspconfig</span><span class="p">.</span><span class="n">cssls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
     <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span>
             <span class="s1">'bash'</span><span class="p">,</span>
             <span class="s1">'-lc'</span><span class="p">,</span>
             <span class="s1">'npx vscode-css-language-server --stdio'</span> <span class="p">},</span>
     <span class="n">on_attach</span> <span class="o">=</span> <span class="n">on_attach</span><span class="p">,</span>
<span class="err">→</span>    <span class="n">before_init</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="err">→</span>      <span class="n">params</span><span class="p">.</span><span class="n">processId</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">NIL</span>
<span class="err">→</span>    <span class="k">end</span><span class="p">,</span>
   <span class="p">})</span>
   <span class="n">lspconfig</span><span class="p">.</span><span class="n">ts_ls</span><span class="p">.</span><span class="n">setup</span><span class="p">({</span>
     <span class="n">cmd</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'dx/exec'</span><span class="p">,</span>
             <span class="s1">'bash'</span><span class="p">,</span>
             <span class="s1">'-lc'</span><span class="p">,</span>
             <span class="s1">'npx typescript-language-server --stdio'</span> <span class="p">},</span>
     <span class="n">on_attach</span> <span class="o">=</span> <span class="n">on_attach</span><span class="p">,</span>
<span class="err">→</span>    <span class="n">before_init</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="err">→</span>      <span class="n">params</span><span class="p">.</span><span class="n">processId</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">NIL</span>
<span class="err">→</span>    <span class="k">end</span><span class="p">,</span>
   <span class="p">})</span>
</code></pre></div></div>

<p>This is all great, but you may not want Neovim trying to connect to LSPs when you have not set them up.</p>

<h2 id="dont-configure-lsp-if-its-not-available">Don’t Configure LSP if It’s Not Available</h2>

<p>When I open up a random Ruby script on my computer, I get errors about LSP servers not being available.  What I decided to do was configure LSP as opt-in in my Neovim configuration.</p>

<p>If the Lua setup script finds the file <code class="language-plaintext highlighter-rouge">.nvim.lua</code> in the project root, it will source it.  If that file sets <code class="language-plaintext highlighter-rouge">useLSP</code> to true, all of the above configuration happens. If <code class="language-plaintext highlighter-rouge">useLSP</code> is absent, no LSP configuration is done:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">project_config</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">fn</span><span class="p">.</span><span class="n">getcwd</span><span class="p">()</span> <span class="o">..</span> <span class="s2">"/.nvim.lua"</span>
<span class="k">if</span> <span class="n">vim</span><span class="p">.</span><span class="n">fn</span><span class="p">.</span><span class="n">filereadable</span><span class="p">(</span><span class="n">project_config</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">then</span>
  <span class="nb">dofile</span><span class="p">(</span><span class="n">project_config</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">if</span> <span class="n">useLSP</span> <span class="o">==</span> <span class="kc">nil</span> <span class="k">then</span>
  <span class="n">useLSP</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">end</span>

<span class="k">if</span> <span class="n">useLSP</span> <span class="k">then</span>
  <span class="c1">-- configuration from above</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="and-now-we-can-work">And Now We Can Work!</h2>

<p>I’ve been using this configuration for a few days and to be honest, I can’t quite tell how well it’s working.  But it doesn’t seem that fragile, and it seems useful to have setup in case other extensions or LSP servers become very useful.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[One Week With Desktop Linux After a 20 Year Absence]]></title>
    <link href="https://naildrivin5.com/blog/2025/03/21/one-week-with-desktop-linux-after-a-20-year-absence.html"/>
    <updated>2025-03-21T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2025/03/21/one-week-with-desktop-linux-after-a-20-year-absence.html</id>
    <content type="html"><![CDATA[<p>I bought a Framework laptop a couple weeks ago, set it up with stock Ubuntu, and used it for my
primary computer for a week.  It’s the first time I’ve used Linux in earnest in <em>20 years</em>.
It’s amazing how much has changed and how much hasn’t.</p>

<!-- more -->

<p>The tl;dr for this post is that I don’t know if I could use Linux as my desktop full time for web development.  While there are many papercuts, the three main issues I can’t see a way around are: lack of integrated API documentation lookup (e.g. <a href="https://kapeli.com/dash">Dash.app</a>), inability to customize keyboard shortcuts consistently across all apps, and the absolute tire-fire of copy and paste.</p>

<h2 id="why-even-do-this">Why Even Do This?</h2>

<p>I actually grew up on UNIX and then Linux.  All through college and for my first 12 years of
professional experience, I used UNIX or Linux.  OS X made it clear that Desktop Linux was
possible, it was just made by Apple and based on BSD Unix.</p>

<p>I didn’t really miss Linux, but when writing <a href="https://devbox.computer">Sustainable Dev Environments with Docker and Bash</a>, I never actually tried anything on a real Linux.  I checked it all on WSL2 and that was that.</p>

<p>It turns out that running a devcontainer as root creates a lot of problems on Linux, and I
figured the best way to truly solve them was to revisit day-to-day Linux.  However, I have lost a lot of enthusiasm for messing with configs and trying to get something working on hardware not
designed for it.</p>

<p>Since it doesn’t seem like you can easily run Linux on an M3 Mac, I decided to get a Linux
laptop whose vendor officially supported some sort of Linux.</p>

<h2 id="the-hardware">The Hardware</h2>

<p>My requirements for hardware were:</p>

<ul>
  <li>Vendor must officially support at least one Linux distro, so I know it will work with their
hardware</li>
  <li>13” Laptop I could take with me</li>
  <li>Can use my LG Ultrafine 4K Thunderbolt monitor as a secondary display</li>
</ul>

<h3 id="the-framework-laptop-13">The Framework Laptop 13</h3>

<p>I considered System76 and <a href="https://frame.work">Framework</a>.  Anecdotal reviews of System76
hardware were mixed, and I liked the idea of a modular laptop, so I went with a <a href="https://frame.work/products/laptop13-intel-ultra-1/configuration/new">Framework Laptop 13 DIY Edition (Intel® Core™ Ultra Series 1)</a>.  This is not the latest one they recently announced, but the now older model.  I also opted for Intel as that was the only processor I could determine should support my Thunderbolt display. I could not figure out if AMD did, and didn’t want to find out that it didn’t.</p>

<p>My configuration was pretty basic.  I stuck with the default processor, but opted for the
“better” of the two screens.  I don’t need HDMI, Ethernet, or an SD card reader, so I went with
three USB-C ports and a USB-A port.  I stuck with the boring black bezel.</p>

<h3 id="assembly-and-setup">Assembly and Setup</h3>

<p>It was fun putting the laptop together - I haven’t shoved RAM into a socket in quite a few years, so it was cool seeing all the parts and putting it all together. Framework’s instructions were spot on and I had no real issues.  Most everything either went into a secure socket or was magnetically attached.  I did need to screw some tiny screws in, but they provided a nice screwdriver for their Torx screws.</p>

<aside class="pullquote">Framework's instructions were spot on</aside>

<p>The last time I did this, I was building a desktop PC and all the parts came in these
anti-static foil thingies in very basic packaging.  Framework has gone the complete other way,
with Apple-style packaging that was very easy to open and deal with.</p>

<p>That said, even though it’s all recylcable, it felt like a ton of waste.  Not having something
to recycle beats recyling any day of the week.</p>

<figure>
  <img src="/images/framework-waste-1024.jpg" alt="Photo of the packaging from the Framework laptop. It's a laptop-sized box open, containing many thinner cardboard boxes and protective sheets of compostable plastic." />
  <figcaption>
  At least it's all recylcable? <a href="/images/framework-waste.jpg" target="_blank">View the
  full size image.</a>
  </figcaption>
</figure>

<p>The DYI edition does not come with an OS, but Framework officially supports Ubuntu and Fedora.
Since I’ve used Ubuntu as the basis for Docker containers, I went with that.  I followed
their instructions to flash a USB drive with an ISO and install from there. It all worked
perfectly, exactly as they had documented.</p>

<p>I’ll discuss the software side in a minute</p>

<h3 id="how-is-the-hardware">How Is The Hardware?</h3>

<p>While the Framework is by no means a Macbook Pro, it does feel high quality.  Yes, it’s made of
plastic and has holes for fans.  And yes, there are seams in it where the modular parts
connect. I’m fine with all of that.</p>

<aside class="pullquote">The screen looked a lot better than I was expecting…the keyboard felt as good as the one on my Macbook</aside>

<p>The screen looked a lot better than I was expecting. It seems retina-quality, or at least
nowhere near as bad as the $400 Lenovo Windows laptop I used for testing my book. It’s not as bright as my
Mac’s, but the only time I really noticed was when I was switching between the two computers.</p>

<p>The keyboard felt as good as the one on my Macbook Pro. My only quibbles are: 1) the placement of the why-is-there-always-an-unmappable-Fn-key, as it’s where Control is on my Mac, 2) a fucking Windows key logo, which I am not kidding almost made me not buy this thing, and 2) The F9 key, whose function I would describe as “completely decimate your display setup and fuck up everything until you keep hitting it and eventually arrive at what you had before”.</p>

<figure class="two-shots">
    <div>
        <img src="/images/linux-desktop/framework-keyboard-1200.jpg" alt="Photo of the left side of the Framework laptop's keyboard.  The bottom row where modifiers are, from left to right is: Control, Fn, Windows/Super, Alt, Spacebar.  There are arrows calling out the location of Control and Fn" />
        <img src="/images/linux-desktop/mac-keyboard-1200.jpg" alt="Photo of the left side of a Mac laptop's keyboard.  The bottom row where modifiers are, from left to right is: Fn, Control, Option, Cmd, Spacebar.  There are arrows calling out the location of Control and Fn" />
    </div>
    <figcaption>
    The location of the Fn and Control keys are swapped. And, of course, neither computer allows re-mapping the Fn key.
    </figcaption>
</figure>

<p>The trackpad…well…it’s OK. It’s not as big as I’d like, but not as tiny as your average PC laptop’s.  I find it extremely hard
to use, but I also find the Magic Trackpad hard to use with Linux.  It’s clear that Apple’s driver does some stuff to avoid
misclicks and other stuff.  You also cannot control the scrolling speed, so I found myself making a lot of mistakes using both
Trackpads.</p>

<p>Battery life seems very good. Not Mac-level, but I have left it closed for 24 hours and the battery is still pretty well charged.  As I type, it’s at 88% and the OS is telling me it’s got 5 hours left.</p>

<p>No issues with WiFi; I was able to connect to my iPhone for tethering.  The fingerprint reader is more finicky than Apple’s
TouchID, but worked almost every time.</p>

<h2 id="the-first-day-was-rough">The First Day Was Rough</h2>

<p>The first day with Linux was rough for me.  Mostly due to two things:</p>

<ul>
  <li>Muscle memory of keyboard shortcuts that aren’t set up or do other bizarre things on Linux. This is purely my fault.</li>
  <li>The trackpad/mouse configuration is woefully inadequate. Scroll speeds are 100MPH, acceleration is weird, and clicking with the trackpad causes numerous mislicks.  Each click on either my Magic Trackpad or the laptop’s resulted in the mouse moving down a tiny bit.  This was often enough to miss a click target.  I have to assume Apple’s drivers account for this.</li>
</ul>

<p>As we’ll see, I somewhat tamed the keyboard shortcuts, though not sufficiently, and I ended up
getting a mouse which was much easier for me to control.</p>

<p>That said, after the first day, I realized I need to slow down and not try things until I knew
they would work. It felt like trying to type on a new keyboard layout or using my left hand for
mousing.</p>

<h2 id="my-workflow">My Workflow</h2>

<p>My goal was to recreate as much of my workflow from the Mac to this new Linux laptop.  In the
end, I was mostly able to do so, however some apps or behaviors were simply not possible.</p>

<p>At a high level, my workflow when programming is something like this:</p>

<ul>
  <li>A terminal is for running command line apps and scripts.</li>
  <li>Neovim is my editor, however it runs as a GUI app, <em>not</em> in the terminal</li>
  <li>All development is done inside a Docker container, with source code mounted. This allows
editing in Neovim, but alleviates me from having to manage a bunch of version managers and
databases.</li>
  <li>A launcher like Alfred responds to <kbd>Cmd-Shift-Space</kbd> to allow me to type the name of an app or initiate a web search.</li>
  <li>I use copy and paste liberally, and have a clipboard manager (Alfred, again) to manage
multiple pastable things.</li>
  <li>I heavily rely on a core set of keyboard shortcuts supported by all Mac software (including bad citizens like Firefox and Electron).</li>
  <li>1Password manages my passwords, which I unlock with TouchID.</li>
  <li><kbd>Cmd-Tab</kbd> allows switching apps..</li>
  <li>Inside Neovim (or through my launcher) I can look up API documentation using Dash. This means
if my cursor is in a <code class="language-plaintext highlighter-rouge">.css</code> file in the middle of <code class="language-plaintext highlighter-rouge">max-height</code>, I can type <code class="language-plaintext highlighter-rouge">K</code> and Dash will
raise to the top, get focus, and show me the MDN docs for the <code class="language-plaintext highlighter-rouge">max-height</code> CSS property.</li>
  <li>For Mail, Calendar, Mastodon, BlueSky, or Plex, I expect to be able to <kbd>Cmd-Tab</kbd> or use my launcher to select them—they do not run as tabs in Firefox.</li>
  <li>Occasionally, I run the iPhone simulator, draw diagrams with Omnigraffle, or edit images with
Pixelmator.</li>
  <li>I rarely use the Finder</li>
</ul>

<p>I was able to get <em>most</em> of this working, however, keyboard shortcuts were challenging, copy
and paste is confusing, and the API documentation options pale in comparison to Dash.</p>

<h2 id="software">Software</h2>

<p>I basically got to the setup below by working on real projects and, when I hit a wall, figured
out the Linux way of addressing the issue.  This usually involved installing a lot of stuff
with <code class="language-plaintext highlighter-rouge">sudo apt-get install</code> and I did not write down any of what I did.</p>

<p>I quickly learned that the Ubuntu “App Center” should never be used unless the people building
the software say to use it.  Several apps did not work until I uninstalled them via App
Center and re-installed them on command line.</p>

<p>Below is a breakdown of which Mac app I use and what I used on Linux as a replacement.</p>

<div class="comparison">
    <h6>Terminal Emulator</h6>
    <img src="/images/linux-desktop/terminal-192.png" role="none" />
    <p><span class="name">Terminal.app</span> on Mac</p>
    <img src="/images/linux-desktop/ghostty-192.png" role="none" />
    <p><span class="name">Ghostty</span> on Linux</p>
    <div class="description">
            <span role="img" aria-label="thumbs up"> 👍</span>
        <p>
            Gnome-terminal was very hard to configure and very limited.  Ghostty allowed me to configure all of my keyboard shortcuts and worked well. I don't use it on Mac since Terminal.app works well and Ghostty killed my battery life.  It doesn't seem to have that issue on Linux.
        </p>
    </div>
</div>
<div class="comparison">
    <h6>Vim GUI</h6>
    <img src="/images/linux-desktop/vimr-app-icon-192.png" role="none" />
    <p><span class="name">VimR</span> on Mac</p>
    <img src="/images/linux-desktop/neovide-128x128.png" role="none" />
    <p><span class="name">Neovide</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
    <p>
        Neovide has the most bizarre set of defaults I've ever seen. It is configured to animate every move of the cursor at an extremely slow animation rate.  It is almost impossible to use.  But, this can all be turned off very easily and the documentation is great.
    </p>
    <p>
    I was also unable to get Neovide to behave as its own "app". I can Cmd-Tab to it as a separate
    window, but it shows a default icon and is considered a window of Ghostty.  I tried creating
    magic <code>.desktop</code> files, but they didn't seem to do the trick.
    </p>
    </div>
    </div>
</div>
<div class="comparison">
    <h6>Docker</h6>
    <img src="/images/linux-desktop/docker-mark-blue-192.png" role="none" />
    <p><span class="name">Docker Desktop</span> on Mac</p>
    <img src="/images/linux-desktop/docker-mark-blue-192.png" role="none" />
    <p><span class="name">Docker Daemon</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
            <p>
                I installed Docker per their Linux instructions, which does not include a
                desktop GUI. That's fine, as I never use the GUI anyway.  I 
                <a href="https://docs.docker.com/engine/install/linux-postinstall/">
                followed the Linux post-install instructions
                </a> to allow my user to access Docker.  This was 1) because it was much easier
                than the rootless setup, and 2) required for me to get the details in the book
                right.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>App Launcher</h6>
    <img src="/images/linux-desktop/alfred-192.png" role="none" />
    <p><span class="name">Alfred</span> on Mac</p>
    <img src="/images/linux-desktop/albert-192.png" role="none" />
    <p><span class="name">Albert</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
            <p>
            Albert looks inspried by Alfred and works pretty well.  I had to configure a
            system-wide keyboard shortcut inside the Ubuntu/Gnome/??? Settings app. Albert
            doesn't seem able to do this, even though it has a configuration option for it.
            Fortunately, <code>albert open</code> will open, raise, and focus Albert.
            </p>
            <p>
            Albert includes a clipboard manager, but it didn't work as well as Alfred's. I
            think this is because Linux lacks a system-wide clipboard and/or has multiple
            clipboards. I eventually gave up on relying on it.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Password Manager</h6>
    <img src="/images/linux-desktop/1password-192.png" role="none" />
    <p><span class="name">1Password</span> on Mac</p>
    <img src="/images/linux-desktop/1password-192.png" role="none" />
    <p><span class="name">1Password</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
        <p>1Password worked great…once I got it installed</p>
                <p>
                Installing 1Password and Firefox from App Center resulted in the browser
                extension not working and caused 1Password to ask for 2FA every time I opened it.
                It was actually not easy to delete both apps from App Center, but once I
                re-installed them, they both worked fine.
                </p>
                <p>
                On Linux only, the browser extension will randomly open up 1Password's web page
                and ask to me log in, though if I ignore this, everything still works.  This is
                what it does on Safari on macOS (except there it just doesn't work).
                </p>
                <p>
                Even now, the browser extension can no longer be unlocked with the fingerprint
                scanner and I have to enter my passphrase frequently. It's extremely annoying.
                But at least it works, unlike on Safari, which just does not.
                </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>App and Window Switching</h6>
    <kbd>Cmd-Tab</kbd>
    <p><span class="name">Switch apps</span> on Mac</p>
    <kbd>Cmd-Tab</kbd>
    <p><span class="name">Switch <strong>windows</strong> of all apps</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs down">👎</span>
        <div>
                <p>
                In theory, the Gnome task switcher allows switching between apps like on macOS.
                In practice, because I could not get Neovide to run as a separate “app” in the
                eyes of Gnome, I could not <kbd>Cmd-Tab</kbd> to it. I would have had to
                <kbd>Cmd-Tab</kbd> to
                Ghostty, then <kbd>Cmd-~</kbd> to Neovide.
                </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Fast API Doc Lookup</h6>
    <img src="/images/linux-desktop/dash-192.png" role="none" />
    <p><span class="name">Dash</span> on Mac</p>
    <img src="/images/linux-desktop/mess-192.png" role="none" itemref="mess-icon-acd-version mess-icon-acd-level mess-icon-acd-ai-name mess-icon-acd-details" />
    <p><span class="name">A Mess</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs down">👎</span>
        <div>
            <p>
                More on this below, but Zeal didn't work for me. I ended up using a hacky
                system of FirefoxPWA, DevDocs, and some config.
            </p>
        </div>
    </div>
</div>
<data id="mess-icon-acd-version" itemprop="ai-content-declaration:version" content="1.0.0"></data>
<data id="mess-icon-acd-level" itemprop="ai-content-declaration:level" content="total"></data>
<data id="mess-icon-acd-ai-name" itemprop="ai-content-declaration:ai-name" content="ChatGPT"></data>
<data id="mess-icon-acd-details" itemprop="ai-content-declaration:details" content="create me an app icon of a mess"></data>
<div class="comparison">
    <h6>Fastmail</h6>
    <img src="/images/linux-desktop/fastmail-192.png" role="none" />
    <p><span class="name">FastMail</span> on Mac (as Safari Web App)</p>
    <img src="/images/linux-desktop/fastmail-192.png" role="none" />
    <p><span class="name">FastMail</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
            <p>
                More on FirefoxPWA below, but this worked great.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Calendar</h6>
    <img src="/images/linux-desktop/fantastical-mac-icon-192.png" role="none" />
    <p><span class="name">Fantastical</span> on Mac</p>
    <img src="/images/linux-desktop/fastmail-192.png" role="none" />
    <p><span class="name">Fastmail</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
            <p>
                More on FirefoxPWA below. I could not set a custom icon, but this works fine.
                FastMail's calender UI is pretty good.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Mastodon Client</h6>
    <img src="/images/linux-desktop/ivory-192.png" role="none" />
    <p><span class="name">Ivory</span> on Mac</p>
    <img src="/images/linux-desktop/mastodon-192.png" role="none" />
    <p><span class="name">Mastodon.com</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
            <p>
                First time really using Mastodon's web UI and it's not very good. But works
                well enough.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Bluesky Client</h6>
    <img src="/images/linux-desktop/Bluesky_Logo-192.png" role="none" />
    <p><span class="name">Bluesky</span> on Mac (as Safari Web App)</p>
    <img src="/images/linux-desktop/Bluesky_Logo-192.png" role="none" />
    <p><span class="name">Bluesky</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
            <p>
            It's the same crappy UI in both.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Plex</h6>
    <img src="/images/linux-desktop/plex-192.png" role="none" />
    <p><span class="name">Plex</span> on Mac (as Safari Web App)</p>
    <img src="/images/linux-desktop/plex-192.png" role="none" />
    <p><span class="name">Plex</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
            <p>
            Plex is plex.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Streaming Music</h6>
    <img src="/images/linux-desktop/apple-music-192.png" role="none" />
    <p><span class="name">Apple Music</span> on Mac</p>
    <img src="/images/linux-desktop/apple-music-192.png" role="none" />
    <p><span class="name">Apple Music</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
            <p>
        Kindof amazed this existed and works!  Firefox insists on "installing DRM" but also
        didn't seem to actually do anything.  I think the Apple Music app is really just this
        web player. It works the same, and is still just as crappy as it was the day it
        launched as iTunes.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Messaging</h6>
    <img src="/images/linux-desktop/messages-192.png" role="none" />
    <p><span class="name">Messages</span> on Mac</p>
    <span class="linux" role="img" aria-description="Crying Emoji">😢</span>
    <p><span class="name">Nothing</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs down">👎</span>
        <div>
            <p>
                Messages is an Apple-only thing, so I was stuck taking out my phone <strong>a
                lot</strong>.  I just didn't interact with my friends on text nearly as much.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Maps</h6>
    <img src="/images/linux-desktop/maps-192.png" role="none" />
    <p><span class="name">Maps</span> on Mac</p>
    <img src="/images/linux-desktop/wego-192.png" role="none" />
    <p><span class="name">Wego Here</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="handing facing right">🫱</span>
        <div>
            <p>
                I have tried to avoid having Google in my life, and Apple's maps are pretty
                good, at least in the US.  OpenStreetMap is servicable, but <a href="https://wego.here.com">Wego Here</a> is a bit nicer.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Keyboard Configurator</h6>
    <img src="/images/linux-desktop/keychron-192.png" role="none" />
    <p><span class="name">Keychron Launcher</span> on Mac</p>
    <span class="linux" role="img" aria-description="Crying Emoji">😢</span>
    <p><span class="name">Nothing</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs down">👎</span>
        <div>
            <p>
                Chrome could not access USB no matter what I did. I <code>chmod</code>'ed
                everything in <code>/dev/</code> to be owned by me and it didn't work.
                I ended up having to program the keyboard on my Mac, plug it into the Framework
                to check it and switch it back. Ugh.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>RSS</h6>
    <img src="/images/linux-desktop/reeder-192.png" role="none" />
    <p><span class="name">Reeder</span> on Mac</p>
    <img src="/images/linux-desktop/feedbin-192.png" role="none" />
    <p><span class="name">Feedbin</span> on Linux (as Firefox PWA)</p>
    <div class="description">
        <span role="img" aria-label="thumbs up"> 👍</span>
        <div>
            <p>
            Reeder has always required an RSS back-end. I initially used David Smith's excellent Feed Wrangler, but he sold it
            to Feedbin a few years back. I've never actually used the Feedbin web UI before, but it's pretty good!  Installed
            as a PWA, it works great.
            </p>
        </div>
    </div>
</div>
<div class="comparison">
    <h6>Light Image Editing</h6>
    <img src="/images/linux-desktop/preview-192.png" role="none" />
    <p><span class="name">Preview</span> on Mac</p>
    <img src="/images/linux-desktop/krita-192.png" role="none" />
    <p><span class="name">Krita</span> on Linux</p>
    <div class="description">
        <span role="img" aria-label="thumbs down">👎</span>
        <div>
            <p>
            Preview is an underrated app. It's truly amazing and demonstrates what Apple is—or at least used to be—capable
            of.  It views PDFs, lets you edit them, sign them, save them. It views images and lets you crop them, adjust their
            colors, and do a lot of the 80 in 80/20 changes.
            </p>
            <p>
            Krita is more like a Pixelmator or Photoshop.  I had to consult the documentation to figure out how
            to crop an image.  It's coloring didn't respect my desktop light/dark mode, and it made me miss a
            hugely helpful thing almost every mac has: keyword search of menus.
            </p>
        </div>
    </div>
</div>

<h2 id="how-it-worked">How it Worked</h2>

<p>To toot my own horn, every single dev environment built and passed tests without any changes. I pulled down a
repo, ran <code>dx/build</code> to build images, ran <code>dx/start</code> to start containers,
<code>dx/exec bin/setup</code> to set up the app inside the container and <code>dx/exec
bin/ci</code> to run all tests, which all passed.</p>

<aside class="pullquote">Every single dev environment built and passed tests without any changes</aside>

<p>Once I started actually making changes to my code, I <strong>did</strong> run into issues running all containers as root. I have sorted that out and <a href="https://devbox.computer">the book will be updated</a> to reflect this.</p>

<p>Aside from that, the experience was overall pretty great once I had installed everything and
configured as much as I could to have keyboard shortcuts I could live with.  To use this full
time for software development, however, a few things would need to change.</p>

<p>Instead of “The Good, The Bad, and The Ugly”, I’m going to list Dealbreakers, Papercuts, and
Pleasntries.</p>

<h3 id="dealbreakers">Dealbreakers</h3>

<p>There are three major issues that I would have to address to use  Linux full time: clipboard
craptitude, keyboard shortcut inconsistency, and API documentation lookup.</p>

<h4 id="clipoard-craptitude">Clipoard Craptitude</h4>

<p>Just do a web search for “Copy and Paste on Linux” or “Clipboards on Linux” and you will find a
stream of confusion about the clipboard situation on Linux.  I mean, just <a href="https://www.jwz.org/doc/x-cut-and-paste.html">read this</a>.</p>

<p>On Mac (and on Windows, I believe), there is a single system-wide clipboard. When you copy
something into it, it becomes available to paste anywhere. You can run an optional clipboard
manager that stores the history of stuff you have copied and allow you to paste it.</p>

<aside class="pullquote">On Linux, the clipboard absolutely does not work this way</aside>

<p>On Linux, it absolutely does not work this way.  There is a clipboard that’s filled when you
select text (and there may be one for X11 apps and one for Wayland apps?). There’s a system
clipboard, too, but it’s not always clear if you’ve copied to it, or if you are pasting from
it.</p>

<p>In Neovim, I could not tell what was happening.  Usually <kbd>:*p</kbd> will paste and
<kbd>:*y</kbd> will yank. Neither seemed to do what I expected.  I ended up mapping
<kbd>Alt-C</kbd> and <kbd>Alt-V</kbd> in NeoVim’s configuration to do copy and paste. It worked better, but not 100% of the time. <strong>Update</strong> I also realized that <code class="language-plaintext highlighter-rouge">+</code> is a better option than <code class="language-plaintext highlighter-rouge">*</code> because unlike on Mac, <code class="language-plaintext highlighter-rouge">*</code> on Linux is the “whatever you had selected in X11” and not “the system clipboard”, which is <code class="language-plaintext highlighter-rouge">+</code>. FML.</p>

<p>I’m sure the way copy and paste works on Linux can be learned, but I found myself failing to successfully copy and/or paste on numerous occasions.  Eventually, I learned to always type <kbd>#</kbd> in the terminal before pasting, because I never knew exactly what was going to be pasted there.</p>

<p>Albert’s clipboard manager didn’t allow pasting in all apps, and it didn’t store an accurate
history of stuff I had copied (and pasted). I’m not sure how it works, but I just stopped using
it after a while.</p>

<p>I cannot fathom why this clipboard behavior is the default, and I would probably need to go on a deep dive to understand and remedy it, if I were to use Linux full time. It’s hard to overstate how frustrating it is to not have copy and paste work.  Christ, it works better on <strong>iOS</strong>, which I thought had the worst copy and paste experience ever created.</p>

<h4 id="keyboard-shortcut-inconsistency">Keyboard Shortcut Inconsistency</h4>

<p>On a Mac, there are certain keyboard shortcuts that every app responds to, even bad citizens
like Firefox and Electron. Not so on Linux.</p>

<p>The source of this problem is, I guess, due to terminals relying on the Control key for
non-alphanumeric sequences.  The Control key was adopted by Linux GUI apps as the key to use
when you want to invoke a shortcut.</p>

<p>On a Mac, this has historically been either the “Apple Key” or now the Command (Cmd) key.  This
key not only is irrelevant to any terminal emulator, it also is right under your left thumb.</p>

<figure>
  <img src="/images/linux-desktop/hand-on-mac-1200.jpg" alt="Photo of my left hand on the keyboard of a MacBook Pro.  My forefingers are on the homerow and my thumb is moved slightly under my hand to be on top of the Cmd key" />
  <figcaption>
  The Cmd key is within easy reach of your thumb.
  </figcaption>
</figure>

<p>This makes it ergonomically easy to use, even with other modifiers.  <kbd>Cmd-Shift</kbd> or <kbd>Cmd-Option</kbd> are far easier to use than <kbd>Control-Shift</kbd> or <kbd>Control-Alt</kbd>.</p>

<p>I have a ton of muscle memory built up using the key under my thumb. On Mac, that key is Cmd or Super. On the Framework laptop, it’s Alt.  I rarely use Control, since it’s almost never a modifier on Mac.  I also have Caps Lock mapped as Escape, since I have not had a keyboard with Escape on it in quite a while (thus, I cannot use that for Control, which would be slightly better, ergonomically).</p>

<aside class="pullquote"> Apps on Linux do not have consistent keyboard shortcuts…Firefox provides no way to customize keyboard shortcuts </aside>

<p>To make matters worse, apps on Linux do not have consistent keyboard shortcuts.  Copying and
Pasting in Gnome-Terminal is different than in Firefox.  And to make matters <strong>even
worse</strong>, Firefox provides no way to customize keyboard shortcuts. You
<strong>have</strong> to use <kbd>Ctrl-C</kbd> to copy (e.g.).</p>

<p>What this means is twofold:</p>

<ul>
  <li>Any new app I install, I have to inspect its configuration to see if I can configure the
keyboard shortcuts I want. Namely, using the “key under my thumb that is not Control” as the
modifier.</li>
  <li>When using Firefox, I have to internalize and use its Control-based shortcuts, since they
can’t be modified.</li>
</ul>

<p>To try to deal with this situation, I accepted that Linux uses <kbd>Alt</kbd> way more than
<kbd>Super</kbd>, so I set the “Windows Mode” of my Keychron keyboard to have <kbd>Alt</kbd> as
the “key under my thumb” (which is the left space bar).</p>

<figure>
  <img src="/images/linux-desktop/keychron-keyboard-1200.png" alt="Photo of the left side of a Keychron Alice
  layout keyboard.  The left space bar, under the 'C' and 'V' is labeled 'Cmd(Mac) Alt(Linux)'. There are two
  identical modifiers to the left of it that are labeled 'Option(Mac) Super(Linux)'" />
  <figcaption>
  The thumb key is easy to reach and my muscle memory can be re-used.
  </figcaption>
</figure>

<p>This meant switching from my desktop keyboard to the laptop wasn’t so jarring.  It also meant, a few default shortcuts worked like on Mac.</p>

<p>For Firefox, the situation is dire, but I did find an extension that gave me some ability to
control the shortcuts. I was able to get it to allow changing tabs with <kbd>Alt-{</kbd> and <kbd>Alt-}</kbd>, however it only works 90% of the time.</p>

<p>I’m not sure how I would deal with this day-to-day. I guess I’d just build up muscle memory
that when I’m in Firefox, I do things differently.</p>

<p>I <strong>do</strong> think this inconsistency is a contributor to some of my copy and paste
woes.  I’m sure I <em>thought</em> I copied something when I didn’t, only to paste whatever was
last on the clipboard.</p>

<h4 id="api-documentation-lookup">API Documentation Lookup</h4>

<p>For my entire career, I’m used to being able to quickly go from a symbol in my editor (which has always been some form of vi) to API documentation. Yes, <a href="https://github.com/davetron5000/vimdoclet">I wrote a Java Doclet that generates vimdoc for the Java Standard Library</a>.  Nowadays, I use Dash.</p>

<p>I use it in two ways.  First, I can use Alfred to lookup something in Dash, say “max-height”, hit return, and
have Dash show me the docs directly:</p>

<figure>
  <img src="/images/linux-desktop/alfred-dash.gif" alt="Animated gif showing Alfred pop up as a text field above Dash, 'min-height' being typed in, and Dash showing the MDN documentation for 'min-height'" />
</figure>

<p>Second, I can do this directly from vim by placing the cursor on a symbol and hitting <kbd>K</kbd>. This brings up Dash, performing a search in the context of only Ruby documentation:</p>

<figure>
  <img src="/images/linux-desktop/vim-dash.gif" alt="Animated gif showing vim editing Ruby code. The cursor
  moves to the class KeyError, then hits 'K'. Dash shows up showing a search result for all Ruby libraries that
  have KeyError listed" />
</figure>

<p>As you can see, it’s very fast, and easy to get used to.  It’s nice to have docs at your fingertips, and it’s
nice having them in another app/window.  I find a documentation-reading-based approach encourages exploration of
the docs, which can be a great way to learn new stuff you can do with your existing libraries.</p>

<p>One bit that you can’t see from these examples is that Dash allows you to install documentation
for pretty much any Ruby Gem (or other code library).  That means I can have esoteric stuff
like the Ruby Playwright bindings or the Faker gem’s RubyDoc available.  Searching in Dash is
far faster than web searching or trying to use GitHub.</p>

<p>First, I tried <a href="https://zealdocs.org/">Zeal</a>.  Zeal did not seem to have a way to install
documentation for arbitrary Ruby Gems.  Worse, I could not ever get Zeal to get focus, raise to
the top, and show me search results.  I tried a lot of options and it just never worked.</p>

<p>Next, I tried <a href="https://devdocs.io">DevDocs</a>.  I ran DevDocs in a FirefoxPWA (more on that
below), so it behaved like a separate app. I configured both Albert and Neovim to use
<code>open</code> to open a magic URL that would search DevDocs and show the results in the
PWA.</p>

<p>This behavior works about 90% of the time (the other 10% it shows as different URL).  DevDocs
does not allow installing arbitrary documentation. You get whatever they have, and they aren’t
looking to install docs for RubyGems.</p>

<p>This sucks for my workflow. I would have to sort this out.  I guess devs today who aren’t
letting AI write their code are just hitting Cmd-Space and seeing what VSCode shows them?  I
just can’t work that way.</p>

<h3 id="papercuts">Papercuts</h3>

<p>These are issues that were annoying, but I could live with.</p>

<ul>
  <li>Mouse/Trackpad scrolling speed cannot be adjusted. I could not find a way that worked on
Wayland and X11.</li>
  <li>I <em>did</em> find a way to configure my extra mouse buttons and extra mousewheel, using
input-remapper.  It sometimes just stops working and needs to be restarted, but it did exactly
what I wanted.</li>
  <li>Too many ways to install software. Sometimes you use App Center, sometimes you use
<code class="language-plaintext highlighter-rouge">apt-get</code>, sometimes you download a file and dump it into your path, sometimes you do something
else. This is how it was 20 years ago, and it still is like this and it sucks.</li>
  <li>No system wide text replacement. I can type <code class="language-plaintext highlighter-rouge">dtk</code> anywhere on my Mac and it replaces it with
my email address.  I found a version of this for Linux and it just didn’t work. I ultimately
programmed a macro into my Keychron keyboard.</li>
  <li>The font situation is baffling.  I’m going to set aside how utterly hideous most of the fonts are.  For some reason, Linux replaces fonts in websites, making <code class="language-plaintext highlighter-rouge">font-family</code> totally useless.
If I specify, say, <code class="language-plaintext highlighter-rouge">Helvetica, 'URW Gothic', sans-serif</code>, and URW Gothic is installed, Linux
will send Noto Sans, because that is a replacement for Helvetica.  I can’t understand the
reasoning for this.</li>
  <li>No ApplePay on websites means using PayPal or swtiching to my phone.</li>
  <li>Cannot copy on iPhone and paste on Linux or vice-versa.</li>
  <li>Web-only access to iCloud. Apple is not good at web apps.</li>
  <li>Entering non-ASCII characters is either really weird or something I have to learn. <kbd>Control-Shift-U-2026-Enter</kbd> to type an ellipsis just…sucks. I think there’s a better way, so to be researched.</li>
</ul>

<h3 id="pleasantries">Pleasantries</h3>

<p>I realize this is a lot of criticism, but I did actually miss just how <em>fun</em> it was to use an
operating system that I could tweak, and that wasn’t being modified by whims of Apple designers
looking to get promoted.  While Apple is great at hardware, and good at software/hardware
integration, their software has gotten much worse over the last 10 years, often foisting
zero-value features on users.</p>

<aside class="pullquote">Many apps I use are web apps, and that those apps are pretty good</aside>

<p>The other thing that surprised  me was just how many apps I use are web apps, and that those apps are pretty good, especially when run as a standalone “web app” and not as a tab in Firefox.</p>

<p>Ironically, Apple provides a great UI for doing this via Safari Web Apps (it’s too bad they also hamstring what a PWA can do on iOS). The situation on Linux is rather dire. Chrome supports it, but it’s cumbersome, and Firefox, of all browsers, provides zero support for it.  But, <a href="https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/">FirefoxPWA</a> to the rescue!</p>

<h4 id="firefox-pwa">Firefox PWA</h4>

<p>The good and bad parts of Linux are both its flexibility and configurability. FirefoxPWA is no
different.  The general protocol of a “Web App Running as a Standalone App” is, in my mind:</p>

<ul>
  <li>Separate app in the app switcher, complete with icon and title.</li>
  <li>Navigation within the app’s website stays in the app.</li>
  <li>Navigating outside the app’s website opens in the system browser.</li>
  <li>Activating or opening the app from another context (e.g. app launcher or command line) brings
the app into focus, but does not reload the app’s start page.</li>
</ul>

<p>This is possible with FirefoxPWA, but requires some tweaking.</p>

<p>Once you’ve created your app and relaunched it, it will use the Web Manifest to get icons and
names.  It does not respect the <code class="language-plaintext highlighter-rouge">scope</code> attribute, so by default all links open in the PWA and not Firefox.  By default, when a PWA is launched or activated by the OS, it will reload the start page. It also won’t allow you to override icons.  Well, it will, it just doesn’t work.</p>

<p>Fortunately, the author(s) of FirefoxPWA have done a good job with configuration and
documentation.  In any app, you can do <kbd>Control-L</kbd> to go to a URL and type
<code class="language-plaintext highlighter-rouge">about:config</code> to get into the settings.</p>

<p>From there, to fix app activation, set <code class="language-plaintext highlighter-rouge">firefoxpwa.launchType</code> to “3”.  To ensure URLs outside the app are opened in Firefox, set <code class="language-plaintext highlighter-rouge">firefoxpwa.openOutOfScopeInDefaultBrowser</code> to true, and set
<code class="language-plaintext highlighter-rouge">firefoxpwa.allowedDomains</code> to the domains of the app.</p>

<p>I couldn’t figure out a way to make these the defaults, but this is all one time setup, so
worked fine.  Once I had this setup, Mastodon, Bluesky, Plex, and Fastmail all behaved like
their own apps.</p>

<h4 id="devdocs">DevDocs</h4>

<p>While DevDocs isn’t perfect, when used with FirefoxPWA, it is serviceable.  DevDocs has a URL
format that will perform a search.  Basically, <code class="language-plaintext highlighter-rouge">devdocs.io/#q=keyword</code> will search all your
configured docs for “keyword”, and <code class="language-plaintext highlighter-rouge">devdocs.io/#q=ruby keyword</code> will search only Ruby docs for
“keyword”.</p>

<p>Thus, I needed to be able to run <code class="language-plaintext highlighter-rouge">open devdocs.io/#q=keyword</code> and have it open the DevDocs PWA
and load the given URL.  This was somewhat tricky.</p>

<p>First, I had to change <code class="language-plaintext highlighter-rouge">firefoxpwa.launchType</code> for DevDocs to “2”, which replaces the existing
tab with whatever the URL is.  This requires setting “Launch this web app on matching website”
for DevDocs and setting automatic app launching for the extension.</p>

<div data-ad=""></div>

<p>The result is that <code class="language-plaintext highlighter-rouge">open devdocs.io/#q=ruby keywdord</code> will open that URL in regular Firefox,
which triggers Firefox PWA to open that URL in the DevDocs web app.  And this works about
90% of the time.</p>

<p>FirefoxPWA installs Firefox profiles for each Web App, but I could not figure out how to use
this to open a URL directly in the DevDocs PWA.  There might be a way to do it that I have not
discovered.</p>

<h4 id="other-cool-stuff">Other Cool Stuff</h4>

<p>Alt-click anywhere in a window to drag it. Yes! I had forgot how much I loved being able to do
this. Mouse over a window to activate—but not raise—it.  Makes it really handy to enter text in
a browser window while observing dev tools.</p>

<p>I also missed having decent versions of UNIX command-line tools installed.  macOS’s versions
are woefully ancient and underpowered.</p>

<p>Time will tell if I take advantage of Framework’s upgradeability.  At the very least, I can
swap out ports if needed.</p>

<h2 id="where-i-go-from-here">Where I Go from Here</h2>

<p>I still need to update <em>Sustainable Dev Environments with Docker and Bash</em>, and I plan to do
that entirely on the Framework laptop.  My book-writing toolchain is Docker-based, so should
work.</p>

<p>Depending on how that goes, I may spend more time addressing the various major issues and
papercuts to see if I could use it for real, full time, for web development and writing.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[A Simple Explanation of Postgres' <code>Timestamp with Time Zone</code>]]></title>
    <link href="https://naildrivin5.com/blog/2024/10/10/a-simple-explanation-of-postgres-timestamp-with-time-zone.html"/>
    <updated>2024-10-10T09:00:00+00:00</updated>
    <id>https://naildrivin5.com/blog/2024/10/10/a-simple-explanation-of-postgres-timestamp-with-time-zone.html</id>
    <content type="html"><![CDATA[<p>Postgres provides two ways to store a timestamp: <code class="language-plaintext highlighter-rouge">TIMESTAMP</code> and <code class="language-plaintext highlighter-rouge">TIMESTAMP WITH TIME ZONE</code> (or <code class="language-plaintext highlighter-rouge">timestamptz</code>).
I’ve always recommended using the later, as it alleviates all confusion about time zones. Let’s see why.</p>

<!-- more -->

<h2 id="what-is-a-time-stamp">What is a “time stamp”?</h2>

<p>The terms “date”, “time”, “datetime”, “calendar”, and “timestamp” can feel interchangeable but the are not.  A “timestamp” is a specific point in time, as measured from a reference time.  Right now it is Oct 10, 2024 18:00 in the UK, which is the same timestamp as Oct 10 2024 14:00 in Washington, DC.</p>

<p>To be able to compare two timestamps, you <em>have</em> to include some sort of reference time.  Thus, “Oct 10, 2025
18:00” is not a timestamp, since you don’t know what the reference time is.</p>

<p>Time zones are a way to manage these references. They can be confusing, especially when presenting timestamps or
storing them in a database.</p>

<h2 id="storing-time-stamps-without-time-zones">Storing time stamps without time zones</h2>

<p>Consider this series of SQL statements:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# create temp table no_tz(recorded_at timestamp);
CREATE TABLE
db=# insert into no_tz(recorded_at) values (now());
INSERT 0 1
adrpg_development=# select * from no_tz;
         recorded_at          
----------------------------
 2024-10-10 18:03:11.771989
(1 row)
</code></pre></div></div>

<p>The value for <code class="language-plaintext highlighter-rouge">recorded_at</code> is a SQL <code class="language-plaintext highlighter-rouge">timestamp</code> which does not encode timezone information (and thus, I would argue, is not an actual time stamp).  Thus, to interpret this value, there must be some reference.  In this case, Postgres uses whatever its currently configured timezone is.  While this is often UTC, it is not guaranteed to be UTC.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# show timezone;
 TimeZone 
----------
 UTC
(1 row)
</code></pre></div></div>

<p>This value can be changed in many ways.  We can change it per session with <code class="language-plaintext highlighter-rouge">set session timezone</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# set session timezone to 'America/New_York';
SET
</code></pre></div></div>

<p>Once we’ve done that, the value in <code class="language-plaintext highlighter-rouge">no_tz</code> is, technically, different:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# select * from no_tz;
         recorded_at          
----------------------------
 2024-10-10 18:03:11.771989
(1 row)
</code></pre></div></div>

<p>Because the SQL <code class="language-plaintext highlighter-rouge">timestamp</code> is implicitly in reference to the session or server’s time zone, this value is now
technically four hours off, since it’s now being referenced to eastern time, not UTC.</p>

<p>This can be solved by storing the referenced time zone.</p>

<h2 id="storing-timestamps-with-time-zones">Storing timestamps with time zones</h2>

<p>Let’s create a new table that stores the time both with and without a timezone:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TEMP</span> <span class="k">TABLE</span>
  <span class="n">tz_test</span><span class="p">(</span>
    <span class="n">with_tz</span>    <span class="nb">TIMESTAMP</span> <span class="k">WITH</span>    <span class="nb">TIME</span> <span class="k">ZONE</span><span class="p">,</span>
    <span class="n">without_tz</span> <span class="nb">TIMESTAMP</span> <span class="k">WITHOUT</span> <span class="nb">TIME</span> <span class="k">ZONE</span>
<span class="p">);</span>
</code></pre></div></div>

<p>We can see that, by default, the Postgres server I’m running is set to UTC:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# show timezone;
 TimeZone 
----------
 Etc/UTC
</code></pre></div></div>

<p>Now, let’s insert the same timestamp into both fields:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span>
  <span class="n">tz_test</span><span class="p">(</span>
    <span class="n">with_tz</span><span class="p">,</span>
    <span class="n">without_tz</span>
  <span class="p">)</span>
  <span class="k">VALUES</span> <span class="p">(</span>
    <span class="n">now</span><span class="p">(),</span>
    <span class="n">now</span><span class="p">()</span>
  <span class="p">)</span>
<span class="p">;</span>
</code></pre></div></div>

<p>The same timestamp should be stored:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# select * from tz_test;
           with_tz            |        without_tz         
------------------------------+---------------------------
 2024-10-10 18:09:35.11292+00 | 2024-10-10 18:09:35.11292
(1 row)

</code></pre></div></div>

<p>Note the difference in how these values are presented.  <code class="language-plaintext highlighter-rouge">with_tz</code> is showing us the time zone offset—<code class="language-plaintext highlighter-rouge">+00</code>.  Let’s change to eastern time and run the query again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>db=# set session timezone to 'America/New_York';
SET
db=# select * from tz_test;
           with_tz            |        without_tz         
------------------------------+---------------------------
 2024-10-10 14:09:35.11292-04 | 2024-10-10 18:09:35.11292
(1 row)
</code></pre></div></div>

<p>The value for <code class="language-plaintext highlighter-rouge">with_tz</code> is still correct. There’s no way to misinterpret that value.  It’s the same timestamp we
inserted.  <code class="language-plaintext highlighter-rouge">without_tz</code>, however, is now wrong or, at best, unclear.</p>

<h2 id="why-not-just-always-stay-in-utc">Why not Just Always stay in UTC?</h2>

<p>It’s true that if you are always careful to stay in UTC (or any time zone, really), the values for a <code class="language-plaintext highlighter-rouge">TIMESTAMP
WITHOUT TIME ZONE</code> will be correct.  But, it’s not always easy to do this.  You saw already that I changed the
session’s timezone.  That a basic configuration option can invalidate all your timestamps should give you pause.</p>

<p>Imagine an ops person wanting to simplify reporting by changing the server’s time zone to pacific time.  If your
timestamps are stored without time zones, they are now all wrong.  If you had used <code class="language-plaintext highlighter-rouge">TIMESTAMP WITH TIME ZONE</code> it
wouldn’t matter.</p>

<h2 id="always-use-timestamp-with-time-zone">Always use <code class="language-plaintext highlighter-rouge">TIMESTAMP WITH TIME ZONE</code></h2>

<p>There’s really no reason <em>not</em> to do this.  If you are a Rails developer, you can make Active Record default to
this like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/postgres.rb</span>
<span class="nb">require</span> <span class="s2">"active_record/connection_adapters/postgresql_adapter"</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">ConnectionAdapters</span><span class="o">::</span><span class="no">PostgreSQLAdapter</span><span class="p">.</span><span class="nf">datetime_type</span> <span class="o">=</span> <span class="ss">:timestamptz</span>
</code></pre></div></div>

<p>This can be extremely helpful if you are setting time zones in your code. It’s not uncommon to temporarily
change the time zone to display values to a user in their time zone.  If you write a timestamp to the database
while doing this, <code class="language-plaintext highlighter-rouge">TIMESTAMP WITH TIME ZONE</code> will always store the correct value.</p>

<p>Note that <a href="https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_timestamp_.28without_time_zone.29">Postgres</a> also recommends you use <code class="language-plaintext highlighter-rouge">TIMESTAMP WITH TIME ZONE</code>.</p>
]]></content>
  </entry>
  
</feed>
