<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
  <title>Julia Evans</title>
  <link>https://aggr.aymericbeaumet.com/sources/jvns.ca/</link>
  <description>Browse aymeric&#39;s, an independent, searchable archive of readable snapshots from followed feeds, preserved in Git with aggr.</description>
  <language>en</language>
  <atom:link href="https://aggr.aymericbeaumet.com/sources/jvns.ca/rss.xml" rel="self" type="application/rss+xml"/>
  <lastBuildDate>Sun, 27 Sep 2026 00:00:00 +0000</lastBuildDate>
  <generator>aggr</generator>
  <item>
    <title>Replacing the old battery on rechargeable bike lights</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-09-27-replacing-the-old-battery-on-rechargeable-bike-lights/</link>
    <guid isPermaLink="false">urn:aggr:item:c2c37a19e0d4b31d99e4803288493d9c6d9e733c</guid>
    <atom:link href="https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/" rel="via" type="text/html"/>
    <pubDate>Sun, 27 Sep 2026 00:00:00 +0000</pubDate>
    <description>Hello! Recently I needed bike lights for my bike. And I remembered that I already had rechargeable bike lights that I bought ten years ago, that I hadn’t tried in a long time. I tried to recharge them, but after fully charging them, they…</description>
    <content:encoded>&lt;p&gt;Hello! Recently I needed bike lights for my bike. And I remembered that I already had rechargeable bike lights that I bought ten years ago, that I hadn’t tried in a long time. I tried to recharge them, but after fully charging them, they only worked for maybe 5 minutes before they turned off again.&lt;/p&gt;
&lt;p&gt;I don’t know much about electronics, but I’ve been curious about whether it’s possible to fix old electronics for a long time, and this seemed like the perfect repair project because I might just need to replace the battery.&lt;/p&gt;
&lt;p&gt;So I went to the local queer makerspace where I’m a member to use the soldering iron and try to do it! I don’t know much about electronics and this post does not contain any safety advice because I don’t know much about safety. I think it’s nice to do projects in a community space where you can get help.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-1-cut-it-open&quot;&gt;step 1: cut it open&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The bike light felt like it was made of silicone, so I cut open the silicone in a haphazard way along something that vaguely looked like a seam.&lt;/p&gt;
&lt;p&gt;I definitely ripped some silicone in the process and it was pretty messy but I got it open and found the circuit board.&lt;/p&gt;
&lt;p&gt;I don’t know the model number of the bike lights but there’s a photo of them at the end of the post.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-2-remove-the-screws&quot;&gt;step 2: remove the screws&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There were some screws attaching things together so I removed them so I could get the circuit board out.&lt;/p&gt;
&lt;p&gt;Mostly I tried to remove as few screws as possible because I was worried about losing them or not being able to put them back after. I probably put the screws in a bag or something.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-3-get-the-circuit-board-out&quot;&gt;step 3: get the circuit board out&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I took out the circuit board. Here’s what it looked like:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/pcb.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;You can see where the battery is attached, I think it’s left of RI3 and above Q2.&lt;/p&gt;
&lt;p&gt;Here’s what the battery looked like:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/battery.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-4-desolder-the-battery&quot;&gt;step 4: desolder the battery&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’d never desoldered anything before, so I found &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.ifixit.com/Guide/How+To+Solder+and+Desolder+Connections/750#s4052&quot;&gt;the iFixit guide to desoldering&lt;/a&gt; and read it. Also I asked my friends Lee and Lauria for advice.&lt;/p&gt;
&lt;p&gt;Here were the steps I ended up following based on the guide &amp;amp; the advice I got:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a desoldering pump to remove most of the solder&lt;/li&gt;
&lt;li&gt;Once most of it is gone, kind of pull them apart to try to separate them&lt;/li&gt;
&lt;li&gt;Also try to avoid getting the battery too hot in the process by taking breaks to let it cool down. I’m not very good with a soldering iron so it took a while.&lt;/li&gt;
&lt;li&gt;The battery has an attachment that is welded to the top. For a while I thought I needed to remove this and it seemed impossible, but it turned out the replacement battery comes with that part so actually I was supposed to leave it alone.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-5-identify-the-battery&quot;&gt;step 5: identify the battery&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the picture of the battery in Step 3, you can see it says something like “3” and “LI???77”. There’s a piece of metal that I think is welded or something to the top of the battery. It seemed impossible and also maybe not smart to try to remove so I wasn’t sure how to find out what an “LI????77” was or how to order another one.&lt;/p&gt;
&lt;p&gt;I’ve been trying to avoid using LLMs (though I will not get into that because I am exhausted by LLM discourse and I’m sure you are too), but I really had no idea how to figure out what the battery was so I asked an LLM. It gave the response “LIR2477”, which (when I looked it up) looked exactly the same as my battery so I figured that was plausible.&lt;/p&gt;
&lt;p&gt;I would be interested to learn non-LLM ways to figure this out though. There must be a way. Lauria showed me how to use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.digikey.ca/en/products&quot;&gt;DigiKey’s search&lt;/a&gt; which was very cool though DigiKey didn’t have that part.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-6-buy-the-battery&quot;&gt;step 6: buy the battery&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I went to AliExpress and ordered:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;2 batteries (I had 2 bike lights and I wanted to fix them both)&lt;/li&gt;
&lt;li&gt;some silicone glue to glue things back together&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I think the batteries were $3 each and the glue was $8.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#step-7-solder-the-new-batteries-in-and-glue-it-back-together&quot;&gt;step 7: solder the new batteries in and glue it back together&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The parts took maybe 2 weeks to arive, and once they arrived, I went back to the makerspace and:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;soldered in the new batteries&lt;/li&gt;
&lt;li&gt;put the screws back in. The screws were very small and hard to hold, so at this point I dropped some screws on the ground and couldn’t find them because they were too small. So I just used fewer screws and hoped for the best.&lt;/li&gt;
&lt;li&gt;used the glue to try to put everything back together.&lt;/li&gt;
&lt;li&gt;Make a somewhat halfhearted attempt to clamp the parts I was gluing together&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then after waiting some amount of time for the glue to dry I took it home and waited 24 hours for the glue to cure.&lt;/p&gt;
&lt;p&gt;Also I took the old batteries to somewhere nearby that accepts old batteries.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/09/27/replacing-the-old-battery-on-rechargeable-bike-lights/#it-works&quot;&gt;it works!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The lights work! I have used them to bike at night! I still haven’t needed to recharge them (and tragically I had to order a new Mini USB cable because I got rid of all my Mini USB cables, so I’m still waiting for that), so I still don’t know for sure how long the lifetime of the new battery will be.&lt;/p&gt;
&lt;p&gt;Here’s what the light looks like after re-gluing. You can see that I didn’t glue very carefully. It didn’t really go back together that well but I’m hoping it’ll be good enough.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/light-after-1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt; &lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/light-after-2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I thought it was really cool that I was able to do this with extremely minimal electronics skills! It cost about $20 CAD to buy the parts, and (whether or not the repair holds up, I’ll try to update this post in the future!), it was fun to try to repair something and learn something new.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Some more things about Django I&#39;ve been enjoying</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-07-21-some-more-things-about-django-i-ve-been-enjoying/</link>
    <guid isPermaLink="false">urn:aggr:item:345261a9ffd1c9e200b29794f634d25e61a76614</guid>
    <atom:link href="https://jvns.ca/blog/2026/07/21/more-nice-django-things/" rel="via" type="text/html"/>
    <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
    <description>Hello! I’m on a funny journey right now where I’m trying to learn how to make websites in a sort of 2010 style, where I have an SQL database and render some HTML on the backend. It’s kind of an interesting journey because it doesn’t…</description>
    <content:encoded>&lt;p&gt;Hello! I’m on a funny journey right now where I’m trying to learn how to make websites in a sort of 2010 style, where I have an SQL database and render some HTML on the backend.&lt;/p&gt;
&lt;p&gt;It’s kind of an interesting journey because it doesn’t necessarily feel “easy” to me to make websites in this way: I never learned how to do it in the 2000s or 2010s, and there’s a lot I need to learn.&lt;/p&gt;
&lt;p&gt;So here are some Django features that make building this kind of site feel more achievable than when I was trying and failing to use Go’s standard library or Flask. And I’ll talk about a couple of issues with Django I’ve run into.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#why-learn-to-make-websites-like-it-s-2010&quot;&gt;why learn to make websites like it’s 2010?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Previously the toolkit I felt confident with for making websites was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static site generators (like for this blog)&lt;/li&gt;
&lt;li&gt;static sites that do some fun stuff with Javascript (like this &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sql-playground.wizardzines.com/&quot;&gt;sql playground&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;simple Vue.js single page apps with either a Lambda as a backend or a Go backend (like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://messwithdns.net/&quot;&gt;mess with dns&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I really liked this frontend-heavy approach for these super simple applications but when I started thinking about making something with a lot of different pages (instead of literally just one page), I didn’t feel so excited about the options I saw that involved a lot of frontend code. So I figured I’d try the backend.&lt;/p&gt;
&lt;p&gt;Writing a backend-focused site that uses as little JS as possible feels the same to me in a way as writing a single-page JS website that does as little on the backend as possible, even though they might seem like opposites. In both cases I’m just trying to keep as much of the logic as possible in one place.&lt;/p&gt;
&lt;p&gt;Now for some thoughts about Django!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#i-m-enjoying-query-builders&quot;&gt;I’m enjoying query builders&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I learned that I can define a “query set” class in Django with a bunch of methods with different &lt;code&gt;WHERE&lt;/code&gt; statements I might want to use while constructing a query:&lt;/p&gt;
&lt;p&gt;Here’s how I use it in my view code once I’ve defined what all the methods mean:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;Events.objects.approved()
  .for_tab(tab)
  .with_festivals(tab_params.festival_slugs)
  .is_free(tab_params.free)
  .is_outdoors(tab_params.outdoors)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and here’s how I define the methods:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Python&quot;&gt;&lt;span class=&quot;syntax-source syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-class syntax-python&quot;&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-class syntax-python&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-name syntax-class syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;EventQuerySet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-class syntax-inheritance syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-inheritance syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-class syntax-inheritance syntax-python&quot;&gt;&lt;span class=&quot;syntax-entity syntax-other syntax-inherited-class syntax-python&quot;&gt;SearchableQuerySetMixin&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-inheritance syntax-python&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-inherited-class syntax-python&quot;&gt;models&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;QuerySet&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-inheritance syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-class syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-class syntax-begin syntax-python&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-python&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;approved&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-function syntax-begin syntax-python&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-return syntax-python&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;filter&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;approved_at__isnull&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-python&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-constant syntax-language syntax-python&quot;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-python&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;future&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-function syntax-begin syntax-python&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;today&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-python&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;localdate&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-return syntax-python&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;filter&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;end__gt&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-python&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;_midnight&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;today&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-python&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;with_tags&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-parameters syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-parameters syntax-python&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-function syntax-begin syntax-python&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-statement syntax-if syntax-python&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;tags&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-return syntax-python&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-python&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;filter&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-python&quot;&gt;tags__name__in&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-python&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-generic-name syntax-python&quot;&gt;tags&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-python&quot;&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-dot syntax-python&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-python&quot;&gt;distinct&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-return syntax-python&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-qualified-name syntax-python&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-python&quot;&gt;self&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax for defining the filters isn’t my favourite, but I spend most of my time just using the methods, and it feels super readable and nice to use, and it makes me want to look into other query builder libraries in the future. In the past I thought “I know SQL, who needs a query builder?”, but this kind of structure does make it really nice to read.&lt;/p&gt;
&lt;p&gt;I found an example of someone who &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/lemon24/reader/blob/15121f667a6f2e388f0072a3fcd715f533883899/src/reader/_sql_utils.py&quot;&gt;wrote their own small query builder in Python&lt;/a&gt; that I want to read later to think about whether I would enjoy using a more minimal version of this.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#the-template-filters-are-awesome&quot;&gt;the template filters are awesome&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are a bunch of little quality of life &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.djangoproject.com/en/6.0/ref/templates/builtins/&quot;&gt;filters available in Django templates&lt;/a&gt; that are super useful for generating HTML. The ones I’ve used so far are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;translating plain text URLs into links, or line breaks into &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; (&lt;code&gt;{{ event.description|urlize|linebreaksbr }}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;formatting dates (&lt;code&gt;{{ row.date|date:&amp;quot;M j&amp;quot; }}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;json_script&lt;/code&gt;, which takes a Python dictionary and automatically converts it to JSON and inserts it into the HTML as a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in a safe way&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are all small things individually but I feel like it makes a big difference somehow to just have them available.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#querystring-is-cool&quot;&gt;&lt;code&gt;querystring&lt;/code&gt; is cool&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think my favourite template filter is &lt;code&gt;querystring&lt;/code&gt;: in this site sometimes we use filters like &lt;code&gt;?date=2026-06-01&lt;/code&gt; to decide what’s displayed. &lt;code&gt;querystring&lt;/code&gt; that will make a link to the same query string with one change, like this to link to the previous date:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;a href=&amp;quot;{% querystring date=nav.prev_date%}&amp;quot;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or to remove the &lt;code&gt;outdoors&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;a href=&amp;quot;{% querystring outdoors=None %}&amp;quot;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#automatic-database-migrations-are-still-great&quot;&gt;automatic database migrations are still great&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I still really love Django’s automatic database system. It’s amazing to be able to just edit a model to add a new field or whatever, and then Django automatically generates the migration.&lt;/p&gt;
&lt;p&gt;So far we have done 19 database migrations and I think there will probably be more! It makes a huge difference for me to be able to just easily change the database as my understanding of the problem changes.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#i-do-not-want-to-organize-my-code-with-inheritance&quot;&gt;I do not want to organize my code with inheritance&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Django’s documentation sometimes offers the option of using class-based views and inheritance to organize the code in your views. For example I have four views that share a lot of code, and I could use inheritance to manage that by defining some kind of parent class and then having my other views inherit from it.&lt;/p&gt;
&lt;p&gt;I tried it out and I did not enjoy the experience of using inheritance to share code between views. I switched to using functions instead, sort of how &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://spookylukey.github.io/django-views-the-right-way/&quot;&gt;this post&lt;/a&gt; advocates, and that was a lot more straightforward. I’ve never had a good experience using inheritance in Python and I don’t think I’ll try to use it again.&lt;/p&gt;
&lt;p&gt;But I don’t mind using inheritance to use the interfaces Django itself provides: for example if I want to define a query set I need to write something like &lt;code&gt;class EventQuerySet(SearchableQuerySetMixin, models.QuerySet)&lt;/code&gt;. I don’t think too hard about it and it seems to work.&lt;/p&gt;
&lt;p&gt;(as a meta comment: I’ve been working on talking about my programming opinions by just saying “THING does not feel good to me, I prefer OTHER THING instead”. That post I linked to says that function-based views are the “right way”. I’m not very invested in whether it’s “right”, but it’s validating to know that other people feel similarly to me about inheritance)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#i-don-t-know-how-to-think-about-django-performance&quot;&gt;I don’t know how to think about Django performance&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At some point the LLM scrapers discovered our site, and started sending us maybe 10 requests per second. I blocked them which is working for now, but it made me think about what the site’s capacity is. I’m used to writing Go backends where the performance situation is pretty straightforward (usually everything is just fast enough), and a Django site is very different.&lt;/p&gt;
&lt;p&gt;Some light load testing (with (&lt;code&gt;ab -n 1000 -c 1&lt;/code&gt;) shows that right now we can serve about 2-3 requests per second (on a ~$10/month VM).&lt;/p&gt;
&lt;p&gt;It’s tempting for me to go down a rabbit hole where I do a bunch of profiling to figure out what’s slow and try to make it faster (there’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/benfred/py-spy&quot;&gt;py-spy&lt;/a&gt; for that, and py-spy is great and super easy to use, and profiling is fun!) But I really don’t understand what I should expect in terms of performance from a Django site and how I should be thinking about at a higher level.&lt;/p&gt;
&lt;p&gt;Some things I haven’t figured out yet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If I have a site that’s going to be getting occasional bursts of traffic, do I want to be able to scale up?&lt;/li&gt;
&lt;li&gt;Do I want to design the site so that more things can be cached? (and do I really have to? caches are so annoying to get right!)&lt;/li&gt;
&lt;li&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.djangoproject.com/en/6.0/topics/performance/&quot;&gt;django performance docs&lt;/a&gt; say that Jinja is faster for templating, do I want to think about switching templating systems?&lt;/li&gt;
&lt;li&gt;Those docs also say “{% block %} is faster than using {% include %}”, I wonder if it’s a big difference and if so why&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#template-caching-might-be-important&quot;&gt;template caching might be important&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think one thing I’m learning about Django is that because it’s a Framework (tm), it’s easy to accidentally misconfigure it. For example, when I was thinking about why my site was slow just now, I read the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.djangoproject.com/en/6.0/topics/performance/&quot;&gt;django performance docs&lt;/a&gt; and I noticed a comment saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Enabling the cached template loader often improves performance drastically, as it avoids compiling each template every time it needs to be rendered.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When I’d done CPU profiling I’d noticed that it was spending a lot of time rendering templates! Maybe this could help me!&lt;/p&gt;
&lt;p&gt;Clicking through the link, I saw that the cached template loader was supposed to be on by default, but I’d turned it off by accident while trying to do something else. I think this “I turned off the cached template loader by default” things is an example of how I still find the django settings file to be pretty confusing and difficult. I guess I should just be careful when I go in there.&lt;/p&gt;
&lt;p&gt;After turning on template caching, it seems like the site can now pretty easily handle 12 requests per second or so without using all of the CPU. I have not carefully benchmarked the before and after but it seems like it’s made a pretty big difference.&lt;/p&gt;
&lt;p&gt;One thing that’s been surprising to me about Django performance is that I’ve always heard the advice “if you have a performance problem, check your database queries! Maybe add an index!”. But I’ve been running into a variety of performance issues (like this template caching thing) that are not because of slow queries, so instead it’s been more useful for me so far to start by running a CPU profile. And since I’m using SQLite, any slow database query problem will show up on the CPU profile anyway.&lt;/p&gt;
&lt;p&gt;Anyway I don’t want to get too far into site performance. Like I said it’s easy for me to get interested in profiling, but actually I know a lot about profiling and it’s not the most important thing for me to learn about.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/21/more-nice-django-things/#that-s-all-for-now&quot;&gt;that’s all for now!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I might say more about what I’m enjoying (or having a hard time with!) about Django later. Trying to write some shorter blog posts recently.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Learning a few things about running SQLite</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-07-17-learning-a-few-things-about-running-sqlite/</link>
    <guid isPermaLink="false">urn:aggr:item:35e43ad6c904d81468fa2e2857a994278fa4d5e1</guid>
    <atom:link href="https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/" rel="via" type="text/html"/>
    <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
    <description>Hello! I’ve been working on a Django site recently, and I decided to use SQLite as the database. When I was getting started with using SQLite as database for a website I read a bunch of blog posts about how it is totally fine to use SQLite…</description>
    <content:encoded>&lt;p&gt;Hello! I’ve been working on a Django site recently, and I decided to use SQLite as the database. When I was getting started with using SQLite as database for a website I read a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://alldjango.com/articles/definitive-guide-to-using-django-sqlite-in-production&quot;&gt;bunch&lt;/a&gt; of blog posts about how it is totally fine to use SQLite in production for a small site and I think it &lt;em&gt;is&lt;/em&gt; totally fine, but what I did not fully appreciate is that SQLite is still a database, databases are complicated, and I do not know a lot about operating databases.&lt;/p&gt;
&lt;p&gt;So here are a couple of small things I’ve been learning about running SQLite. This is the 4th website I’ve used SQLite for, and I think this one is harder because with the power of the Django ORM I’ve been making the database do more work than I was previously without Django.&lt;/p&gt;
&lt;p&gt;I started by turning on WAL mode like all the blog posts said to do and hoping for the best.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#analyze-is-apparently-important&quot;&gt;&lt;code&gt;ANALYZE&lt;/code&gt; is apparently important&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Today I was running a query (using &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.sqlite.org/fts5.html&quot;&gt;SQLite’s FTS5&lt;/a&gt; for full-text search) on a table with 4000 rows and it took 5 seconds. That seemed wrong to me: computers are fast!&lt;/p&gt;
&lt;p&gt;It turned out that what I needed to do was to run &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sqlite.org/lang_analyze.html&quot;&gt;&lt;code&gt;ANALYZE&lt;/code&gt;&lt;/a&gt;! Immediately the problem query went from taking 5 seconds to like 0.05 seconds (or some other number small enough that I didn’t care to investigate further). I still don’t know exactly what went wrong in the query plan, but my best guess is that it was some sort of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://accidentallyquadratic.tumblr.com/&quot;&gt;accidentally quadratic&lt;/a&gt; thing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ANALYZE&lt;/code&gt; generates “statistics” (I guess about the number of rows in each table? and presumably other things?) so that the query planner can make better choices.&lt;/p&gt;
&lt;p&gt;Maybe one day I’ll learn to read a query plan.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#cleaning-up-the-database-is-tricky&quot;&gt;cleaning up the database is tricky&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Occasionally I’ve run into situations where I accidentally put a bunch of rows in my database that I don’t want to be there (for example completed tasks from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/RealOrangeOne/django-tasks-db&quot;&gt;django-tasks-db&lt;/a&gt;), and I want to clean them up.&lt;/p&gt;
&lt;p&gt;What’s happened to me a few times in this case is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I run some kind of command to clean up the rows&lt;/li&gt;
&lt;li&gt;The command takes more than 5 seconds, since there are a lot of rows (though I still have some questions about why these DELETE statements are so slow honestly, maybe there’s a bunch of Python code running inside a transaction, I’m not sure)&lt;/li&gt;
&lt;li&gt;One of the other workers tries to write the database while this is happening, and times out after 5 seconds (I have a timeout of 5 seconds set)&lt;/li&gt;
&lt;li&gt;The worker crashes because it couldn’t write to the database and the VM shuts down&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My approach so far has been to just do these cleanup operations in small batches so that I don’t need to do database queries that take more than 5 seconds to run. This whole experience has given me more of an appreciation for why someone might want to use a “real” database like Postgres which can have more than one writer at the same time though.&lt;/p&gt;
&lt;p&gt;Maybe in the future I’ll just take the site down for scheduled maintenance instead when I need to do this kind of thing, but I haven’t figured out a workflow for that yet.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#no-notes-on-performance-of-orm-queries-yet&quot;&gt;no notes on performance of ORM queries yet&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So far I’ve been using Django’s ORM to make any query I want without paying any attention at all to query performance and it’s mostly been going okay other than the &lt;code&gt;ANALYZE&lt;/code&gt; thing. The database is pretty small (maybe 10000 rows?) and I expect it to stay pretty small forever, so I’m hoping that that plan will keep working.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#backing-up-sqlite&quot;&gt;backing up sqlite&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’ve done SQLite backups a couple of ways. I don’t think I’ve actually tested restoring from my backups but I do usually try to monitor them with a dead man’s switch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;way 1: restic&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;sqlite3 /data/calendar.db &amp;quot;VACUUM INTO &amp;#39;/tmp/calendar.sqlite&amp;#39;&amp;quot;
gzip /tmp/calendar.sqlite

# Upload backup to S3
# Sometimes the backup gets OOM killed and so it stays locked, do an unlock
restic -r s3://s3.amazonaws.com/some_bucket/ unlock
# Do the backup &amp;amp; prune old backups
restic -r s3://s3.amazonaws.com/some_bucket/ backup /tmp/calendar.sqlite.gz
restic -r s3://s3.amazonaws.com/some_bucket/ snapshots
restic -r s3://s3.amazonaws.com/some_bucket/ forget -l 1 -H 6 -d 2 -w 2 -m 2 -y 2
restic -r s3://s3.amazonaws.com/some_bucket/ prune
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;way 2: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://litestream.io/&quot;&gt;litestream&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I started trying out Litestream recently because I felt like doing incremental backups might be more efficient: my restic backups were sometimes getting OOM killed, and I was a bit tired of it. Basically I just write a config file and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;litestream replicate -config litestream.yml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I set &lt;code&gt;retention: 400h&lt;/code&gt; in my config file in an attempt to retain some amount of history of the database but I have no idea if it works.&lt;/p&gt;
&lt;p&gt;I’ve been backing up to AWS, which is always a pain because it’s annoying to navigate the AWS console to generate credentials. Maybe one day I’ll move away to some other S3-compatible alternative.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#you-can-use-multiple-databases&quot;&gt;you can use multiple databases&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My current project only has one database, but one trick I used with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://messwithdns.net/&quot;&gt;Mess with DNS&lt;/a&gt; was to split the tables into three separate database files because I didn’t actually need my tables to be in the same db. I think it was helpful.&lt;/p&gt;
&lt;p&gt;Mess with DNS has been running on SQLite for 4 years now (since 2022) and it’s been great, I think the move from Postgres was a great choice for that project.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#that-s-all&quot;&gt;that’s all!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It’s always kind of fun to see how long it takes me to learn sort of basic things about the technologies I’m using. I think I used SQLite for a web project for the first time in 2022 and I only learned that &lt;code&gt;ANALYZE&lt;/code&gt; existed today! I imagine in a year or two I’ll be learning about some other very basic feature.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/07/17/learning-about-running-sqlite/#some-references&quot;&gt;some references&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some blog posts I’ve looked at, other than the official docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://alldjango.com/articles/definitive-guide-to-using-django-sqlite-in-production&quot;&gt;The definitive guide to using Django with SQLite in production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/phiresky/978d8e204f77feaa0ab5cca08d2d5b27&quot;&gt;a gist on sqlite performance tuning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Moving away from Tailwind, and learning to structure my CSS</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-15-moving-away-from-tailwind-and-learning-to-structure-my-css/</link>
    <guid isPermaLink="false">urn:aggr:item:b12e25cd3459fda90521d316b91772bcdd177a5b</guid>
    <atom:link href="https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/" rel="via" type="text/html"/>
    <pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate>
    <description>Hello! 8 years ago, I wrote excitedly about discovering Tailwind. At that time I really had no idea how to structure my CSS code and given the choice between a pile of complete chaos and Tailwind, I was really happy to choose Tailwind. It…</description>
    <content:encoded>&lt;p&gt;Hello! 8 years ago, I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2018/11/01/tailwind--write-css-without-the-css/&quot;&gt;wrote excitedly about discovering Tailwind&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At that time I really had no idea how to structure my CSS code and given the choice between a pile of complete chaos and Tailwind, I was really happy to choose Tailwind. It helped me make a lot of tiny sites!&lt;/p&gt;
&lt;p&gt;I spent the last week or so migrating a couple of sites away from Tailwind and towards more semantic HTML + vanilla CSS, and it was SO fun and SO interesting, so here are some things I learned!&lt;/p&gt;
&lt;p&gt;As usual I’m not a full-time frontend developer and so all of my CSS learning has happened in fits and starts over many years.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#it-turns-out-tailwind-taught-me-a-lot&quot;&gt;it turns out Tailwind taught me a lot&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When I started thinking about structuring CSS, I was intimidated at first: I’m not very good at structuring my CSS! But then I started reading blog posts talking about how to structure CSS (like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.miriamsuzanne.com/2022/09/06/layers/&quot;&gt;A whole cascade of layers&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jacobb.nyc/writing/how-i-write-css-in-2024&quot;&gt;How I write CSS in 2024&lt;/a&gt;) and I realized a couple of things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every CSS code base has a bunch of different things going on (layouts! fonts! colours! common components!)&lt;/li&gt;
&lt;li&gt;It’s extremely useful to have systems or guidelines to manage each of those things, otherwise things descend into chaos&lt;/li&gt;
&lt;li&gt;Tailwind has systems for some of these, and I already know those systems! Maybe I can imitate the systems I like!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, Tailwind has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a reset stylesheet&lt;/li&gt;
&lt;li&gt;a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/04/css-colour-palettes/&quot;&gt;colour palette&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://v2.tailwindcss.com/docs/font-size&quot;&gt;font scale&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#the-systems-i-m-going-to-talk-about&quot;&gt;the systems I’m going to talk about&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’m going to talk about a few aspects of my CSS codebase and my thoughts so far what kind of rules I want to impose on the codebase for each one. Some of them are copied from Tailwind and some aren’t.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;reset&lt;/li&gt;
&lt;li&gt;components&lt;/li&gt;
&lt;li&gt;colours&lt;/li&gt;
&lt;li&gt;font sizes&lt;/li&gt;
&lt;li&gt;utility classes&lt;/li&gt;
&lt;li&gt;the base&lt;/li&gt;
&lt;li&gt;spacing&lt;/li&gt;
&lt;li&gt;responsive design&lt;/li&gt;
&lt;li&gt;the build system&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#1-reset&quot;&gt;1. reset&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I just copied Tailwind’s “&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://v2.tailwindcss.com/docs/preflight&quot;&gt;preflight styles&lt;/a&gt;” by going into &lt;code&gt;tailwind.css&lt;/code&gt; and copying the first 200 lines or so.&lt;/p&gt;
&lt;p&gt;I noticed that I’ve developed a relationship with Tailwind’s CSS reset over time, for example Tailwind sets &lt;code&gt;box-sizing: border-box&lt;/code&gt; on every element (which means that an element’s width includes its padding and border):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;* { box-sizing: border-box; }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think it would be a real adjustment for me to switch to writing CSS without these, and I’m sure there are lots of other things in the Tailwind reset (like &lt;code&gt;html {line-height: 1.5;}&lt;/code&gt;) that I’m subconsciously used to and don’t even realize are there.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#2-components&quot;&gt;2. components&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This next part is the bulk of the CSS!&lt;/p&gt;
&lt;p&gt;The idea here is to organize CSS by “components”, in a way that’s spiritually related to Vue or React components. (though there might not actually be any Javascript at all in the site)&lt;/p&gt;
&lt;p&gt;Basically the idea is that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Each “component” has a unique class&lt;/li&gt;
&lt;li&gt;The CSS for one component never overrides the CSS for any other component&lt;/li&gt;
&lt;li&gt;Each component has its own CSS file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So editing the CSS for one component won’t mysteriously break something in another component. And probably like 80% of the CSS that I would actually want to change is in various component files, so if I’m editing a 100-line component, I just have to think about those 100 lines. It’s way easier for me to think about.&lt;/p&gt;
&lt;p&gt;For example, this HTML might be the &lt;code&gt;.zine&lt;/code&gt; “component”.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;figure class=&amp;quot;zine horizontal&amp;quot;&amp;gt;
    &amp;lt;img src=&amp;quot;whatever.jpg&amp;quot;&amp;gt;
&amp;lt;/figure&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the CSS looks something like this, using nested selectors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;.zine {
  ...
  &amp;amp;.horizontal {
    ...
  }
  &amp;amp;.vertical {
    ...
  }
  &amp;amp;:hover {
    ...
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I haven’t done anything programmatic (like web components or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope&quot;&gt;@scope&lt;/a&gt;) that ensures that components won’t interfere with each other, but just having a convention and trying my best already feels like a big improvement.&lt;/p&gt;
&lt;p&gt;Next: conventions to maintain some consistency across the site and keep these components in line with each other!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#3-colours&quot;&gt;3. colours&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;colours.css&lt;/code&gt; has a bunch of variables like this which I can use as necessary. Colour is really hard and I didn’t want to revisit my use of colour in this refactor, so I left this alone.&lt;/p&gt;
&lt;p&gt;The only guideline I’m trying to enforce here is that all colours used in the site are listed in this file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;:root {
  --pink: #fea0c2;
  --pink-light: #F9B9B9;
  --red: #f91a55;
  --orange: rgb(222, 117, 31);
  ...
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#4-font-sizes&quot;&gt;4. font sizes&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One thing I appreciated about Tailwind was that if I wanted to set a font size, I could just think “hm, I want the text to be big”, write &lt;code&gt;text-lg&lt;/code&gt;, and be done with it! And maybe if it’s not big enough I’d use &lt;code&gt;xl&lt;/code&gt; or &lt;code&gt;2xl&lt;/code&gt; instead. No trying to remember whether I’m using &lt;code&gt;em&lt;/code&gt; or &lt;code&gt;px&lt;/code&gt; or &lt;code&gt;rem&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So I defined a bunch of variables, taken from Tailwind, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;  --size-xs: 0.75rem;
  --line-height-xs: 1rem;

  --size-sm: 0.875rem;
  --line-height-sm: 1.25rem;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then if I want to set a font size, I can do it like this. It’s a little more verbose than Tailwind but I’m happy with it for now.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;h3 {
  font-size: var(--size-lg);
  line-height: var(--line-height-lg);
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#5-utilities&quot;&gt;5. utilities&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are some things like buttons that appear in many different components. I’m calling these “utilities”.&lt;/p&gt;
&lt;p&gt;I copied some utility classes from Tailwind (like &lt;code&gt;.sr-only&lt;/code&gt; for things that should only appear for screenreader users).&lt;/p&gt;
&lt;p&gt;This section is pretty small and I try to be careful about making changes here.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#6-the-base&quot;&gt;6. the base&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;“base” styles are styles that apply across the whole site that I chose myself. I have to keep this section really small because I’m not confident enough to enforce a lot of styles across the whole site. These are the only two I feel okay about right now, and I might change the &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;CSS&quot;&gt;&lt;span class=&quot;syntax-source syntax-css&quot;&gt;&lt;span class=&quot;syntax-comment syntax-block syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-css&quot;&gt;/*&lt;/span&gt; put a 950px column in the middle of each &amp;lt;section&amp;gt; &lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-css&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-selector syntax-css&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-css&quot;&gt;section&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-list syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-property-list syntax-css&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-property-name syntax-css&quot;&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-custom-property syntax-css&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-name syntax-css&quot;&gt;inner-width&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-css&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt;&lt;span class=&quot;syntax-constant syntax-numeric syntax-css&quot;&gt;950&lt;span class=&quot;syntax-keyword syntax-other syntax-unit syntax-css&quot;&gt;px&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-rule syntax-css&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-property-name syntax-css&quot;&gt;&lt;span class=&quot;syntax-support syntax-type syntax-property-name syntax-css&quot;&gt;padding&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-css&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt;&lt;span class=&quot;syntax-constant syntax-numeric syntax-css&quot;&gt;3&lt;span class=&quot;syntax-keyword syntax-other syntax-unit syntax-css&quot;&gt;rem&lt;/span&gt;&lt;/span&gt; max(&lt;span class=&quot;syntax-constant syntax-numeric syntax-css&quot;&gt;1&lt;span class=&quot;syntax-keyword syntax-other syntax-unit syntax-css&quot;&gt;rem&lt;/span&gt;&lt;/span&gt;, (&lt;span class=&quot;syntax-constant syntax-numeric syntax-css&quot;&gt;100&lt;span class=&quot;syntax-keyword syntax-other syntax-unit syntax-css&quot;&gt;%&lt;/span&gt;&lt;/span&gt; - &lt;span class=&quot;syntax-meta syntax-function-call syntax-css&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-var syntax-css&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-group syntax-begin syntax-css&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-custom-property syntax-css&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-name syntax-css&quot;&gt;inner-width&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-group syntax-end syntax-css&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;)/2);
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-property-list syntax-css&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-selector syntax-css&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-css&quot;&gt;a&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-list syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-property-list syntax-css&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-property-name syntax-css&quot;&gt;&lt;span class=&quot;syntax-support syntax-type syntax-property-name syntax-css&quot;&gt;color&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-css&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property-value syntax-css&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-css&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-var syntax-css&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-group syntax-begin syntax-css&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-custom-property syntax-css&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-custom-property syntax-name syntax-css&quot;&gt;orange&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-css&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-group syntax-end syntax-css&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-rule syntax-css&quot;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-property-list syntax-css&quot;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think for the base styles it’s going to be easiest for me to work kind of bottom up – first start with almost nothing in the base styles, and then move some styles from the components into base styles as I identify common things I want.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#7-spacing&quot;&gt;7. spacing&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I haven’t completely worked out an approach to managing padding and margins yet. I’m definitely trying to be more principled than how I was doing it in Tailwind though, where I would just haphazardly put padding and margins everywhere until it looked the way I wanted.&lt;/p&gt;
&lt;p&gt;Right now I’m working towards making the outer layout components in charge of spacing as much as possible. For example if I have a &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; with a bunch of children that I want to have space between them, I might use this to space the children evenly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;section &amp;gt; *+* {
  margin-top: 1rem;
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some inspiration blog posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://piccalil.li/blog/my-favourite-3-lines-of-css/&quot;&gt;the owl selector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://kyleshevlin.com/no-outer-margin/&quot;&gt;“no outer margin”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#8-responsive-design-use-more-grid&quot;&gt;8. responsive design: use more grid!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The way I was doing responsive design in Tailwind was to use a lot of media queries. Tailwind has this &lt;code&gt;md:text-xl&lt;/code&gt; syntax that means “apply the &lt;code&gt;text-xl&lt;/code&gt; style at sizes &lt;code&gt;md&lt;/code&gt; or larger”.&lt;/p&gt;
&lt;p&gt;I’m trying something pretty different now, which is to make more flexible CSS grid layouts that don’t need as many breakpoints. This is hard but it’s really interesting to learn about what’s possible with grid, and it’s a good example of something that I don’t think is possible with Tailwind.&lt;/p&gt;
&lt;p&gt;For example, I’ve been learning about how to use &lt;code&gt;auto-fit&lt;/code&gt; to automatically use 2 columns on a big screen and 1 column on a small screen like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), max-content));
  justify-content: center;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also used &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/comics/css-grid-areas/&quot;&gt;&lt;code&gt;grid-template-areas&lt;/code&gt;&lt;/a&gt; a lot which is an amazing feature that I don’t think you can use with Tailwind.&lt;/p&gt;
&lt;p&gt;Some inspiration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://css-tricks.com/a-responsive-grid-layout-with-no-media-queries/&quot;&gt;A responsive grid layout with no media queries&lt;/a&gt; from CSS Tricks&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#9-the-build-system-esbuild&quot;&gt;9. the build system: esbuild&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In development, I don’t need a build system: CSS now has both built in import statements, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;@import &amp;quot;reset.css&amp;quot;;
@import &amp;quot;typography.css&amp;quot;;
@import &amp;quot;colors.css&amp;quot;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and built in nested selectors, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;.page {
  h2 { ...}
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I want, I can use &lt;code&gt;esbuild&lt;/code&gt; to bundle the CSS file for production. That looks something like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;esbuild style.css --bundle --loader:.svg=dataurl  --loader:.woff2=file --outfile=/tmp/out.css
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though I usually avoid using CSS and JS build systems, I don’t mind using esbuild (which I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2021/11/15/esbuild-vue/&quot;&gt;wrote about in 2021 here&lt;/a&gt;) because it’s based on web standards and because it’s a static Go binary.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#why-migrate-away-from-tailwind&quot;&gt;why migrate away from Tailwind?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A few people asked why I was migrating away from Tailwind. A few factors that contributed are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tailwind has become much more reliant on a build system since 2018, I think it’s impossible (?) to use newer versions of Tailwind without using a build system. So I’ve been using Tailwind v2 for years. (there’s also &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://litewindcss.com/&quot;&gt;litewind&lt;/a&gt; apparently)&lt;/li&gt;
&lt;li&gt;It’s always been true that you’re supposed to use Tailwind with a build system, but I’ve never really done that, so I have 2.8MB &lt;code&gt;tailwind.min.css&lt;/code&gt; files (270K gzipped) in a lot of my projects and it feels a little silly.&lt;/li&gt;
&lt;li&gt;I’m a lot better at CSS than I was when I started using Tailwind&lt;/li&gt;
&lt;li&gt;Ultimately Tailwind is limiting: if you want to do Weird Stuff in your CSS, it’s not always possible with Tailwind. Those limits can be extremely useful (a lot of this post is about me reimplementing some of Tailwind’s limits!) but at this point I’d like to be able to pick and choose.&lt;/li&gt;
&lt;li&gt;I ended up with sites that mixed both vanilla CSS and Tailwind in the same project and that was not fun to maintain&lt;/li&gt;
&lt;li&gt;I got curious about what writing more semantic HTML would feel like.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#css-features-i-m-curious-about&quot;&gt;CSS features I’m curious about&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;While doing this I learned about a lot of CSS features that I didn’t use but am curious about learning about one day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@layer&lt;/code&gt; (from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.miriamsuzanne.com/2022/09/06/layers/&quot;&gt;A Whole Cascade of Layers&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope&quot;&gt;@scope&lt;/a&gt;) (especially &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://drafts.csswg.org/css-cascade-6/#example-463550a5&quot;&gt;this example of how to use @scope in a “component” CSS design from the specification!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries&quot;&gt;container queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Subgrid&quot;&gt;subgrid&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#one-last-reason-i-moved-away-from-tailwind&quot;&gt;one last reason I moved away from Tailwind&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’ve been talking a lot in this post about what I learned from using Tailwind, and that’s all true.&lt;/p&gt;
&lt;p&gt;But I read this post 3 years ago called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://thoughtbot.com/blog/tailwind-and-the-femininity-of-css&quot;&gt;Tailwind and the Femininity of CSS&lt;/a&gt; that really stuck with me. I honestly probably started out with an attitude towards CSS a little like that post describes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;They’ve heard it’s simple, so they assume it’s easy. But then when they try to use it, it doesn’t work. It must be the fault of the language, because they know that they are smart, and this is supposed to be easy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But in the last 10 years I’ve learned to really love and respect CSS as a technology.&lt;/p&gt;
&lt;p&gt;So I decided years ago that I wanted to react to “CSS is hard” by getting better at CSS and taking it seriously as a technology, instead of devaluing it. Doing that changed everything for me: I learned that so many of my frustrations (“centering is impossible”) had been addressed in CSS a long time ago, and that also what “centering” means is not always straightforward and it makes sense that there are many ways to do it. CSS is hard because it’s solving a hard problem!&lt;/p&gt;
&lt;p&gt;I’ve been so impressed by the new CSS features that have been built in the last 10-15 years (some of which I’ve talked about in this post!) and how they make it easier to use CSS, and spending the time to improve my CSS skills has been a really cool experience.&lt;/p&gt;
&lt;p&gt;And that post made me feel like Tailwind contributes to the devaluing of CSS expertise, and like that’s not something I want to be a part of, even if Tailwind has been a useful tool for me personally. Especially in this time of LLMs where it feels more important than ever to value humans’ expertise.&lt;/p&gt;
&lt;p&gt;Another blog post criticizing Tailwind that influenced me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://joshcollinsworth.com/blog/tailwind-is-smart-steering&quot;&gt;Classic rock, Mario Kart, and why we can’t agree on Tailwind&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/15/moving-away-from-tailwind--and-learning-to-structure-my-css-/#that-s-all-for-now&quot;&gt;that’s all for now!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Thanks to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://melody.dev/&quot;&gt;Melody Starling&lt;/a&gt; who originally designed and wrote the CSS for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com&quot;&gt;wizardzines.com&lt;/a&gt;, everything cool and fun about the site is thanks to Melody.&lt;/p&gt;
&lt;p&gt;Also I read so many incredible blog posts about CSS while working on this (from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://css-tricks.com/&quot;&gt;CSS Tricks&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.smashingmagazine.com/&quot;&gt;Smashing Magazine&lt;/a&gt;, and more), I’ve tried to link some of them throughout this post and I really appreciate how much folks in the CSS community share their practices.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Links to CSS colour palettes</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-04-links-to-css-colour-palettes/</link>
    <guid isPermaLink="false">urn:aggr:item:bb03cd9ff82ea660ea5f474262fefcc62e589f49</guid>
    <atom:link href="https://jvns.ca/blog/2026/05/04/css-colour-palettes/" rel="via" type="text/html"/>
    <pubDate>Mon, 4 May 2026 00:00:00 +0000</pubDate>
    <description>A while back I decided to stop using Tailwind for new projects and to just write vanilla CSS instead. But one thing I missed about Tailwind was the colour palette (here as CSS). If I wanted a light blue I could just use blue-100 and if I…</description>
    <content:encoded>&lt;p&gt;A while back I decided to stop using Tailwind for new projects and to just write vanilla CSS instead.&lt;/p&gt;
&lt;p&gt;But one thing I missed about Tailwind was the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://v2.tailwindcss.com/docs/customizing-colors#color-palette-reference&quot;&gt;colour palette&lt;/a&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/jvns/9e59b2cd1fe12601084ba78dded072fe&quot;&gt;here as CSS&lt;/a&gt;). If I wanted a light blue I could just use &lt;code&gt;blue-100&lt;/code&gt; and if I didn’t like it maybe try &lt;code&gt;blue-200&lt;/code&gt; or &lt;code&gt;blue-50&lt;/code&gt;. I’m not very good with colours so it makes a big difference to me to have a reasonable colour palette that somebody who is better at colour than me has thought about.&lt;/p&gt;
&lt;p&gt;But I’m also a little tired of those Tailwind colours, so I asked on Mastodon today what other colour palettes were out there. And then a friend said they wanted links to those colour palettes, so here’s a blog post so my friend can see them, and all the rest of you too :)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/04/css-colour-palettes/#my-favourites&quot;&gt;my favourites&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The ones I liked the most were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://uchu.style/&quot;&gt;uchū&lt;/a&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://code.webb.page/nevercease/uchu.git/tree/dist/uchu.css&quot;&gt;css file&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://code.webb.page/nevercease/uchu.git/about/documentation/FAQ.md&quot;&gt;FAQ&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://stephango.com/flexoki&quot;&gt;flexoki&lt;/a&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/kepano/flexoki/blob/main/css/flexoki.css&quot;&gt;css file&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.reasonable.work/artifacts/ra005-reasonable-colors/&quot;&gt;reasonable colours&lt;/a&gt;, which seems to have a focus on accessibility (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/matthewhowell/reasonable-colors/blob/master/reasonable-colors.css&quot;&gt;css file&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/04/css-colour-palettes/#more-colour-palettes&quot;&gt;more colour palettes&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://webawesome.com/docs/color-palettes&quot;&gt;web awesome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.radix-ui.com/colors/docs/palette-composition/scales&quot;&gt;radix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://designsystem.digital.gov/design-tokens/color/system-tokens/&quot;&gt;US web design systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://m2.material.io/design/color/the-color-system.html&quot;&gt;material design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/04/css-colour-palettes/#colourscheme-generators&quot;&gt;colourscheme generators&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Folks also linked to a bunch of colour palette generators&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://harmonizer.evilmartians.com/&quot;&gt;harmonizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.tints.dev/&quot;&gt;tints.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://coolors.co/&quot;&gt;coolors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://colorpalette.pro/&quot;&gt;colorpalette.pro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve always found these types of generators too hard to use but maybe one day I will get better enough at colour that I’m able to use a colour palette generator successfully so I’ll leave those links there anyway.&lt;/p&gt;
&lt;p&gt;and more colour tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.colorhexa.com/E97339&quot;&gt;colorhexa&lt;/a&gt; has some info about colorblindness&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/04/css-colour-palettes/#oklch&quot;&gt;&lt;code&gt;oklch&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gomakethings.com/generative-colors-with-css/&quot;&gt;Generative colors with CSS&lt;/a&gt; gives an example of how to use the &lt;code&gt;oklch&lt;/code&gt; CSS function to dynamically generate colors.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Testing Vue components in the browser</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-05-02-testing-vue-components-in-the-browser/</link>
    <guid isPermaLink="false">urn:aggr:item:ba0256e658a06abb3af18997aafbad693de1223c</guid>
    <atom:link href="https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/" rel="via" type="text/html"/>
    <pubDate>Sat, 2 May 2026 00:00:00 +0000</pubDate>
    <description>Hello! One of my long term projects on here is figuring out how to write frontend Javascript without using Node or any other server JS runtime. One issue I run into a lot in my frontend JS projects is that I don’t know how to write tests…</description>
    <content:encoded>&lt;p&gt;Hello! One of my long term projects on here is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/#javascript&quot;&gt;figuring out how to write frontend Javascript without using Node&lt;/a&gt; or any other server JS runtime.&lt;/p&gt;
&lt;p&gt;One issue I run into a lot in my frontend JS projects is that I don’t know how to write tests for them. I’ve tried to use Playwright in the past, but it felt slow and unwieldy to be starting these new browser processes all the time, and it involved some Node code to orchestrate the tests.&lt;/p&gt;
&lt;p&gt;The result is that I just don’t test my frontend code which doesn’t feel great. Usually I don’t update my projects much either so it doesn’t come up that much, but it would be nice to be able to make changes with more confidence! So a way to do frontend testing that I like has been on my wishlist for a long time.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#idea-just-run-the-tests-in-the-browser-tab&quot;&gt;idea: just run the tests in the browser tab&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Alex Chan wrote a great post a while back called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://alexwlchan.net/2023/testing-javascript-without-a-framework/&quot;&gt;Testing JavaScript without a (third-party) framework&lt;/a&gt; in response to one of my previous posts in this series that explained how to write a tiny unit-testing framework that runs in a page in browser.&lt;/p&gt;
&lt;p&gt;I loved this post at the time, but it only talked about unit testing and I wanted to write end-to-end integration tests for my Vue components, and I didn’t know how to do that.&lt;/p&gt;
&lt;p&gt;So when I was talking to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://bsky.app/profile/polotek.bsky.social&quot;&gt;Marco&lt;/a&gt; the other day and he said something like “you know, you can just run tests for your Vue components in the browser”, I thought “hey, I should try that again!!!”&lt;/p&gt;
&lt;p&gt;I just did all of this yesterday so certainly there’s a lot to improve but I wanted to write down a few things I noticed about the process before I forget.&lt;/p&gt;
&lt;p&gt;This was a bit tricky for me because the Vue site usually assumes that you’re using Node as part of your build process in some way (there’s a lot of “step 1: &lt;code&gt;npm install THING&lt;/code&gt;), and I didn’t want to use Node/Deno/etc. But it turned out to not be too complicated.&lt;/p&gt;
&lt;p&gt;The project I’m going to talk about testing is this &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2023/03/31/zine-feedback-site/&quot;&gt;zine feedback site I wrote in 2023&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#the-test-framework-qunit&quot;&gt;the test framework: QUnit&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I used &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://qunitjs.com/&quot;&gt;QUnit&lt;/a&gt;. It worked great but I don’t have anything interesting to say about how it works so I’ll leave it at that. I think that Alex’s “write your own test framework” approach would have worked too. I followed &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://qunitjs.com/browser/&quot;&gt;these directions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I did appreciate that QUnit has a “rerun test” button that will only rerun 1 test. Because there are so many network requests in my tests, having a way to run just 1 test makes it a lot less confusing to debug the test.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-1-set-up-the-component-for-testing&quot;&gt;step 1: set up the component for testing&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The first thing I needed to do was get my Vue components set up in the test environment.&lt;/p&gt;
&lt;p&gt;I changed my main app to put all my components in &lt;code&gt;window._components&lt;/code&gt;, kind of like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;const components = {
  &amp;#39;Feedback&amp;#39;: FeedbackComponent,
  ...
}
window._components = components;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I was able to write a &lt;code&gt;mountComponent&lt;/code&gt; function which does basically exactly the same thing my normal main app does (render a tiny template with the component I want to use). The only differences are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I can optionally pass some some extra data to use as its props.&lt;/li&gt;
&lt;li&gt;It mounts the component to a temporary invisible div which will get removed from the DOM after the test is done. The div is positioned off the page (&lt;code&gt;position: absolute; top: -10000, ...&lt;/code&gt;) so you can’t see it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s what using the &lt;code&gt;mountComponent&lt;/code&gt; function looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;const {div} = mountComponent(
  &amp;#39;&amp;lt;Page :feedbacks=&amp;quot;feedbacks&amp;quot; id=2 /&amp;gt;&amp;#39;,
  {feedbacks: [testFeedback]},
);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and here’s the code for it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JavaScript&quot;&gt;&lt;span class=&quot;syntax-source syntax-js&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-js&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-js&quot;&gt;mountComponent&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-begin syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-function syntax-js&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-parameter syntax-function syntax-js&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-function syntax-js&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-end syntax-js&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-js&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;Vue&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;createApp&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-object-literal syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-object-literal syntax-key syntax-js&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-js&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-meta syntax-object-literal syntax-key syntax-js&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-js&quot;&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-js&quot;&gt;:&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-begin syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-end syntax-js&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt; &lt;/span&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-arrow syntax-js&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-for syntax-js&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-loop syntax-js&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-sequence syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-brackets syntax-js&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-brackets syntax-js&quot;&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-js&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;syntax-support syntax-class syntax-builtin syntax-js&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-object syntax-dom syntax-js&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property syntax-object syntax-js&quot;&gt;_components&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;v&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-js&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-support syntax-type syntax-object syntax-dom syntax-js&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-dom syntax-js&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;qunit-fixture&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
             &lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-dom syntax-js&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-type syntax-object syntax-dom syntax-js&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-dom syntax-js&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;div&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-keyword syntax-control syntax-flow syntax-js&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is a div where I can programmatically click, fill in form data, check that the right content appears, etc.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-2-add-some-fixture-data&quot;&gt;step 2: add some fixture data&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Because I was writing end-to-end integration tests to make sure my client JS worked properly with my server, I needed to have some test data in my database. So I wrote ~25 lines of SQL to set up some test data in my database, and added an endpoint to my dev server to run the SQL to reset the test data to a known state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;async function reset() {
    return fetch(&amp;#39;/api/reset_test_data&amp;#39;, {method: &amp;quot;POST&amp;quot;})
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I just run &lt;code&gt;await reset()&lt;/code&gt; at the beginning of any test that needs the test data.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;reset()&lt;/code&gt; function actually doesn’t always totally reset everything which is kind of bad, but it was workable to start with and can always be improved.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#step-3-a-basic-test&quot;&gt;step 3: a basic test&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s what a basic test looks like! Basically we’re rendering the div and make sure it contains some approximately correct data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JavaScript&quot;&gt;&lt;span class=&quot;syntax-source syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;QUnit&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;renders feedback content&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-js&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-begin syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-function syntax-js&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-end syntax-js&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-object-literal syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-js&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;mountComponent&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&amp;lt;Page :feedbacks=&amp;quot;feedbacks&amp;quot; id=2 image=2 page_hash=2 /&amp;gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-object-literal syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-object-literal syntax-key syntax-js&quot;&gt;feedbacks&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-js&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-sequence syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-brackets syntax-js&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;testFeedback&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-brackets syntax-js&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt;
  &lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-property syntax-object syntax-js&quot;&gt;textContent&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;loved this section&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are all the basic pieces! Now here are a few issues I ran into along the way&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#waiting-for-parts-of-the-page-to-render&quot;&gt;waiting for parts of the page to render&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I have a lot of network requests in my tests, and it takes time for them to finish and for the Vue code to do what it has to do with the results and update the DOM.&lt;/p&gt;
&lt;p&gt;I think we all learned a long time ago that putting random &lt;code&gt;sleep()&lt;/code&gt; calls in your tests and hoping that the timings are right is slow and flaky and extremely frustrating, so I needed a different way.&lt;/p&gt;
&lt;p&gt;As far as I can tell the normal way to deal with this is to figure out a way to tell from the DOM whether it’s okay to proceed or not. Like “if this button is visible, we can “.&lt;/p&gt;
&lt;p&gt;So I wrote a little &lt;code&gt;waitFor()&lt;/code&gt; function that polls every 20ms to see if a condition has finished yet. It times out after 2 seconds.&lt;/p&gt;
&lt;p&gt;Here’s what using it looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JavaScript&quot;&gt;&lt;span class=&quot;syntax-source syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;QUnit&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;quot;&lt;/span&gt;click item&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-js&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-begin syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-function syntax-js&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-end syntax-js&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-object-literal syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-js&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;mountComponent&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&amp;lt;Feedback zine_id=&amp;quot;test123&amp;quot; image_width=&amp;quot;800px&amp;quot; /&amp;gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-comma syntax-js&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-object-literal syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-storage syntax-type syntax-js&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-js&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;waitFor&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-begin syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-end syntax-js&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-declaration syntax-js&quot;&gt; &lt;/span&gt;&lt;span class=&quot;syntax-storage syntax-type syntax-function syntax-arrow syntax-js&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-js&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;.feedback-item&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-variable syntax-other syntax-object syntax-js&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-accessor syntax-js&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-method syntax-js&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-dom syntax-js&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;syntax-comment syntax-line syntax-double-slash syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-js&quot;&gt;//&lt;/span&gt; rest of test goes here... 
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-group syntax-js&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks like there are a lot of implementations of this concept out there and they’re all better thought-through than mine. (from a quick Google: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.npmjs.com/package/qunit-wait-for&quot;&gt;qunit-wait-for&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://playwright.dev/docs/testing-library#replacing-waitfor&quot;&gt;playwright expect.poll&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#figuring-out-the-right-thing-to-wait-for-is-not-straightforward&quot;&gt;figuring out the right thing to wait for is not straightforward&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In some cases I &lt;em&gt;thought&lt;/em&gt; I’d identified the right thing to wait for in the DOM (“just wait for this textarea to appear!’) but it turned out that because of some internal details of how my program works, actually I needed to wait for something else later on which was hard to pin down.&lt;/p&gt;
&lt;p&gt;I ended up changing one of my components to add some random value to the DOM when it was finished an important action (like &lt;code&gt;data-this-thing-is-ready=true&lt;/code&gt;) which didn’t feel great.&lt;/p&gt;
&lt;p&gt;My best guess is that the right way to fix this kind of test issue is a refactor that also makes the app more reliable for the users: if there’s an element in the DOM that isn’t actually ready for the user to interact with, maybe I shouldn’t be displaying it yet!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#adding-some-css-classes-to-identify-things-but-is-that-right&quot;&gt;adding some CSS classes to identify things (but is that right?)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I ended up adding a few classes to HTML elements that I needed to find in the tests, either because I needed to click on them or wait for them to appear in the DOM.&lt;/p&gt;
&lt;p&gt;I might want to change this approach later - frontend testing frameworks seem to suggest avoiding using CSS classes and instead using something like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role&quot;&gt;getByRole&lt;/a&gt; or as a last resort something like a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://testing-library.com/docs/dom-testing-library/intro/&quot;&gt;data-testid&lt;/a&gt;. Feels like there’s a way to make the app more accessible and easier to test at the same time.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#filling-out-forms-is-tricky&quot;&gt;filling out forms is tricky&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To fill out a form, I can’t just set the &lt;code&gt;value&lt;/code&gt;, I also need to dispatch an event to tell Vue that the element has changed. For example, &lt;code&gt;checkbox&lt;/code&gt; and &lt;code&gt;textarea&lt;/code&gt; need different kinds of events.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;textarea.value = &amp;#39;banana banana banana&amp;#39;;
textarea.dispatchEvent(new Event(&amp;#39;input&amp;#39;));
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;checkbox.checked = true;
checkbox.dispatchEvent(new Event(&amp;#39;change&amp;#39;));
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is kind of annoying and it made me realize why I might want to use some kind of UI testing library, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Testing Library’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://testing-library.com/docs/example-react-formik&quot;&gt;example of filling out a form&lt;/a&gt; looks extremely different from what I’m doing&lt;/li&gt;
&lt;li&gt;Vue Test Utils: their &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/content/post/2026-05-02-testing-javascript-in-the-browser.markdown&quot;&gt;section on form handling&lt;/a&gt; looks like it simplifies this a lot.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#test-coverage&quot;&gt;test coverage&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I want to have an idea of what my test coverage was, and it turns out that Chrome actually has a built-in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.chrome.com/docs/devtools/coverage&quot;&gt;code coverage&lt;/a&gt; feature for JS and CSS!&lt;/p&gt;
&lt;p&gt;My JS is bundled into a file called &lt;code&gt;bundle.js&lt;/code&gt; with esbuild, so I could just look at &lt;code&gt;bundle.js&lt;/code&gt; and see which lines weren’t covered.&lt;/p&gt;
&lt;p&gt;The process was a little finicky: I had to turn off sourcemaps in the Chrome devtools to get this to work, and there’s a specific not super obvious series of actions I have to do in order to see the coverage data.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/05/02/testing-vue-components-in-the-browser/#this-was-so-fun&quot;&gt;this was so fun!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As usual with these posts I’ve never really worked as a frontend or backend developer (other than for myself!) and I feel like I’m constantly learning how to do super basic tasks.&lt;/p&gt;
&lt;p&gt;I really had a blast doing this. My frontend projects always feel so fragile because they’re untested, and maybe one day I’ll have a test suite I’m confident in!&lt;/p&gt;
&lt;p&gt;Some things I’m still thinking about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;While writing this post I found this frontend testing library called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://testing-library.com/&quot;&gt;Testing Library&lt;/a&gt; that has a lot of guidelines for how to write tests that are very different from my initial ideas. I experimented with rewriting everything to use Testing Library and it felt pretty good, so we’ll see how that goes. They distribute a &lt;code&gt;.umd.js&lt;/code&gt; file that works without Node.&lt;/li&gt;
&lt;li&gt;I’m not sure how I feel about not having a way to run these tests on the command line at all. Maybe there’s a simple way to work primarily in the browser but have an way to run them in CI too if I want?&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Examples for the tcpdump and dig man pages</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-03-10-examples-for-the-tcpdump-and-dig-man-pages/</link>
    <guid isPermaLink="false">urn:aggr:item:25a4216e85796247ed77707b74dc0e52462309d5</guid>
    <atom:link href="https://jvns.ca/blog/2026/03/10/examples-for-the-tcpdump-and-dig-man-pages/" rel="via" type="text/html"/>
    <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
    <description>Hello! My big takeaway from last month’s musings about man pages was that examples in man pages are really great, so I worked on adding (or improving) examples to two of my favourite tools’ man pages. Here they are: the dig man page (now…</description>
    <content:encoded>&lt;p&gt;Hello! My big takeaway from last month’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/&quot;&gt;musings about man pages&lt;/a&gt; was that examples in man pages are really great, so I worked on adding (or improving) examples to two of my favourite tools’ man pages.&lt;/p&gt;
&lt;p&gt;Here they are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://bind9.readthedocs.io/en/stable/manpages.html#dig-dns-lookup-utility&quot;&gt;dig man page (now with examples)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.tcpdump.org/manpages/tcpdump.1.html#lbAF&quot;&gt;tcpdump man page examples&lt;/a&gt; (this one is an update to the previous examples)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/03/10/examples-for-the-tcpdump-and-dig-man-pages/#the-goal-include-the-most-basic-examples&quot;&gt;the goal: include the most basic examples&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The goal here was really just to give the absolute most basic examples of how to use the tool, for people who use tcpdump or dig infrequently (or have never used it before!) and don’t remember how it works.&lt;/p&gt;
&lt;p&gt;So far saying “hey, I want to write an examples section for beginners and infrequent users of this tools” has been working really well. It’s easy to explain, I think it makes sense from everything I’ve heard from users about what they want from a man page, and maintainers seem to find it compelling.&lt;/p&gt;
&lt;p&gt;Thanks to Denis Ovsienko, Guy Harris, Ondřej Surý, and everyone else who reviewed the docs changes, it was a good experience and left me motivated to do a little more work on man pages.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/03/10/examples-for-the-tcpdump-and-dig-man-pages/#why-improve-the-man-pages&quot;&gt;why improve the man pages?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’m interested in working on tools’ official documentation right now because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Man pages can actually have close to 100% accurate information! Going through a review process to make sure that the information is actually true has a lot of value.&lt;/li&gt;
&lt;li&gt;Even with basic questions “what are the most commonly used tcpdump flags”, often maintainers are aware of useful features that I’m not! For example I learned by working on these tcpdump examples that if you’re saving packets to a file with &lt;code&gt;tcpdump -w out.pcap&lt;/code&gt;, it’s useful to pass &lt;code&gt;-v&lt;/code&gt; to print a live summary of how many packets have been captured so far. That’s really useful, I didn’t know it, and I don’t think I ever would have noticed it on my own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s kind of a weird place for me to be because honestly I always kind of assume documentation is going to be hard to read, and I usually just skip it and read a blog post or Stack Overflow comment or ask a friend instead. But right now I’m feeling optimistic, like maybe the documentation doesn’t have to be bad? Maybe it could be just as good as reading a really great blog post, but with the benefit of also being actually correct? I’ve been using the Django documentation recently, and it’s really good! We’ll see.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/03/10/examples-for-the-tcpdump-and-dig-man-pages/#on-avoiding-writing-the-man-page-language&quot;&gt;on avoiding writing the man page language&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tcpdump&lt;/code&gt; project tool’s man page is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://raw.githubusercontent.com/the-tcpdump-group/tcpdump/refs/heads/master/tcpdump.1.in&quot;&gt;written in the roff language&lt;/a&gt;, which is kind of hard to use and that I really did not feel like learning it.&lt;/p&gt;
&lt;p&gt;I handled this by writing a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/jvns/a31036bf70f0675811b1b2a86b122aeb&quot;&gt;very basic markdown-to-roff script&lt;/a&gt; to convert Markdown to roff, using similar conventions to what the man page was already using. I could maybe have just used pandoc, but the output pandoc produced seemed pretty different, so I thought it might be better to write my own script instead. Who knows.&lt;/p&gt;
&lt;p&gt;I did think it was cool to be able to just use an existing Markdown library’s ability to parse the Markdown AST and then implement my own code-emitting methods to format things in a way that seemed to make sense in this context.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/03/10/examples-for-the-tcpdump-and-dig-man-pages/#man-pages-are-complicated&quot;&gt;man pages are complicated&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I went on a whole rabbit hole learning about the history of &lt;code&gt;roff&lt;/code&gt;, how it’s evolved since the 70s, and who’s working on it today, inspired by learning about the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mandoc.bsd.lv/&quot;&gt;mandoc&lt;/a&gt; project that BSD systems (and some Linux systems, and I think Mac OS) use for formatting man pages. I won’t say more about that today though, maybe another time.&lt;/p&gt;
&lt;p&gt;In general it seems like there’s a technical and cultural divide in how documentation works on BSD and on Linux that I still haven’t really understood, but I have been feeling curious about what’s going on in the BSD world.&lt;/p&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://comments.jvns.ca/post/116206906990442943&quot;&gt;comments section is here&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Notes on clarifying man pages</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-02-18-notes-on-clarifying-man-pages/</link>
    <guid isPermaLink="false">urn:aggr:item:7b689d179a5c11de426402b974099867f8609f57</guid>
    <atom:link href="https://jvns.ca/blog/2026/02/18/man-pages/" rel="via" type="text/html"/>
    <pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
    <description>Hello! After spending some time working on the Git man pages last year, I’ve been thinking a little more about what makes a good man page. I’ve spent a lot of time writing cheat sheets for tools (tcpdump, git, dig, etc) which have a man…</description>
    <content:encoded>&lt;p&gt;Hello! After &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/&quot;&gt;spending some time working on the Git man pages&lt;/a&gt; last year, I’ve been thinking a little more about what makes a good man page.&lt;/p&gt;
&lt;p&gt;I’ve spent a lot of time writing cheat sheets for tools (tcpdump, git, dig, etc) which have a man page as their primary documentation. This is because I often find the man pages hard to navigate to get the information I want.&lt;/p&gt;
&lt;p&gt;Lately I’ve wondering – could the man page &lt;em&gt;itself&lt;/em&gt; have an amazing cheat sheet in it? What might make a man page easier to use? I’m still very early in thinking about this but I wanted to write down some quick notes.&lt;/p&gt;
&lt;p&gt;I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://social.jvns.ca/@b0rk/116092583707187579&quot;&gt;asked some people on Mastodon&lt;/a&gt; for their favourite man pages, and here are some examples of interesting things I saw on those man pages.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#an-options-summary&quot;&gt;an OPTIONS SUMMARY&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you’ve read a lot of man pages you’ve probably seen something like this in the &lt;code&gt;SYNOPSIS&lt;/code&gt;: once you’re listing almost the entire alphabet, it’s hard&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,]

grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://download.samba.org/pub/rsync/rsync.1&quot;&gt;rsync man page&lt;/a&gt; has a solution I’ve never seen before: it keeps its SYNOPSIS very terse, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt; Local:
     rsync [OPTION...] SRC... [DEST]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then has an “OPTIONS SUMMARY” section with a 1-line summary of each option, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;--verbose, -v            increase verbosity
--info=FLAGS             fine-grained informational verbosity
--debug=FLAGS            fine-grained debug verbosity
--stderr=e|a|c           change stderr output mode (default: errors)
--quiet, -q              suppress non-error messages
--no-motd                suppress daemon-mode MOTD
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then later there’s the usual OPTIONS section with a full description of each option.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#an-options-section-organized-by-category&quot;&gt;an OPTIONS section organized by category&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man7.org/linux/man-pages/man1/strace.1.html&quot;&gt;strace man page&lt;/a&gt; organizes its options by category (like “General”, “Startup”, “Tracing”, and “Filtering”, “Output Format”) instead of alphabetically.&lt;/p&gt;
&lt;p&gt;As an experiment I tried to take the &lt;code&gt;grep&lt;/code&gt; man page and make an “OPTIONS SUMMARY” section grouped by category, you can &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/jvns/9f5966633875a4758e0d947a5b4dbdcf&quot;&gt;see the results here&lt;/a&gt;. I’m not sure what I think of the results but it was a fun exercise. When I was writing that I was thinking about how I can never remember the name of the &lt;code&gt;-l&lt;/code&gt; grep option. It always takes me what feels like forever to find it in the man page and I was trying to think of what structure would make it easier for me to find. Maybe categories?&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#a-cheat-sheet&quot;&gt;a cheat sheet&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A couple of people pointed me to the suite of Perl man pages (&lt;code&gt;perlfunc&lt;/code&gt;, &lt;code&gt;perlre&lt;/code&gt;, etc), and one thing I noticed was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://linux.die.net/man/1/perlcheat&quot;&gt;man perlcheat&lt;/a&gt;, which has cheat sheet sections like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt; SYNTAX
 foreach (LIST) { }     for (a;b;c) { }
 while   (e) { }        until (e)   { }
 if      (e) { } elsif (e) { } else { }
 unless  (e) { } elsif (e) { } else { }
 given   (e) { when (e) {} default {} }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think this is so cool and it makes me wonder if there are other ways to write condensed ASCII 80-character-wide cheat sheets for use in man pages.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#examples-are-very-popular&quot;&gt;examples are very popular&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A common comment was something to the effect of “I like any man page that has examples”. Someone mentioned the OpenBSD man pages, and the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man.openbsd.org/tail&quot;&gt;openbsd tail&lt;/a&gt; man page has examples of the exact 2 ways I use tail at the end.&lt;/p&gt;
&lt;p&gt;I think I’ve most often seen the EXAMPLES section at the end of the man page, but some man pages (like the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://download.samba.org/pub/rsync/rsync.1&quot;&gt;rsync man page&lt;/a&gt; from earlier) start with the examples. When I was working on the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git-scm.com/docs/git-add&quot;&gt;git-add&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git-scm.com/docs/git-rebase&quot;&gt;git rebase&lt;/a&gt; man pages I put a short example at the beginning.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#a-table-of-contents-and-links-between-sections&quot;&gt;a table of contents, and links between sections&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This isn’t a property of the man page itself, but one issue with man pages in the terminal is it’s hard to know what sections the man page has.&lt;/p&gt;
&lt;p&gt;When working on the Git man pages, one thing Marie and I did was to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git-scm.com/docs/git-rebase&quot;&gt;add a table of contents&lt;/a&gt; to the sidebar of the HTML versions of the man pages hosted on the Git site.&lt;/p&gt;
&lt;p&gt;I’d also like to add more hyperlinks to the HTML versions of the Git man pages at some point, so that you can click on “INCOMPATIBLE OPTIONS” to get to that section. It’s very easy to add links like this in the Git project since Git’s man pages are generated with AsciiDoc.&lt;/p&gt;
&lt;p&gt;I think adding a table of contents and adding internal hyperlinks is kind of a nice middle ground where we can make some improvements to the man page format (in the HTML version of the man page at least) without maintaining a totally different form of documentation. Though for this to work you do need to set up a toolchain like Git’s AsciiDoc system.&lt;/p&gt;
&lt;p&gt;It would be amazing if there were some kind of universal system to make it easy to look up a specific option in a man page (“what does -a do?”). The best trick I know is use the man pager to search for something like &lt;code&gt;^ *-a&lt;/code&gt; but I never remember to do it and instead just end up going through every instance of &lt;code&gt;-a&lt;/code&gt; in the man page until I find what I’m looking for.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#examples-for-every-option&quot;&gt;examples for every option&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://curl.se/docs/manpage.html&quot;&gt;curl man page&lt;/a&gt; has examples for every option, and there’s also a table of contents on the HTML version so you can more easily jump to the option you’re interested in.&lt;/p&gt;
&lt;p&gt;For instance the example for &lt;code&gt;--cert&lt;/code&gt; makes it easy to see that you likely also want to pass the &lt;code&gt;--key&lt;/code&gt; option, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;  curl --cert certfile --key keyfile https://example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The way they implement this is that there’s [one file for each option](&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/curl/curl/blob/dc08922a61efe546b318daf964514ffbf41583&quot;&gt;https://github.com/curl/curl/blob/dc08922a61efe546b318daf964514ffbf41583&lt;/a&gt; 25/docs/cmdline-opts/append.md) and there’s an “Example” field in that file.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#formatting-data-in-a-table&quot;&gt;formatting data in a table&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Quite a few people said that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.man7.org/linux/man-pages/man7/ascii.7.html&quot;&gt;man ascii&lt;/a&gt; was their favourite man page, which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt; Oct   Dec   Hex   Char                     
 ───────────────────────────────────────────
 000   0     00    NUL &amp;#39;\0&amp;#39; (null character)
 001   1     01    SOH (start of heading)   
 002   2     02    STX (start of text)      
 003   3     03    ETX (end of text)        
 004   4     04    EOT (end of transmission)
 005   5     05    ENQ (enquiry)            
 006   6     06    ACK (acknowledge)        
 007   7     07    BEL &amp;#39;\a&amp;#39; (bell)          
 010   8     08    BS  &amp;#39;\b&amp;#39; (backspace)     
 011   9     09    HT  &amp;#39;\t&amp;#39; (horizontal tab)
 012   10    0A    LF  &amp;#39;\n&amp;#39; (new line)      
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously &lt;code&gt;man ascii&lt;/code&gt; is an unusual man page but I think what’s cool about this man page (other than the fact that it’s always useful to have an ASCII reference) is it’s very easy to scan to find the information you need because of the table format. It makes me wonder if there are more opportunities to display information in a “table” in a man page to make it easier to scan.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#the-gnu-approach&quot;&gt;the GNU approach&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When I talk about man pages it often comes up that the GNU coreutils man pages (for example &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man7.org/linux/man-pages/man1/tail.1.html&quot;&gt;man tail&lt;/a&gt;) don’t have examples, unlike the OpenBSD man pages, which &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man.openbsd.org/tail&quot;&gt;do have examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m not going to get into this too much because it seems like a fairly political topic and I definitely can’t do it justice here, but here are some things I believe to be true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The GNU project prefers to maintain documentation in “info” manuals instead of man pages. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.gnu.org/software/coreutils/manual/coreutils.html&quot;&gt;This page&lt;/a&gt; says “the man pages are no longer being maintained”.&lt;/li&gt;
&lt;li&gt;There are 3 ways to read “info” manuals: their HTML version, in Emacs, or with a standalone &lt;code&gt;info&lt;/code&gt; tool. I’ve heard from some Emacs users that they like the Emacs info browser. I don’t think I’ve ever talked to anyone who uses the standalone &lt;code&gt;info&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.gnu.org/software/coreutils/manual/html_node/tail-invocation.html&quot;&gt;info manual entry for tail&lt;/a&gt; is linked at the bottom of the man page, and it does have examples&lt;/li&gt;
&lt;li&gt;The FSF used to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.fsf.org/gnu-press&quot;&gt;sell print books&lt;/a&gt; of the GNU software manuals (and maybe they &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://shop.fsf.org/&quot;&gt;still do&lt;/a&gt; sometimes?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After a certain level of complexity a man page gets really hard to navigate: while I’ve never used the coreutils info manual and probably won’t, I would almost certainly prefer to use the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.gnu.org/software/bash/manual/bash.html&quot;&gt;GNU Bash reference manual&lt;/a&gt; or the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sourceware.org/glibc/manual/latest/html_mono/libc.html&quot;&gt;The GNU C Library Reference Manual&lt;/a&gt; via their HTML documentation rather than through a man page.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#a-few-more-man-page-adjacent-things&quot;&gt;a few more man-page-adjacent things&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here are some tools I think are interesting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://fishshell.com/&quot;&gt;The fish shell&lt;/a&gt; comes with a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/fish-shell/fish-shell/blob/master/share/tools/create_manpage_completions.py&quot;&gt;Python script&lt;/a&gt; to automatically generate tab completions from man pages&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://tldr.sh&quot;&gt;tldr.sh&lt;/a&gt; is a community maintained database of examples, for example you can run it as &lt;code&gt;tldr grep&lt;/code&gt;. Lots of people have told me they find it useful.&lt;/li&gt;
&lt;li&gt;the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://kapeli.com/dash&quot;&gt;Dash&lt;/a&gt; Mac docs browser has a nice man page viewer in it. I still use the terminal man page viewer but I like that it includes a table of contents, it looks like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/dash.webp&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/02/18/man-pages/#it-s-interesting-to-think-about-a-constrained-format&quot;&gt;it’s interesting to think about a constrained format&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Man pages are such a constrained format and it’s fun to think about what you can do with such limited formatting options.&lt;/p&gt;
&lt;p&gt;Even though I’m very into writing I’ve always had a bad habit of never reading documentation and so it’s a little bit hard for me to think about what I actually find useful in man pages, I’m not sure whether I think most of the things in this post would improve my experience or not. (Except for examples, I LOVE examples)&lt;/p&gt;
&lt;p&gt;So I’d be interested to hear about other man pages that you think are well designed and what you like about them, the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://comments.jvns.ca/post/116093529820975727&quot;&gt;comments section is here&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Some notes on starting to use Django</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-01-27-some-notes-on-starting-to-use-django/</link>
    <guid isPermaLink="false">urn:aggr:item:ae0452670d10237318b5a70edaf1cff8423ae98e</guid>
    <atom:link href="https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/" rel="via" type="text/html"/>
    <pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
    <description>Hello! One of my favourite things is starting to learn an Old Boring Technology that I’ve never tried before but that has been around for 20+ years. It feels really good when every problem I’m ever going to have has been solved already…</description>
    <content:encoded>&lt;p&gt;Hello! One of my favourite things is starting to learn an Old Boring Technology that I’ve never tried before but that has been around for 20+ years. It feels really good when every problem I’m ever going to have has been solved already 1000 times and I can just get stuff done easily.&lt;/p&gt;
&lt;p&gt;I’ve thought it would be cool to learn a popular web framework like Rails or Django or Laravel for a long time, but I’d never really managed to make it happen. But I started learning Django to make a website a few months back, I’ve been liking it so far, and here are a few quick notes!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#less-magic-than-rails&quot;&gt;less magic than Rails&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I spent some time &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2020/11/09/day-1--a-little-rails-/&quot;&gt;trying to learn Rails&lt;/a&gt; in 2020, and while it was cool and I really wanted to like Rails (the Ruby community is great!), I found that if I left my Rails project alone for months, when I came back to it it was hard for me to remember how to get anything done because (for example) if it says &lt;code&gt;resources :topics&lt;/code&gt; in your &lt;code&gt;routes.rb&lt;/code&gt;, on its own that doesn’t tell you where the &lt;code&gt;topics&lt;/code&gt; routes are configured, you need to remember or look up the convention.&lt;/p&gt;
&lt;p&gt;Being able to abandon a project for months or years and then come back to it is really important to me (that’s how all my projects work!), and Django feels easier to me because things are more explicit.&lt;/p&gt;
&lt;p&gt;In my small Django project it feels like I just have 5 main files (other than the settings files): &lt;code&gt;urls.py&lt;/code&gt;, &lt;code&gt;models.py&lt;/code&gt;, &lt;code&gt;views.py&lt;/code&gt;, &lt;code&gt;admin.py&lt;/code&gt;, and &lt;code&gt;tests.py&lt;/code&gt;, and if I want to know where something else is (like an HTML template) is then it’s usually explicitly referenced from one of those files.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#a-built-in-admin&quot;&gt;a built-in admin&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For this project I wanted to have an admin interface to manually edit or view some of the data in the database. Django has a really nice built-in admin interface, and I can customize it with just a little bit of code.&lt;/p&gt;
&lt;p&gt;For example, here’s part of one of my admin classes, which sets up which fields to display in the “list” view, which field to search on, and how to order them by default.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;@admin.register(Zine)
class ZineAdmin(admin.ModelAdmin):
    list_display = [&amp;quot;name&amp;quot;, &amp;quot;publication_date&amp;quot;, &amp;quot;free&amp;quot;, &amp;quot;slug&amp;quot;, &amp;quot;image_preview&amp;quot;]
    search_fields = [&amp;quot;name&amp;quot;, &amp;quot;slug&amp;quot;]
    readonly_fields = [&amp;quot;image_preview&amp;quot;]
    ordering = [&amp;quot;-publication_date&amp;quot;]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#it-s-fun-to-have-an-orm&quot;&gt;it’s fun to have an ORM&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the past my attitude has been “ORMs? Who needs them? I can just write my own SQL queries!”. I’ve been enjoying Django’s ORM so far though, and I think it’s cool how Django uses &lt;code&gt;__&lt;/code&gt; to represent a &lt;code&gt;JOIN&lt;/code&gt;, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;Zine.objects
    .exclude(product__order__email_hash=email_hash)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query involves 5 tables: &lt;code&gt;zines&lt;/code&gt;, &lt;code&gt;zine_products&lt;/code&gt;, &lt;code&gt;products&lt;/code&gt;, &lt;code&gt;order_products&lt;/code&gt;, and &lt;code&gt;orders&lt;/code&gt;. To make this work I just had to tell Django that there’s a &lt;code&gt;ManyToManyField&lt;/code&gt; relating “orders” and “products”, and another &lt;code&gt;ManyToManyField&lt;/code&gt; relating “zines”, and “products”, so that it knows how to connect &lt;code&gt;zines&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;products&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I definitely &lt;em&gt;could&lt;/em&gt; write that query, but writing &lt;code&gt;product__order__email_hash&lt;/code&gt; is a lot less typing, it feels a lot easier to read, and honestly I think it would take me a little while to figure out how to construct the query (which needs to do a few other things than just those joins).&lt;/p&gt;
&lt;p&gt;I have zero concern about the performance of my ORM-generated queries so I’m pretty excited about ORMs for now, though I’m sure I’ll find things to be frustrated with eventually.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#automatic-migrations&quot;&gt;automatic migrations!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The other great thing about the ORM is migrations!&lt;/p&gt;
&lt;p&gt;If I add, delete, or change a field in &lt;code&gt;models.py&lt;/code&gt;, Django will automatically generate a migration script like &lt;code&gt;migrations/0006_delete_imageblob.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I assume that I could edit those scripts if I wanted, but so far I’ve just been running the generated scripts with no change and it’s been going great. It really feels like magic.&lt;/p&gt;
&lt;p&gt;I’m realizing that being able to do migrations easily is important for me right now because I’m changing my data model fairly often as I figure out how I want it to work.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#i-like-the-docs&quot;&gt;I like the docs&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I had a bad habit of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=krMw1QTP2no&quot;&gt;never reading the documentation&lt;/a&gt; but I’ve been really enjoying the parts of Django’s docs that I’ve read so far. This isn’t by accident: Jacob Kaplan-Moss has a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://pyvideo.org/pycon-us-2011/pycon-2011--writing-great-documentation.html&quot;&gt;talk from PyCon 2011&lt;/a&gt; on Django’s documentation culture.&lt;/p&gt;
&lt;p&gt;For example the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.djangoproject.com/en/6.0/topics/db/models/&quot;&gt;intro to models&lt;/a&gt; lists the most important common fields you might want to set when using the ORM.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#using-sqlite&quot;&gt;using sqlite&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After having a bad experience trying to operate Postgres and not being able to understand what was going on, I decided to run all of my small websites with SQLite instead. It’s been going way better, and I love being able to backup by just doing a &lt;code&gt;VACUUM INTO&lt;/code&gt; and then copying the resulting single file.&lt;/p&gt;
&lt;p&gt;I’ve been following &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://alldjango.com/articles/definitive-guide-to-using-django-sqlite-in-production&quot;&gt;these instructions&lt;/a&gt; for using SQLite with Django in production.&lt;/p&gt;
&lt;p&gt;I think it should be fine because I’m expecting the site to have a few hundred writes per day at most, much less than &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://messwithdns.net/&quot;&gt;Mess with DNS&lt;/a&gt; which has a lot more of writes and has been working well (though the writes are split across 3 different SQLite databases).&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#built-in-email-and-more&quot;&gt;built in email (and more)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Django seems to be very “batteries-included”, which I love – if I want CSRF protection, or a &lt;code&gt;Content-Security-Policy&lt;/code&gt;, or I want to send email, it’s all in there!&lt;/p&gt;
&lt;p&gt;For example, I wanted to save the emails Django sends to a file in dev mode (so that it didn’t send real email to real people), which was just a little bit of configuration.&lt;/p&gt;
&lt;p&gt;I just put this &lt;code&gt;settings/dev.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;EMAIL_BACKEND = &amp;quot;django.core.mail.backends.filebased.EmailBackend&amp;quot;
EMAIL_FILE_PATH = BASE_DIR / &amp;quot;emails&amp;quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then set up the production email like this in &lt;code&gt;settings/production.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;EMAIL_BACKEND = &amp;quot;django.core.mail.backends.smtp.EmailBackend&amp;quot;
EMAIL_HOST = &amp;quot;smtp.whatever.com&amp;quot;
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = &amp;quot;xxxx&amp;quot;
EMAIL_HOST_PASSWORD = os.getenv(&amp;#39;EMAIL_API_KEY&amp;#39;)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That made me feel like if I want some other basic website feature, there’s likely to be an easy way to do it built into Django already.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#the-settings-file-still-feels-like-a-lot&quot;&gt;the settings file still feels like a lot&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’m still a bit intimidated by the &lt;code&gt;settings.py&lt;/code&gt; file: Django’s settings system works by setting a bunch of global variables in a file, and I feel a bit stressed about… what if I make a typo in the name of one of those variables? How will I know? What if I type &lt;code&gt;WSGI_APPLICATOIN = &amp;quot;config.wsgi.application&amp;quot;&lt;/code&gt; instead of &lt;code&gt;WSGI_APPLICATION&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;I guess I’ve gotten used to having a Python language server tell me when I’ve made a typo and so now it feels a bit disorienting when I can’t rely on the language server support.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/27/some-notes-on-starting-to-use-django/#that-s-all-for-now&quot;&gt;that’s all for now!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I haven’t really successfully used an actual web framework for a project before (right now almost all of my websites are either a single Go binary or static sites), so I’m interested in seeing how it goes!&lt;/p&gt;
&lt;p&gt;There’s still lots for me to learn about, I still haven’t really gotten into Django’s form validation tooling or authentication systems.&lt;/p&gt;
&lt;p&gt;Thanks to Marco Rogers for convincing me to give ORMs a chance.&lt;/p&gt;
&lt;p&gt;(we’re still experimenting with the comments-on-Mastodon system! &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://comments.jvns.ca/post/115969229107460589&quot;&gt;Here are the comments on Mastodon&lt;/a&gt;! tell me your favourite Django feature!)&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>A data model for Git (and other docs updates)</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2026-01-08-a-data-model-for-git-and-other-docs-updates/</link>
    <guid isPermaLink="false">urn:aggr:item:8864fa7c083fee7f7e50f75ed212264e0834b497</guid>
    <atom:link href="https://jvns.ca/blog/2026/01/08/a-data-model-for-git/" rel="via" type="text/html"/>
    <pubDate>Thu, 8 Jan 2026 00:00:00 +0000</pubDate>
    <description>Hello! This past fall, I decided to take some time to work on Git’s documentation. I’ve been thinking about working on open source docs for a long time – usually if I think the documentation for something could be improved, I’ll write a…</description>
    <content:encoded>&lt;p&gt;Hello! This past fall, I decided to take some time to work on Git’s documentation. I’ve been thinking about working on open source docs for a long time – usually if I think the documentation for something could be improved, I’ll write a blog post or a zine or something. But this time I wondered: could I instead make a few improvements to the official documentation?&lt;/p&gt;
&lt;p&gt;So &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://marieflanagan.com/&quot;&gt;Marie&lt;/a&gt; and I made a few changes to the Git documentation!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/#a-data-model-for-git&quot;&gt;a data model for Git&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After a while working on the documentation, we noticed that Git uses the terms “object”, “reference”, or “index” in its documentation a lot, but that it didn’t have a great explanation of what those terms mean or how they relate to other core concepts like “commit” and “branch”. So we wrote a new “data model” document!&lt;/p&gt;
&lt;p&gt;You can &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/master/Documentation/gitdatamodel.adoc&quot;&gt;read the data model here for now&lt;/a&gt;. I assume at some point (after the next release?) it’ll also be on the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git-scm.com&quot;&gt;Git website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m excited about this because understanding how Git organizes its commit and branch data has really helped me reason about how Git works over the years, and I think it’s important to have a short (1600 words!) version of the data model that’s accurate.&lt;/p&gt;
&lt;p&gt;The “accurate” part turned out to not be that easy: I knew the basics of how Git’s data model worked, but during the review process I learned some new details and had to make quite a few changes (for example how merge conflicts are stored in the staging area).&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/#updates-to-git-push-git-pull-and-more&quot;&gt;updates to &lt;code&gt;git push&lt;/code&gt;, &lt;code&gt;git pull&lt;/code&gt;, and more&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also worked on updating the introduction to some of Git’s core man pages. I quickly realized that “just try to improve it according to my best judgement” was not going to work: why should the maintainers believe me that my version is better?&lt;/p&gt;
&lt;p&gt;I’ve seen a problem a lot when discussing open source documentation changes where 2 expert users of the software argue about whether an explanation is clear or not (“I think X would be a good way to explain it! Well, I think Y would be better!”)&lt;/p&gt;
&lt;p&gt;I don’t think this is very productive (expert users of a piece of software are notoriously bad at being able to tell if an explanation will be clear to non-experts), so I needed to find a way to identify problems with the man pages that was a little more evidence-based.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/#getting-test-readers-to-identify-problems&quot;&gt;getting test readers to identify problems&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I asked for test readers on Mastodon to read the current version of documentation and tell me what they find confusing or what questions they have. About 80 test readers left comments, and I learned so much!&lt;/p&gt;
&lt;p&gt;People left a huge amount of great feedback, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;terminology they didn’t understand (what’s a pathspec? what does “reference” mean? does “upstream” have a specific meaning in Git?)&lt;/li&gt;
&lt;li&gt;specific confusing sentences&lt;/li&gt;
&lt;li&gt;suggestions of things things to add (“I do X all the time, I think it should be included here”)&lt;/li&gt;
&lt;li&gt;inconsistencies (“here it implies X is the default, but elsewhere it implies Y is the default”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of the test readers had been using Git for at least 5-10 years, which I think worked well – if a group of test readers who have been using Git regularly for 5+ years find a sentence or term impossible to understand, it makes it easy to argue that the documentation should be updated to make it clearer.&lt;/p&gt;
&lt;p&gt;I thought this “get users of the software to comment on the existing documentation and then fix the problems they find” pattern worked really well and I’m excited about potentially trying it again in the future.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/#the-man-page-changes&quot;&gt;the man page changes&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;We ended updating these 4 man pages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git add&lt;/code&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/2b3ae040/Documentation/git-add.adoc&quot;&gt;before&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/git-add.adoc&quot;&gt;after&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git checkout&lt;/code&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/2b3ae040/Documentation/git-checkout.adoc&quot;&gt;before&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/git-checkout.adoc&quot;&gt;after&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push&lt;/code&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/2b3ae040/Documentation/git-push.adoc&quot;&gt;before&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/git-push.adoc&quot;&gt;after&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git pull&lt;/code&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/2b3ae040/Documentation/git-pull.adoc&quot;&gt;before&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/git-pull.adoc&quot;&gt;after&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;git push&lt;/code&gt; and &lt;code&gt;git pull&lt;/code&gt; changes were the most interesting to me: in addition to updating the intro to those pages, we also ended up writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/urls-remotes.adoc#upstream-branches&quot;&gt;a section describing what the term “upstream branch” means&lt;/a&gt; (which previously wasn’t really explained)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/git/git/blob/e0bfec3dfc356f7d808eb5ee546a54116b794397/Documentation/git-push.adoc#options&quot;&gt;a cleaned-up description of what a “push refspec” is&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Making those changes really gave me an appreciation for how much work it is to maintain open source documentation: it’s not easy to write things that are both clear and true, and sometimes we had to make compromises, for example the sentence “&lt;code&gt;git push&lt;/code&gt; may fail if you haven’t set an upstream for the current branch, depending on what &lt;code&gt;push.default&lt;/code&gt; is set to.” is a little vague, but the exact details of what “depending” means are really complicated and untangling that is a big project.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2026/01/08/a-data-model-for-git/#on-the-process-for-contributing-to-git&quot;&gt;on the process for contributing to Git&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It took me a while to understand Git’s development process. I’m not going to try to describe it here (that could be a whole other post!), but a few quick notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git has a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git-scm.com/community#discord&quot;&gt;Discord server&lt;/a&gt; with a “my first contribution” channel for help with getting started contributing. I found people to be very welcoming on the Discord.&lt;/li&gt;
&lt;li&gt;I used &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gitgitgadget.github.io/&quot;&gt;GitGitGadget&lt;/a&gt; to make all of my contributions. This meant that I could make a GitHub pull request (a workflow I’m comfortable with) and GitGitGadget would convert my PRs into the system the Git developers use (emails with patches attached). GitGitGadget worked great and I was very grateful to not have to learn how to send patches by email with Git.&lt;/li&gt;
&lt;li&gt;Otherwise I used my normal email client (Fastmail’s web interface) to reply to emails, wrapping my text to 80 character lines since that’s the mailing list norm.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also found the mailing list archives on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://lore.kernel.org/git/&quot;&gt;lore.kernel.org&lt;/a&gt; hard to navigate, so I hacked together &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/jvns/git-list-viewer&quot;&gt;my own git list viewer&lt;/a&gt; to make it easier to read the long mailing list threads.&lt;/p&gt;
&lt;p&gt;Many people helped me navigate the contribution process and review the changes: thanks to Emily Shaffer, Johannes Schindelin (the author of GitGitGadget), Patrick Steinhardt, Ben Knoble, Junio Hamano, and more.&lt;/p&gt;
&lt;p&gt;(I’m experimenting with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://comments.jvns.ca/post/115861337435768520&quot;&gt;comments on Mastodon, you can see the comments here&lt;/a&gt;)&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Notes on switching to Helix from vim</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-10-10-notes-on-switching-to-helix-from-vim/</link>
    <guid isPermaLink="false">urn:aggr:item:a1aad6b21ad2f9e509a9cd349f168a5681f7039c</guid>
    <atom:link href="https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/" rel="via" type="text/html"/>
    <pubDate>Fri, 10 Oct 2025 00:00:00 +0000</pubDate>
    <description>Hello! Earlier this summer I was talking to a friend about how much I love using fish, and how I love that I don’t have to configure it. They said that they feel the same way about the helix text editor, and so I decided to give it a try.…</description>
    <content:encoded>&lt;p&gt;Hello! Earlier this summer I was talking to a friend about how much I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/09/12/reasons-i--still--love-fish/&quot;&gt;love using fish&lt;/a&gt;, and how I love that I don’t have to configure it. They said that they feel the same way about the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://helix-editor.com/&quot;&gt;helix&lt;/a&gt; text editor, and so I decided to give it a try.&lt;/p&gt;
&lt;p&gt;I’ve been using it for 3 months now and here are a few notes.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#why-helix-language-servers&quot;&gt;why helix: language servers&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think what motivated me to try Helix is that I’ve been trying to get a working language server setup (so I can do things like “go to definition”) and getting a setup that feels good in Vim or Neovim just felt like too much work.&lt;/p&gt;
&lt;p&gt;After using Vim/Neovim for 20 years, I’ve tried both “build my own custom configuration from scratch” and “use someone else’s pre-buld configuration system” and even though I love Vim I was excited about having things just work without having to work on my configuration at all.&lt;/p&gt;
&lt;p&gt;Helix comes with built in language server support, and it feels nice to be able to do things like “rename this symbol” in any language.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#the-search-is-great&quot;&gt;the search is great&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One of my favourite things about Helix is the search! If I’m searching all the files in my repository for a string, it lets me scroll through the potential matching files and see the full context of the match, like this:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/helix-search.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;For comparison, here’s what the vim ripgrep plugin I’ve been using looks like:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/vim-ripgrep.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;There’s no context for what else is around that line.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#the-quick-reference-is-nice&quot;&gt;the quick reference is nice&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One thing I like about Helix is that when I press &lt;code&gt;g&lt;/code&gt;, I get a little help popup telling me places I can go. I really appreciate this because I don’t often use the “go to definition” or “go to reference” feature and I often forget the keyboard shortcut.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/goto.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#some-vim-helix-translations&quot;&gt;some vim -&amp;gt; helix translations&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Helix doesn’t have marks like &lt;code&gt;ma&lt;/code&gt;, &lt;code&gt;&#39;a&lt;/code&gt;, instead I’ve been using &lt;code&gt;Ctrl+O&lt;/code&gt; and &lt;code&gt;Ctrl+I&lt;/code&gt; to go back (or forward) to the last cursor location&lt;/li&gt;
&lt;li&gt;I think Helix does have macros, but I’ve been using multiple cursors in every case that I would have previously used a macro. I like multiple cursors a lot more than writing macros all the time. If I want to batch change something in the document, my workflow is to press &lt;code&gt;%&lt;/code&gt; (to highlight everything), then &lt;code&gt;s&lt;/code&gt; to select (with a regex) the things I want to change, then I can just edit all of them as needed.&lt;/li&gt;
&lt;li&gt;Helix doesn’t have neovim-style tabs, instead it has a nice buffer switcher (&lt;code&gt;&amp;lt;space&amp;gt;b&lt;/code&gt;) I can use to switch to the buffer I want. There’s a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/helix-editor/helix/pull/7109&quot;&gt;pull request here&lt;/a&gt; to implement neovim-style tabs. There’s also a setting &lt;code&gt;bufferline=&amp;quot;multiple&amp;quot;&lt;/code&gt; which can act a bit like tabs with &lt;code&gt;gp&lt;/code&gt;, &lt;code&gt;gn&lt;/code&gt; for prev/next “tab” and &lt;code&gt;:bc&lt;/code&gt; to close a “tab”.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#some-helix-annoyances&quot;&gt;some helix annoyances&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s everything that’s annoyed me about Helix so far.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I like the way Helix’s &lt;code&gt;:reflow&lt;/code&gt; works much less than how vim reflows text with &lt;code&gt;gq&lt;/code&gt;. It doesn’t work as well with lists. (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/helix-editor/helix/issues/3332&quot;&gt;github issue&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;If I’m making a Markdown list, pressing “enter” at the end of a list item won’t continue the list. There’s a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/helix-editor/helix/wiki/Recipes#continue-markdown-lists--quotes&quot;&gt;partial workaround&lt;/a&gt; for bulleted lists but I don’t know one for numbered lists.&lt;/li&gt;
&lt;li&gt;No persistent undo yet: in vim I could use an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vimdoc.sourceforge.net/htmldoc/options.html#&amp;#x27;undofile&amp;#x27;&quot;&gt;undofile&lt;/a&gt; so that I could undo changes even after quitting. Helix doesn’t have that feature yet. (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/helix-editor/helix/pull/9154&quot;&gt;github PR&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Helix doesn’t autoreload files after they change on disk, I have to run &lt;code&gt;:reload-all&lt;/code&gt; (&lt;code&gt;:ra&amp;lt;tab&amp;gt;&lt;/code&gt;) to manually reload them. Not a big deal.&lt;/li&gt;
&lt;li&gt;Sometimes it panics, maybe every week or so. I think it might be &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/helix-editor/helix/issues/12582&quot;&gt;this issue&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The crashes look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;thread &amp;#39;main&amp;#39; panicked at helix-core/src/transaction.rs:499:9:
Positions [(2959, AfterSticky), (2959, AfterSticky)] are out of range for changeset len 2945!
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The “markdown list” and reflowing issues come up a lot for me because I spend a lot of time editing Markdown lists, but I keep using Helix anyway so I guess they can’t be making me that mad.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#switching-was-easier-than-i-thought&quot;&gt;switching was easier than I thought&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I was worried that relearning 20 years of Vim muscle memory would be really hard.&lt;/p&gt;
&lt;p&gt;It turned out to be easier than I expected, I started using Helix on a vacation for a little low-stakes coding project I was doing on the side and after a week or two it didn’t feel so disorienting anymore. I think it might be hard to switch back and forth between Vim and Helix, but I haven’t needed to use Vim recently so I don’t know if that’ll ever become an issue for me.&lt;/p&gt;
&lt;p&gt;The first time I tried Helix I tried to force it to use keybindings that were more similar to Vim and that did not work for me. Just learning the “Helix way” was a lot easier.&lt;/p&gt;
&lt;p&gt;There are still some things that throw me off: for example &lt;code&gt;w&lt;/code&gt; in vim and &lt;code&gt;w&lt;/code&gt; in Helix don’t have the same idea of what a “word” is (the Helix one includes the space after the word, the Vim one doesn’t).&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#using-a-terminal-based-text-editor&quot;&gt;using a terminal-based text editor&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For many years I’d mostly been using a GUI version of vim/neovim, so switching to actually using an editor in the terminal was a bit of an adjustment.&lt;/p&gt;
&lt;p&gt;I ended up deciding on:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every project gets its own terminal window, and all of the tabs in that window (mostly) have the same working directory&lt;/li&gt;
&lt;li&gt;I make my Helix tab the first tab in the terminal window&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It works pretty well, I might actually like it better than my previous workflow.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#my-configuration&quot;&gt;my configuration&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I appreciate that my configuration is really simple, compared to my neovim configuration which is hundreds of lines. It’s mostly just 4 keyboard shortcuts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;TOML&quot;&gt;theme = &amp;quot;solarized_light&amp;quot;
[editor]
# Sync clipboard with system clipboard
default-yank-register = &amp;quot;+&amp;quot;

[keys.normal]
# I didn&amp;#39;t like that Ctrl+C was the default &amp;quot;toggle comments&amp;quot; shortcut
&amp;quot;#&amp;quot; = &amp;quot;toggle_comments&amp;quot;

# I didn&amp;#39;t feel like learning a different way
# to go to the beginning/end of a line so
# I remapped ^ and $
&amp;quot;^&amp;quot; = &amp;quot;goto_first_nonwhitespace&amp;quot;
&amp;quot;$&amp;quot; = &amp;quot;goto_line_end&amp;quot;

[keys.select]
&amp;quot;^&amp;quot; = &amp;quot;goto_first_nonwhitespace&amp;quot;
&amp;quot;$&amp;quot; = &amp;quot;goto_line_end&amp;quot;

[keys.normal.space]
# I write a lot of text so I need to constantly reflow,
# and missed vim&amp;#39;s `gq` shortcut
l = &amp;quot;:reflow&amp;quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s a separate &lt;code&gt;languages.toml&lt;/code&gt; configuration where I set some language preferences, like turning off autoformatting. For example, here’s my Python configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;TOML&quot;&gt;[[language]]
name = &amp;quot;python&amp;quot;
formatter = { command = &amp;quot;black&amp;quot;, args = [&amp;quot;--stdin-filename&amp;quot;, &amp;quot;%{buffer_name}&amp;quot;, &amp;quot;-&amp;quot;] }
language-servers = [&amp;quot;pyright&amp;quot;]
auto-format = false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/10/10/notes-on-switching-to-helix-from-vim/#we-ll-see-how-it-goes&quot;&gt;we’ll see how it goes&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Three months is not that long, and it’s possible that I’ll decide to go back to Vim at some point. For example, I wrote a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2023/02/28/some-notes-on-using-nix/&quot;&gt;post about switching to nix&lt;/a&gt; a while back but after maybe 8 months I switched back to Homebrew (though I’m still using NixOS to manage one little server, and I’m still satisfied with that).&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>New zine: The Secret Rules of the Terminal</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-06-26-new-zine-the-secret-rules-of-the-terminal/</link>
    <guid isPermaLink="false">urn:aggr:item:da3a2d5287a823e963d0a308d6afa32333bf0e4a</guid>
    <atom:link href="https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/" rel="via" type="text/html"/>
    <pubDate>Thu, 26 Jun 2025 00:00:00 +0000</pubDate>
    <description>Hello! After many months of writing deep dive blog posts about the terminal, on Tuesday I released a new zine called “The Secret Rules of the Terminal”! You can get it for $12 here: https://wizardzines.com/zines/terminal, or get an 15-pack…</description>
    <content:encoded>&lt;p&gt;Hello! After many months of writing deep dive blog posts about the terminal, on Tuesday I released a new zine called “The Secret Rules of the Terminal”!&lt;/p&gt;
&lt;p&gt;You can get it for $12 here: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/terminal&quot;&gt;https://wizardzines.com/zines/terminal&lt;/a&gt;, or get an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/all-the-zines/&quot;&gt;15-pack of all my zines here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here’s the cover:&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/terminal&quot;&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/terminal-cover-small.jpg&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#the-table-of-contents&quot;&gt;the table of contents&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s the table of contents:&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/terminal/toc.png&quot;&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://jvns.ca/images/terminal-toc-small.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#why-the-terminal&quot;&gt;why the terminal?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’ve been using the terminal every day for 20 years but even though I’m very confident in the terminal, I’ve always had a bit of an uneasy feeling about it. Usually things work fine, but sometimes something goes wrong and it just feels like investigating it is impossible, or at least like it would open up a huge can of worms.&lt;/p&gt;
&lt;p&gt;So I started trying to write down a list of weird problems I’ve run into in terminal and I realized that the terminal has a lot of tiny inconsistencies like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sometimes you can use the arrow keys to move around, but sometimes pressing the arrow keys just prints &lt;code&gt;^[[D&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;sometimes you can use the mouse to select text, but sometimes you can’t&lt;/li&gt;
&lt;li&gt;sometimes your commands get saved to a history when you run them, and sometimes they don’t&lt;/li&gt;
&lt;li&gt;some shells let you use the up arrow to see the previous command, and some don’t&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you use the terminal daily for 10 or 20 years, even if you don’t understand exactly &lt;em&gt;why&lt;/em&gt; these things happen, you’ll probably build an intuition for them.&lt;/p&gt;
&lt;p&gt;But having an intuition for them isn’t the same as understanding why they happen. When writing this zine I actually had to do a lot of work to figure out exactly what was &lt;em&gt;happening&lt;/em&gt; in the terminal to be able to talk about how to reason about it.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#the-rules-aren-t-written-down-anywhere&quot;&gt;the rules aren’t written down anywhere&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It turns out that the “rules” for how the terminal works (how do you edit a command you type in? how do you quit a program? how do you fix your colours?) are extremely hard to fully understand, because “the terminal” is actually made of many different pieces of software (your terminal emulator, your operating system, your shell, the core utilities like &lt;code&gt;grep&lt;/code&gt;, and every other random terminal program you’ve installed) which are written by different people with different ideas about how things should work.&lt;/p&gt;
&lt;p&gt;So I wanted to write something that would explain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how the 4 pieces of the terminal (your shell, terminal emulator, programs, and TTY driver) fit together to make everything work&lt;/li&gt;
&lt;li&gt;some of the core conventions for how you can expect things in your terminal to work&lt;/li&gt;
&lt;li&gt;lots of tips and tricks for how to use terminal programs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#this-zine-explains-the-most-useful-parts-of-terminal-internals&quot;&gt;this zine explains the most useful parts of terminal internals&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Terminal internals are a mess. A lot of it is just the way it is because someone made a decision in the 80s and now it’s impossible to change, and honestly I don’t think learning everything about terminal internals is worth it.&lt;/p&gt;
&lt;p&gt;But some parts are not that hard to understand and can really make your experience in the terminal better, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if you understand what &lt;strong&gt;your shell&lt;/strong&gt; is responsible for, you can configure your shell (or use a different one!) to access your history more easily, get great tab completion, and so much more&lt;/li&gt;
&lt;li&gt;if you understand &lt;strong&gt;escape codes&lt;/strong&gt;, it’s much less scary when &lt;code&gt;cat&lt;/code&gt;ing a binary to stdout messes up your terminal, you can just type &lt;code&gt;reset&lt;/code&gt; and move on&lt;/li&gt;
&lt;li&gt;if you understand how &lt;strong&gt;colour&lt;/strong&gt; works, you can get rid of bad colour contrast in your terminal so you can actually read the text&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#i-learned-a-surprising-amount-writing-this-zine&quot;&gt;I learned a surprising amount writing this zine&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When I wrote &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/git&quot;&gt;How Git Works&lt;/a&gt;, I thought I knew how Git worked, and I was right. But the terminal is different. Even though I feel totally confident in the terminal and even though I’ve used it every day for 20 years, I had a lot of misunderstandings about how the terminal works and (unless you’re the author of &lt;code&gt;tmux&lt;/code&gt; or something) I think there’s a good chance you do too.&lt;/p&gt;
&lt;p&gt;A few things I learned that are actually useful to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I understand the structure of the terminal better and so I feel more confident debugging weird terminal stuff that happens to me (I was even able to suggest a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/fish-shell/fish-shell/issues/10834&quot;&gt;small improvement&lt;/a&gt; to fish!). Identifying exactly which piece of software is causing a weird thing to happen in my terminal still isn’t &lt;em&gt;easy&lt;/em&gt; but I’m a lot better at it now.&lt;/li&gt;
&lt;li&gt;you can write a shell script to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/til/vim-osc52/&quot;&gt;copy to your clipboard over SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;how &lt;code&gt;reset&lt;/code&gt; works under the hood (it does the equivalent of &lt;code&gt;stty sane; sleep 1; tput reset&lt;/code&gt;) – basically I learned that I don’t ever need to worry about remembering &lt;code&gt;stty sane&lt;/code&gt; or &lt;code&gt;tput reset&lt;/code&gt; and I can just run &lt;code&gt;reset&lt;/code&gt; instead&lt;/li&gt;
&lt;li&gt;how to look at the invisible escape codes that a program is printing out (run &lt;code&gt;unbuffer program &amp;gt; out; less out&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;why the builtin REPLs on my Mac like &lt;code&gt;sqlite3&lt;/code&gt; are so annoying to use (they use &lt;code&gt;libedit&lt;/code&gt; instead of &lt;code&gt;readline&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#blog-posts-i-wrote-along-the-way&quot;&gt;blog posts I wrote along the way&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As usual these days I wrote a bunch of blog posts about various side quests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/&quot;&gt;How to add a directory to your PATH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/&quot;&gt;“rules” that terminal problems follow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/&quot;&gt;why pipes sometimes get “stuck”: buffering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/&quot;&gt;some terminal frustrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/10/31/ascii-control-characters/&quot;&gt;ASCII control characters in my terminal&lt;/a&gt; on “what’s the deal with Ctrl+A, Ctrl+B, Ctrl+C, etc?”&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/07/08/readline/&quot;&gt;entering text in the terminal is complicated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/&quot;&gt;what’s involved in getting a “modern” terminal setup?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/07/03/reasons-to-use-job-control/&quot;&gt;reasons to use your shell’s job control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/&quot;&gt;standards for ANSI escape codes&lt;/a&gt;, which is really me trying to figure out if I think the &lt;code&gt;terminfo&lt;/code&gt; database is serving us well today&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#people-who-helped-with-this-zine&quot;&gt;people who helped with this zine&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A long time ago I used to write zines mostly by myself but with every project I get more and more help. I met with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://marieflanagan.com&quot;&gt;Marie Claire LeBlanc Flanagan&lt;/a&gt; every weekday from September to June to work on this one.&lt;/p&gt;
&lt;p&gt;The cover is by Vladimir Kašiković, Lesley Trites did copy editing, Simon Tatham (who wrote &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.chiark.greenend.org.uk/~sgtatham/putty/&quot;&gt;PuTTY&lt;/a&gt;) did technical review, our Operations Manager Lee did the transcription as well as a million other things, and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/doy&quot;&gt;Jesse Luehrs&lt;/a&gt; (who is one of the very few people I know who actually understands the terminal’s cursed inner workings) had so many incredibly helpful conversations with me about what is going on in the terminal.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/24/new-zine--the-secret-rules-of-the-terminal/#get-the-zine&quot;&gt;get the zine&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here are some links to get the zine again:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/terminal&quot;&gt;The Secret Rules of the Terminal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;get a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/zines/all-the-zines/&quot;&gt;15-pack of all my zines here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As always, you can get either a PDF version to print at home or a print version shipped to your house. The only caveat is print orders will ship in &lt;strong&gt;August&lt;/strong&gt; – I need to wait for orders to come in to get an idea of how many I should print before sending it to the printer.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Using `make` to compile C programs (for non-C-programmers)</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-06-10-using-make-to-compile-c-programs-for-non-c-programmers/</link>
    <guid isPermaLink="false">urn:aggr:item:00dae2af311ffa436f66661f4614f067e10fb439</guid>
    <atom:link href="https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/" rel="via" type="text/html"/>
    <pubDate>Tue, 10 Jun 2025 00:00:00 +0000</pubDate>
    <description>I have never been a C programmer but every so often I need to compile a C/C++ program from source. This has been kind of a struggle for me: for a long time, my approach was basically “install the dependencies, run make, if it doesn’t work,…</description>
    <content:encoded>&lt;p&gt;I have never been a C programmer but every so often I need to compile a C/C++ program from source. This has been kind of a struggle for me: for a long time, my approach was basically “install the dependencies, run &lt;code&gt;make&lt;/code&gt;, if it doesn’t work, either try to find a binary someone has compiled or give up”.&lt;/p&gt;
&lt;p&gt;“Hope someone else has compiled it” worked pretty well when I was running Linux but since I’ve been using a Mac for the last couple of years I’ve been running into more situations where I have to actually compile programs myself.&lt;/p&gt;
&lt;p&gt;So let’s talk about what you might have to do to compile a C program! I’ll use a couple of examples of specific C programs I’ve compiled and talk about a few things that can go wrong. Here are three programs we’ll be talking about compiling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mj.ucw.cz/sw/paperjam/&quot;&gt;paperjam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.sqlite.org/download.html&quot;&gt;sqlite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git.causal.agency/src/tree/bin/qf.c&quot;&gt;qf&lt;/a&gt; (a pager you can run to quickly open files from a search with &lt;code&gt;rg -n THING | qf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-1-install-a-c-compiler&quot;&gt;step 1: install a C compiler&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is pretty simple: on an Ubuntu system if I don’t already have a C compiler I’ll install one with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;sudo apt-get install build-essential
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs &lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;g++&lt;/code&gt;, and &lt;code&gt;make&lt;/code&gt;. The situation on a Mac is more confusing but it’s something like “install xcode command line tools”.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-2-install-the-program-s-dependencies&quot;&gt;step 2: install the program’s dependencies&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Unlike some newer programming languages, C doesn’t have a dependency manager. So if a program has any dependencies, you need to hunt them down yourself. Thankfully because of this, C programmers usually keep their dependencies very minimal and often the dependencies will be available in whatever package manager you’re using.&lt;/p&gt;
&lt;p&gt;There’s almost always a section explaining how to get the dependencies in the README, for example in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mj.ucw.cz/sw/paperjam/&quot;&gt;paperjam&lt;/a&gt;’s README, it says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To compile PaperJam, you need the headers for the libqpdf and libpaper libraries (usually available as libqpdf-dev and libpaper-dev packages).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;You may need &lt;code&gt;a2x&lt;/code&gt; (found in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;http://www.methods.co.nz/asciidoc/a2x.1.html&quot;&gt;AsciiDoc&lt;/a&gt;) for building manual pages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So on a Debian-based system you can install the dependencies like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;sudo apt install -y libqpdf-dev libpaper-dev
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a README gives a name for a package (like &lt;code&gt;libqpdf-dev&lt;/code&gt;), I’d basically always assume that they mean “in a Debian-based Linux distro”: if you’re on a Mac &lt;code&gt;brew install libqpdf-dev&lt;/code&gt; will not work. I still have not 100% gotten the hang of developing on a Mac yet so I don’t have many tips there yet. I guess in this case it would be &lt;code&gt;brew install qpdf&lt;/code&gt; if you’re using Homebrew.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-3-run-configure-if-needed&quot;&gt;step 3: run &lt;code&gt;./configure&lt;/code&gt; (if needed)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some C programs come with a &lt;code&gt;Makefile&lt;/code&gt; and some instead come with a script called &lt;code&gt;./configure&lt;/code&gt;. For example, if you download &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.sqlite.org/download.html&quot;&gt;sqlite’s source code&lt;/a&gt;, it has a &lt;code&gt;./configure&lt;/code&gt; script in it instead of a Makefile.&lt;/p&gt;
&lt;p&gt;My understanding of this &lt;code&gt;./configure&lt;/code&gt; script is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You run it, it prints out a lot of somewhat inscrutable output, and then it either generates a &lt;code&gt;Makefile&lt;/code&gt; or fails because you’re missing some dependency&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;./configure&lt;/code&gt; script is part of a system called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html&quot;&gt;autotools&lt;/a&gt; that I have never needed to learn anything about beyond “run it to generate a &lt;code&gt;Makefile&lt;/code&gt;”.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I think there might be some options you can pass to get the &lt;code&gt;./configure&lt;/code&gt; script to produce a different &lt;code&gt;Makefile&lt;/code&gt; but I have never done that.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-4-run-make&quot;&gt;step 4: run &lt;code&gt;make&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The next step is to run &lt;code&gt;make&lt;/code&gt; to try to build a program. Some notes about &lt;code&gt;make&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sometimes you can run &lt;code&gt;make -j8&lt;/code&gt; to parallelize the build and make it go faster&lt;/li&gt;
&lt;li&gt;It usually prints out a million compiler warnings when compiling the program. I always just ignore them. I didn’t write the software! The compiler warnings are not my problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#compiler-errors-are-often-dependency-problems&quot;&gt;compiler errors are often dependency problems&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s an error I got while compiling &lt;code&gt;paperjam&lt;/code&gt; on my Mac:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
   85 |     qpdf_offset_t last_offset{0};
      |                   ^
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Over the years I’ve learned it’s usually best not to overthink problems like this: if it’s talking about &lt;code&gt;qpdf&lt;/code&gt;, there’s a good change it just means that I’ve done something wrong with how I’m including the &lt;code&gt;qpdf&lt;/code&gt; dependency.&lt;/p&gt;
&lt;p&gt;Now let’s talk about some ways to get the &lt;code&gt;qpdf&lt;/code&gt; dependency included in the right way.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#the-world-s-shortest-introduction-to-the-compiler-and-linker&quot;&gt;the world’s shortest introduction to the compiler and linker&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Before we talk about how to fix dependency problems: building C programs is split into 2 steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compiling&lt;/strong&gt; the code into &lt;strong&gt;object files&lt;/strong&gt; (with &lt;code&gt;gcc&lt;/code&gt; or &lt;code&gt;clang&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linking&lt;/strong&gt; those object files into a final binary (with &lt;code&gt;ld&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s important to know this when building a C program because sometimes you need to pass the right flags to the compiler and linker to tell them where to find the dependencies for the program you’re compiling.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#make-uses-environment-variables-to-configure-the-compiler-and-linker&quot;&gt;&lt;code&gt;make&lt;/code&gt; uses environment variables to configure the compiler and linker&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If I run &lt;code&gt;make&lt;/code&gt; on my Mac to install &lt;code&gt;paperjam&lt;/code&gt;, I get this error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library &amp;#39;qpdf&amp;#39; not found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not because &lt;code&gt;qpdf&lt;/code&gt; is not installed on my system (it actually is!). But the compiler and linker don’t know how to &lt;em&gt;find&lt;/em&gt; the &lt;code&gt;qpdf&lt;/code&gt; library. To fix this, we need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pass &lt;code&gt;&amp;quot;-I/opt/homebrew/include&amp;quot;&lt;/code&gt; to the compiler (to tell it where to find the header files)&lt;/li&gt;
&lt;li&gt;pass &lt;code&gt;&amp;quot;-L/opt/homebrew/lib -liconv&amp;quot;&lt;/code&gt; to the linker (to tell it where to find library files and to link in &lt;code&gt;iconv&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And we can get &lt;code&gt;make&lt;/code&gt; to pass those extra parameters to the compiler and linker using environment variables! To see how this works: inside &lt;code&gt;paperjam&lt;/code&gt;’s Makefile you can see a bunch of environment variables, like &lt;code&gt;LDLIBS&lt;/code&gt; here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Makefile&quot;&gt;&lt;span class=&quot;syntax-source syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-makefile&quot;&gt;paperjam&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-makefile&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function syntax-arguments syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-begin syntax-makefile&quot;&gt;$(&lt;/span&gt;OBJS&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-end syntax-makefile&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-body syntax-makefile&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-body syntax-makefile&quot;&gt;	&lt;span class=&quot;syntax-source syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-begin syntax-makefile&quot;&gt;$(&lt;/span&gt;LD&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-end syntax-makefile&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;-o&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-variable syntax-language syntax-automatic syntax-makefile&quot;&gt;$@&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-language syntax-automatic syntax-makefile&quot;&gt;$^&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-makefile&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-begin syntax-makefile&quot;&gt;$(&lt;/span&gt;LDLIBS&lt;span class=&quot;syntax-keyword syntax-other syntax-block syntax-end syntax-makefile&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything you put into the &lt;code&gt;LDLIBS&lt;/code&gt; environment variable gets passed to the linker (&lt;code&gt;ld&lt;/code&gt;) as a command line argument.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#secret-environment-variable-cppflags&quot;&gt;secret environment variable: &lt;code&gt;CPPFLAGS&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Makefiles&lt;/code&gt; sometimes define their own environment variables that they pass to the compiler/linker, but &lt;code&gt;make&lt;/code&gt; also has a bunch of “implicit” environment variables which it will automatically pass to the C compiler and linker. There’s a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html#index-CFLAGS0&quot;&gt;full list of implicit environment variables here&lt;/a&gt;, but one of them is &lt;code&gt;CPPFLAGS&lt;/code&gt;, which gets automatically passed to the C compiler.&lt;/p&gt;
&lt;p&gt;(technically it would be more normal to use &lt;code&gt;CXXFLAGS&lt;/code&gt; for this, but this particular &lt;code&gt;Makefile&lt;/code&gt; hardcodes &lt;code&gt;CXXFLAGS&lt;/code&gt; so setting &lt;code&gt;CPPFLAGS&lt;/code&gt; was the only way I could find to set the compiler flags without editing the &lt;code&gt;Makefile&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;As an aside: it took me a long time to realize how closely tied to C/C++ `make` is -- I used to think that `make` was just a general build system (and of course you can use it for anything!) but it has a lot of affordances for building C/C++ programs that it doesn&#39;t have for building any other kind of program.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#two-ways-to-pass-environment-variables-to-make&quot;&gt;two ways to pass environment variables to &lt;code&gt;make&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I learned thanks to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.owlfolio.org/&quot;&gt;@zwol&lt;/a&gt; that there are actually two ways to pass environment variables to &lt;code&gt;make&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CXXFLAGS=xyz make&lt;/code&gt; (the usual way)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;make CXXFLAGS=xyz&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The difference between them is that &lt;code&gt;make CXXFLAGS=xyz&lt;/code&gt; will override the value of &lt;code&gt;CXXFLAGS&lt;/code&gt; set in the &lt;code&gt;Makefile&lt;/code&gt; but &lt;code&gt;CXXFLAGS=xyz make&lt;/code&gt; won’t.&lt;/p&gt;
&lt;p&gt;I’m not sure which way is the norm but I’m going to use the first way in this post.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#how-to-use-cppflags-and-ldlibs-to-fix-this-compiler-error&quot;&gt;how to use &lt;code&gt;CPPFLAGS&lt;/code&gt; and &lt;code&gt;LDLIBS&lt;/code&gt; to fix this compiler error&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now that we’ve talked about how &lt;code&gt;CPPFLAGS&lt;/code&gt; and &lt;code&gt;LDLIBS&lt;/code&gt; get passed to the compiler and linker, here’s the final incantation that I used to get the program to build successfully!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;CPPFLAGS=&amp;quot;-I/opt/homebrew/include&amp;quot; LDLIBS=&amp;quot;-L/opt/homebrew/lib -liconv&amp;quot; make paperjam
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This passes &lt;code&gt;-I/opt/homebrew/include&lt;/code&gt; to the compiler and &lt;code&gt;-L/opt/homebrew/lib -liconv&lt;/code&gt; to the linker.&lt;/p&gt;
&lt;p&gt;Also I don’t want to pretend that I “magically” knew that those were the right arguments to pass, figuring them out involved a bunch of confused Googling that I skipped over in this post. I will say that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;-I&lt;/code&gt; compiler flag tells the compiler which directory to find header files in, like &lt;code&gt;/opt/homebrew/include/qpdf/QPDF.hh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;-L&lt;/code&gt; linker flag tells the linker which directory to find libraries in, like &lt;code&gt;/opt/homebrew/lib/libqpdf.a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;-l&lt;/code&gt; linker flag tells the linker which libraries to link in, like &lt;code&gt;-liconv&lt;/code&gt; means “link in the &lt;code&gt;iconv&lt;/code&gt; library”, or &lt;code&gt;-lm&lt;/code&gt; means “link &lt;code&gt;math&lt;/code&gt;”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#tip-how-to-just-build-1-specific-file-make-filename&quot;&gt;tip: how to just build 1 specific file: &lt;code&gt;make $FILENAME&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Yesterday I discovered this cool tool called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://git.causal.agency/src/tree/bin/qf.c&quot;&gt;qf&lt;/a&gt; which you can use to quickly open files from the output of &lt;code&gt;ripgrep&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;qf&lt;/code&gt; is in a big directory of various tools, but I only wanted to compile &lt;code&gt;qf&lt;/code&gt;. So I just compiled &lt;code&gt;qf&lt;/code&gt;, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;make qf
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically if you know (or can guess) the output filename of the file you’re trying to build, you can tell &lt;code&gt;make&lt;/code&gt; to just build that file by running &lt;code&gt;make $FILENAME&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#tip-you-don-t-need-a-makefile&quot;&gt;tip: you don’t need a Makefile&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I sometimes write 5-line C programs with no dependencies, and I just learned that if I have a file called &lt;code&gt;blah.c&lt;/code&gt;, I can just compile it like this without creating a &lt;code&gt;Makefile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;make blah
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It gets automaticaly expanded to &lt;code&gt;cc -o blah blah.c&lt;/code&gt;, which saves a bit of typing. I have no idea if I’m going to remember this (I might just keep typing &lt;code&gt;gcc -o blah blah.c&lt;/code&gt; anyway) but it seems like a fun trick.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#tip-look-at-how-other-packaging-systems-built-the-same-c-program&quot;&gt;tip: look at how other packaging systems built the same C program&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you’re having trouble building a C program, maybe other people had problems building it too! Every Linux distribution has build files for every package that they build, so even if you can’t install packages from that distribution directly, maybe you can get tips from that Linux distro for how to build the package. Realizing this (thanks to my friend Dave) was a huge ah-ha moment for me.&lt;/p&gt;
&lt;p&gt;For example, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/NixOS/nixpkgs/blob/405624e81a9b65378328accb0a11c3e5369e651c/pkgs/by-name/pa/paperjam/package.nix#L35&quot;&gt;this line from the nix package for &lt;code&gt;paperjam&lt;/code&gt;&lt;/a&gt; says:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;  env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin &amp;quot;-liconv&amp;quot;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is basically saying “pass the linker flag &lt;code&gt;-liconv&lt;/code&gt; to build this on a Mac”, so that’s a clue we could use to build it.&lt;/p&gt;
&lt;p&gt;That same file also says &lt;code&gt;env.NIX_CFLAGS_COMPILE = &amp;quot;-DPOINTERHOLDER_TRANSITION=1&amp;quot;;&lt;/code&gt;. I’m not sure what this means, but when I try to build the &lt;code&gt;paperjam&lt;/code&gt; package I do get an error about something called a &lt;code&gt;PointerHolder&lt;/code&gt;, so I guess that’s somehow related to the “PointerHolder transition”.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-5-installing-the-binary&quot;&gt;step 5: installing the binary&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once you’ve managed to compile the program, probably you want to install it somewhere! Some &lt;code&gt;Makefile&lt;/code&gt;s have an &lt;code&gt;install&lt;/code&gt; target that let you install the tool on your system with &lt;code&gt;make install&lt;/code&gt;. I’m always a bit scared of this (where is it going to put the files? what if I want to uninstall them later?), so if I’m compiling a pretty simple program I’ll often just manually copy the binary to install it instead, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;cp qf ~/bin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#step-6-maybe-make-your-own-package&quot;&gt;step 6: maybe make your own package!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once I figured out how to do all of this, I realized that I could use my new &lt;code&gt;make&lt;/code&gt; knowledge to contribute a &lt;code&gt;paperjam&lt;/code&gt; package to Homebrew! Then I could just &lt;code&gt;brew install paperjam&lt;/code&gt; on future systems.&lt;/p&gt;
&lt;p&gt;The good thing is that even if the details of how all of the different packaging systems, they fundamentally all use C compilers and linkers.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/#it-can-be-useful-to-understand-a-little-about-c-even-if-you-re-not-a-c-programmer&quot;&gt;it can be useful to understand a little about C even if you’re not a C programmer&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think all of this is an interesting example of how it can useful to understand some basics of how C programs work (like “they have header files”) even if you’re never planning to write a nontrivial C program if your life.&lt;/p&gt;
&lt;p&gt;It feels good to have some ability to compile C/C++ programs myself, even though I’m still not totally confident about all of the compiler and linker flags and I still plan to never learn anything about how autotools works other than “you run &lt;code&gt;./configure&lt;/code&gt; to generate the &lt;code&gt;Makefile&lt;/code&gt;”.&lt;/p&gt;
&lt;p&gt;Two things I left out of this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LD_LIBRARY_PATH / DYLD_LIBRARY_PATH&lt;/code&gt; (which you use to tell the dynamic linker at runtime where to find dynamically linked files) because I can’t remember the last time I ran into an &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; issue and couldn’t find an example.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pkg-config&lt;/code&gt;, which I think is important but I don’t understand yet&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Standards for ANSI escape codes</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-03-07-standards-for-ansi-escape-codes/</link>
    <guid isPermaLink="false">urn:aggr:item:8bd6925044143c99cb4452b185c8c36a51df01e4</guid>
    <atom:link href="https://jvns.ca/blog/2025/03/07/escape-code-standards/" rel="via" type="text/html"/>
    <pubDate>Fri, 7 Mar 2025 00:00:00 +0000</pubDate>
    <description>Hello! Today I want to talk about ANSI escape codes. For a long time I was vaguely aware of ANSI escape codes (“that’s how you make text red in the terminal and stuff”) but I had no real understanding of where they were supposed to be…</description>
    <content:encoded>&lt;p&gt;Hello! Today I want to talk about ANSI escape codes.&lt;/p&gt;
&lt;p&gt;For a long time I was vaguely aware of ANSI escape codes (“that’s how you make text red in the terminal and stuff”) but I had no real understanding of where they were supposed to be defined or whether or not there were standards for them. I just had a kind of vague “there be dragons” feeling around them. While learning about the terminal this year, I’ve learned that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ANSI escape codes are responsible for a lot of usability improvements in the terminal (did you know there’s a way to copy to your system clipboard when SSHed into a remote machine?? It’s an escape code called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/til/vim-osc52/&quot;&gt;OSC 52&lt;/a&gt;!)&lt;/li&gt;
&lt;li&gt;They aren’t completely standardized, and because of that they don’t always work reliably. And because they’re also invisible, it’s extremely frustrating to troubleshoot escape code issues.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So I wanted to put together a list for myself of some standards that exist around escape codes, because I want to know if they &lt;em&gt;have&lt;/em&gt; to feel unreliable and frustrating, or if there’s a future where we could all rely on them with more confidence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#what-s-an-escape-code&quot;&gt;what’s an escape code?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#ecma-48&quot;&gt;ECMA-48&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#xterm-control-sequences&quot;&gt;xterm control sequences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#terminfo&quot;&gt;terminfo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#should-programs-use-terminfo&quot;&gt;should programs use terminfo?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#is-there-a-single-common-set-of-escape-codes&quot;&gt;is there a “single common set” of escape codes?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#some-reasons-to-use-terminfo&quot;&gt;some reasons to use terminfo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#some-more-documents-standards&quot;&gt;some more documents/standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#why-i-think-this-is-interesting&quot;&gt;why I think this is interesting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#what-s-an-escape-code&quot;&gt;what’s an escape code?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Have you ever pressed the left arrow key in your terminal and seen &lt;code&gt;^[[D&lt;/code&gt;? That’s an escape code! It’s called an “escape code” because the first character is the “escape” character, which is usually written as &lt;code&gt;ESC&lt;/code&gt;, &lt;code&gt;\x1b&lt;/code&gt;, &lt;code&gt;\E&lt;/code&gt;, &lt;code&gt;\033&lt;/code&gt;, or &lt;code&gt;^[&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Escape codes are how your terminal emulator communicates various kinds of information (colours, mouse movement, etc) with programs running in the terminal. There are two kind of escape codes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;input codes&lt;/strong&gt; which your terminal emulator sends for keypresses or mouse movements that don’t fit into Unicode. For example “left arrow key” is &lt;code&gt;ESC[D&lt;/code&gt;, “Ctrl+left arrow” might be &lt;code&gt;ESC[1;5D&lt;/code&gt;, and clicking the mouse might be something like &lt;code&gt;ESC[M :3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;output codes&lt;/strong&gt; which programs can print out to colour text, move the cursor around, clear the screen, hide the cursor, copy text to the clipboard, enable mouse reporting, set the window title, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now let’s talk about standards!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#ecma-48&quot;&gt;ECMA-48&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The first standard I found relating to escape codes was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf&quot;&gt;ECMA-48&lt;/a&gt;, which was originally published in 1976.&lt;/p&gt;
&lt;p&gt;ECMA-48 does two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define some general &lt;em&gt;formats&lt;/em&gt; for escape codes (like “CSI” codes, which are &lt;code&gt;ESC[&lt;/code&gt; + something and “OSC” codes, which are &lt;code&gt;ESC]&lt;/code&gt; + something)&lt;/li&gt;
&lt;li&gt;Define some specific escape codes, like how “move the cursor to the left” is &lt;code&gt;ESC[D&lt;/code&gt;, or “turn text red” is &lt;code&gt;ESC[31m&lt;/code&gt;. In the spec, the “cursor left” one is called &lt;code&gt;CURSOR LEFT&lt;/code&gt; and the one for changing colours is called &lt;code&gt;SELECT GRAPHIC RENDITION&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The formats are extensible, so there’s room for others to define more escape codes in the future. Lots of escape codes that are popular today aren’t defined in ECMA-48: for example it’s pretty common for terminal applications (like vim, htop, or tmux) to support using the mouse, but ECMA-48 doesn’t define escape codes for the mouse.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#xterm-control-sequences&quot;&gt;xterm control sequences&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are a bunch of escape codes that aren’t defined in ECMA-48, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;enabling mouse reporting (where did you click in your terminal?)&lt;/li&gt;
&lt;li&gt;bracketed paste (did you paste that text or type it in?)&lt;/li&gt;
&lt;li&gt;OSC 52 (which terminal applications can use to copy text to your system clipboard)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I believe (correct me if I’m wrong!) that these and some others came from xterm, are documented in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://invisible-island.net/xterm/ctlseqs/ctlseqs.html&quot;&gt;XTerm Control Sequences&lt;/a&gt;, and have been widely implemented by other terminal emulators.&lt;/p&gt;
&lt;p&gt;This list of “what xterm supports” is not a standard exactly, but xterm is extremely influential and so it seems like an important document.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#terminfo&quot;&gt;terminfo&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the 80s (and to some extent today, but my understanding is that it was MUCH more dramatic in the 80s) there was a huge amount of variation in what escape codes terminals actually supported.&lt;/p&gt;
&lt;p&gt;To deal with this, there’s a database of escape codes for various terminals called “terminfo”.&lt;/p&gt;
&lt;p&gt;It looks like the standard for terminfo is called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://publications.opengroup.org/c243-1&quot;&gt;X/Open Curses&lt;/a&gt;, though you need to create an account to view that standard for some reason. It defines the database format as well as a C library interface (“curses”) for accessing the database.&lt;/p&gt;
&lt;p&gt;For example you can run this bash snippet to see every possible escape code for “clear screen” for all of the different terminals your system knows about:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;for term in $(toe -a | awk &amp;#39;{print $1}&amp;#39;)
do
  echo $term
  infocmp -1 -T &amp;quot;$term&amp;quot; 2&amp;gt;/dev/null | grep &amp;#39;clear=&amp;#39; | sed &amp;#39;s/clear=//g;s/,//g&amp;#39;
done
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On my system (and probably every system I’ve ever used?), the terminfo database is managed by ncurses.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#should-programs-use-terminfo&quot;&gt;should programs use terminfo?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think it’s interesting that there are two main approaches that applications take to handling ANSI escape codes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the terminfo database to figure out which escape codes to use, depending on what’s in the &lt;code&gt;TERM&lt;/code&gt; environment variable. Fish does this, for example.&lt;/li&gt;
&lt;li&gt;Identify a “single common set” of escape codes which works in “enough” terminal emulators and just hardcode those.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Some examples of programs/libraries that take approach #2 (“don’t use terminfo”) include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/mawww/kakoune/commit/c12699d2e9c2806d6ed184032078d0b84a3370bb&quot;&gt;kakoune&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/prompt-toolkit/python-prompt-toolkit/blob/165258d2f3ae594b50f16c7b50ffb06627476269/src/prompt_toolkit/input/ansi_escape_sequences.py#L5-L8&quot;&gt;python-prompt-toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/antirez/linenoise&quot;&gt;linenoise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/rockorager/libvaxis&quot;&gt;libvaxis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/chalk/chalk&quot;&gt;chalk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I got curious about why folks might be moving away from terminfo and I found this very interesting and extremely detailed &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twoot.site/@bean/113056942625234032&quot;&gt;rant about terminfo from one of the fish maintainers&lt;/a&gt;, which argues that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[the terminfo authors] have done a lot of work that, at the time, was extremely important and helpful. My point is that it no longer is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m not going to do it justice so I’m not going to summarize it, I think it’s worth reading.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#is-there-a-single-common-set-of-escape-codes&quot;&gt;is there a “single common set” of escape codes?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I was just talking about the idea that you can use a “common set” of escape codes that will work for most people. But what is that set? Is there any agreement?&lt;/p&gt;
&lt;p&gt;I really do not know the answer to this at all, but from doing some reading it seems like it’s some combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The codes that the VT100 supported (though some aren’t relevant on modern terminals)&lt;/li&gt;
&lt;li&gt;what’s in ECMA-48 (which I think also has some things that are no longer relevant)&lt;/li&gt;
&lt;li&gt;What xterm supports (though I’d guess that not everything in there is actually widely supported enough)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and maybe ultimately “identify the terminal emulators you think your users are going to use most frequently and test in those”, the same way web developers do when deciding which CSS features are okay to use&lt;/p&gt;
&lt;p&gt;I don’t think there are any resources like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://caniuse.com/&quot;&gt;Can I use…?&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://web-platform-dx.github.io/web-features/&quot;&gt;Baseline&lt;/a&gt; for the terminal though. (in theory terminfo is supposed to be the “caniuse” for the terminal but it seems like it often takes 10+ years to add new terminal features when people invent them which makes it very limited)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#some-reasons-to-use-terminfo&quot;&gt;some reasons to use terminfo&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also asked on Mastodon why people found terminfo valuable in 2025 and got a few reasons that made sense to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;some people expect to be able to use the &lt;code&gt;TERM&lt;/code&gt; environment variable to control how programs behave (for example with &lt;code&gt;TERM=dumb&lt;/code&gt;), and there’s no standard for how that should work in a post-terminfo world&lt;/li&gt;
&lt;li&gt;even though there’s &lt;em&gt;less&lt;/em&gt; variation between terminal emulators than there was in the 80s, there’s far from zero variation: there are graphical terminals, the Linux framebuffer console, the situation you’re in when connecting to a server via its serial console, Emacs shell mode, and probably more that I’m missing&lt;/li&gt;
&lt;li&gt;there is no one standard for what the “single common set” of escape codes is, and sometimes programs use escape codes which aren’t actually widely supported enough&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#terminfo-user-agent-detection&quot;&gt;terminfo &amp;amp; user agent detection&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The way that ncurses uses the &lt;code&gt;TERM&lt;/code&gt; environment variable to decide which escape codes to use reminds me of how webservers used to sometimes use the browser user agent to decide which version of a website to serve.&lt;/p&gt;
&lt;p&gt;It also seems like it’s had some of the same results – the way iTerm2 reports itself as being “xterm-256color” feels similar to how Safari’s user agent is “Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15”. In both cases the terminal emulator / browser ends up changing its user agent to get around user agent detection that isn’t working well.&lt;/p&gt;
&lt;p&gt;On the web we ended up deciding that user agent detection was not a good practice and to instead focus on standardization so we can serve the same HTML/CSS to all browsers. I don’t know if the same approach is the future in the terminal though – I think the terminal landscape today is much more fragmented than the web ever was as well as being much less well funded.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#some-more-documents-standards&quot;&gt;some more documents/standards&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A few more documents and standards related to escape codes, in no particular order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man7.org/linux/man-pages/man4/console_codes.4.html&quot;&gt;Linux console_codes man page&lt;/a&gt; documents escape codes that Linux supports&lt;/li&gt;
&lt;li&gt;how the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vt100.net/docs/vt100-ug/chapter3.html&quot;&gt;VT 100&lt;/a&gt; handles escape codes &amp;amp; control sequences&lt;/li&gt;
&lt;li&gt;the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sw.kovidgoyal.net/kitty/keyboard-protocol/&quot;&gt;kitty keyboard protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda&quot;&gt;OSC 8&lt;/a&gt; for links in the terminal (and notes on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/Alhadis/OSC8-Adoption?tab=readme-ov-file&quot;&gt;adoption&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/tmux/tmux/blob/882fb4d295deb3e4b803eb444915763305114e4f/tools/ansicode.txt&quot;&gt;summary of ANSI standards from tmux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;this &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://iterm2.com/feature-reporting/&quot;&gt;terminal features reporting specification from iTerm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;sixel graphics&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/03/07/escape-code-standards/#why-i-think-this-is-interesting&quot;&gt;why I think this is interesting&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I sometimes see people saying that the unix terminal is “outdated”, and since I love the terminal so much I’m always curious about what incremental changes might make it feel less “outdated”.&lt;/p&gt;
&lt;p&gt;Maybe if we had a clearer standards landscape (like we do on the web!) it would be easier for terminal emulator developers to build new features and for authors of terminal applications to more confidently adopt those features so that we can all benefit from them and have a richer experience in the terminal.&lt;/p&gt;
&lt;p&gt;Obviously standardizing ANSI escape codes is not easy (ECMA-48 was first published almost 50 years ago and we’re still not there!). I don’t even know what all of the challenges are. But the situation with HTML/CSS/JS used to be extremely bad too and now it’s MUCH better, so maybe there’s hope.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>How to add a directory to your PATH</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-02-13-how-to-add-a-directory-to-your-path/</link>
    <guid isPermaLink="false">urn:aggr:item:671997761a38c47b1cabb3782dfcf90dbd345aa5</guid>
    <atom:link href="https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/" rel="via" type="text/html"/>
    <pubDate>Thu, 13 Feb 2025 12:27:56 +0000</pubDate>
    <description>I was talking to a friend about how to add a directory to your PATH today. It’s something that feels “obvious” to me since I’ve been using the terminal for a long time, but when I searched for instructions for how to do it, I actually…</description>
    <content:encoded>&lt;p&gt;I was talking to a friend about how to add a directory to your PATH today. It’s something that feels “obvious” to me since I’ve been using the terminal for a long time, but when I searched for instructions for how to do it, I actually couldn’t find something that explained all of the steps – a lot of them just said “add this to &lt;code&gt;~/.bashrc&lt;/code&gt;”, but what if you’re not using bash? What if your bash config is actually in a different file? And how are you supposed to figure out which directory to add anyway?&lt;/p&gt;
&lt;p&gt;So I wanted to try to write down some more complete directions and mention some of the gotchas I’ve run into over the years.&lt;/p&gt;
&lt;p&gt;Here’s a table of contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-1-what-shell-are-you-using&quot;&gt;step 1: what shell are you using?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-2-find-your-shell-s-config-file&quot;&gt;step 2: find your shell’s config file&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-bash-s-config-file&quot;&gt;a note on bash’s config file&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-3-figure-out-which-directory-to-add&quot;&gt;step 3: figure out which directory to add&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-3-1-double-check-it-s-the-right-directory&quot;&gt;step 3.1: double check it’s the right directory&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-4-edit-your-shell-config&quot;&gt;step 4: edit your shell config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-5-restart-your-shell&quot;&gt;step 5: restart your shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;problems:
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-1-it-ran-the-wrong-program&quot;&gt;problem 1: it ran the wrong program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-2-the-program-isn-t-being-run-from-your-shell&quot;&gt;problem 2: the program isn’t being run from your shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-3-duplicate-path-entries-making-it-harder-to-debug&quot;&gt;problem 3: duplicate PATH entries making it harder to debug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-4-losing-your-history-after-updating-your-path&quot;&gt;problem 4: losing your history after updating your PATH&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;notes:
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-source&quot;&gt;a note on source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-fish-add-path&quot;&gt;a note on fish_add_path&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-1-what-shell-are-you-using&quot;&gt;step 1: what shell are you using?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you’re not sure what shell you’re using, here’s a way to find out. Run this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;ps -p $$ -o pid,comm=
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;if you’re using &lt;strong&gt;bash&lt;/strong&gt;, it’ll print out &lt;code&gt;97295 bash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;if you’re using &lt;strong&gt;zsh&lt;/strong&gt;, it’ll print out &lt;code&gt;97295 zsh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;if you’re using &lt;strong&gt;fish&lt;/strong&gt;, it’ll print out an error like “In fish, please use &lt;span class=&quot;math&quot;&gt;fishₚid” (`&lt;/span&gt;$` isn’t valid syntax in fish, but in any case the error message tells you that you’re using fish, which you probably already knew)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also bash is the default on Linux and zsh is the default on Mac OS (as of 2024). I’ll only cover bash, zsh, and fish in these directions.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-2-find-your-shell-s-config-file&quot;&gt;step 2: find your shell’s config file&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;in zsh, it’s probably &lt;code&gt;~/.zshrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;in bash, it might be &lt;code&gt;~/.bashrc&lt;/code&gt;, but it’s complicated, see the note in the next section&lt;/li&gt;
&lt;li&gt;in fish, it’s probably &lt;code&gt;~/.config/fish/config.fish&lt;/code&gt; (you can run &lt;code&gt;echo $__fish_config_dir&lt;/code&gt; if you want to be 100% sure)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-bash-s-config-file&quot;&gt;a note on bash’s config file&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Bash has three possible config files: &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.bash_profile&lt;/code&gt;, and &lt;code&gt;~/.profile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you’re not sure which one your system is set up to use, I’d recommend testing this way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;add &lt;code&gt;echo hi there&lt;/code&gt; to your &lt;code&gt;~/.bashrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Restart your terminal&lt;/li&gt;
&lt;li&gt;If you see “hi there”, that means &lt;code&gt;~/.bashrc&lt;/code&gt; is being used! Hooray!&lt;/li&gt;
&lt;li&gt;Otherwise remove it and try the same thing with &lt;code&gt;~/.bash_profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can also try &lt;code&gt;~/.profile&lt;/code&gt; if the first two options don’t work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(there are a lot of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.flowblok.id.au/2013-02/shell-startup-scripts.html&quot;&gt;elaborate flow charts&lt;/a&gt; out there that explain how bash decides which config file to use but IMO it’s not worth it to internalize them and just testing is the fastest way to be sure)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-3-figure-out-which-directory-to-add&quot;&gt;step 3: figure out which directory to add&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let’s say that you’re trying to install and run a program called &lt;code&gt;http-server&lt;/code&gt; and it doesn’t work, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; npm install&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;g&lt;/span&gt; http-server&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; http-server&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;bash:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; http-server: command not found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How do you find what directory &lt;code&gt;http-server&lt;/code&gt; is in? Honestly in general this is not that easy – often the answer is something like “it depends on how npm is configured”. A few ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Often when setting up a new installer (like &lt;code&gt;cargo&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;homebrew&lt;/code&gt;, etc), when you first set it up it’ll print out some directions about how to update your PATH. So if you’re paying attention you can get the directions then.&lt;/li&gt;
&lt;li&gt;Sometimes installers will automatically update your shell’s config file to update your &lt;code&gt;PATH&lt;/code&gt; for you&lt;/li&gt;
&lt;li&gt;Sometimes just Googling “where does npm install things?” will turn up the answer&lt;/li&gt;
&lt;li&gt;Some tools have a subcommand that tells you where they’re configured to install things, like:
&lt;ul&gt;
&lt;li&gt;Node/npm: &lt;code&gt;npm config get prefix&lt;/code&gt; (then append &lt;code&gt;/bin/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Go: &lt;code&gt;go env GOPATH&lt;/code&gt; (then append &lt;code&gt;/bin/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;asdf: &lt;code&gt;asdf info | grep ASDF_DIR&lt;/code&gt; (then append &lt;code&gt;/bin/&lt;/code&gt; and &lt;code&gt;/shims/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-3-1-double-check-it-s-the-right-directory&quot;&gt;step 3.1: double check it’s the right directory&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once you’ve found a directory you think might be the right one, make sure it’s actually correct! For example, I found out that on my machine, &lt;code&gt;http-server&lt;/code&gt; is in &lt;code&gt;~/.npm-global/bin&lt;/code&gt;. I can make sure that it’s the right directory by trying to run the program &lt;code&gt;http-server&lt;/code&gt; in that directory like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-tilde&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-tilde syntax-shell&quot;&gt;~&lt;/span&gt;&lt;/span&gt;/.npm-global/bin/http-server&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;Starting&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; up http-server, serving ./public&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked! Now that you know what directory you need to add to your &lt;code&gt;PATH&lt;/code&gt;, let’s move to the next step!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-4-edit-your-shell-config&quot;&gt;step 4: edit your shell config&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now we have the 2 critical pieces of information we need:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which directory you’re trying to add to your PATH (like &lt;code&gt;~/.npm-global/bin/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Where your shell’s config is (like &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt;, or &lt;code&gt;~/.config/fish/config.fish&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now what you need to add depends on your shell:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bash instructions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open your shell’s config file, and add a line like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;export PATH=$PATH:~/.npm-global/bin/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(obviously replace &lt;code&gt;~/.npm-global/bin&lt;/code&gt; with the actual directory you’re trying to add)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;zsh instructions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can do the same thing as in bash, but zsh also has some slightly fancier syntax you can use if you prefer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;path=(
  $path
  ~/.npm-global/bin
)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;fish instructions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In fish, the syntax is different:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;set PATH $PATH ~/.npm-global/bin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(in fish you can also use &lt;code&gt;fish_add_path&lt;/code&gt;, some notes on that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-fish-add-path&quot;&gt;further down&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#step-5-restart-your-shell&quot;&gt;step 5: restart your shell&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now, an extremely important step: updating your shell’s config won’t take effect if you don’t restart it!&lt;/p&gt;
&lt;p&gt;Two ways to do this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;open a new terminal (or terminal tab), and maybe close the old one so you don’t get confused&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;bash&lt;/code&gt; to start a new shell (or &lt;code&gt;zsh&lt;/code&gt; if you’re using zsh, or &lt;code&gt;fish&lt;/code&gt; if you’re using fish)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’ve found that both of these usually work fine.&lt;/p&gt;
&lt;p&gt;And you should be done! Try running the program you were trying to run and hopefully it works now.&lt;/p&gt;
&lt;p&gt;If not, here are a couple of problems that you might run into:&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-1-it-ran-the-wrong-program&quot;&gt;problem 1: it ran the wrong program&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If the wrong &lt;strong&gt;version&lt;/strong&gt; of a program is running, you might need to add the directory to the &lt;em&gt;beginning&lt;/em&gt; of your PATH instead of the end.&lt;/p&gt;
&lt;p&gt;For example, on my system I have two versions of &lt;code&gt;python3&lt;/code&gt; installed, which I can see by running &lt;code&gt;which -a&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; which&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;a&lt;/span&gt; python3&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;/usr/bin/python3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;/opt/homebrew/bin/python3&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one your shell will use is the &lt;strong&gt;first one listed&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you want to use the Homebrew version, you need to add that directory (&lt;code&gt;/opt/homebrew/bin&lt;/code&gt;) to the &lt;strong&gt;beginning&lt;/strong&gt; of your PATH instead, by putting this in your shell’s config file (it’s &lt;code&gt;/opt/homebrew/bin/:$PATH&lt;/code&gt; instead of the usual &lt;code&gt;$PATH:/opt/homebrew/bin/&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;export PATH=/opt/homebrew/bin/:$PATH
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or in fish:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;set PATH ~/.cargo/bin $PATH
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-2-the-program-isn-t-being-run-from-your-shell&quot;&gt;problem 2: the program isn’t being run from your shell&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;All of these directions only work if you’re running the program &lt;strong&gt;from your shell&lt;/strong&gt;. If you’re running the program from an IDE, from a GUI, in a cron job, or some other way, you’ll need to add the directory to your PATH in a different way, and the exact details might depend on the situation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;in a cron job&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use the full path to the program you’re running, like &lt;code&gt;/home/bork/bin/my-program&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;put the full PATH you want as the first line of your crontab (something like PATH=/bin:/usr/bin:/usr/local/bin:….). You can get the full PATH you’re using in your shell by running &lt;code&gt;echo &amp;quot;PATH=$PATH&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m honestly not sure how to handle it in an IDE/GUI because I haven’t run into that in a long time, will add directions here if someone points me in the right direction.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-3-duplicate-path-entries-making-it-harder-to-debug&quot;&gt;problem 3: duplicate &lt;code&gt;PATH&lt;/code&gt; entries making it harder to debug&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you edit your path and start a new shell by running &lt;code&gt;bash&lt;/code&gt; (or &lt;code&gt;zsh&lt;/code&gt;, or &lt;code&gt;fish&lt;/code&gt;), you’ll often end up with duplicate &lt;code&gt;PATH&lt;/code&gt; entries, because the shell keeps adding new things to your &lt;code&gt;PATH&lt;/code&gt; every time you start your shell.&lt;/p&gt;
&lt;p&gt;Personally I don’t think I’ve run into a situation where this kind of duplication breaks anything, but the duplicates can make it harder to debug what’s going on with your &lt;code&gt;PATH&lt;/code&gt; if you’re trying to understand its contents.&lt;/p&gt;
&lt;p&gt;Some ways you could deal with this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If you’re debugging your &lt;code&gt;PATH&lt;/code&gt;, open a new terminal to do it in so you get a “fresh” state. This should avoid the duplication.&lt;/li&gt;
&lt;li&gt;Deduplicate your &lt;code&gt;PATH&lt;/code&gt; at the end of your shell’s config (for example in zsh apparently you can do this with &lt;code&gt;typeset -U path&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Check that the directory isn’t already in your &lt;code&gt;PATH&lt;/code&gt; when adding it (for example in fish I believe you can do this with &lt;code&gt;fish_add_path --path /some/directory&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;How to deduplicate your &lt;code&gt;PATH&lt;/code&gt; is shell-specific and there isn’t always a built in way to do it so you’ll need to look up how to accomplish it in your shell.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-4-losing-your-history-after-updating-your-path&quot;&gt;problem 4: losing your history after updating your &lt;code&gt;PATH&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s a situation that’s easy to get into in bash or zsh:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run a command (it fails)&lt;/li&gt;
&lt;li&gt;Update your &lt;code&gt;PATH&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;bash&lt;/code&gt; to reload your config&lt;/li&gt;
&lt;li&gt;Press the up arrow a couple of times to rerun the failed command (or open a new terminal)&lt;/li&gt;
&lt;li&gt;The failed command isn’t in your history! Why not?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This happens because in bash, by default, history is not saved until you exit the shell.&lt;/p&gt;
&lt;p&gt;Some options for fixing this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of running &lt;code&gt;bash&lt;/code&gt; to reload your config, run &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or &lt;code&gt;source ~/.zshrc&lt;/code&gt; in zsh). This will reload the config inside your current session.&lt;/li&gt;
&lt;li&gt;Configure your shell to continuously save your history instead of only saving the history when the shell exits. (How to do this depends on whether you’re using bash or zsh, the history options in zsh are a bit complicated and I’m not exactly sure what the best way is)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-source&quot;&gt;a note on &lt;code&gt;source&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When you install &lt;code&gt;cargo&lt;/code&gt; (Rust’s installer) for the first time, it gives you these instructions for how to set up your PATH, which don’t mention a specific directory at all.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;This is usually done by running one of the following (note the leading DOT):

. &amp;quot;$HOME/.cargo/env&amp;quot;        	# For sh/bash/zsh/ash/dash/pdksh
source &amp;quot;$HOME/.cargo/env.fish&amp;quot;  # For fish
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea is that you add that line to your shell’s config, and their script automatically sets up your &lt;code&gt;PATH&lt;/code&gt; (and potentially other things) for you.&lt;/p&gt;
&lt;p&gt;This is pretty common (for example &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/Homebrew/install/blob/deacfa6a6e62e5f4002baf9e1fac7a96e9aa5d41/install.sh#L1072-L1087&quot;&gt;Homebrew&lt;/a&gt; suggests you eval &lt;code&gt;brew shellenv&lt;/code&gt;), and there are two ways to approach this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Just do what the tool suggests (like adding &lt;code&gt;. &amp;quot;$HOME/.cargo/env&amp;quot;&lt;/code&gt; to your shell’s config)&lt;/li&gt;
&lt;li&gt;Figure out which directories the script they’re telling you to run would add to your PATH, and then add those manually. Here’s how I’d do that:
&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;. &amp;quot;$HOME/.cargo/env&amp;quot;&lt;/code&gt; in my shell (or the fish version if using fish)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;echo &amp;quot;$PATH&amp;quot; | tr &#39;:&#39; &#39;\n&#39; | grep cargo&lt;/code&gt; to figure out which directories it added&lt;/li&gt;
&lt;li&gt;See that it says &lt;code&gt;/Users/bork/.cargo/bin&lt;/code&gt; and shorten that to &lt;code&gt;~/.cargo/bin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add the directory &lt;code&gt;~/.cargo/bin&lt;/code&gt; to PATH (with the directions in this post)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I don’t think there’s anything wrong with doing what the tool suggests (it might be the “best way”!), but personally I usually use the second approach because I prefer knowing exactly what configuration I’m changing.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#a-note-on-fish-add-path&quot;&gt;a note on &lt;code&gt;fish_add_path&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;fish has a handy function called &lt;code&gt;fish_add_path&lt;/code&gt; that you can run to add a directory to your &lt;code&gt;PATH&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;fish_add_path /some/directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is cool (it’s such a simple command!) but I’ve stopped using it for a couple of reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sometimes &lt;code&gt;fish_add_path&lt;/code&gt; will update the &lt;code&gt;PATH&lt;/code&gt; for every session in the future (with a “universal variable”) and sometimes it will update the &lt;code&gt;PATH&lt;/code&gt; just for the current session and it’s hard for me to tell which one it will do. In theory the docs explain this but I could not understand them.&lt;/li&gt;
&lt;li&gt;If you ever need to &lt;em&gt;remove&lt;/em&gt; the directory from your &lt;code&gt;PATH&lt;/code&gt; a few weeks or months later because maybe you made a mistake, it’s kind of hard to do (there are &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/fish-shell/fish-shell/issues/8604&quot;&gt;instructions in this comments of this github issue though&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#that-s-all&quot;&gt;that’s all&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Hopefully this will help some people. Let me know (on Mastodon or Bluesky) if you there are other major gotchas that have tripped you up when adding a directory to your PATH, or if you have questions about this post!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Some terminal frustrations</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-02-05-some-terminal-frustrations/</link>
    <guid isPermaLink="false">urn:aggr:item:8bfcad18acd8ab8b9f16d5d636c16cd8397cbd7f</guid>
    <atom:link href="https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/" rel="via" type="text/html"/>
    <pubDate>Wed, 5 Feb 2025 16:57:00 +0000</pubDate>
    <description>A few weeks ago I ran a terminal survey (you can read the results here) and at the end I asked: What’s the most frustrating thing about using the terminal for you? 1600 people answered, and I decided to spend a few days categorizing all…</description>
    <content:encoded>&lt;p&gt;A few weeks ago I ran a terminal survey (you can &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/terminal-survey/results-bsky&quot;&gt;read the results here&lt;/a&gt;) and at the end I asked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What’s the most frustrating thing about using the terminal for you?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1600 people answered, and I decided to spend a few days categorizing all the responses. Along the way I learned that classifying qualitative data is not easy but I gave it my best shot. I ended up building a custom &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/jvns/classificator&quot;&gt;tool&lt;/a&gt; to make it faster to categorize everything.&lt;/p&gt;
&lt;p&gt;As with all of my surveys the methodology isn’t particularly scientific. I just posted the survey to Mastodon and Twitter, ran it for a couple of days, and got answers from whoever happened to see it and felt like responding.&lt;/p&gt;
&lt;p&gt;Here are the top categories of frustrations!&lt;/p&gt;
&lt;p&gt;I think it’s worth keeping in mind while reading these comments that&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;40% of people answering this survey have been using the terminal for &lt;strong&gt;21+ years&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;95% of people answering the survey have been using the terminal for at least 4 years&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These comments aren’t coming from total beginners.&lt;/p&gt;
&lt;p&gt;Here are the categories of frustrations! The number in brackets is the number of people with that frustration. I’m mostly writing this up for myself because I’m trying to write a zine about the terminal and I wanted to get a sense for what people are having trouble with.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#remembering-syntax-115&quot;&gt;remembering syntax (115)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;People talked about struggles remembering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the syntax for CLI tools like awk, jq, sed, etc&lt;/li&gt;
&lt;li&gt;the syntax for redirects&lt;/li&gt;
&lt;li&gt;keyboard shortcuts for tmux, text editing, etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are just so many little “trivia” details to remember for full functionality. Even after all these years I’ll sometimes forget where it’s 2 or 1 for stderr, or forget which is which for &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#switching-terminals-is-hard-91&quot;&gt;switching terminals is hard (91)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;People talked about struggling with switching systems (for example home/work computer or when SSHing) and running into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OS differences in keyboard shortcuts (like Linux vs Mac)&lt;/li&gt;
&lt;li&gt;systems which don’t have their preferred text editor (“no vim” or “only vim”)&lt;/li&gt;
&lt;li&gt;different versions of the same command (like Mac OS grep vs GNU grep)&lt;/li&gt;
&lt;li&gt;no tab completion&lt;/li&gt;
&lt;li&gt;a shell they aren’t used to (“the subtle differences between zsh and bash”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;as well as differences inside the same system like pagers being not consistent with each other (git diff pagers, other pagers).&lt;/p&gt;
&lt;p&gt;One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I got used to fish and vi mode which are not available when I ssh into servers, containers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#color-85&quot;&gt;color (85)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Lots of problems with color, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;programs setting colors that are unreadable with a light background color&lt;/li&gt;
&lt;li&gt;finding a colorscheme they like (and getting it to work consistently across different apps)&lt;/li&gt;
&lt;li&gt;color not working inside several layers of SSH/tmux/etc&lt;/li&gt;
&lt;li&gt;not liking the defaults&lt;/li&gt;
&lt;li&gt;not wanting color at all and struggling to turn it off&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This comment felt relatable to me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Getting my terminal theme configured in a reasonable way between the terminal emulator and fish (I did this years ago and remember it being tedious and fiddly and now feel like I’m locked into my current theme because it works and I dread touching any of that configuration ever again).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#keyboard-shortcuts-84&quot;&gt;keyboard shortcuts (84)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Half of the comments on keyboard shortcuts were about how on Linux/Windows, the keyboard shortcut to copy/paste in the terminal is different from in the rest of the OS.&lt;/p&gt;
&lt;p&gt;Some other issues with keyboard shortcuts other than copy/paste:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;using &lt;code&gt;Ctrl-W&lt;/code&gt; in a browser-based terminal and closing the window&lt;/li&gt;
&lt;li&gt;the terminal only supports a limited set of keyboard shortcuts (no &lt;code&gt;Ctrl-Shift-&lt;/code&gt;, no &lt;code&gt;Super&lt;/code&gt;, no &lt;code&gt;Hyper&lt;/code&gt;, lots of &lt;code&gt;ctrl-&lt;/code&gt; shortcuts aren’t possible like &lt;code&gt;Ctrl-,&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the OS stopping you from using a terminal keyboard shortcut (like by default Mac OS uses &lt;code&gt;Ctrl+left arrow&lt;/code&gt; for something else)&lt;/li&gt;
&lt;li&gt;issues using emacs in the terminal&lt;/li&gt;
&lt;li&gt;backspace not working (2)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#other-copy-and-paste-issues-75&quot;&gt;other copy and paste issues (75)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Aside from “the keyboard shortcut for copy and paste is different”, there were a lot of OTHER issues with copy and paste, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;copying over SSH&lt;/li&gt;
&lt;li&gt;how tmux and the terminal emulator both do copy/paste in different ways&lt;/li&gt;
&lt;li&gt;dealing with many different clipboards (system clipboard, vim clipboard, the “middle click” clipboard on Linux, tmux’s clipboard, etc) and potentially synchronizing them&lt;/li&gt;
&lt;li&gt;random spaces added when copying from the terminal&lt;/li&gt;
&lt;li&gt;pasting multiline commands which automatically get run in a terrifying way&lt;/li&gt;
&lt;li&gt;wanting a way to copy text without using the mouse&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#discoverability-55&quot;&gt;discoverability (55)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There were lots of comments about this, which all came down to the same basic complaint – it’s hard to discover useful tools or features! This comment kind of summed it all up:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How difficult it is to learn independently. Most of what I know is an assorted collection of stuff I’ve been told by random people over the years.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#steep-learning-curve-44&quot;&gt;steep learning curve (44)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A lot of comments about it generally having a steep learning curve. A couple of example comments:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;After 15 years of using it, I’m not much faster than using it than I was 5 or maybe even 10 years ago.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That I know I could make my life easier by learning more about the shortcuts and commands and configuring the terminal but I don’t spend the time because it feels overwhelming.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#history-42&quot;&gt;history (42)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some issues with shell history:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;history not being shared between terminal tabs (16)&lt;/li&gt;
&lt;li&gt;limits that are too short (4)&lt;/li&gt;
&lt;li&gt;history not being restored when terminal tabs are restored&lt;/li&gt;
&lt;li&gt;losing history because the terminal crashed&lt;/li&gt;
&lt;li&gt;not knowing how to search history&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It wasted a lot of time until I figured it out and still annoys me that “history” on zsh has such a small buffer; I have to type “history 0” to get any useful length of history.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#bad-documentation-37&quot;&gt;bad documentation (37)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;People talked about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;documentation being generally opaque&lt;/li&gt;
&lt;li&gt;lack of examples in man pages&lt;/li&gt;
&lt;li&gt;programs which don’t have man pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a representative comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Finding good examples and docs. Man pages often not enough, have to wade through stack overflow&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#scrollback-36&quot;&gt;scrollback (36)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A few issues with scrollback:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;programs printing out too much data making you lose scrollback history&lt;/li&gt;
&lt;li&gt;resizing the terminal messes up the scrollback&lt;/li&gt;
&lt;li&gt;lack of timestamps&lt;/li&gt;
&lt;li&gt;GUI programs that you start in the background printing stuff out that gets in the way of other programs’ outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When resizing the terminal (in particular: making it narrower) leads to broken rewrapping of the scrollback content because the commands formatted their output based on the terminal window width.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#it-feels-outdated-33&quot;&gt;“it feels outdated” (33)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Lots of comments about how the terminal feels hampered by legacy decisions and how users often end up needing to learn implementation details that feel very esoteric. One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Most of the legacy cruft, it would be great to have a green field implementation of the CLI interface.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#shell-scripting-32&quot;&gt;shell scripting (32)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Lots of complaints about POSIX shell scripting. There’s a general feeling that shell scripting is difficult but also that switching to a different less standard scripting language (fish, nushell, etc) brings its own problems.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shell scripting. My tolerance to ditch a shell script and go to a scripting language is pretty low. It’s just too messy and powerful. Screwing up can be costly so I don’t even bother.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#more-issues&quot;&gt;more issues&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some more issues that were mentioned at least 10 times:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(31) inconsistent command line arguments: is it -h or help or –help?&lt;/li&gt;
&lt;li&gt;(24) keeping dotfiles in sync across different systems&lt;/li&gt;
&lt;li&gt;(23) performance (e.g. “my shell takes too long to start”)&lt;/li&gt;
&lt;li&gt;(20) window management (potentially with some combination of tmux tabs, terminal tabs, and multiple terminal windows. Where did that shell session go?)&lt;/li&gt;
&lt;li&gt;(17) generally feeling scared/uneasy (“The debilitating fear that I’m going to do some mysterious Bad Thing with a command and I will have absolutely no idea how to fix or undo it or even really figure out what happened”)&lt;/li&gt;
&lt;li&gt;(16) terminfo issues (“Having to learn about terminfo if/when I try a new terminal emulator and ssh elsewhere.”)&lt;/li&gt;
&lt;li&gt;(16) lack of image support (sixel etc)&lt;/li&gt;
&lt;li&gt;(15) SSH issues (like having to start over when you lose the SSH connection)&lt;/li&gt;
&lt;li&gt;(15) various tmux/screen issues (for example lack of integration between tmux and the terminal emulator)&lt;/li&gt;
&lt;li&gt;(15) typos &amp;amp; slow typing&lt;/li&gt;
&lt;li&gt;(13) the terminal getting messed up for various reasons (pressing &lt;code&gt;Ctrl-S&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;ing a binary, etc)&lt;/li&gt;
&lt;li&gt;(12) quoting/escaping in the shell&lt;/li&gt;
&lt;li&gt;(11) various Windows/PowerShell issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#n-a-122&quot;&gt;n/a (122)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There were also 122 answers to the effect of “nothing really” or “only that I can’t do EVERYTHING in the terminal”&lt;/p&gt;
&lt;p&gt;One example comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Think I’ve found work arounds for most/all frustrations&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/#that-s-all&quot;&gt;that’s all!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’m not going to make a lot of commentary on these results, but here are a couple of categories that feel related to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;remembering syntax &amp;amp; history (often the thing you need to remember is something you’ve run before!)&lt;/li&gt;
&lt;li&gt;discoverability &amp;amp; the learning curve (the lack of discoverability is definitely a big part of what makes it hard to learn)&lt;/li&gt;
&lt;li&gt;“switching systems is hard” &amp;amp; “it feels outdated” (tools that haven’t really changed in 30 or 40 years have many problems but they do tend to be always &lt;em&gt;there&lt;/em&gt; no matter what system you’re on, which is very useful and makes them hard to stop using)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trying to categorize all these results in a reasonable way really gave me an appreciation for social science researchers’ skills.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>What&#39;s involved in getting a &quot;modern&quot; terminal setup?</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2025-01-11-what-s-involved-in-getting-a-modern-terminal-setup/</link>
    <guid isPermaLink="false">urn:aggr:item:8d27cc827beb83f6f9ab19baa4e20dfac73c44cf</guid>
    <atom:link href="https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/" rel="via" type="text/html"/>
    <pubDate>Sat, 11 Jan 2025 09:46:01 +0000</pubDate>
    <description>Hello! Recently I ran a terminal survey and I asked people what frustrated them. One person commented: There are so many pieces to having a modern terminal experience. I wish it all came out of the box. My immediate reaction was “oh,…</description>
    <content:encoded>&lt;p&gt;Hello! Recently I ran a terminal survey and I asked people what frustrated them. One person commented:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are so many pieces to having a modern terminal experience. I wish it all came out of the box.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My immediate reaction was “oh, getting a modern terminal experience isn’t that hard, you just need to….”, but the more I thought about it, the longer the “you just need to…” list got, and I kept thinking about more and more caveats.&lt;/p&gt;
&lt;p&gt;So I thought I would write down some notes about what it means to me personally to have a “modern” terminal experience and what I think can make it hard for people to get there.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#what-is-a-modern-terminal-experience&quot;&gt;what is a “modern terminal experience”?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here are a few things that are important to me, with which part of the system is responsible for them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;multiline support for copy and paste&lt;/strong&gt;: if you paste 3 commands in your shell, it should not immediately run them all! That’s scary! (&lt;strong&gt;shell&lt;/strong&gt;, &lt;strong&gt;terminal emulator&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;infinite shell history&lt;/strong&gt;: if I run a command in my shell, it should be saved forever, not deleted after 500 history entries or whatever. Also I want commands to be saved to the history immediately when I run them, not only when I exit the shell session (&lt;strong&gt;shell&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a useful prompt&lt;/strong&gt;: I can’t live without having my &lt;strong&gt;current directory&lt;/strong&gt; and &lt;strong&gt;current git branch&lt;/strong&gt; in my prompt (&lt;strong&gt;shell&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;24-bit colour&lt;/strong&gt;: this is important to me because I find it MUCH easier to theme neovim with 24-bit colour support than in a terminal with only 256 colours (&lt;strong&gt;terminal emulator&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;clipboard integration&lt;/strong&gt; between vim and my operating system so that when I copy in Firefox, I can just press &lt;code&gt;p&lt;/code&gt; in vim to paste (&lt;strong&gt;text editor&lt;/strong&gt;, maybe the OS/terminal emulator too)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;good autocomplete&lt;/strong&gt;: for example commands like git should have command-specific autocomplete (&lt;strong&gt;shell&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;having colours in &lt;code&gt;ls&lt;/code&gt;&lt;/strong&gt; (&lt;strong&gt;shell config&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a terminal theme I like&lt;/strong&gt;: I spend a lot of time in my terminal, I want it to look nice and I want its theme to match my terminal editor’s theme. (&lt;strong&gt;terminal emulator&lt;/strong&gt;, &lt;strong&gt;text editor&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;automatic terminal fixing&lt;/strong&gt;: If a programs prints out some weird escape codes that mess up my terminal, I want that to automatically get reset so that my terminal doesn’t get messed up (&lt;strong&gt;shell&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;keybindings&lt;/strong&gt;: I want &lt;code&gt;Ctrl+left arrow&lt;/code&gt; to work (&lt;strong&gt;shell&lt;/strong&gt; or &lt;strong&gt;application&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;being able to use the scroll wheel in programs like &lt;code&gt;less&lt;/code&gt;&lt;/strong&gt;: (&lt;strong&gt;terminal emulator&lt;/strong&gt; and &lt;strong&gt;applications&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a million other terminal conveniences out there and different people value different things, but those are the ones that I would be really unhappy without.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#how-i-achieve-a-modern-experience&quot;&gt;how I achieve a “modern experience”&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My basic approach is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;use the &lt;code&gt;fish&lt;/code&gt; shell. Mostly don’t configure it, except to:
&lt;ul&gt;
&lt;li&gt;set the &lt;code&gt;EDITOR&lt;/code&gt; environment variable to my favourite terminal editor&lt;/li&gt;
&lt;li&gt;alias &lt;code&gt;ls&lt;/code&gt; to &lt;code&gt;ls --color=auto&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;use any terminal emulator with 24-bit colour support. In the past I’ve used GNOME Terminal, Terminator, and iTerm, but I’m not picky about this. I don’t really configure it other than to choose a font.&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;neovim&lt;/code&gt;, with a configuration that I’ve been very slowly building over the last 9 years or so (the last time I deleted my vim config and started from scratch was 9 years ago)&lt;/li&gt;
&lt;li&gt;use the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/chriskempson/base16&quot;&gt;base16 framework&lt;/a&gt; to theme everything&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A few things that affect my approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don’t spend a lot of time SSHed into other machines&lt;/li&gt;
&lt;li&gt;I’d rather use the mouse a little than come up with keyboard-based ways to do everything&lt;/li&gt;
&lt;li&gt;I work on a lot of small projects, not one big project&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#some-out-of-the-box-options-for-a-modern-experience&quot;&gt;some “out of the box” options for a “modern” experience&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;What if you want a nice experience, but don’t want to spend a lot of time on configuration? Figuring out how to configure vim in a way that I was satisfied with really did take me like ten years, which is a long time!&lt;/p&gt;
&lt;p&gt;My best ideas for how to get a reasonable terminal experience with minimal config are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shell: either &lt;code&gt;fish&lt;/code&gt; or &lt;code&gt;zsh&lt;/code&gt; with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ohmyz.sh/&quot;&gt;oh-my-zsh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;terminal emulator: almost anything with 24-bit colour support, for example all of these are popular:
&lt;ul&gt;
&lt;li&gt;linux: GNOME Terminal, Konsole, Terminator, xfce4-terminal&lt;/li&gt;
&lt;li&gt;mac: iTerm (Terminal.app doesn’t have 256-colour support)&lt;/li&gt;
&lt;li&gt;cross-platform: kitty, alacritty, wezterm, or ghostty&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;shell config:
&lt;ul&gt;
&lt;li&gt;set the &lt;code&gt;EDITOR&lt;/code&gt; environment variable to your favourite terminal text editor&lt;/li&gt;
&lt;li&gt;maybe alias &lt;code&gt;ls&lt;/code&gt; to &lt;code&gt;ls --color=auto&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;text editor: this is a tough one, maybe &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://micro-editor.github.io/&quot;&gt;micro&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://helix-editor.com/&quot;&gt;helix&lt;/a&gt;? I haven’t used either of them seriously but they both seem like very cool projects and I think it’s amazing that you can just use all the usual GUI editor commands (&lt;code&gt;Ctrl-C&lt;/code&gt; to copy, &lt;code&gt;Ctrl-V&lt;/code&gt; to paste, &lt;code&gt;Ctrl-A&lt;/code&gt; to select all) in micro and they do what you’d expect. I would probably try switching to helix except that retraining my vim muscle memory seems way too hard. Also helix doesn’t have a GUI or plugin system yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Personally I &lt;strong&gt;wouldn’t&lt;/strong&gt; use xterm, rxvt, or Terminal.app as a terminal emulator, because I’ve found in the past that they’re missing core features (like 24-bit colour in Terminal.app’s case) that make the terminal harder to use for me.&lt;/p&gt;
&lt;p&gt;I don’t want to pretend that getting a “modern” terminal experience is easier than it is though – I think there are two issues that make it hard. Let’s talk about them!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#issue-1-with-getting-to-a-modern-experience-the-shell&quot;&gt;issue 1 with getting to a “modern” experience: the shell&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;bash and zsh are by far the two most popular shells, and neither of them provide a default experience that I would be happy using out of the box, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you need to customize your prompt&lt;/li&gt;
&lt;li&gt;they don’t come with git completions by default, you have to set them up&lt;/li&gt;
&lt;li&gt;by default, bash only stores 500 (!) lines of history and (at least on Mac OS) zsh is only configured to store 2000 lines, which is still not a lot&lt;/li&gt;
&lt;li&gt;I find bash’s tab completion very frustrating, if there’s more than one match then you can’t tab through them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And even though &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/09/12/reasons-i--still--love-fish/&quot;&gt;I love fish&lt;/a&gt;, the fact that it isn’t POSIX does make it hard for a lot of folks to make the switch.&lt;/p&gt;
&lt;p&gt;Of course it’s totally possible to learn how to customize your prompt in bash or whatever, and it doesn’t even need to be that complicated (in bash I’d probably start with something like &lt;code&gt;export PS1=&#39;[\u@\h \W$(__git_ps1 &amp;quot; (%s)&amp;quot;)]\$ &#39;&lt;/code&gt;, or maybe use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://starship.rs/&quot;&gt;starship&lt;/a&gt;). But each of these “not complicated” things really does add up and it’s especially tough if you need to keep your config in sync across several systems.&lt;/p&gt;
&lt;p&gt;An extremely popular solution to getting a “modern” shell experience is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ohmyz.sh/&quot;&gt;oh-my-zsh&lt;/a&gt;. It seems like a great project and I know a lot of people use it very happily, but I’ve struggled with configuration systems like that in the past – it looks like right now the base oh-my-zsh adds about 3000 lines of config, and often I find that having an extra configuration system makes it harder to debug what’s happening when things go wrong. I personally have a tendency to use the system to add a lot of extra plugins, make my system slow, get frustrated that it’s slow, and then delete it completely and write a new config from scratch.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#issue-2-with-getting-to-a-modern-experience-the-text-editor&quot;&gt;issue 2 with getting to a “modern” experience: the text editor&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the terminal survey I ran recently, the most popular terminal text editors by far were &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;emacs&lt;/code&gt;, and &lt;code&gt;nano&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I think the main options for terminal text editors are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use vim or emacs and configure it to your liking, you can probably have any feature you want if you put in the work&lt;/li&gt;
&lt;li&gt;use nano and accept that you’re going to have a pretty limited experience (for example I don’t think you can select text with the mouse and then “cut” it in nano)&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;micro&lt;/code&gt; or &lt;code&gt;helix&lt;/code&gt; which seem to offer a pretty good out-of-the-box experience, potentially occasionally run into issues with using a less mainstream text editor&lt;/li&gt;
&lt;li&gt;just avoid using a terminal text editor as much as possible, maybe use VSCode, use VSCode’s terminal for all your terminal needs, and mostly never edit files in the terminal. Or I know a lot of people use &lt;code&gt;code&lt;/code&gt; as their &lt;code&gt;EDITOR&lt;/code&gt; in the terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#issue-3-individual-applications&quot;&gt;issue 3: individual applications&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The last issue is that sometimes individual programs that I use are kind of annoying. For example on my Mac OS machine, &lt;code&gt;/usr/bin/sqlite3&lt;/code&gt; doesn’t support the &lt;code&gt;Ctrl+Left Arrow&lt;/code&gt; keyboard shortcut. Fixing this to get a reasonable terminal experience in SQLite was a little complicated, I had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;realize why this is happening (Mac OS won’t ship GNU tools, and “Ctrl-Left arrow” support comes from GNU readline)&lt;/li&gt;
&lt;li&gt;find a workaround (install sqlite from homebrew, which does have readline support)&lt;/li&gt;
&lt;li&gt;adjust my environment (put Homebrew’s sqlite3 in my PATH)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I find that debugging application-specific issues like this is really not easy and often it doesn’t feel “worth it” – often I’ll end up just dealing with various minor inconveniences because I don’t want to spend hours investigating them. The only reason I was even able to figure this one out at all is that I’ve been spending a huge amount of time thinking about the terminal recently.&lt;/p&gt;
&lt;p&gt;A big part of having a “modern” experience using terminal programs is just using newer terminal programs, for example I can’t be bothered to learn a keyboard shortcut to sort the columns in &lt;code&gt;top&lt;/code&gt;, but in &lt;code&gt;htop&lt;/code&gt; I can just click on a column heading with my mouse to sort it. So I use htop instead! But discovering new more “modern” command line tools isn’t easy (though I made &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2022/04/12/a-list-of-new-ish--command-line-tools/&quot;&gt;a list here&lt;/a&gt;), finding ones that I actually like using in practice takes time, and if you’re SSHed into another machine, they won’t always be there.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#everything-affects-everything-else&quot;&gt;everything affects everything else&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Something I find tricky about configuring my terminal to make everything “nice” is that changing one seemingly small thing about my workflow can really affect everything else. For example right now I don’t use tmux. But if I needed to use tmux again (for example because I was doing a lot of work SSHed into another machine), I’d need to think about a few things, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if I wanted tmux’s copy to synchronize with my system clipboard over SSH, I’d need to make sure that my terminal emulator has &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://old.reddit.com/r/vim/comments/k1ydpn/a_guide_on_how_to_copy_text_from_anywhere/&quot;&gt;OSC 52 support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;if I wanted to use iTerm’s tmux integration (which makes tmux tabs into iTerm tabs), I’d need to change how I configure colours – right now I set them with a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/chriskempson/base16-shell/blob/588691ba71b47e75793ed9edfcfaa058326a6f41/scripts/base16-solarized-light.sh&quot;&gt;shell script&lt;/a&gt; that I run when my shell starts, but that means the colours get lost when restoring a tmux session.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and probably more things I haven’t thought of. “Using tmux means that I have to change how I manage my colours” sounds unlikely, but that really did happen to me and I decided “well, I don’t want to change how I manage colours right now, so I guess I’m not using that feature!”.&lt;/p&gt;
&lt;p&gt;It’s also hard to remember which features I’m relying on – for example maybe my current terminal &lt;em&gt;does&lt;/em&gt; have OSC 52 support and because copying from tmux over SSH has always Just Worked I don’t even realize that that’s something I need, and then it mysteriously stops working when I switch terminals.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#change-things-slowly&quot;&gt;change things slowly&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Personally even though I think my setup is not &lt;em&gt;that&lt;/em&gt; complicated, it’s taken me 20 years to get to this point! Because terminal config changes are so likely to have unexpected and hard-to-understand consequences, I’ve found that if I change a lot of terminal configuration all at once it makes it much harder to understand what went wrong if there’s a problem, which can be really disorienting.&lt;/p&gt;
&lt;p&gt;So I usually prefer to make pretty small changes, and accept that changes can might take me a REALLY long time to get used to. For example I switched from using &lt;code&gt;ls&lt;/code&gt; to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/eza-community/eza&quot;&gt;eza&lt;/a&gt;&lt;br /&gt;
a year or two ago and while I like it (because &lt;code&gt;eza -l&lt;/code&gt; prints human-readable file sizes by default) I’m still not quite sure about it. But also sometimes it’s worth it to make a big change, like I made the switch to fish (from bash) 10 years ago and I’m very happy I did.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2025/01/11/getting-a-modern-terminal-setup/#getting-a-modern-terminal-is-not-that-easy&quot;&gt;getting a “modern” terminal is not that easy&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Trying to explain how “easy” it is to configure your terminal really just made me think that it’s kind of hard and that I still sometimes get confused.&lt;/p&gt;
&lt;p&gt;I’ve found that there’s never one perfect way to configure things in the terminal that will be compatible with every single other thing. I just need to try stuff, figure out some kind of locally stable state that works for me, and accept that if I start using a new tool it might disrupt the system and I might need to rethink things.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>&quot;Rules&quot; that terminal programs follow</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2024-12-12-rules-that-terminal-programs-follow/</link>
    <guid isPermaLink="false">urn:aggr:item:6ef023e6c594b4cee4f654f130a4e5fcaef1a6f4</guid>
    <atom:link href="https://jvns.ca/blog/2024/11/26/terminal-rules/" rel="via" type="text/html"/>
    <pubDate>Thu, 12 Dec 2024 09:28:22 +0000</pubDate>
    <description>Recently I’ve been thinking about how everything that happens in the terminal is some combination of: Your operating system’s job Your shell’s job Your terminal emulator’s job The job of whatever program you happen to be running (like top…</description>
    <content:encoded>&lt;p&gt;Recently I’ve been thinking about how everything that happens in the terminal is some combination of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your &lt;strong&gt;operating system&lt;/strong&gt;’s job&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;shell&lt;/strong&gt;’s job&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;terminal emulator&lt;/strong&gt;’s job&lt;/li&gt;
&lt;li&gt;The job of &lt;strong&gt;whatever program you happen to be running&lt;/strong&gt; (like &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;vim&lt;/code&gt; or &lt;code&gt;cat&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first three (your operating system, shell, and terminal emulator) are all kind of known quantities – if you’re using bash in GNOME Terminal on Linux, you can more or less reason about how how all of those things interact, and some of their behaviour is standardized by POSIX.&lt;/p&gt;
&lt;p&gt;But the fourth one (“whatever program you happen to be running”) feels like it could do ANYTHING. How are you supposed to know how a program is going to behave?&lt;/p&gt;
&lt;p&gt;This post is kind of long so here’s a quick table of contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#programs-behave-surprisingly-consistently&quot;&gt;programs behave surprisingly consistently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#these-are-meant-to-be-descriptive-not-prescriptive&quot;&gt;these are meant to be descriptive, not prescriptive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#it-s-not-always-obvious-which-rules-are-the-program-s-responsibility-to-implement&quot;&gt;it’s not always obvious which “rules” are the program’s responsibility to implement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-1-noninteractive-programs-should-quit-when-you-press-ctrl-c&quot;&gt;rule 1: noninteractive programs should quit when you press &lt;code&gt;Ctrl-C&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-2-tuis-should-quit-when-you-press-q&quot;&gt;rule 2: TUIs should quit when you press &lt;code&gt;q&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-3-repls-should-quit-when-you-press-ctrl-d-on-an-empty-line&quot;&gt;rule 3: REPLs should quit when you press &lt;code&gt;Ctrl-D&lt;/code&gt; on an empty line&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-4-don-t-use-more-than-16-colours&quot;&gt;rule 4: don’t use more than 16 colours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-5-vaguely-support-readline-keybindings&quot;&gt;rule 5: vaguely support readline keybindings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-5-1-ctrl-w-should-delete-the-last-word&quot;&gt;rule 5.1: &lt;code&gt;Ctrl-W&lt;/code&gt; should delete the last word&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-6-disable-colours-when-writing-to-a-pipe&quot;&gt;rule 6: disable colours when writing to a pipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-7-means-stdin-stdout&quot;&gt;rule 7: &lt;code&gt;-&lt;/code&gt; means stdin/stdout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#these-rules-take-a-long-time-to-learn&quot;&gt;these “rules” take a long time to learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#programs-behave-surprisingly-consistently&quot;&gt;programs behave surprisingly consistently&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As far as I know, there are no real standards for how programs in the terminal should behave – the closest things I know of are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;POSIX, which mostly dictates how your terminal emulator / OS / shell should work together. I think it does specify a few things about how core utilities like &lt;code&gt;cp&lt;/code&gt; should work but AFAIK it doesn’t have anything to say about how for example &lt;code&gt;htop&lt;/code&gt; should behave.&lt;/li&gt;
&lt;li&gt;these &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://clig.dev/&quot;&gt;command line interface guidelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But even though there are no standards, in my experience programs in the terminal behave in a pretty consistent way. So I wanted to write down a list of “rules” that in my experience programs mostly follow.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#these-are-meant-to-be-descriptive-not-prescriptive&quot;&gt;these are meant to be descriptive, not prescriptive&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My goal here isn’t to convince authors of terminal programs that they &lt;em&gt;should&lt;/em&gt; follow any of these rules. There are lots of exceptions to these and often there’s a good reason for those exceptions.&lt;/p&gt;
&lt;p&gt;But it’s very useful for me to know what behaviour to expect from a random new terminal program that I’m using. Instead of “uh, programs could do literally anything”, it’s “ok, here are the basic rules I expect, and then I can keep a short mental list of exceptions”.&lt;/p&gt;
&lt;p&gt;So I’m just writing down what I’ve observed about how programs behave in my 20 years of using the terminal, why I think they behave that way, and some examples of cases where that rule is “broken”.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#it-s-not-always-obvious-which-rules-are-the-program-s-responsibility-to-implement&quot;&gt;it’s not always obvious which “rules” are the program’s responsibility to implement&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are a bunch of common conventions that I think are pretty clearly the program’s responsibility to implement, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;config files should go in &lt;code&gt;~/.BLAHrc&lt;/code&gt; or &lt;code&gt;~/.config/BLAH/FILE&lt;/code&gt; or &lt;code&gt;/etc/BLAH/&lt;/code&gt; or something&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--help&lt;/code&gt; should print help text&lt;/li&gt;
&lt;li&gt;programs should print “regular” output to stdout and errors to stderr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But in this post I’m going to focus on things that it’s not 100% obvious are the program’s responsibility. For example it feels to me like a “law of nature” that pressing &lt;code&gt;Ctrl-D&lt;/code&gt; should quit a REPL, but programs often need to explicitly implement support for it – even though &lt;code&gt;cat&lt;/code&gt; doesn’t need to implement &lt;code&gt;Ctrl-D&lt;/code&gt; support, &lt;code&gt;ipython&lt;/code&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/prompt-toolkit/python-prompt-toolkit/blob/a2a12300c635ab3c051566e363ed27d853af4b21/src/prompt_toolkit/shortcuts/prompt.py#L824-L837&quot;&gt;does&lt;/a&gt;. (more about that in “rule 3” below)&lt;/p&gt;
&lt;p&gt;Understanding which things are the program’s responsibility makes it much less surprising when different programs’ implementations are slightly different.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-1-noninteractive-programs-should-quit-when-you-press-ctrl-c&quot;&gt;rule 1: noninteractive programs should quit when you press &lt;code&gt;Ctrl-C&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The main reason for this rule is that noninteractive programs will quit by default on &lt;code&gt;Ctrl-C&lt;/code&gt; if they don’t set up a &lt;code&gt;SIGINT&lt;/code&gt; signal handler, so this is kind of a “you should act like the default” rule.&lt;/p&gt;
&lt;p&gt;Something that trips a lot of people up is that this doesn’t apply to &lt;strong&gt;interactive&lt;/strong&gt; programs like &lt;code&gt;python3&lt;/code&gt; or &lt;code&gt;bc&lt;/code&gt; or &lt;code&gt;less&lt;/code&gt;. This is because in an interactive program, &lt;code&gt;Ctrl-C&lt;/code&gt; has a different job – if the program is running an operation (like for example a search in &lt;code&gt;less&lt;/code&gt; or some Python code in &lt;code&gt;python3&lt;/code&gt;), then &lt;code&gt;Ctrl-C&lt;/code&gt; will interrupt that operation but not stop the program.&lt;/p&gt;
&lt;p&gt;As an example of how this works in an interactive program: here’s the code &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/prompt-toolkit/python-prompt-toolkit/blob/a2a12300c635ab3c051566e363ed27d853af4b21/src/prompt_toolkit/key_binding/bindings/vi.py#L2225&quot;&gt;in prompt-toolkit&lt;/a&gt; (the library that iPython uses for handling input) that aborts a search when you press &lt;code&gt;Ctrl-C&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-2-tuis-should-quit-when-you-press-q&quot;&gt;rule 2: TUIs should quit when you press &lt;code&gt;q&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;TUI programs (like &lt;code&gt;less&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt;) will usually quit when you press &lt;code&gt;q&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This rule doesn’t apply to any program where pressing &lt;code&gt;q&lt;/code&gt; to quit wouldn’t make sense, like &lt;code&gt;tmux&lt;/code&gt; or text editors.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-3-repls-should-quit-when-you-press-ctrl-d-on-an-empty-line&quot;&gt;rule 3: REPLs should quit when you press &lt;code&gt;Ctrl-D&lt;/code&gt; on an empty line&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;REPLs (like &lt;code&gt;python3&lt;/code&gt; or &lt;code&gt;ed&lt;/code&gt;) will usually quit when you press &lt;code&gt;Ctrl-D&lt;/code&gt; on an empty line. This rule is similar to the &lt;code&gt;Ctrl-C&lt;/code&gt; rule – the reason for this is that by default if you’re running a program (like &lt;code&gt;cat&lt;/code&gt;) in “cooked mode”, then the operating system will return an &lt;code&gt;EOF&lt;/code&gt; when you press &lt;code&gt;Ctrl-D&lt;/code&gt; on an empty line.&lt;/p&gt;
&lt;p&gt;Most of the REPLs I use (sqlite3, python3, fish, bash, etc) don’t actually use cooked mode, but they all implement this keyboard shortcut anyway to mimic the default behaviour.&lt;/p&gt;
&lt;p&gt;For example, here’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/prompt-toolkit/python-prompt-toolkit/blob/a2a12300c635ab3c051566e363ed27d853af4b21/src/prompt_toolkit/shortcuts/prompt.py#L824-L837&quot;&gt;the code in prompt-toolkit&lt;/a&gt; that quits when you press Ctrl-D, and here’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/bminor/bash/blob/6794b5478f660256a1023712b5fc169196ed0a22/lib/readline/readline.c#L658-L672&quot;&gt;the same code in readline&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I actually thought that this one was a “Law of Terminal Physics” until very recently because I’ve basically never seen it broken, but you can see that it’s just something that each individual input library has to implement in the links above.&lt;/p&gt;
&lt;p&gt;Someone pointed out that the Erlang REPL does not quit when you press &lt;code&gt;Ctrl-D&lt;/code&gt;, so I guess not every REPL follows this “rule”.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-4-don-t-use-more-than-16-colours&quot;&gt;rule 4: don’t use more than 16 colours&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Terminal programs rarely use colours other than the base 16 ANSI colours. This is because if you specify colours with a hex code, it’s very likely to clash with some users’ background colour. For example if I print out some text as &lt;code&gt;#EEEEEE&lt;/code&gt;, it would be almost invisible on a white background, though it would look fine on a dark background.&lt;/p&gt;
&lt;p&gt;But if you stick to the default 16 base colours, you have a much better chance that the user has configured those colours in their terminal emulator so that they work reasonably well with their background color. Another reason to stick to the default base 16 colours is that it makes less assumptions about what colours the terminal emulator supports.&lt;/p&gt;
&lt;p&gt;The only programs I usually see breaking this “rule” are text editors, for example Helix by default will use a purple background which is not a default ANSI colour. It seems fine for Helix to break this rule since Helix isn’t a “core” program and I assume any Helix user who doesn’t like that colorscheme will just change the theme.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-5-vaguely-support-readline-keybindings&quot;&gt;rule 5: vaguely support readline keybindings&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Almost every program I use supports &lt;code&gt;readline&lt;/code&gt; keybindings if it would make sense to do so. For example, here are a bunch of different programs and a link to where they define &lt;code&gt;Ctrl-E&lt;/code&gt; to go to the end of the line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ipython (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/prompt-toolkit/python-prompt-toolkit/blob/a2a12300c635ab3c051566e363ed27d853af4b21/src/prompt_toolkit/key_binding/bindings/emacs.py#L72&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;atuin (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/atuinsh/atuin/blob/a67cfc82fe0dc907a01f07a0fd625701e062a33b/crates/atuin/src/command/client/search/interactive.rs#L407&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;fzf (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/junegunn/fzf/blob/bb55045596d6d08445f3c6d320c3ec2b457462d1/src/terminal.go#L611&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;zsh (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/zsh-users/zsh/blob/86d5f24a3d28541f242eb3807379301ea976de87/Src/Zle/zle_bindings.c#L94&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;fish (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/fish-shell/fish-shell/blob/99fa8aaaa7956178973150a03ce4954ab17a197b/share/functions/fish_default_key_bindings.fish#L43&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;tmux’s command prompt (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/tmux/tmux/blob/ae8f2208c98e3c2d6e3fe4cad2281dce8fd11f31/key-bindings.c#L490&quot;&gt;Ctrl-E defined here&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of those programs actually uses &lt;code&gt;readline&lt;/code&gt; directly, they just sort of mimic emacs/readline keybindings. They don’t always mimic them &lt;em&gt;exactly&lt;/em&gt;: for example atuin seems to use &lt;code&gt;Ctrl-A&lt;/code&gt; as a prefix, so &lt;code&gt;Ctrl-A&lt;/code&gt; doesn’t go to the beginning of the line.&lt;/p&gt;
&lt;p&gt;Also all of these programs seem to implement their own internal cut and paste buffers so you can delete a line with &lt;code&gt;Ctrl-U&lt;/code&gt; and then paste it with &lt;code&gt;Ctrl-Y&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The exceptions to this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;some programs (like &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, and &lt;code&gt;nc&lt;/code&gt;) don’t have any line editing support at all (except for backspace, &lt;code&gt;Ctrl-W&lt;/code&gt;, and &lt;code&gt;Ctrl-U&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;as usual text editors are an exception, every text editor has its own approach to editing text&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote more about this “what keybindings does a program support?” question in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/07/08/readline/&quot;&gt;entering text in the terminal is complicated&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-5-1-ctrl-w-should-delete-the-last-word&quot;&gt;rule 5.1: Ctrl-W should delete the last word&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’ve never seen a program (other than a text editor) where &lt;code&gt;Ctrl-W&lt;/code&gt; &lt;em&gt;doesn’t&lt;/em&gt; delete the last word. This is similar to the &lt;code&gt;Ctrl-C&lt;/code&gt; rule – by default if a program is in “cooked mode”, the OS will delete the last word if you press &lt;code&gt;Ctrl-W&lt;/code&gt;, and delete the whole line if you press &lt;code&gt;Ctrl-U&lt;/code&gt;. So usually programs will imitate that behaviour.&lt;/p&gt;
&lt;p&gt;I can’t think of any exceptions to this other than text editors but if there are I’d love to hear about them!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-6-disable-colours-when-writing-to-a-pipe&quot;&gt;rule 6: disable colours when writing to a pipe&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Most programs will disable colours when writing to a pipe. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rg blah&lt;/code&gt; will highlight all occurrences of &lt;code&gt;blah&lt;/code&gt; in the output, but if the output is to a pipe or a file, it’ll turn off the highlighting.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls --color=auto&lt;/code&gt; will use colour when writing to a terminal, but not when writing to a pipe&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of those programs will also format their output differently when writing to the terminal: &lt;code&gt;ls&lt;/code&gt; will organize files into columns, and ripgrep will group matches with headings.&lt;/p&gt;
&lt;p&gt;If you want to force the program to use colour (for example because you want to look at the colour), you can use &lt;code&gt;unbuffer&lt;/code&gt; to force the program’s output to be a tty like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;unbuffer rg blah |  less -R
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’m sure that there are some programs that “break” this rule but I can’t think of any examples right now. Some programs have an &lt;code&gt;--color&lt;/code&gt; flag that you can use to force colour to be on, in the example above you could also do &lt;code&gt;rg --color=always | less -R&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#rule-7-means-stdin-stdout&quot;&gt;rule 7: &lt;code&gt;-&lt;/code&gt; means stdin/stdout&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Usually if you pass &lt;code&gt;-&lt;/code&gt; to a program instead of a filename, it’ll read from stdin or write to stdout (whichever is appropriate). For example, if you want to format the Python code that’s on your clipboard with &lt;code&gt;black&lt;/code&gt; and then copy it, you could run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;pbpaste | black - | pbcopy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;pbpaste&lt;/code&gt; is a Mac program, you can do something similar on Linux with &lt;code&gt;xclip&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;My impression is that most programs implement this if it would make sense and I can’t think of any exceptions right now, but I’m sure there are many exceptions.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/26/terminal-rules/#these-rules-take-a-long-time-to-learn&quot;&gt;these “rules” take a long time to learn&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;These rules took me a long time for me to learn because I had to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;learn that the rule applied anywhere at all (&amp;quot;&lt;code&gt;Ctrl-C&lt;/code&gt; will exit programs&amp;quot;)&lt;/li&gt;
&lt;li&gt;notice some exceptions (“okay, &lt;code&gt;Ctrl-C&lt;/code&gt; will exit &lt;code&gt;find&lt;/code&gt; but not &lt;code&gt;less&lt;/code&gt;”)&lt;/li&gt;
&lt;li&gt;subconsciously figure out what the pattern is (&amp;quot;&lt;code&gt;Ctrl-C&lt;/code&gt; will generally quit noninteractive programs, but in interactive programs it might interrupt the current operation instead of quitting the program&amp;quot;)&lt;/li&gt;
&lt;li&gt;eventually maybe formulate it into an explicit rule that I know&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A lot of my understanding of the terminal is honestly still in the “subconscious pattern recognition” stage. The only reason I’ve been taking the time to make things explicit at all is because I’ve been trying to explain how it works to others. Hopefully writing down these “rules” explicitly will make learning some of this stuff a little bit faster for others.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Why pipes sometimes get &quot;stuck&quot;: buffering</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2024-11-29-why-pipes-sometimes-get-stuck-buffering/</link>
    <guid isPermaLink="false">urn:aggr:item:39aaea75553b1a300fb49d102e8233f03c95f1ea</guid>
    <atom:link href="https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/" rel="via" type="text/html"/>
    <pubDate>Fri, 29 Nov 2024 08:23:31 +0000</pubDate>
    <description>Here’s a niche terminal problem that has bothered me for years but that I never really understood until a few weeks ago. Let’s say you’re running this command to watch for some specific output in a log file: tail -f /some/log/file | grep…</description>
    <content:encoded>&lt;p&gt;Here’s a niche terminal problem that has bothered me for years but that I never really understood until a few weeks ago. Let’s say you’re running this command to watch for some specific output in a log file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | grep thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If log lines are being added to the file relatively slowly, the result I’d see is… nothing! It doesn’t matter if there were matches in the log file or not, there just wouldn’t be any output.&lt;/p&gt;
&lt;p&gt;I internalized this as “uh, I guess pipes just get stuck sometimes and don’t show me the output, that’s weird”, and I’d handle it by just running &lt;code&gt;grep thing1 /some/log/file | grep thing2&lt;/code&gt; instead, which would work.&lt;/p&gt;
&lt;p&gt;So as I’ve been doing a terminal deep dive over the last few months I was really excited to finally learn exactly why this happens.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#why-this-happens-buffering&quot;&gt;why this happens: buffering&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The reason why “pipes get stuck” sometimes is that it’s VERY common for programs to buffer their output before writing it to a pipe or file. So the pipe is working fine, the problem is that the program never even wrote the data to the pipe!&lt;/p&gt;
&lt;p&gt;This is for performance reasons: writing all output immediately as soon as you can uses more system calls, so it’s more efficient to save up data until you have 8KB or so of data to write (or until the program exits) and THEN write it to the pipe.&lt;/p&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | grep thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the problem is that &lt;code&gt;grep thing1&lt;/code&gt; is saving up all of its matches until it has 8KB of data to write, which might literally never happen.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#programs-don-t-buffer-when-writing-to-a-terminal&quot;&gt;programs don’t buffer when writing to a terminal&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Part of why I found this so disorienting is that &lt;code&gt;tail -f file | grep thing&lt;/code&gt; will work totally fine, but then when you add the second &lt;code&gt;grep&lt;/code&gt;, it stops working!! The reason for this is that the way &lt;code&gt;grep&lt;/code&gt; handles buffering depends on whether it’s writing to a terminal or not.&lt;/p&gt;
&lt;p&gt;Here’s how &lt;code&gt;grep&lt;/code&gt; (and many other programs) decides to buffer its output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check if stdout is a terminal or not using the &lt;code&gt;isatty&lt;/code&gt; function
&lt;ul&gt;
&lt;li&gt;If it’s a terminal, use line buffering (print every line immediately as soon as you have it)&lt;/li&gt;
&lt;li&gt;Otherwise, use “block buffering” – only print data if you have at least 8KB or so of data to print&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So if &lt;code&gt;grep&lt;/code&gt; is writing directly to your terminal then you’ll see the line as soon as it’s printed, but if it’s writing to a pipe, you won’t.&lt;/p&gt;
&lt;p&gt;Of course the buffer size isn’t always 8KB for every program, it depends on the implementation. For &lt;code&gt;grep&lt;/code&gt; the buffering is handled by libc, and libc’s buffer size is defined in the &lt;code&gt;BUFSIZ&lt;/code&gt; variable. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/bminor/glibc/blob/c69e8cccaff8f2d89cee43202623b33e6ef5d24a/libio/stdio.h#L100&quot;&gt;Here’s where that’s defined in glibc&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;(as an aside: “programs do not use 8KB output buffers when writing to a terminal” isn’t, like, a law of terminal physics, a program COULD use an 8KB buffer when writing output to a terminal if it wanted, it would just be extremely weird if it did that, I can’t think of any program that behaves that way)&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#commands-that-buffer-commands-that-don-t&quot;&gt;commands that buffer &amp;amp; commands that don’t&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One annoying thing about this buffering behaviour is that you kind of need to remember which commands buffer their output when writing to a pipe.&lt;/p&gt;
&lt;p&gt;Some commands that &lt;strong&gt;don’t&lt;/strong&gt; buffer their output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tail&lt;/li&gt;
&lt;li&gt;cat&lt;/li&gt;
&lt;li&gt;tee&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think almost everything else will buffer output, especially if it’s a command where you’re likely to be using it for batch processing. Here’s a list of some common commands that buffer their output when writing to a pipe, along with the flag that disables block buffering.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;grep (&lt;code&gt;--line-buffered&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;sed (&lt;code&gt;-u&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;awk (there’s a &lt;code&gt;fflush()&lt;/code&gt; function)&lt;/li&gt;
&lt;li&gt;tcpdump (&lt;code&gt;-l&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;jq (&lt;code&gt;-u&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;tr (&lt;code&gt;-u&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;cut (can’t disable buffering)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those are all the ones I can think of, lots of unix commands (like &lt;code&gt;sort&lt;/code&gt;) may or may not buffer their output but it doesn’t matter because &lt;code&gt;sort&lt;/code&gt; can’t do anything until it finishes receiving input anyway.&lt;/p&gt;
&lt;p&gt;Also I did my best to test both the Mac OS and GNU versions of these but there are a lot of variations and I might have made some mistakes.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#programming-languages-where-the-default-print-statement-buffers&quot;&gt;programming languages where the default “print” statement buffers&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Also, here are a few programming language where the default print statement will buffer output when writing to a pipe, and some ways to disable buffering if you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C (disable with &lt;code&gt;setvbuf&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Python (disable with &lt;code&gt;python -u&lt;/code&gt;, or &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt;, or &lt;code&gt;sys.stdout.reconfigure(line_buffering=False)&lt;/code&gt;, or &lt;code&gt;print(x, flush=True)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ruby (disable with &lt;code&gt;STDOUT.sync = true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Perl (disable with &lt;code&gt;$| = 1&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I assume that these languages are designed this way so that the default print function will be fast when you’re doing batch processing.&lt;/p&gt;
&lt;p&gt;Also whether output is buffered or not might depend on how you print, for example in C++ &lt;code&gt;cout &amp;lt;&amp;lt; &amp;quot;hello\n&amp;quot;&lt;/code&gt; buffers when writing to a pipe but &lt;code&gt;cout &amp;lt;&amp;lt; &amp;quot;hello&amp;quot; &amp;lt;&amp;lt; endl&lt;/code&gt; will flush its output.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#when-you-press-ctrl-c-on-a-pipe-the-contents-of-the-buffer-are-lost&quot;&gt;when you press &lt;code&gt;Ctrl-C&lt;/code&gt; on a pipe, the contents of the buffer are lost&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let’s say you’re running this command as a hacky way to watch for DNS requests to &lt;code&gt;example.com&lt;/code&gt;, and you forgot to pass &lt;code&gt;-l&lt;/code&gt; to tcpdump:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;sudo tcpdump -ni any port 53 | grep example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you press &lt;code&gt;Ctrl-C&lt;/code&gt;, what happens? In a magical perfect world, what I would &lt;em&gt;want&lt;/em&gt; to happen is for &lt;code&gt;tcpdump&lt;/code&gt; to flush its buffer, &lt;code&gt;grep&lt;/code&gt; would search for &lt;code&gt;example.com&lt;/code&gt;, and I would see all the output I missed.&lt;/p&gt;
&lt;p&gt;But in the real world, what happens is that all the programs get killed and the output in &lt;code&gt;tcpdump&lt;/code&gt;’s buffer is lost.&lt;/p&gt;
&lt;p&gt;I think this problem is probably unavoidable – I spent a little time with &lt;code&gt;strace&lt;/code&gt; to see how this works and &lt;code&gt;grep&lt;/code&gt; receives the &lt;code&gt;SIGINT&lt;/code&gt; before &lt;code&gt;tcpdump&lt;/code&gt; anyway so even if &lt;code&gt;tcpdump&lt;/code&gt; tried to flush its buffer &lt;code&gt;grep&lt;/code&gt; would already be dead.&lt;/p&gt;
&lt;p&gt;After a little more investigation, there is a workaround: if you find &lt;code&gt;tcpdump&lt;/code&gt;’s PID and &lt;code&gt;kill -TERM $PID&lt;/code&gt;, then tcpdump will flush the buffer so you can see the output. That’s kind of a pain but I tested it and it seems to work.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#redirecting-to-a-file-also-buffers&quot;&gt;redirecting to a file also buffers&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It’s not just pipes, this will also buffer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;sudo tcpdump -ni any port 53 &amp;gt; output.txt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redirecting to a file doesn’t have the same “&lt;code&gt;Ctrl-C&lt;/code&gt; will totally destroy the contents of the buffer” problem though – in my experience it usually behaves more like you’d want, where the contents of the buffer get written to the file before the program exits. I’m not 100% sure whether this is something you can always rely on or not.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#a-bunch-of-potential-ways-to-avoid-buffering&quot;&gt;a bunch of potential ways to avoid buffering&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Okay, let’s talk solutions. Let’s say you’ve run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | grep thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I asked people on Mastodon how they would solve this in practice and there were 5 basic approaches. Here they are:&lt;/p&gt;
&lt;h4&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#solution-1-run-a-program-that-finishes-quickly&quot;&gt;solution 1: run a program that finishes quickly&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Historically my solution to this has been to just avoid the “command writing to pipe slowly” situation completely and instead run a program that will finish quickly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;cat /some/log/file | grep thing1 | grep thing2 | tail
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This doesn’t do the same thing as the original command but it does mean that you get to avoid thinking about these weird buffering issues.&lt;/p&gt;
&lt;p&gt;(you could also do &lt;code&gt;grep thing1 /some/log/file&lt;/code&gt; but I often prefer to use an “unnecessary” &lt;code&gt;cat&lt;/code&gt;)&lt;/p&gt;
&lt;h4&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#solution-2-remember-the-line-buffer-flag-to-grep&quot;&gt;solution 2: remember the “line buffer” flag to grep&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;You could remember that grep has a flag to avoid buffering and pass it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#solution-3-use-awk&quot;&gt;solution 3: use awk&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Some people said that if they’re specifically dealing with a multiple greps situation, they’ll rewrite it to use a single &lt;code&gt;awk&lt;/code&gt; instead, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file |  awk &amp;#39;/thing1/ &amp;amp;&amp;amp; /thing2/&amp;#39;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or you would write a more complicated &lt;code&gt;grep&lt;/code&gt;, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file |  grep -E &amp;#39;thing1.*thing2&amp;#39;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;awk&lt;/code&gt; also buffers, so for this to work you’ll want &lt;code&gt;awk&lt;/code&gt; to be the last command in the pipeline)&lt;/p&gt;
&lt;h4&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#solution-4-use-stdbuf&quot;&gt;solution 4: use &lt;code&gt;stdbuf&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;stdbuf&lt;/code&gt; uses LD_PRELOAD to turn off libc’s buffering, and you can use it to turn off output buffering like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like any &lt;code&gt;LD_PRELOAD&lt;/code&gt; solution it’s a bit unreliable – it doesn’t work on static binaries, I think won’t work if the program isn’t using libc’s buffering, and doesn’t always work on Mac OS. Harry Marr has a really nice &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://hmarr.com/blog/how-stdbuf-works/&quot;&gt;How stdbuf works&lt;/a&gt; post.&lt;/p&gt;
&lt;h4&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#solution-5-use-unbuffer&quot;&gt;solution 5: use &lt;code&gt;unbuffer&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;unbuffer program&lt;/code&gt; will force the program’s output to be a TTY, which means that it’ll behave the way it normally would on a TTY (less buffering, colour output, etc). You could use it in this example like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;tail -f /some/log/file | unbuffer grep thing1 | grep thing2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike &lt;code&gt;stdbuf&lt;/code&gt; it will always work, though it might have unwanted side effects, for example &lt;code&gt;grep thing1&lt;/code&gt;’s will also colour matches.&lt;/p&gt;
&lt;p&gt;If you want to install unbuffer, it’s in the &lt;code&gt;expect&lt;/code&gt; package.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#that-s-all-the-solutions-i-know-about&quot;&gt;that’s all the solutions I know about!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It’s a bit hard for me to say which one is “best”, I think personally I’m mostly likely to use &lt;code&gt;unbuffer&lt;/code&gt; because I know it’s always going to work.&lt;/p&gt;
&lt;p&gt;If I learn about more solutions I’ll try to add them to this post.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#i-m-not-really-sure-how-often-this-comes-up&quot;&gt;I’m not really sure how often this comes up&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think it’s not very common for me to have a program that slowly trickles data into a pipe like this, normally if I’m using a pipe a bunch of data gets written very quickly, processed by everything in the pipeline, and then everything exits. The only examples I can come up with right now are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tcpdump&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail -f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;watching log files in a different way like with &lt;code&gt;kubectl logs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the output of a slow computation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#what-if-there-were-an-environment-variable-to-disable-buffering&quot;&gt;what if there were an environment variable to disable buffering?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think it would be cool if there were a standard environment variable to turn off buffering, like &lt;code&gt;PYTHONUNBUFFERED&lt;/code&gt; in Python. I got this idea from a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.plover.com/Unix/stdio-buffering.html&quot;&gt;couple&lt;/a&gt; of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.plover.com/Unix/stdio-buffering-2.html&quot;&gt;blog posts&lt;/a&gt; by Mark Dominus in 2018. Maybe &lt;code&gt;NO_BUFFER&lt;/code&gt; like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://no-color.org/&quot;&gt;NO_COLOR&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;The design seems tricky to get right; Mark points out that NETBSD has &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://man.netbsd.org/setbuf.3&quot;&gt;environment variables called &lt;code&gt;STDBUF&lt;/code&gt;, &lt;code&gt;STDBUF1&lt;/code&gt;, etc&lt;/a&gt; which gives you a ton of control over buffering but I imagine most developers don’t want to implement many different environment variables to handle a relatively minor edge case.&lt;/p&gt;
&lt;p&gt;I’m also curious about whether there are any programs that just automatically flush their output buffers after some period of time (like 1 second). It feels like it would be nice in theory but I can’t think of any program that does that so I imagine there are some downsides.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/#stuff-i-left-out&quot;&gt;stuff I left out&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some things I didn’t talk about in this post since these posts have been getting pretty long recently and seriously does anyone REALLY want to read 3000 words about buffering?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the difference between line buffering and having totally unbuffered output&lt;/li&gt;
&lt;li&gt;how buffering to stderr is different from buffering to stdout&lt;/li&gt;
&lt;li&gt;this post is only about buffering that happens &lt;strong&gt;inside the program&lt;/strong&gt;, your operating system’s TTY driver also does a little bit of buffering sometimes&lt;/li&gt;
&lt;li&gt;other reasons you might need to flush your output other than “you’re writing to a pipe”&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Importing a frontend Javascript library without a build system</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2024-11-18-importing-a-frontend-javascript-library-without-a-build/</link>
    <guid isPermaLink="false">urn:aggr:item:baa4e772187bab963b209f20988e3148a3ed2d68</guid>
    <atom:link href="https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/" rel="via" type="text/html"/>
    <pubDate>Mon, 18 Nov 2024 09:35:42 +0000</pubDate>
    <description>I like writing Javascript without a build system and for the millionth time yesterday I ran into a problem where I needed to figure out how to import a Javascript library in my code without using a build system, and it took FOREVER to…</description>
    <content:encoded>&lt;p&gt;I like writing Javascript &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2023/02/16/writing-javascript-without-a-build-system/&quot;&gt;without a build system&lt;/a&gt; and for the millionth time yesterday I ran into a problem where I needed to figure out how to import a Javascript library in my code without using a build system, and it took FOREVER to figure out how to import it because the library’s setup instructions assume that you’re using a build system.&lt;/p&gt;
&lt;p&gt;Luckily at this point I’ve mostly learned how to navigate this situation and either successfully use the library or decide it’s too difficult and switch to a different library, so here’s the guide I wish I had to importing Javascript libraries years ago.&lt;/p&gt;
&lt;p&gt;I’m only going to talk about using Javacript libraries on the frontend, and only about how to use them in a no-build-system setup.&lt;/p&gt;
&lt;p&gt;In this post I’m going to talk about:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the three main types of Javascript files a library might provide (ES Modules, the “classic” global variable kind, and CommonJS)&lt;/li&gt;
&lt;li&gt;how to figure out which types of files a Javascript library includes in its build&lt;/li&gt;
&lt;li&gt;ways to import each type of file in your code&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#the-three-kinds-of-javascript-files&quot;&gt;the three kinds of Javascript files&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are 3 basic types of Javascript files a library can provide:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the “classic” type of file that defines a global variable. This is the kind of file that you can just &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; and it’ll Just Work. Great if you can get it but not always available&lt;/li&gt;
&lt;li&gt;an ES module (which may or may not depend on other files, we’ll get to that)&lt;/li&gt;
&lt;li&gt;a “CommonJS” module. This is for Node, you can’t use it in a browser at all without using a build system.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m not sure if there’s a better name for the “classic” type but I’m just going to call it “classic”. Also there’s a type called “AMD” but I’m not sure how relevant it is in 2024.&lt;/p&gt;
&lt;p&gt;Now that we know the 3 types of files, let’s talk about how to figure out which of these the library actually provides!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#where-to-find-the-files-the-npm-build&quot;&gt;where to find the files: the NPM build&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Every Javascript library has a &lt;strong&gt;build&lt;/strong&gt; which it uploads to NPM. You might be thinking (like I did originally) – Julia! The whole POINT is that we’re not using Node to build our library! Why are we talking about NPM?&lt;/p&gt;
&lt;p&gt;But if you’re using a link from a CDN like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js&quot;&gt;https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js&lt;/a&gt;, you’re still using the NPM build! All the files on the CDNs originally come from NPM.&lt;/p&gt;
&lt;p&gt;Because of this, I sometimes like to &lt;code&gt;npm install&lt;/code&gt; the library even if I’m not planning to use Node to build my library at all – I’ll just create a new temp folder, &lt;code&gt;npm install&lt;/code&gt; there, and then delete it when I’m done. I like being able to poke around in the files in the NPM build on my filesystem, because then I can be 100% sure that I’m seeing everything that the library is making available in its build and that the CDN isn’t hiding something from me.&lt;/p&gt;
&lt;p&gt;So let’s &lt;code&gt;npm install&lt;/code&gt; a few libraries and try to figure out what types of Javascript files they provide in their builds!&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#example-library-1-chart-js&quot;&gt;example library 1: chart.js&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;First let’s look inside &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.chartjs.org&quot;&gt;Chart.js&lt;/a&gt;, a plotting library.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; cd /tmp/whatever&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; npm install chart.js&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; cd node_modules/chart.js/dist&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; ls &lt;span class=&quot;syntax-keyword syntax-operator syntax-regexp syntax-quantifier syntax-shell&quot;&gt;*&lt;/span&gt;.&lt;span class=&quot;syntax-keyword syntax-operator syntax-regexp syntax-quantifier syntax-shell&quot;&gt;*&lt;/span&gt;js&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;chart.cjs&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;  chart.js  chart.umd.js  helpers.cjs  helpers.js&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This library seems to have 3 basic options:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;option 1:&lt;/strong&gt; &lt;code&gt;chart.cjs&lt;/code&gt;. The &lt;code&gt;.cjs&lt;/code&gt; suffix tells me that this is a &lt;strong&gt;CommonJS file&lt;/strong&gt;, for using in Node. This means it’s impossible to use it directly in the browser without some kind of build step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;option 2:&lt;code&gt;chart.js&lt;/code&gt;&lt;/strong&gt;. The &lt;code&gt;.js&lt;/code&gt; suffix by itself doesn’t tell us what kind of file it is, but if I open it up, I see &lt;code&gt;import &#39;@kurkle/color&#39;;&lt;/code&gt; which is an immediate sign that this is an ES module – the &lt;code&gt;import ...&lt;/code&gt; syntax is ES module syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;option 3: &lt;code&gt;chart.umd.js&lt;/code&gt;&lt;/strong&gt;. “UMD” stands for “Universal Module Definition”, which I think means that you can use this file either with a basic &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;, CommonJS, or some third thing called AMD that I don’t understand.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#how-to-use-a-umd-file&quot;&gt;how to use a UMD file&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When I was using Chart.js I picked Option 3. I just needed to add this to my code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;script src=&amp;quot;./chart.umd.js&amp;quot;&amp;gt; &amp;lt;/script&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then I could use the library with the global &lt;code&gt;Chart&lt;/code&gt; environment variable. Couldn’t be easier. I just copied &lt;code&gt;chart.umd.js&lt;/code&gt; into my Git repository so that I didn’t have to worry about using NPM or the CDNs going down or anything.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#the-build-files-aren-t-always-in-the-dist-directory&quot;&gt;the build files aren’t always in the &lt;code&gt;dist&lt;/code&gt; directory&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A lot of libraries will put their build in the &lt;code&gt;dist&lt;/code&gt; directory, but not always! The build files’ location is specified in the library’s &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example here’s an excerpt from Chart.js’s &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;  &amp;quot;jsdelivr&amp;quot;: &amp;quot;./dist/chart.umd.js&amp;quot;,
  &amp;quot;unpkg&amp;quot;: &amp;quot;./dist/chart.umd.js&amp;quot;,
  &amp;quot;main&amp;quot;: &amp;quot;./dist/chart.cjs&amp;quot;,
  &amp;quot;module&amp;quot;: &amp;quot;./dist/chart.js&amp;quot;,
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think this is saying that if you want to use an ES Module (&lt;code&gt;module&lt;/code&gt;) you should use &lt;code&gt;dist/chart.js&lt;/code&gt;, but the jsDelivr and unpkg CDNs should use &lt;code&gt;./dist/chart.umd.js&lt;/code&gt;. I guess &lt;code&gt;main&lt;/code&gt; is for Node.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chart.js&lt;/code&gt;’s &lt;code&gt;package.json&lt;/code&gt; also says &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt;, which &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://nodejs.org/api/packages.html#modules-packages&quot;&gt;according to this documentation&lt;/a&gt; tells Node to treat files as ES modules by default. I think it doesn’t tell us specifically which files are ES modules and which ones aren’t but it does tell us that &lt;em&gt;something&lt;/em&gt; in there is an ES module.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#example-library-2-atcute-oauth-browser-client&quot;&gt;example library 2: &lt;code&gt;@atcute/oauth-browser-client&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/mary-ext/atcute/tree/trunk/packages/oauth/browser-client&quot;&gt;&lt;code&gt;@atcute/oauth-browser-client&lt;/code&gt;&lt;/a&gt; is a library for logging into Bluesky with OAuth in the browser.&lt;/p&gt;
&lt;p&gt;Let’s see what kinds of Javascript files it provides in its build!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; npm install @atcute/oauth-browser-client&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; cd node_modules/@atcute/oauth-browser-client/dist&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; ls &lt;span class=&quot;syntax-keyword syntax-operator syntax-regexp syntax-quantifier syntax-shell&quot;&gt;*&lt;/span&gt;js&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;constants.js&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;  dpop.js  environment.js  errors.js  index.js  resolvers.js&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It seems like the only plausible root file in here is &lt;code&gt;index.js&lt;/code&gt;, which looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JavaScript&quot;&gt;&lt;span class=&quot;syntax-source syntax-js&quot;&gt;&lt;span class=&quot;syntax-meta syntax-export syntax-js&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-block syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-js&quot;&gt;configureOAuth&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-block syntax-js&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;./environment.js&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-export syntax-js&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-other syntax-js&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;./errors.js&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-export syntax-js&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-other syntax-js&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-import-export syntax-js&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-js&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;./resolvers.js&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-js&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-terminator syntax-statement syntax-js&quot;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;export&lt;/code&gt; syntax means it’s an &lt;strong&gt;ES module&lt;/strong&gt;. That means we can use it in the browser without a build step! Let’s see how to do that.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#how-to-use-an-es-module-with-importmaps&quot;&gt;how to use an ES module with importmaps&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Using an ES module isn’t an easy as just adding a &lt;code&gt;&amp;lt;script src=&amp;quot;whatever.js&amp;quot;&amp;gt;&lt;/code&gt;. Instead, if the ES module has dependencies (like &lt;code&gt;@atcute/oauth-browser-client&lt;/code&gt; does) the steps are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set up an import map in your HTML&lt;/li&gt;
&lt;li&gt;Put import statements like &lt;code&gt;import { configureOAuth } from &#39;@atcute/oauth-browser-client&#39;;&lt;/code&gt; in your JS code&lt;/li&gt;
&lt;li&gt;Include your JS code in your HTML like this: &lt;code&gt;&amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;YOURSCRIPT.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The reason we need an import map instead of just doing something like &lt;code&gt;import { BrowserOAuthClient } from &amp;quot;./oauth-client-browser.js&amp;quot;&lt;/code&gt; is that internally the module has more import statements like &lt;code&gt;import {something} from @atcute/client&lt;/code&gt;, and we need to tell the browser where to get the code for &lt;code&gt;@atcute/client&lt;/code&gt; and all of its other dependencies.&lt;/p&gt;
&lt;p&gt;Here’s what the importmap I used looks like for &lt;code&gt;@atcute/oauth-browser-client&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;script type=&amp;quot;importmap&amp;quot;&amp;gt;
{
  &amp;quot;imports&amp;quot;: {
    &amp;quot;nanoid&amp;quot;: &amp;quot;./node_modules/nanoid/bin/dist/index.js&amp;quot;,
    &amp;quot;nanoid/non-secure&amp;quot;: &amp;quot;./node_modules/nanoid/non-secure/index.js&amp;quot;,
    &amp;quot;nanoid/url-alphabet&amp;quot;: &amp;quot;./node_modules/nanoid/url-alphabet/dist/index.js&amp;quot;,
    &amp;quot;@atcute/oauth-browser-client&amp;quot;: &amp;quot;./node_modules/@atcute/oauth-browser-client/dist/index.js&amp;quot;,
    &amp;quot;@atcute/client&amp;quot;: &amp;quot;./node_modules/@atcute/client/dist/index.js&amp;quot;,
    &amp;quot;@atcute/client/utils/did&amp;quot;: &amp;quot;./node_modules/@atcute/client/dist/utils/did.js&amp;quot;
  }
}
&amp;lt;/script&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Getting these import maps to work is pretty fiddly, I feel like there must be a tool to generate them automatically but I haven’t found one yet. It’s definitely possible to write a script that automatically generates the importmaps using &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esbuild.github.io/api/#metafile&quot;&gt;esbuild’s metafile&lt;/a&gt; but I haven’t done that and maybe there’s a better way.&lt;/p&gt;
&lt;p&gt;I decided to set up importmaps yesterday to get &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/jvns/bsky-oauth-example&quot;&gt;github.com/jvns/bsky-oauth-example&lt;/a&gt; to work, so there’s some example code in that repo.&lt;/p&gt;
&lt;p&gt;Also someone pointed me to Simon Willison’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://simonwillison.net/2023/May/2/download-esm/&quot;&gt;download-esm&lt;/a&gt;, which will download an ES module and rewrite the imports to point to the JS files directly so that you don’t need importmaps. I haven’t tried it yet but it seems like a great idea.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#problems-with-importmaps-too-many-files&quot;&gt;problems with importmaps: too many files&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I did run into some problems with using importmaps in the browser though – it needed to download dozens of Javascript files to load my site, and my webserver in development couldn’t keep up for some reason. I kept seeing files fail to load randomly and then had to reload the page and hope that they would succeed this time.&lt;/p&gt;
&lt;p&gt;It wasn’t an issue anymore when I deployed my site to production, so I guess it was a problem with my local dev environment.&lt;/p&gt;
&lt;p&gt;Also one slightly annoying thing about ES modules in general is that you need to be running a webserver to use them, I’m sure this is for a good reason but it’s easier when you can just open your &lt;code&gt;index.html&lt;/code&gt; file without starting a webserver.&lt;/p&gt;
&lt;p&gt;Because of the “too many files” thing I think actually using ES modules with importmaps in this way isn’t actually that appealing to me, but it’s good to know it’s possible.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#how-to-use-an-es-module-without-importmaps&quot;&gt;how to use an ES module without importmaps&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If the ES module doesn’t have dependencies then it’s even easier – you don’t need the importmaps! You can just:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;put &lt;code&gt;&amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;YOURCODE.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; in your HTML. The &lt;code&gt;type=&amp;quot;module&amp;quot;&lt;/code&gt; is important.&lt;/li&gt;
&lt;li&gt;put &lt;code&gt;import {whatever} from &amp;quot;https://example.com/whatever.js&amp;quot;&lt;/code&gt; in &lt;code&gt;YOURCODE.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#alternative-use-esbuild&quot;&gt;alternative: use esbuild&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you don’t want to use importmaps, you can also use a build system like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt;. I talked about how to do that in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2021/11/15/esbuild-vue/&quot;&gt;Some notes on using esbuild&lt;/a&gt;, but this blog post is about ways to avoid build systems completely so I’m not going to talk about that option here. I do still like esbuild though and I think it’s a good option in this case.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#what-s-the-browser-support-for-importmaps&quot;&gt;what’s the browser support for importmaps?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://caniuse.com/import-maps&quot;&gt;CanIUse&lt;/a&gt; says that importmaps are in “Baseline 2023: newly available across major browsers” so my sense is that in 2024 that’s still maybe a little bit too new? I think I would use importmaps for some fun experimental code that I only wanted like myself and 12 people to use, but if I wanted my code to be more widely usable I’d use &lt;code&gt;esbuild&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#example-library-3-atproto-oauth-client-browser&quot;&gt;example library 3: &lt;code&gt;@atproto/oauth-client-browser&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let’s look at one final example library! This is a different Bluesky auth library than &lt;code&gt;@atcute/oauth-browser-client&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; npm install @atproto/oauth-client-browser&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; cd node_modules/@atproto/oauth-client-browser/dist&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; ls &lt;span class=&quot;syntax-keyword syntax-operator syntax-regexp syntax-quantifier syntax-shell&quot;&gt;*&lt;/span&gt;js&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;browser-oauth-client.js&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;  browser-oauth-database.js  browser-runtime-implementation.js  errors.js  index.js  indexed-db-store.js  util.js&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, it seems like only real candidate file here is &lt;code&gt;index.js&lt;/code&gt;. But this is a different situation from the previous example library! Let’s take a look at &lt;code&gt;index.js&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;There’s a bunch of stuff like this in &lt;code&gt;index.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;__exportStar(require(&amp;quot;@atproto/oauth-client&amp;quot;), exports);
__exportStar(require(&amp;quot;./browser-oauth-client.js&amp;quot;), exports);
__exportStar(require(&amp;quot;./errors.js&amp;quot;), exports);
var util_js_1 = require(&amp;quot;./util.js&amp;quot;);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;require()&lt;/code&gt; syntax is CommonJS syntax, which means that we can’t use this file in the browser at all, we need to use some kind of build step, and ESBuild won’t work either.&lt;/p&gt;
&lt;p&gt;Also in this library’s &lt;code&gt;package.json&lt;/code&gt; it says &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;commonjs&amp;quot;&lt;/code&gt; which is another way to tell it’s CommonJS.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#how-to-use-a-commonjs-module-with-esm-sh-https-esm-sh&quot;&gt;how to use a CommonJS module with&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Originally I thought it was impossible to use CommonJS modules without learning a build system, but then someone Bluesky told me about &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt;! It’s a CDN that will translate anything into an ES Module. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.skypack.dev/&quot;&gt;skypack.dev&lt;/a&gt; does something similar, I’m not sure what the difference is but one person mentioned that if one doesn’t work sometimes they’ll try the other one.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;@atproto/oauth-client-browser&lt;/code&gt; using it seems pretty simple, I just need to put this in my HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;&amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;script.js&amp;quot;&amp;gt; &amp;lt;/script&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then put this in &lt;code&gt;script.js&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;code-snippet&quot;&gt;import { BrowserOAuthClient } from &amp;quot;https://esm.sh/@atproto/oauth-client-browser@0.3.0&amp;quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It seems to Just Work, which is cool! Of course this is still sort of using a build system – it’s just that esm.sh is running the build instead of me. My main concerns with this approach are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don’t really trust CDNs to keep working forever – usually I like to copy dependencies into my repository so that they don’t go away for some reason in the future.&lt;/li&gt;
&lt;li&gt;I’ve heard of some issues with CDNs having security compromises which scares me.&lt;/li&gt;
&lt;li&gt;I don’t really understand what esm.sh is doing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#esbuild-can-also-convert-commonjs-modules-into-es-modules&quot;&gt;esbuild can also convert CommonJS modules into ES modules&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also learned that you can also use &lt;code&gt;esbuild&lt;/code&gt; to convert a CommonJS module into an ES module, though there are some limitations – the &lt;code&gt;import { BrowserOAuthClient } from&lt;/code&gt; syntax doesn’t work. Here’s a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/evanw/esbuild/issues/442&quot;&gt;github issue about that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I think the &lt;code&gt;esbuild&lt;/code&gt; approach is probably more appealing to me than the &lt;code&gt;esm.sh&lt;/code&gt; approach because it’s a tool that I already have on my computer so I trust it more. I haven’t experimented with this much yet though.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#summary-of-the-three-types-of-files&quot;&gt;summary of the three types of files&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s a summary of the three types of JS files you might encounter, options for how to use them, and how to identify them.&lt;/p&gt;
&lt;p&gt;Unhelpfully a &lt;code&gt;.js&lt;/code&gt; or &lt;code&gt;.min.js&lt;/code&gt; file extension could be any of these 3 options, so if the file is &lt;code&gt;something.js&lt;/code&gt; you need to do more detective work to figure out what you’re dealing with.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;“classic” JS files&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How to use it:&lt;/strong&gt;: &lt;code&gt;&amp;lt;script src=&amp;quot;whatever.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ways to identify it:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;The website has a big friendly banner in its setup instructions saying “Use this with a CDN!” or something&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.umd.js&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;Just try to put it in a &lt;code&gt;&amp;lt;script src=...&lt;/code&gt; tag and see if it works&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ES Modules&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ways to use it:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If there are no dependencies, just &lt;code&gt;import {whatever} from &amp;quot;./my-module.js&amp;quot;&lt;/code&gt; directly in your code&lt;/li&gt;
&lt;li&gt;If there are dependencies, create an importmap and &lt;code&gt;import {whatever} from &amp;quot;my-module&amp;quot;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;or use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://simonwillison.net/2023/May/2/download-esm/&quot;&gt;download-esm&lt;/a&gt; to remove the need for an importmap&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; or any ES Module bundler&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ways to identify it:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Look for an &lt;code&gt;import&lt;/code&gt; or &lt;code&gt;export&lt;/code&gt; statement. (not &lt;code&gt;module.exports = ...&lt;/code&gt;, that’s CommonJS)&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;.mjs&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; (though it’s not clear to me which file exactly this refers to)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CommonJS Modules&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ways to use it:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esm.sh/#docs&quot;&gt;https://esm.sh&lt;/a&gt; to convert it into an ES module, like &lt;code&gt;https://esm.sh/@atproto/oauth-client-browser@0.3.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use a build somehow (??)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ways to identify it:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Look for &lt;code&gt;require()&lt;/code&gt; or &lt;code&gt;module.exports = ...&lt;/code&gt; in the code&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.cjs&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;commonjs&amp;quot;&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; (though it’s not clear to me which file exactly this refers to)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#it-s-really-nice-to-have-es-modules-standardized&quot;&gt;it’s really nice to have ES modules standardized&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The main difference between CommonJS modules and ES modules from my perspective is that ES modules are actually a standard. This makes me feel a lot more confident using them, because browsers commit to backwards compatibility for web standards forever – if I write some code using ES modules today, I can feel sure that it’ll still work the same way in 15 years.&lt;/p&gt;
&lt;p&gt;It also makes me feel better about using tooling like &lt;code&gt;esbuild&lt;/code&gt; because even if the esbuild project dies, because it’s implementing a standard it feels likely that there will be another similar tool in the future that I can replace it with.&lt;/p&gt;
&lt;p&gt;A lot of the time when I talk about this stuff I get responses like “I hate javascript!!! it’s the worst!!!”. But my experience is that there are a lot of great tools for Javascript (I just learned about &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esm.sh&quot;&gt;https://esm.sh&lt;/a&gt; yesterday which seems great! I love esbuild!), and that if I take the time to learn how things works I can take advantage of some of those tools and make my life a lot easier.&lt;/p&gt;
&lt;p&gt;So the goal of this post is definitely not to complain about Javascript, it’s to understand the landscape so I can use the tooling in a way that feels good to me.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#questions-i-still-have&quot;&gt;questions I still have&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here are some questions I still have, I’ll add the answers into the post if I learn the answer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is there a tool that automatically generates importmaps for an ES Module that I have set up locally? (apparently yes: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jspm.org/getting-started&quot;&gt;jspm&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;How can I convert a CommonJS module into an ES module on my computer, the way &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esm.sh&quot;&gt;https://esm.sh&lt;/a&gt; does? (apparently esbuild can sort of do this, though &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/evanw/esbuild/issues/442&quot;&gt;named exports don’t work&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;When people normally build CommonJS modules into regular JS code, what’s code is doing that? Obviously there are tools like webpack, rollup, esbuild, etc, but do those tools all implement their own JS parsers/static analysis? How many JS parsers are there out there?&lt;/li&gt;
&lt;li&gt;Is there any way to bundle an ES module into a single file (like &lt;code&gt;atcute-client.js&lt;/code&gt;), but so that in the browser I can still import multiple different paths from that file (like both &lt;code&gt;@atcute/client/lexicons&lt;/code&gt; and &lt;code&gt;@atcute/client&lt;/code&gt;)?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#all-the-tools&quot;&gt;all the tools&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s a list of every tool we talked about in this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simon Willison’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://simonwillison.net/2023/May/2/download-esm/&quot;&gt;download-esm&lt;/a&gt; which will download an ES module and convert the imports to point at JS files so you don’t need an importmap&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/esm.sh&quot;&gt;https://esm.sh/&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.skypack.dev/&quot;&gt;skypack.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jspm.org/getting-started&quot;&gt;JSPM&lt;/a&gt; can generate importmaps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing this post has made me think that even though I usually don’t want to have a build that I run every time I update the project, I might be willing to have a build step (using &lt;code&gt;download-esm&lt;/code&gt; or something) that I run &lt;strong&gt;only once&lt;/strong&gt; when setting up the project and never run again except maybe if I’m updating my dependency versions.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/#that-s-all&quot;&gt;that’s all!&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Thanks to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://polotek.net/&quot;&gt;Marco Rogers&lt;/a&gt; who taught me a lot of the things in this post. I’ve probably made some mistakes in this post and I’d love to know what they are – let me know on Bluesky or Mastodon!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>New microblog with TILs</title>
    <link>https://aggr.aymericbeaumet.com/items/jvns-ca/2024-11-09-new-microblog-with-tils/</link>
    <guid isPermaLink="false">urn:aggr:item:97a6ad129556aea72de4ed0f8c6ba07141bc8c01</guid>
    <atom:link href="https://jvns.ca/blog/2024/11/09/new-microblog/" rel="via" type="text/html"/>
    <pubDate>Sat, 9 Nov 2024 09:24:29 +0000</pubDate>
    <description>I added a new section to this site a couple weeks ago called TIL (“today I learned”). One kind of thing I like to post on Mastodon/Bluesky is “hey, here’s a cool thing”, like the great SQLite repl litecli, or the fact that cross compiling…</description>
    <content:encoded>&lt;p&gt;I added a new section to this site a couple weeks ago called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/til/&quot;&gt;TIL&lt;/a&gt; (“today I learned”).&lt;/p&gt;
&lt;p&gt;One kind of thing I like to post on Mastodon/Bluesky is “hey, here’s a cool thing”, like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/dbcli/litecli&quot;&gt;the great SQLite repl litecli&lt;/a&gt;, or the fact that cross compiling in Go Just Works and it’s amazing, or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.latacora.com/blog/2018/04/03/cryptographic-right-answers/&quot;&gt;cryptographic right answers&lt;/a&gt;, or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://diffdiff.net/&quot;&gt;this great diff tool&lt;/a&gt;. Usually I don’t want to write a whole blog post about those things because I really don’t have much more to say than “hey this is useful!”&lt;/p&gt;
&lt;p&gt;It started to bother me that I didn’t have anywhere to put those things: for example recently I wanted to use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://diffdiff.net/&quot;&gt;diffdiff&lt;/a&gt; and I just could not remember what it was called.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/09/new-microblog/#the-solution-make-a-new-section-of-this-blog&quot;&gt;the solution: make a new section of this blog&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So I quickly made a new folder called &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/til/&quot;&gt;/til/&lt;/a&gt;, added some custom styling (I wanted to style the posts to look a little bit like a tweet), made a little Rake task to help me create new posts quickly (&lt;code&gt;rake new_til&lt;/code&gt;), and set up a separate RSS Feed for it.&lt;/p&gt;
&lt;p&gt;I think this new section of the blog might be more for myself than anything, now when I forget the link to Cryptographic Right Answers I can hopefully look it up on the TIL page. (you might think “julia, why not use bookmarks??” but I have been failing to use bookmarks for my whole life and I don’t see that changing ever, putting things in public is for whatever reason much easier for me)&lt;/p&gt;
&lt;p&gt;So far it’s been working, often I can actually just make a quick post in 2 minutes which was the goal.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/09/new-microblog/#inspired-by-simon-willison-s-til-blog&quot;&gt;inspired by Simon Willison’s TIL blog&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My page is inspired by &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://til.simonwillison.net/&quot;&gt;Simon Willison’s great TIL blog&lt;/a&gt;, though my TIL posts are a lot shorter.&lt;/p&gt;
&lt;h3&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/blog/2024/11/09/new-microblog/#i-don-t-necessarily-want-everything-to-be-archived&quot;&gt;I don’t necessarily want everything to be archived&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This came about because I spent a lot of time on Twitter, so I’ve been thinking about what I want to do about all of my tweets.&lt;/p&gt;
&lt;p&gt;I keep reading the advice to “POSSE” (“post on your own site, syndicate elsewhere”), and while I find the idea appealing in principle, for me part of the appeal of social media is that it’s a little bit ephemeral. I can post polls or questions or observations or jokes and then they can just kind of fade away as they become less relevant.&lt;/p&gt;
&lt;p&gt;I find it a lot easier to identify specific categories of things that I actually want to have on a Real Website That I Own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;blog posts here!&lt;/li&gt;
&lt;li&gt;comics at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wizardzines.com/comics/&quot;&gt;https://wizardzines.com/comics/&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;now TILs at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jvns.ca/til/&quot;&gt;https://jvns.ca/til/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and then let everything else be kind of ephemeral.&lt;/p&gt;
&lt;p&gt;I really believe in the advice to make email lists though – the first two (blog posts &amp;amp; comics) both have email lists and RSS feeds that people can subscribe to if they want. I might add a quick summary of any TIL posts from that week to the “blog posts from this week” mailing list.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
</channel>
</rss>
