<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>typovrak</title><description>Notes on Arch Linux, NixOS, CLI tooling and web development, written up from problems I had to solve myself.</description><link>https://typovrak.tv</link><language>en</language><lastBuildDate>Fri, 16 Oct 2026 03:39:30 GMT</lastBuildDate><atom:link href="https://typovrak.tv/rss.xml" rel="self" type="application/rss+xml"/><item><title>Serving a blog post to curl: ANSI text from a static Astro site</title><link>https://typovrak.tv/posts/serving-a-blog-to-curl</link><guid isPermaLink="true">https://typovrak.tv/posts/serving-a-blog-to-curl</guid><description>curl https://typovrak.tv/posts/&lt;slug&gt; gets the post as coloured terminal text and a browser keeps the HTML. One Vercel rewrite on the user-agent, two prerendered text files per post, no function.</description><pubDate>Wed, 30 Sep 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;curl https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/code&gt; prints the post as coloured text, 80 columns wide, with its headings, tables, callouts and code blocks intact. A browser asking for the same URL gets the HTML. The switch is one rewrite rule in the Vercel config, keyed on the user-agent, in front of two prerendered text files per post. No function runs and no JavaScript is involved.&lt;/p&gt;
&lt;p&gt;I shipped it on 2 September 2026 in &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/commit/c6493703c8c4a5251d2b837dc1cc913432a8400e&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;one commit&lt;/a&gt; of 1,201 lines, about half of them tests. The switch is two of those lines. Everything else is the renderer, which is the part that decides whether the output is worth reading.&lt;/p&gt;
&lt;p&gt;Versions below are Astro &lt;a href=&quot;https://github.com/withastro/astro/releases/tag/astro@7.0.3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;7.0.3&lt;/a&gt; with &lt;code&gt;@astrojs/vercel&lt;/code&gt; &lt;a href=&quot;https://github.com/withastro/astro/releases/tag/@astrojs/vercel@11.0.3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;11.0.3&lt;/a&gt;, checked against the live site on 29 September 2026.&lt;/p&gt;

&lt;figure class=&quot;not-prose my-6&quot; data-astro-cid-2cxdht5w&gt;&lt;div class=&quot;border-accent w-full overflow-hidden rounded-md border&quot; data-asciinema=&quot;/casts/curl-typovrak-tv.cast&quot; data-cols=&quot;87&quot; data-rows=&quot;23&quot; data-idle-time-limit=&quot;2&quot; data-markers=&quot;[[0.4,&amp;quot;curl https://typovrak.tv&amp;quot;],[7.8,&amp;quot;curl a post | less -R&amp;quot;],[26,&amp;quot;the post, paged and coloured&amp;quot;],[32.1,&amp;quot;browser user-agent, html comes back&amp;quot;]]&quot; data-astro-cid-2cxdht5w&gt;&lt;button type=&quot;button&quot; class=&quot;focus-outline group relative block w-full cursor-pointer&quot; aria-label=&quot;Play the recording: The same two URLs in a terminal, then the html coming back for a browser user-agent.&quot; data-astro-cid-2cxdht5w&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/curl-index-in-a-terminal-ansi.avif&quot; alt=&quot;&quot; width=&quot;766&quot; height=&quot;455&quot; class=&quot;block w-full scale-105 border-0 blur-[3px]&quot; data-astro-cid-2cxdht5w&gt;&lt;span class=&quot;bg-background/40 group-hover:bg-background/25 absolute inset-0 flex items-center justify-center transition-colors&quot; data-astro-cid-2cxdht5w&gt;&lt;svg viewBox=&quot;0 0 1000 1000&quot; aria-hidden=&quot;true&quot; class=&quot;text-accent h-24 w-24 transition-transform group-hover:scale-110&quot; data-astro-cid-2cxdht5w&gt;&lt;defs data-astro-cid-2cxdht5w&gt;&lt;mask id=&quot;asciinema-play-cut&quot; data-astro-cid-2cxdht5w&gt;&lt;rect width=&quot;100%&quot; height=&quot;100%&quot; fill=&quot;white&quot; data-astro-cid-2cxdht5w&gt;&lt;/rect&gt;&lt;polygon points=&quot;700 500, 400 326.7949192431122, 400 673.2050807568877&quot; fill=&quot;black&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;/mask&gt;&lt;/defs&gt;&lt;polygon points=&quot;1000 500, 250 66.98729810778059, 250 933.0127018922192&quot; mask=&quot;url(#asciinema-play-cut)&quot; fill=&quot;currentColor&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;polyline points=&quot;673.2050807568878 400, 326.7949192431123 600&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;90&quot; fill=&quot;none&quot; data-astro-cid-2cxdht5w&gt;&lt;/polyline&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;figcaption class=&quot;mt-2 text-center text-sm opacity-70&quot; data-astro-cid-2cxdht5w&gt;The same two URLs in a terminal, then the html coming back for a browser user-agent.&lt;/figcaption&gt;&lt;/figure&gt;&lt;script type=&quot;module&quot; src=&quot;https://typovrak.tv/vercel/path0/src/components/AsciinemaPlayer.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-does-curl-get-from-this-site&quot;&gt;What does curl get from this site?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-does-the-server-tell-curl-from-a-browser&quot;&gt;How does the server tell curl from a browser?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-two-text-files-per-post&quot;&gt;Why two text files per post?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-does-the-markdown-become-terminal-text&quot;&gt;How does the markdown become terminal text?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-only-sixteen-colours&quot;&gt;Why only sixteen colours?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#who-else-does-this&quot;&gt;Who else does this?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#try-it&quot;&gt;Try it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;button type=&quot;button&quot; class=&quot;details-close&quot; aria-label=&quot;Close this section&quot;&gt;Close&lt;/button&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-does-curl-get-from-this-site&quot;&gt;What does curl get from this site?&lt;/h2&gt;
&lt;p&gt;The post as text with colour, followed by three lines telling you where else it lives:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; curl https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;From issue to merge in the terminal: the git and gh workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;1 Sep, 2026 · 5 min read · CLI, git, GitHub, Open source&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;The issue-to-merge loop is nine commands in git and gh. The catch: the branch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;linked to an issue does not close it, only a Closes keyword in the PR body does.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;────────────────────────────────────────────────────────────────────────────────&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Filing an issue, branching, opening the pull request and merging it all run in&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;git and gh, with no browser tab open. The whole loop is nine commands.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;────────────────────────────────────────────────────────────────────────────────&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Online: https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Plain text: https://typovrak.tv/posts/github-workflow-in-the-terminal.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Read from the top: curl -s https://typovrak.tv/posts/github-workflow-in-the-terminal | less -R&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/curl-post-in-terminal-ansi.avif&quot; alt=&quot;The end of a curled post in a terminal, a bash block above the footer and its Read from the top line&quot;/&gt;&lt;figcaption&gt;The end of a curled post in a terminal, a bash block above the footer and its Read from the top line&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;That footer is there because a terminal drops you at the end of the output, so the last line is the first one you read. It hands you the command that pages the same thing from the top.&lt;/p&gt;
&lt;p&gt;The colour in it is ANSI escape codes. The terminal reads &lt;code&gt;ESC[1;32m&lt;/code&gt; as an instruction, bold green until further notice, and eats the &lt;code&gt;ESC&lt;/code&gt; byte instead of drawing it. Anything that is not a terminal sees only the bytes, which is why &lt;code&gt;less&lt;/code&gt; needs &lt;code&gt;-R&lt;/code&gt; to pass them through: without the flag you read a literal &lt;code&gt;ESC[1;32m&lt;/code&gt; where the title should be.&lt;/p&gt;
&lt;p&gt;The root does the same for the post list:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; curl https://typovrak.tv&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;typovrak&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Notes on Arch Linux, NixOS, CLI tooling and web development, written up from&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;problems I had to solve myself.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Posts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;1 Sep, 2026   From issue to merge in the terminal: the git and gh workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;              5 min read https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;note&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;line x1=&quot;18&quot; y1=&quot;2&quot; x2=&quot;22&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;path d=&quot;M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;11 KB of coloured text against 110 KB of HTML&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;What &lt;code&gt;curl&lt;/code&gt; gets is 11 KB, ANSI colour codes included. The same URL in a browser is 110 KB of HTML, 9.8 times more, and none of that 110 is needed to read the post. Without the colour the plain &lt;code&gt;.txt&lt;/code&gt; drops to 8.5 KB, a thirteenth of the page.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;how-does-the-server-tell-curl-from-a-browser&quot;&gt;How does the server tell curl from a browser?&lt;/h2&gt;
&lt;p&gt;By the &lt;code&gt;User-Agent&lt;/code&gt; header, matched against a closed list. The whole mechanism is one route in Vercel’s &lt;a href=&quot;https://vercel.com/docs/build-output-api/configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Build Output config&lt;/a&gt;, added after the build by &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/blob/main/scripts/terminal-routes.mjs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;scripts/terminal-routes.mjs&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; terminalClient &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;header&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; key&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;user-agent&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;(curl|[Ww]get|HTTPie|xh)/.*&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; routes &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;^/$&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; has&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; terminalClient&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; dest&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;/index.ansi.txt&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;^/posts/([^./]+)$&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; has&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; terminalClient&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; dest&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;/posts/$1.ansi.txt&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;has&lt;/code&gt; is a condition on the request, so the route only fires when the header matches. &lt;code&gt;dest&lt;/code&gt; is a rewrite, so the URL in the terminal stays &lt;code&gt;/posts/&amp;lt;slug&amp;gt;&lt;/code&gt; while the file served is &lt;code&gt;/posts/&amp;lt;slug&amp;gt;.ansi.txt&lt;/code&gt;. The dot in &lt;code&gt;[^./]+&lt;/code&gt; is what keeps &lt;code&gt;/posts/&amp;lt;slug&amp;gt;.txt&lt;/code&gt; out of the rewrite, so the plain files stay reachable by their own URLs.&lt;/p&gt;
&lt;p&gt;Four clients are on the list, &lt;a href=&quot;https://curl.se/docs/manpage.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;curl&lt;/a&gt;, &lt;a href=&quot;https://www.gnu.org/software/wget/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;wget&lt;/a&gt;, &lt;a href=&quot;https://httpie.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;HTTPie&lt;/a&gt; and &lt;a href=&quot;https://github.com/ducaale/xh&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xh&lt;/a&gt;, because their default user-agent is a bare &lt;code&gt;name/version&lt;/code&gt;. A regex in someone else’s matcher is a regex whose semantics you have not read, so I checked it against production:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;User-agent sent&lt;/th&gt;&lt;th&gt;Served&lt;/th&gt;&lt;th&gt;What it shows&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;curl/8.12.1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The normal case&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;curl/1.0 foo&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The trailing &lt;code&gt;.*&lt;/code&gt; covers anything after the version&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;foo curl/1.0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/html&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The match is anchored at the start of the header&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;CURL/8.0&lt;/code&gt;, &lt;code&gt;WGET/1.0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Vercel compares case-insensitively, so &lt;code&gt;[Ww]&lt;/code&gt; is redundant&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;curl&lt;/code&gt; alone, &lt;code&gt;PowerShell/7.4&lt;/code&gt;, empty&lt;/td&gt;&lt;td&gt;&lt;code&gt;text/html&lt;/code&gt;&lt;/td&gt;&lt;td&gt;No slash, or not on the list, so the HTML&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;tip&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Why the user-agent and not the Accept header&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; sends &lt;code&gt;Accept: */*&lt;/code&gt; and so does &lt;code&gt;wget&lt;/code&gt;, so content negotiation has nothing to negotiate on. The user-agent is the only header a command-line client sends that says what it is, and a script that wants the HTML anyway overrides it in one flag, &lt;code&gt;curl -A Mozilla/5.0&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/same-url-in-a-browser-and-in-a-terminal.avif&quot; alt=&quot;The same typovrak.tv URL in a browser and in a terminal, html on the left and text on the right&quot;/&gt;&lt;figcaption&gt;The same typovrak.tv URL in a browser and in a terminal, html on the left and text on the right&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;why-two-text-files-per-post&quot;&gt;Why two text files per post?&lt;/h2&gt;
&lt;p&gt;Because escape codes are the right output for a terminal and the wrong output for a file: &lt;code&gt;curl -o post.md&lt;/code&gt; on the coloured version writes &lt;code&gt;ESC[1;32m&lt;/code&gt; in front of every heading. So each post is built twice, by two &lt;a href=&quot;https://docs.astro.build/en/guides/endpoints/#static-file-endpoints&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;static endpoints&lt;/a&gt; whose &lt;code&gt;.txt&lt;/code&gt; filename is also what makes Vercel serve them as &lt;code&gt;text/plain&lt;/code&gt;:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;URL&lt;/th&gt;&lt;th&gt;Palette&lt;/th&gt;&lt;th&gt;For&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/posts/&amp;lt;slug&amp;gt;.ansi.txt&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ansi&lt;/code&gt;&lt;/td&gt;&lt;td&gt;What the rewrite serves to a terminal&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/posts/&amp;lt;slug&amp;gt;.txt&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;plain&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;curl -o post.md&lt;/code&gt;, a pager without &lt;code&gt;-R&lt;/code&gt;, or an LLM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/index.ansi.txt&lt;/code&gt;, &lt;code&gt;/index.txt&lt;/code&gt;&lt;/td&gt;&lt;td&gt;both&lt;/td&gt;&lt;td&gt;The post list, same split&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;All four are live for the post above: &lt;a href=&quot;https://typovrak.tv/posts/github-workflow-in-the-terminal.ansi.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the coloured one&lt;/a&gt;, &lt;a href=&quot;https://typovrak.tv/posts/github-workflow-in-the-terminal.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the plain one&lt;/a&gt;, and the list as &lt;a href=&quot;https://typovrak.tv/index.ansi.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;index.ansi.txt&lt;/a&gt; and &lt;a href=&quot;https://typovrak.tv/index.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;index.txt&lt;/a&gt;. Open the coloured one in a browser and the codes come back as text, a boxed &lt;code&gt;ESC&lt;/code&gt; and a &lt;code&gt;[1;32m&lt;/code&gt; in front of every heading, which is the whole argument for keeping the plain one.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/ansi-txt-opened-in-a-browser.avif&quot; alt=&quot;The ansi text file open in a browser, each heading preceded by a boxed ESC and a literal [1;32m&quot;/&gt;&lt;figcaption&gt;The ansi text file open in a browser, each heading preceded by a boxed ESC and a literal [1;32m&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;My first plan, written in a TODO before any code, was one file and a &lt;code&gt;?raw&lt;/code&gt; query string to switch the colour off. That cannot work on a static host, because a query string never reaches a file on disk. Two files cost nothing at build time and each one gets a stable URL, so that is what shipped.&lt;/p&gt;
&lt;h2 id=&quot;how-does-the-markdown-become-terminal-text&quot;&gt;How does the markdown become terminal text?&lt;/h2&gt;
&lt;p&gt;By walking the same tree Astro walks. Its &lt;code&gt;render()&lt;/code&gt; hands back HTML and nothing else, and HTML has already dropped the structure a terminal needs, while parsing the markdown a second time with my own parser would work right up to the day a table or a callout parsed differently from the site. So &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/blob/main/src/utils/postTree.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;postTree.ts&lt;/a&gt; builds the processor Astro builds, and slips in a remark plugin whose only job is to hold on to the root:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; tree&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;font-style:italic&quot;&gt; Node &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7;font-style:italic&quot;&gt; undefined&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; capture&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; ()&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#EBA0AC;font-style:italic&quot;&gt;root&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;font-style:italic&quot;&gt; Node&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;  tree &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; stripMdx&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;(root)&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; processor &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; createMarkdownProcessor&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;  gfm&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;  smartypants&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;  syntaxHighlight&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;  remarkPlugins&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; options&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;mdx &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; [remarkMdx&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; capture] &lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; [capture]&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; processor&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;render&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;(body)&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;smartypants: false&lt;/code&gt; is not cosmetic. With it on, a &lt;code&gt;--rebase&lt;/code&gt; in prose becomes an en dash, and a reader pasting that command out of their terminal gets a broken command. &lt;code&gt;remarkMdx&lt;/code&gt; is in the chain for &lt;code&gt;.mdx&lt;/code&gt; files only: the MDX grammar reads &lt;code&gt;{braces}&lt;/code&gt; in prose as a JavaScript expression, and one of my &lt;code&gt;.md&lt;/code&gt; posts carries &lt;code&gt;${pkgs.prisma-engines_6}&lt;/code&gt; in a Nix snippet.&lt;/p&gt;
&lt;p&gt;From there &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/blob/main/src/utils/terminal.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;terminal.ts&lt;/a&gt; walks the tree with no &lt;code&gt;astro:*&lt;/code&gt; import, which is what keeps it under vitest. The parts that took more than one attempt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paragraphs wrap at 80 columns and a word keeps its style across the break. Code blocks never wrap, so a long command scrolls sideways instead of being sawn in half.&lt;/li&gt;
&lt;li&gt;Tables are padded to their widest cell, measured after stripping the escape codes, or a coloured cell throws the column out of line.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;&amp;gt; [!TIP]&lt;/code&gt; callout becomes a labelled quote under a &lt;code&gt;│&lt;/code&gt; bar, and the &lt;code&gt;## Table of contents&lt;/code&gt; heading becomes the list of headings that follow it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two test files, 541 lines for 651 lines of renderer, and I broke each rule in turn to watch its test go red. The rewrite is the one thing they cannot reach, since only Vercel reads that config, so it gets checked on a preview deploy with a browser and &lt;code&gt;curl&lt;/code&gt; on the same URL.&lt;/p&gt;
&lt;h2 id=&quot;why-only-sixteen-colours&quot;&gt;Why only sixteen colours?&lt;/h2&gt;
&lt;p&gt;Because the reader’s own terminal theme should pick the shade. The palette is nine &lt;a href=&quot;https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SGR codes&lt;/a&gt; and nothing from the 256-colour or truecolour ranges:&lt;/p&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Element&lt;/th&gt;&lt;th&gt;Code&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Title, &lt;code&gt;h1&lt;/code&gt;, &lt;code&gt;h2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1;32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;bold, green&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;h3&lt;/code&gt; and deeper, strong&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;bold&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Emphasis&lt;/td&gt;&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;&lt;td&gt;italic&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Inline code&lt;/td&gt;&lt;td&gt;&lt;code&gt;36&lt;/code&gt;&lt;/td&gt;&lt;td&gt;cyan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Link&lt;/td&gt;&lt;td&gt;&lt;code&gt;4;32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;underline, green&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Metadata, URLs, rules&lt;/td&gt;&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;dim&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Callout label&lt;/td&gt;&lt;td&gt;&lt;code&gt;32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;green&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Warning label&lt;/td&gt;&lt;td&gt;&lt;code&gt;33&lt;/code&gt;&lt;/td&gt;&lt;td&gt;yellow&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/same-post-under-two-terminal-themes.avif&quot; alt=&quot;The same index output under Catppuccin Mocha and Latte, the bold green title taking a different shade from each&quot;/&gt;&lt;figcaption&gt;The same index output under Catppuccin Mocha and Latte, the bold green title taking a different shade from each&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Code 32 leaves the shade to the theme, which is Catppuccin Mocha’s on my machine and yours on yours. A truecolour value would reproduce the site’s exact accent and then sit unreadable on somebody’s light background.&lt;/p&gt;
&lt;p&gt;The plain palette maps those same slots to markdown instead, &lt;code&gt;#&lt;/code&gt; for headings and fences for code, and that paid off a day later: &lt;a href=&quot;https://typovrak.tv/llms.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;/llms.txt&lt;/a&gt; and &lt;a href=&quot;https://typovrak.tv/llms-full.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;/llms-full.txt&lt;/a&gt; are the same renderer called with that palette, because its output was already valid markdown.&lt;/p&gt;
&lt;h2 id=&quot;who-else-does-this&quot;&gt;Who else does this?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://wttr.in/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;wttr.in&lt;/a&gt; and &lt;a href=&quot;https://cheat.sh/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cheat.sh&lt;/a&gt;, both by Igor Chubin, are where I got the idea. Checked on 29 September 2026, each answers &lt;code&gt;text/plain&lt;/code&gt; to a &lt;code&gt;curl/8.12.1&lt;/code&gt; user-agent and &lt;code&gt;text/html&lt;/code&gt; to a Firefox one on the same URL, and wttr.in goes further by painting the weather in 256 colours. &lt;a href=&quot;https://rate.sx/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rate.sx&lt;/a&gt;, from the same author, switches too, but it labels the text &lt;code&gt;text/html&lt;/code&gt; in both cases, so its &lt;code&gt;Content-Type&lt;/code&gt; tells you nothing and the body is the only place the switch shows.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/serving-a-blog-to-curl/curl-wttr-in-256-colours.avif&quot; alt=&quot;curl wttr.in in a terminal, ascii clouds and three days of forecast in colour-coded box-drawing tables&quot;/&gt;&lt;figcaption&gt;curl wttr.in in a terminal, ascii clouds and three days of forecast in colour-coded box-drawing tables&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Those are services with a server behind them. A static site gets there with a rewrite rule and two extra files per page.&lt;/p&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://typovrak.tv&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; less&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -R&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; post.md&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://typovrak.tv/posts/github-workflow-in-the-terminal.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -A&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; Mozilla/5.0&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://typovrak.tv/posts/github-workflow-in-the-terminal&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; head&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first two read in the terminal. The third saves the markdown without a single escape code. The fourth is how a script gets the HTML back, since the switch is keyed on nothing but the user-agent.&lt;/p&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;warning&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z&quot;&gt;&lt;/path&gt;&lt;line x1=&quot;12&quot; y1=&quot;9&quot; x2=&quot;12&quot; y2=&quot;13&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;12&quot; y1=&quot;17&quot; x2=&quot;12.01&quot; y2=&quot;17&quot;&gt;&lt;/line&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Every one of them needs the &lt;code&gt;https://&lt;/code&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;Drop it and curl asks for port 80, Vercel answers a 308 to the https URL, and &lt;code&gt;Redirecting...&lt;/code&gt; is the whole of what you read, unless you add &lt;code&gt;-L&lt;/code&gt;. A trailing slash on the path 308s the same way, since the site emits none.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;</content:encoded><category>Astro</category><category>CLI</category><category>Web</category><author>typovrak@gmail.com (typovrak)</author></item><item><title>Running freeCodeCamp on NixOS: the shell.nix Prisma needs</title><link>https://typovrak.tv/posts/freecodecamp-on-nixos-prisma</link><guid isPermaLink="true">https://typovrak.tv/posts/freecodecamp-on-nixos-prisma</guid><description>Prisma ships no engine for NixOS, so pnpm i on freeCodeCamp dies in postinstall. A 16-line shell.nix fixes it, as long as the nixpkgs engine matches the Prisma version. A bot closed the PR.</description><pubDate>Mon, 14 Sep 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Prisma publishes no engine binary for NixOS, so &lt;code&gt;pnpm i&lt;/code&gt; on &lt;a href=&quot;https://github.com/freeCodeCamp/freeCodeCamp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;freeCodeCamp&lt;/a&gt; dies in the &lt;code&gt;api&lt;/code&gt; postinstall step before you have written a single line. The fix is a &lt;code&gt;shell.nix&lt;/code&gt; at the root of the repo that hands Prisma the engines from nixpkgs, then &lt;code&gt;nix-shell&lt;/code&gt; before &lt;code&gt;pnpm i&lt;/code&gt;. Everything after that behaves like any other machine.&lt;/p&gt;
&lt;p&gt;I hit this on 12 July 2026 while setting up a freeCodeCamp checkout, worked it out, and opened &lt;a href=&quot;https://github.com/freeCodeCamp/freeCodeCamp/issues/68755&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;issue #68755&lt;/a&gt; with the fix, plus &lt;a href=&quot;https://github.com/freeCodeCamp/freeCodeCamp/pull/68756&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #68756&lt;/a&gt; to put it in their troubleshooting docs. A bot closed both three hours later. So the fix lives here instead.&lt;/p&gt;
&lt;p&gt;Every command and every version below comes from my own machine: NixOS &lt;a href=&quot;https://nixos.org/blog/announcements/2026/nixos-2605&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;26.05 (Yarara)&lt;/a&gt;, nixpkgs at &lt;a href=&quot;https://github.com/NixOS/nixpkgs/tree/8f0500b9660505dc3cb647775fe9a978a74b5283&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;8f0500b9&lt;/a&gt;, freeCodeCamp on Prisma &lt;a href=&quot;https://github.com/prisma/orm/releases/tag/6.19.3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;6.19.3&lt;/a&gt;.&lt;/p&gt;

&lt;figure class=&quot;not-prose my-6&quot; data-astro-cid-2cxdht5w&gt;&lt;div class=&quot;border-accent w-full overflow-hidden rounded-md border&quot; data-asciinema=&quot;/casts/freecodecamp-prisma-nixos.cast&quot; data-cols=&quot;87&quot; data-rows=&quot;23&quot; data-idle-time-limit=&quot;2&quot; data-markers=&quot;[[0.7,&amp;quot;pnpm i&amp;quot;],[17.4,&amp;quot;Error: 404 Not Found&amp;quot;],[21,&amp;quot;vim shell.nix&amp;quot;],[26.5,&amp;quot;nix-shell&amp;quot;],[30,&amp;quot;pnpm i&amp;quot;],[40.5,&amp;quot;prisma --version&amp;quot;]]&quot; data-astro-cid-2cxdht5w&gt;&lt;button type=&quot;button&quot; class=&quot;focus-outline group relative block w-full cursor-pointer&quot; aria-label=&quot;Play the recording: pnpm i dying on the 404, then the same install passing inside nix-shell.&quot; data-astro-cid-2cxdht5w&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/pnpm-i-failing-on-the-prisma-404.avif&quot; alt=&quot;&quot; width=&quot;954&quot; height=&quot;520&quot; class=&quot;block w-full scale-105 border-0 blur-[3px]&quot; data-astro-cid-2cxdht5w&gt;&lt;span class=&quot;bg-background/40 group-hover:bg-background/25 absolute inset-0 flex items-center justify-center transition-colors&quot; data-astro-cid-2cxdht5w&gt;&lt;svg viewBox=&quot;0 0 1000 1000&quot; aria-hidden=&quot;true&quot; class=&quot;text-accent h-24 w-24 transition-transform group-hover:scale-110&quot; data-astro-cid-2cxdht5w&gt;&lt;defs data-astro-cid-2cxdht5w&gt;&lt;mask id=&quot;asciinema-play-cut&quot; data-astro-cid-2cxdht5w&gt;&lt;rect width=&quot;100%&quot; height=&quot;100%&quot; fill=&quot;white&quot; data-astro-cid-2cxdht5w&gt;&lt;/rect&gt;&lt;polygon points=&quot;700 500, 400 326.7949192431122, 400 673.2050807568877&quot; fill=&quot;black&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;/mask&gt;&lt;/defs&gt;&lt;polygon points=&quot;1000 500, 250 66.98729810778059, 250 933.0127018922192&quot; mask=&quot;url(#asciinema-play-cut)&quot; fill=&quot;currentColor&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;polyline points=&quot;673.2050807568878 400, 326.7949192431123 600&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;90&quot; fill=&quot;none&quot; data-astro-cid-2cxdht5w&gt;&lt;/polyline&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;figcaption class=&quot;mt-2 text-center text-sm opacity-70&quot; data-astro-cid-2cxdht5w&gt;pnpm i dying on the 404, then the same install passing inside nix-shell.&lt;/figcaption&gt;&lt;/figure&gt;&lt;script type=&quot;module&quot; src=&quot;https://typovrak.tv/vercel/path0/src/components/AsciinemaPlayer.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#why-does-pnpm-i-fail-on-freecodecamp-under-nixos&quot;&gt;Why does pnpm i fail on freeCodeCamp under NixOS?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-goes-in-the-shellnix&quot;&gt;What goes in the shell.nix?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-prisma-engines_6-and-not-prisma-engines&quot;&gt;Why prisma-engines_6 and not prisma-engines?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#do-you-still-need-the-shellhook&quot;&gt;Do you still need the shellHook?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-do-you-run-it&quot;&gt;How do you run it?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#which-pnpm-actually-runs&quot;&gt;Which pnpm actually runs?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-has-prisma-not-fixed-this&quot;&gt;Why has Prisma not fixed this?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-happened-to-the-issue&quot;&gt;What happened to the issue?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#where-does-the-fix-live-now&quot;&gt;Where does the fix live now?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;button type=&quot;button&quot; class=&quot;details-close&quot; aria-label=&quot;Close this section&quot;&gt;Close&lt;/button&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-does-pnpm-i-fail-on-freecodecamp-under-nixos&quot;&gt;Why does pnpm i fail on freeCodeCamp under NixOS?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;api/package.json&lt;/code&gt; runs &lt;code&gt;prisma generate&lt;/code&gt; as its postinstall script, and Prisma has no engine build for the &lt;code&gt;linux-nixos&lt;/code&gt; target. Run the CLI outside a Nix shell and it says so in three stages:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; node node_modules/prisma/build/index.js --version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--warning)&quot;&gt;prisma:warn Prisma failed to detect the libssl/openssl version to use, and may not work as expected. Defaulting to &amp;quot;openssl-1.1.x&amp;quot;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Please manually install OpenSSL and try installing Prisma again.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--warning)&quot;&gt;Warning Precompiled engine files are not available for nixos, please provide the paths via environment variables, see https://pris.ly/d/custom-engines&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--danger)&quot;&gt;Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits/c2990dca591cba766e3b7ef5d9e8a84796e47ab7/linux-nixos/schema-engine.gz.sha256 - 404 Not Found&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it backwards and the whole story is there. Prisma could not find a system OpenSSL, because on NixOS libraries do not sit in &lt;code&gt;/usr/lib&lt;/code&gt;, they sit in the store. It then detected the platform as &lt;code&gt;nixos&lt;/code&gt; and admitted it has no precompiled engine for it. Finally it tried to download one anyway, from a URL that has never existed, and got a 404.&lt;/p&gt;
&lt;p&gt;That commit hash, &lt;code&gt;c2990dca591cba766e3b7ef5d9e8a84796e47ab7&lt;/code&gt;, is the engine revision pinned to Prisma 6.19.3. Keep it in mind, it is the reason the next section is fussy about versions.&lt;/p&gt;
&lt;p&gt;Nothing here is a freeCodeCamp bug. Any project with Prisma in its dependency tree fails the same way on NixOS.&lt;/p&gt;
&lt;h2 id=&quot;what-goes-in-the-shellnix&quot;&gt;What goes in the shell.nix?&lt;/h2&gt;
&lt;p&gt;The file declares four packages and exports four environment variables. Drop this at the root of the repo as &lt;code&gt;shell.nix&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;nix&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#EBA0AC;font-style:italic&quot;&gt; pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; import&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;lt;nixpkgs&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; {}&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; }:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;mkShell &lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA&quot;&gt;  buildInputs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;    pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;nodejs_24&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;    pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;pnpm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;    pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;openssl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;    pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;prisma-engines_6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;  ];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA&quot;&gt;  shellHook&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;    export PRISMA_QUERY_ENGINE_LIBRARY=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/lib/libquery_engine.node&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;    export PRISMA_QUERY_ENGINE_BINARY=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/bin/query-engine&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;    export PRISMA_SCHEMA_ENGINE_BINARY=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/bin/schema-engine&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;    export PRISMA_FMT_BINARY=&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;pkgs&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B4BEFE&quot;&gt;prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/bin/prisma-fmt&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;  &amp;#39;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nodejs_24&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt; come from what freeCodeCamp asks for in its root &lt;code&gt;package.json&lt;/code&gt; (&lt;code&gt;&amp;quot;node&amp;quot;: &amp;quot;&amp;gt;=24&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;pnpm&amp;quot;: &amp;quot;&amp;gt;=10&amp;quot;&lt;/code&gt;). &lt;code&gt;openssl&lt;/code&gt; silences the libssl warning. &lt;code&gt;prisma-engines_6&lt;/code&gt; is the Rust side of Prisma, built by nixpkgs from source, and it is the exact thing the CLI was trying to download.&lt;/p&gt;
&lt;p&gt;Each variable points at a real file in the store, and you can go and look at them:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; ls &lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;nix-build&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; &amp;#39;&amp;lt;nixpkgs&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; -A&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; --no-out-link&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;/{&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;bin,lib}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;bin:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;prisma-fmt  query-engine  schema-engine&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;lib:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;libquery_engine.node&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prisma documents all four in its &lt;a href=&quot;https://www.prisma.io/docs/orm/v6/reference/environment-variables-reference#prisma_query_engine_library&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;environment variables reference&lt;/a&gt;:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Variable&lt;/th&gt;&lt;th&gt;What it points at&lt;/th&gt;&lt;th&gt;Used for&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PRISMA_QUERY_ENGINE_LIBRARY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;lib/libquery_engine.node&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The Node-API engine &lt;code&gt;@prisma/client&lt;/code&gt; loads at runtime to talk to the database&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PRISMA_QUERY_ENGINE_BINARY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;bin/query-engine&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The standalone query engine, used by the binary engine type and by Studio&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PRISMA_SCHEMA_ENGINE_BINARY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;bin/schema-engine&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Migrations, &lt;code&gt;db push&lt;/code&gt;, introspection&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PRISMA_FMT_BINARY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;bin/prisma-fmt&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Formatting and validating &lt;code&gt;schema.prisma&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;tip&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Nix evaluates &lt;code&gt;${}&lt;/code&gt; inside a &lt;code&gt;&amp;#39;&amp;#39;&lt;/code&gt; string&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;In the &lt;code&gt;shellHook&lt;/code&gt; above, &lt;code&gt;${pkgs.prisma-engines_6}&lt;/code&gt; is replaced by Nix with a store path before bash ever sees the line. For a shell variable in there, escape it as &lt;code&gt;&amp;#39;&amp;#39;${HOME}&lt;/code&gt;. Forgetting this is the classic first Nix bug, and the error message does not help.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;why-prisma-engines_6-and-not-prisma-engines&quot;&gt;Why prisma-engines_6 and not prisma-engines?&lt;/h2&gt;
&lt;p&gt;The engine version has to match the Prisma version in the project, and the unsuffixed attribute is a different major. As of nixpkgs 26.05:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Attribute&lt;/th&gt;&lt;th&gt;Version&lt;/th&gt;&lt;th&gt;Built from&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;prisma-engines&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7.8.0&lt;/td&gt;&lt;td&gt;&lt;code&gt;refs/tags/7.8.0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;prisma-engines_6&lt;/code&gt;&lt;/td&gt;&lt;td&gt;6.19.3&lt;/td&gt;&lt;td&gt;&lt;code&gt;refs/tags/6.19.3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;prisma-engines_7&lt;/code&gt;&lt;/td&gt;&lt;td&gt;7.8.0&lt;/td&gt;&lt;td&gt;&lt;code&gt;refs/tags/7.8.0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;freeCodeCamp pins &lt;code&gt;prisma&lt;/code&gt; and &lt;code&gt;@prisma/client&lt;/code&gt; to 6.19.3 in &lt;code&gt;api/package.json&lt;/code&gt;, so &lt;code&gt;prisma-engines_6&lt;/code&gt; matches it to the patch. Reach for &lt;code&gt;prisma-engines&lt;/code&gt; instead and you get 7.8.0, whose engine hash is not the &lt;code&gt;c2990dca...&lt;/code&gt; the CLI wants. I tried it. Prisma ignores the mismatched engine and goes back to downloading:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--warning)&quot;&gt;Warning Precompiled engine files are not available for nixos, please provide the paths via environment variables, see https://pris.ly/d/custom-engines&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--danger)&quot;&gt;Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits/c2990dca591cba766e3b7ef5d9e8a84796e47ab7/linux-nixos/libquery_engine.so.node.sha256 - 404 Not Found&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is the same 404 with the same hash. So the procedure is to read the &lt;code&gt;prisma&lt;/code&gt; version in &lt;code&gt;package.json&lt;/code&gt; and pick the nixpkgs attribute with the same major, then check on &lt;a href=&quot;https://search.nixos.org/packages?channel=unstable&amp;amp;query=prisma-engines&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;search.nixos.org&lt;/a&gt; that the minor lines up too. When no attribute lines up, that project needs a nixpkgs pin.&lt;/p&gt;
&lt;h2 id=&quot;do-you-still-need-the-shellhook&quot;&gt;Do you still need the shellHook?&lt;/h2&gt;
&lt;p&gt;On nixpkgs 26.05 the &lt;code&gt;shellHook&lt;/code&gt; is redundant. &lt;code&gt;prisma-engines_6&lt;/code&gt; ships a setup hook that exports all four variables, and &lt;code&gt;nix-shell&lt;/code&gt; sources it:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; cat &lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;nix-build&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; &amp;#39;&amp;lt;nixpkgs&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; -A&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; prisma-engines_6&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; --no-out-link&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt;/nix-support/setup-hook&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;export PRISMA_SCHEMA_ENGINE_BINARY=&amp;quot;.../bin/schema-engine&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;export PRISMA_QUERY_ENGINE_BINARY=&amp;quot;.../bin/query-engine&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;export PRISMA_QUERY_ENGINE_LIBRARY=&amp;quot;.../lib/libquery_engine.node&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;export PRISMA_FMT_BINARY=&amp;quot;.../bin/prisma-fmt&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;shell.nix&lt;/code&gt; with nothing but &lt;code&gt;buildInputs = [ pkgs.prisma-engines_6 ];&lt;/code&gt; gives you a shell with all four already set. The hook landed on 15 June 2025 (&lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/416928&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#416928&lt;/a&gt;) and reached the 25.05 release branch the same day, so every channel newer than that carries it. A nixpkgs pinned to an older revision does not. I only checked because I had been copying that &lt;code&gt;shellHook&lt;/code&gt; between projects for months, assuming it was required.&lt;/p&gt;
&lt;p&gt;I still ship it. Four lines is a cheap price for a file that reads the same to someone who has never met a Nix setup hook, and it keeps working against a pin from before the hook existed.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-run-it&quot;&gt;How do you run it?&lt;/h2&gt;
&lt;p&gt;Enter the shell first, install second. The order matters, because the postinstall script is what needs the engines:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;nix-shell&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;pnpm&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; i&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then ask Prisma, from the &lt;code&gt;api&lt;/code&gt; directory, whether it agrees:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; node node_modules/prisma/build/index.js --version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;prisma                  : 6.19.3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;@prisma/client          : 6.19.3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Computed binaryTarget   : linux-nixos&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Node.js                 : v24.16.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Query Engine (Node-API) : libquery-engine 0000000000000000000000000000000000000000 (at ../../../../../nix/store/...-prisma-engines_6-6.19.3/lib/libquery_engine.node, resolved by PRISMA_QUERY_ENGINE_LIBRARY)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Schema Engine           : schema-engine-cli 0000000000000000000000000000000000000000 (at ../../../../../nix/store/...-prisma-engines_6-6.19.3/bin/schema-engine, resolved by PRISMA_SCHEMA_ENGINE_BINARY)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;Default Engines Hash    : c2990dca591cba766e3b7ef5d9e8a84796e47ab7&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;resolved by PRISMA_QUERY_ENGINE_LIBRARY&lt;/code&gt; is the line that says it worked. The all-zero hashes are normal: nixpkgs builds from the release tag without stamping a git revision into the binary, and Prisma skips its hash check when you hand it an explicit path. The five &lt;code&gt;../&lt;/code&gt; in front of the store path are Prisma printing it relative to &lt;code&gt;api/&lt;/code&gt;, which looks odd and changes nothing.&lt;/p&gt;
&lt;p&gt;From here the rest of the &lt;a href=&quot;https://contribute.freecodecamp.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;contributing guide&lt;/a&gt; applies unchanged. Seed the database and run &lt;code&gt;pnpm develop&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/freecodecamp-pnpm-develop-running-on-nixos.avif&quot; alt=&quot;The freeCodeCamp client served from localhost:8000 after pnpm develop, with the banner that says the API has no user session yet&quot;/&gt;&lt;figcaption&gt;The freeCodeCamp client served from localhost:8000 after pnpm develop, with the banner that says the API has no user session yet&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;warning&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z&quot;&gt;&lt;/path&gt;&lt;line x1=&quot;12&quot; y1=&quot;9&quot; x2=&quot;12&quot; y2=&quot;13&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;12&quot; y1=&quot;17&quot; x2=&quot;12.01&quot; y2=&quot;17&quot;&gt;&lt;/line&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Add shell.nix to your global gitignore&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;&lt;code&gt;shell.nix&lt;/code&gt; is your environment, and freeCodeCamp’s &lt;code&gt;.gitignore&lt;/code&gt; has no reason to carry it. Put it in &lt;code&gt;~/.config/git/ignore&lt;/code&gt; so it never shows up in &lt;code&gt;git status&lt;/code&gt; on any repo you do this to.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;which-pnpm-actually-runs&quot;&gt;Which pnpm actually runs?&lt;/h2&gt;
&lt;p&gt;pnpm 10.33.3, the version freeCodeCamp pins, even though Nix installed 11.9.0. The root &lt;code&gt;package.json&lt;/code&gt; sets &lt;code&gt;&amp;quot;packageManager&amp;quot;: &amp;quot;pnpm@10.33.3&amp;quot;&lt;/code&gt;, and pnpm 10 and up honour that field by fetching the pinned version and handing over to it:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; which pnpm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;/nix/store/gfrj5gwr59daw5qi72phra770mi0xvh1-pnpm-11.9.0/bin/pnpm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; pnpm --version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--warning)&quot;&gt;[WARN] The &amp;quot;pnpm&amp;quot; field in package.json is no longer read by pnpm. The following keys were ignored: &amp;quot;pnpm.onlyBuiltDependencies&amp;quot;. See https://pnpm.io/settings for the new home of each setting.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;10.33.3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The warning comes from pnpm 11, printed before it hands over, and the version number from pnpm 10.&lt;/p&gt;
&lt;p&gt;Knowing this saves you an evening of wondering why your carefully declared pnpm is not the one producing the lockfile. It also means one part of the install reaches outside the store, which dents the purity a NixOS user came for. pnpm 11 &lt;a href=&quot;https://github.com/pnpm/pnpm/releases/tag/v11.0.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;removed the setting&lt;/a&gt; that used to switch the handover off. What remains is &lt;code&gt;pnpm with current&lt;/code&gt;, which runs the Nix pnpm for a single command, at the cost of running a different version from every other contributor:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;console&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--accent)&quot;&gt; pnpm with current --version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;11.9.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/pnpm-11-from-nix-store-handing-over-to-pnpm-10.avif&quot; alt=&quot;which pnpm, pnpm --version and pnpm with current in one shell, the handover from 11.9.0 down to 10.33.3 and back&quot;/&gt;&lt;figcaption&gt;which pnpm, pnpm --version and pnpm with current in one shell, the handover from 11.9.0 down to 10.33.3 and back&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;why-has-prisma-not-fixed-this&quot;&gt;Why has Prisma not fixed this?&lt;/h2&gt;
&lt;p&gt;The gap has been open for years and there is no sign of it closing. Three threads land on the same workaround:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prisma/orm/discussions/3120&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Running prisma on NixOS&lt;/a&gt;, the original discussion on Prisma’s repository.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://discourse.nixos.org/t/unable-to-use-prisma-on-nixos/54082&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Unable to use Prisma on NixOS&lt;/a&gt; on the NixOS Discourse, which is where most people find the four variables.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prisma/orm/issues/29150&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;prisma/orm#29150&lt;/a&gt;, opened 6 February 2026 and still open: Prisma 7 still tries to download Rust binaries on NixOS, and still needs a &lt;code&gt;shell.nix&lt;/code&gt; to point at the engines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prisma has since renamed its repository from &lt;code&gt;prisma/prisma&lt;/code&gt; to &lt;code&gt;prisma/orm&lt;/code&gt;, and the old issue URLs return a 404 rather than a redirect, so the links above are the new ones.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/github-prisma-orm-issue-29150-still-open.avif&quot; alt=&quot;Issue #29150 on prisma/orm, open and still labelled unconfirmed, reporting the same libssl warning on Prisma 7.3.0&quot;/&gt;&lt;figcaption&gt;Issue #29150 on prisma/orm, open and still labelled unconfirmed, reporting the same libssl warning on Prisma 7.3.0&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The underlying reason is that Prisma distributes prebuilt engines per platform, and a “platform” for them means a libc plus an OpenSSL version at a known filesystem path. NixOS has neither at a predictable path, so it does not fit the matrix. Pointing at store paths is the only interface that design leaves open.&lt;/p&gt;
&lt;p&gt;The upside is that nixpkgs already builds those engines from source and keeps them versioned, so the work is done. It only has to be wired up per project.&lt;/p&gt;
&lt;h2 id=&quot;what-happened-to-the-issue&quot;&gt;What happened to the issue?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;camper-chan&lt;/code&gt;, a bot, closed it as not planned at 18:37 UTC on 12 July 2026, three hours after I opened it. The message is a form letter: the issue is not on the roadmap, request a reopen if you disagree. Eleven seconds later the same bot closed the PR with the matching letter, saying it had been reviewed and would not be merged. Twelve seconds after that, a GitHub Action locked the PR as resolved and limited the conversation to collaborators, so the close and the lock together took twenty-three seconds.&lt;/p&gt;
&lt;p&gt;Another Action had already flagged the PR minutes after it was opened, because its linked issue was not labelled as open for contribution. The linked issue was the one I had filed six minutes earlier.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/github-freecodecamp-issue-68755-closed-as-not-planned-by-camper-chan.avif&quot; alt=&quot;Issue #68755 closed as not planned, the form letter sitting right under the pnpm i that had finished inside nix-shell&quot;/&gt;&lt;figcaption&gt;Issue #68755 closed as not planned, the form letter sitting right under the pnpm i that had finished inside nix-shell&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/github-freecodecamp-pr-68756-closed-by-camper-chan.avif&quot; alt=&quot;Pull request #68756, one commit and sixteen added lines, closed unmerged and then locked to collaborators&quot;/&gt;&lt;figcaption&gt;Pull request #68756, one commit and sixteen added lines, closed unmerged and then locked to collaborators&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The issue itself was never locked, so that is where I disagreed, in writing, on 16 July 2026. I laid out the three upstream threads and explained that NixOS is declarative, with a dev shell as its way of declaring development dependencies. The proposed &lt;code&gt;shell.nix&lt;/code&gt;, I pointed out, only declares binaries the project already needs. Then I asked a direct question: does freeCodeCamp want a frictionless setup for NixOS contributors, or is NixOS too niche to support, with contributors on their own?&lt;/p&gt;
&lt;p&gt;Either answer would have been fine. A project that size has to draw a line somewhere, and “we do not support NixOS” is a legitimate place to draw it, as long as somebody says it.&lt;/p&gt;
&lt;p&gt;Nobody did. On 17 July I followed up, pinging a maintainer by name, to ask whether a human had seen the thread at all, since I could not reopen it myself to make it visible. That went unanswered too. As I write this in September 2026, the issue and the PR are both closed and nothing has moved since 12 July.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/freecodecamp-on-nixos-prisma/github-freecodecamp-issue-68755-reopen-request-unanswered.avif&quot; alt=&quot;The reopen request of 16 July and the follow-up of 17 July, the last two comments the thread ever got&quot;/&gt;&lt;figcaption&gt;The reopen request of 16 July and the follow-up of 17 July, the last two comments the thread ever got&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;freeCodeCamp is a charity with maintainers stretched thin, and triage bots exist because the alternative is a backlog nobody can read. Throughput explains the outcome better than malice does. It still leaves a gap: the bot can close but cannot judge, and a first patch that disappears into that gap teaches a contributor something discouraging about how contributing goes.&lt;/p&gt;
&lt;p&gt;The timing is the part that stings. The PR with the tested fix was open six minutes after the issue, and sat there for three hours before the bot closed both.&lt;/p&gt;
&lt;h2 id=&quot;where-does-the-fix-live-now&quot;&gt;Where does the fix live now?&lt;/h2&gt;
&lt;p&gt;The fix lives in this post, indexed and linkable, for the next person whose &lt;code&gt;pnpm i&lt;/code&gt; explodes on a 404 for a file that was never built. A closed issue does not delete what you worked out to write it, and a page a search engine can reach does the job the troubleshooting section would have done.&lt;/p&gt;
&lt;p&gt;If you are on NixOS and something refuses to install, the answer usually has this shape. Work out what the installer wanted to download, then find the nixpkgs attribute that already builds it and point the tool at the store path. Write it down somewhere public, because the next person is going to hit the same 404.&lt;/p&gt;
&lt;p&gt;The diff is 16 lines, and it is still sitting in &lt;a href=&quot;https://github.com/freeCodeCamp/freeCodeCamp/pull/68756&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the closed PR&lt;/a&gt; if freeCodeCamp ever wants it.&lt;/p&gt;</content:encoded><category>NixOS</category><category>Prisma</category><category>Open source</category><author>typovrak@gmail.com (typovrak)</author></item><item><title>From issue to merge in the terminal: the git and gh workflow</title><link>https://typovrak.tv/posts/github-workflow-in-the-terminal</link><guid isPermaLink="true">https://typovrak.tv/posts/github-workflow-in-the-terminal</guid><description>The issue-to-merge loop is nine commands in git and gh. The catch: the branch linked to an issue does not close it, only a Closes keyword in the PR body does.</description><pubDate>Mon, 31 Aug 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Filing an issue, branching, opening the pull request and merging it all run in &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;gh&lt;/code&gt;, with no browser tab open. The whole loop is nine commands.&lt;/p&gt;
&lt;p&gt;I kept this as a &lt;a href=&quot;https://gist.github.com/typovrak/8034fe822905c3febbe4c3c9a84f065d&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;public gist&lt;/a&gt; for two years and run it on every repo I touch. This post is that gist, with the flags that skip the prompts and the one line that makes the merge close the issue for you.&lt;/p&gt;
&lt;p&gt;Everything below is &lt;code&gt;gh&lt;/code&gt; &lt;a href=&quot;https://github.com/cli/cli/releases/tag/v2.96.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;2.96.0&lt;/a&gt; and &lt;code&gt;git&lt;/code&gt; &lt;a href=&quot;https://github.com/git/git/releases/tag/v2.54.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;2.54.0&lt;/a&gt;, run from inside a repository you can push to.&lt;/p&gt;

&lt;figure class=&quot;not-prose my-6&quot; data-astro-cid-2cxdht5w&gt;&lt;div class=&quot;border-accent w-full overflow-hidden rounded-md border&quot; data-asciinema=&quot;/casts/gh-issue-pr.cast&quot; data-cols=&quot;87&quot; data-rows=&quot;23&quot; data-idle-time-limit=&quot;2&quot; data-markers=&quot;[[0.2,&amp;quot;gh issue create&amp;quot;],[46,&amp;quot;gh issue develop 32 --checkout&amp;quot;],[58.9,&amp;quot;vim README.md&amp;quot;],[80.1,&amp;quot;git add README.md&amp;quot;],[84.1,&amp;quot;git commit&amp;quot;],[98.9,&amp;quot;git push&amp;quot;],[102.7,&amp;quot;gh pr create --fill --body &apos;Closes #32&apos;&amp;quot;],[116.2,&amp;quot;gh pr merge --rebase --delete-branch&amp;quot;],[132.3,&amp;quot;git switch main &amp;amp;&amp;amp; git pull --prune&amp;quot;],[147,&amp;quot;vim README.md&amp;quot;],[171.4,&amp;quot;gh issue list&amp;quot;],[178.3,&amp;quot;lazygit&amp;quot;]]&quot; data-astro-cid-2cxdht5w&gt;&lt;button type=&quot;button&quot; class=&quot;focus-outline group relative block w-full cursor-pointer&quot; aria-label=&quot;Play the recording: The whole loop in one terminal, from gh issue create to a synced main.&quot; data-astro-cid-2cxdht5w&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/github-workflow-in-the-terminal/lazygit-rebase-with-cli-result-on-typovrak.tv-repository.avif&quot; alt=&quot;&quot; width=&quot;1070&quot; height=&quot;658&quot; class=&quot;block w-full scale-105 border-0 blur-[3px]&quot; data-astro-cid-2cxdht5w&gt;&lt;span class=&quot;bg-background/40 group-hover:bg-background/25 absolute inset-0 flex items-center justify-center transition-colors&quot; data-astro-cid-2cxdht5w&gt;&lt;svg viewBox=&quot;0 0 1000 1000&quot; aria-hidden=&quot;true&quot; class=&quot;text-accent h-24 w-24 transition-transform group-hover:scale-110&quot; data-astro-cid-2cxdht5w&gt;&lt;defs data-astro-cid-2cxdht5w&gt;&lt;mask id=&quot;asciinema-play-cut&quot; data-astro-cid-2cxdht5w&gt;&lt;rect width=&quot;100%&quot; height=&quot;100%&quot; fill=&quot;white&quot; data-astro-cid-2cxdht5w&gt;&lt;/rect&gt;&lt;polygon points=&quot;700 500, 400 326.7949192431122, 400 673.2050807568877&quot; fill=&quot;black&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;/mask&gt;&lt;/defs&gt;&lt;polygon points=&quot;1000 500, 250 66.98729810778059, 250 933.0127018922192&quot; mask=&quot;url(#asciinema-play-cut)&quot; fill=&quot;currentColor&quot; data-astro-cid-2cxdht5w&gt;&lt;/polygon&gt;&lt;polyline points=&quot;673.2050807568878 400, 326.7949192431123 600&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;90&quot; fill=&quot;none&quot; data-astro-cid-2cxdht5w&gt;&lt;/polyline&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;figcaption class=&quot;mt-2 text-center text-sm opacity-70&quot; data-astro-cid-2cxdht5w&gt;The whole loop in one terminal, from gh issue create to a synced main.&lt;/figcaption&gt;&lt;/figure&gt;&lt;script type=&quot;module&quot; src=&quot;https://typovrak.tv/vercel/path0/src/components/AsciinemaPlayer.astro?astro&amp;type=script&amp;index=0&amp;lang.ts&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#the-whole-loop&quot;&gt;The whole loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-actually-closes-the-issue&quot;&gt;What actually closes the issue?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#which-merge-strategy&quot;&gt;Which merge strategy?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#does-this-work-on-a-repo-you-cannot-push-to&quot;&gt;Does this work on a repo you cannot push to?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;button type=&quot;button&quot; class=&quot;details-close&quot; aria-label=&quot;Close this section&quot;&gt;Close&lt;/button&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-whole-loop&quot;&gt;The whole loop&lt;/h2&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; issue&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;                         # file it, note the number (say 32)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; issue&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; develop&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; 32&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --checkout&lt;/span&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;          # branch linked to #32, checked out locally&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# write the code, then:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; .&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;#39;feat(translation): this is an example&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; push&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pr&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --fill&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --body&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;Closes #32&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt; # open the PR, keyword set&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pr&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; merge&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --rebase&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --delete-branch&lt;/span&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;    # rebase onto main, close #32, drop the branch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; switch&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pull&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --prune&lt;/span&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;     # bring the merge home, tidy stale refs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;gh auth login&lt;/code&gt; once and every command after that runs as you. &lt;code&gt;gh&lt;/code&gt; works out which repository you mean from the git remote of the current directory, so all of this happens from inside the checkout.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gh issue create&lt;/code&gt; prints the URL of the issue it filed, and the number on the end of that URL is the &lt;code&gt;32&lt;/code&gt; every later command refers to. To pick up an issue that already exists instead, list them and read the number off the left column:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; issue&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here it is on &lt;a href=&quot;https://github.com/olivierlambert/calrs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;olivierlambert/calrs&lt;/a&gt;, a project with rather more open issues than this site has. The &lt;code&gt;#208&lt;/code&gt; on the first row is the number &lt;code&gt;gh issue develop&lt;/code&gt; wants:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/github-workflow-in-the-terminal/calrs-repository-gh-issue-list-cli.avif&quot; alt=&quot;gh issue list on the calrs repository, its 23 open issues in a table of number, title, labels and age&quot;/&gt;&lt;figcaption&gt;gh issue list on the calrs repository, its 23 open issues in a table of number, title, labels and age&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Three flags do the work of the prompts. &lt;a href=&quot;https://cli.github.com/manual/gh_issue_develop&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;--checkout&lt;/code&gt;&lt;/a&gt; creates the linked branch and switches to it in one go, so the branch name never has to be typed. &lt;a href=&quot;https://cli.github.com/manual/gh_pr_create&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;--fill&lt;/code&gt;&lt;/a&gt; takes the PR title and body from your commits. &lt;a href=&quot;https://cli.github.com/manual/gh_pr_merge&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;--rebase --delete-branch&lt;/code&gt;&lt;/a&gt; skips the merge menu and removes the branch from both your machine and the remote.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;git push&lt;/code&gt; in that block needs no &lt;code&gt;-u&lt;/code&gt;. gh created the branch on the remote before checking it out, so its upstream is already set. A branch you make yourself with &lt;code&gt;git switch -c&lt;/code&gt; has none, which is why the fork section below pushes with &lt;code&gt;-u origin &amp;lt;branch&amp;gt;&lt;/code&gt;. One setting covers both cases:&lt;/p&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;tip&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Stop typing &lt;code&gt;-u&lt;/code&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --global&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; push.autoSetupRemote&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Available since git 2.37, it makes the first push on a new branch a plain &lt;code&gt;git push&lt;/code&gt;. It is why &lt;code&gt;-u&lt;/code&gt; never shows up in my own shell history.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;what-actually-closes-the-issue&quot;&gt;What actually closes the issue?&lt;/h2&gt;
&lt;p&gt;Only a closing keyword in the PR body, merged into the default branch, closes an issue.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gh issue develop&lt;/code&gt; links the branch to the issue, and the link shows up in the issue’s Development section. That is enough to make you assume the merge closes it. It does not. GitHub documents auto-close for a &lt;a href=&quot;https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;closing keyword&lt;/a&gt; landing on the repository’s default branch, and for nothing else.&lt;/p&gt;
&lt;p&gt;So the keyword goes in the PR body:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pr&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --fill&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --body&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;Closes #32&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two flags are not redundant. &lt;code&gt;--fill&lt;/code&gt; takes the title and the body from your commits, then &lt;code&gt;--body&lt;/code&gt; overwrites the body it just filled in and leaves the title alone, &lt;a href=&quot;https://cli.github.com/manual/gh_pr_create&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;which gh documents&lt;/a&gt;. So the PR gets a title from your commits, exactly &lt;code&gt;Closes #32&lt;/code&gt; as its body, and no prompt. Drop &lt;code&gt;--fill&lt;/code&gt; and gh has no title, so it stops to ask for one.&lt;/p&gt;
&lt;p&gt;Which title depends on how many commits the branch carries. With a single commit, &lt;code&gt;--fill&lt;/code&gt; takes that commit’s subject. With several, it takes the branch name and turns the dashes into spaces, so a branch named by &lt;code&gt;gh issue develop&lt;/code&gt; gives &lt;code&gt;32 adding french translation&lt;/code&gt;, which is the title &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/pull/33&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#33&lt;/a&gt; below ended up with. Pass &lt;code&gt;--fill-first&lt;/code&gt; to use the first commit’s subject either way.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;close&lt;/code&gt;, &lt;code&gt;closes&lt;/code&gt;, &lt;code&gt;closed&lt;/code&gt;, &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;fixes&lt;/code&gt;, &lt;code&gt;fixed&lt;/code&gt;, &lt;code&gt;resolve&lt;/code&gt;, &lt;code&gt;resolves&lt;/code&gt; and &lt;code&gt;resolved&lt;/code&gt; all work, each followed by &lt;code&gt;#&lt;/code&gt; and the number, anywhere in the body. To keep the commit text in the body as well, drop &lt;code&gt;--body&lt;/code&gt; and write the keyword into the commit message instead.&lt;/p&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;warning&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z&quot;&gt;&lt;/path&gt;&lt;line x1=&quot;12&quot; y1=&quot;9&quot; x2=&quot;12&quot; y2=&quot;13&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;12&quot; y1=&quot;17&quot; x2=&quot;12.01&quot; y2=&quot;17&quot;&gt;&lt;/line&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;The keyword only fires on the default branch&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;&lt;code&gt;Closes #32&lt;/code&gt; in a PR that targets a release branch or a &lt;code&gt;develop&lt;/code&gt; branch links the issue and leaves it open. GitHub documents auto-close for the repository’s default branch and nothing else, so a team that merges through a staging branch closes its issues by hand no matter what the PR body says.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Here is &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/issues/32&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;issue #32&lt;/a&gt; on this repository, closed by the merge with nobody touching the issue itself:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/github-workflow-in-the-terminal/github-typovrak.tv-issue-32-closed-with-cli.avif&quot; alt=&quot;Issue #32 on typovrak.tv, closed automatically by the pull request that merged&quot;/&gt;&lt;figcaption&gt;Issue #32 on typovrak.tv, closed automatically by the pull request that merged&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;which-merge-strategy&quot;&gt;Which merge strategy?&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;gh pr merge&lt;/code&gt; with no strategy flag and it asks. The three differ only in what lands on the default branch:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Strategy&lt;/th&gt;&lt;th&gt;Flag&lt;/th&gt;&lt;th&gt;What lands on &lt;code&gt;main&lt;/code&gt;&lt;/th&gt;&lt;th&gt;Resulting history&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Rebase&lt;/td&gt;&lt;td&gt;&lt;code&gt;--rebase&lt;/code&gt;&lt;/td&gt;&lt;td&gt;your commits, replayed onto &lt;code&gt;main&lt;/code&gt;&lt;/td&gt;&lt;td&gt;linear, every commit kept&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Squash&lt;/td&gt;&lt;td&gt;&lt;code&gt;--squash&lt;/code&gt;&lt;/td&gt;&lt;td&gt;one new commit combining the branch&lt;/td&gt;&lt;td&gt;linear, one commit per PR&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Merge commit&lt;/td&gt;&lt;td&gt;&lt;code&gt;--merge&lt;/code&gt;&lt;/td&gt;&lt;td&gt;your commits plus a merge commit&lt;/td&gt;&lt;td&gt;keeps the PR as a visible bubble&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;tip&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Default to rebase&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;&lt;code&gt;--rebase&lt;/code&gt; is the one to reach for. Your commits land on &lt;code&gt;main&lt;/code&gt; one by one, so every message you wrote stays in the tree and &lt;code&gt;git log&lt;/code&gt; reads as a straight line. Squash collapses all of that into a single commit, which is only worth it when the branch is a pile of &lt;code&gt;wip&lt;/code&gt; and &lt;code&gt;typo&lt;/code&gt; commits you would rather not keep. Take the merge commit when you specifically want the PR boundary to stay visible.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/typovrak/typovrak.tv/pull/33&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pull request #33&lt;/a&gt; is what that looks like once landed:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/github-workflow-in-the-terminal/github-typovrak.tv-pr-33-merged-with-cli.avif&quot; alt=&quot;Pull request #33 on typovrak.tv, shown as merged after a rebase onto main&quot;/&gt;&lt;figcaption&gt;Pull request #33 on typovrak.tv, shown as merged after a rebase onto main&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;And &lt;a href=&quot;https://github.com/typovrak/typovrak.tv/commits/main/?since=2026-08-01&amp;amp;until=2026-09-01&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the commit history for that month&lt;/a&gt; is a straight line, every commit from the branch still there under its own message:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/github-workflow-in-the-terminal/github-typovrak.tv-repository-after-rebase.avif&quot; alt=&quot;The typovrak.tv commit history on main after the rebase, linear with no merge commit&quot;/&gt;&lt;figcaption&gt;The typovrak.tv commit history on main after the rebase, linear with no merge commit&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;does-this-work-on-a-repo-you-cannot-push-to&quot;&gt;Does this work on a repo you cannot push to?&lt;/h2&gt;
&lt;p&gt;Yes, with two changes. You cannot create a linked branch on a repo you lack push access to, so fork it, branch the ordinary way, and leave the merge to the maintainer:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4;overflow-x:auto;--file-name-offset:-0.75rem&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; repo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fork&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; owner/repo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --clone&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; repo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; switch&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fix-stale-search&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# work, commit, then push to your fork:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -u&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fix-stale-search&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pr&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --fill&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --body&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &amp;quot;Closes #123&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gh notices the clone is a fork and defaults the PR base to the upstream default branch, with your fork’s branch as the head. &lt;code&gt;Closes #123&lt;/code&gt; still closes their issue when they merge it.&lt;/p&gt;</content:encoded><category>CLI</category><category>git</category><category>GitHub</category><category>Open source</category><author>typovrak@gmail.com (typovrak)</author></item><item><title>My Arch Linux setup (2024, before I switched to NixOS)</title><link>https://typovrak.tv/posts/arch-linux-2024-setup</link><guid isPermaLink="true">https://typovrak.tv/posts/arch-linux-2024-setup</guid><description>The exact setup I built on a fresh 2024 Arch install: GNOME, yay, my package list, zsh with zinit and Powerlevel10k, Docker, SSH and Git. Since rebuilt as a declarative NixOS config.</description><pubDate>Sat, 15 Aug 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the exact setup I built on a fresh Arch install in 2024: GNOME, yay, my package list, a zsh config with zinit and Powerlevel10k, then Docker, SSH and Git. I have since rebuilt the whole thing as a declarative &lt;a href=&quot;https://typovrak.tv/nixos&quot;&gt;NixOS config&lt;/a&gt;, so read this as a time capsule, proof of how far the setup has come. Every command below is one I actually ran.&lt;/p&gt;
&lt;p&gt;I ran it on two machines: a ROG Strix G731GU, and a 500-euro 15-inch Asus that shipped with 8 GB of RAM. Windows barely codes on the Asus. Arch runs it like a much pricier laptop, within the limits of what 8 GB allows. That gap is most of why I left Windows behind.&lt;/p&gt;
&lt;p&gt;What I run today grew straight out of this. It has since moved to NixOS, picked up far more tools, swapped several for more modern and more secure ones, and gained an end-to-end Catppuccin Mocha green theme. This is where it started.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#which-desktop-did-i-run&quot;&gt;Which desktop did I run?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-yay-instead-of-plain-pacman&quot;&gt;Why yay instead of plain pacman?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#which-packages-do-i-install-first&quot;&gt;Which packages do I install first?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-do-i-run-docker-without-sudo&quot;&gt;How do I run Docker without sudo?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-do-i-make-zsh-the-default-shell&quot;&gt;How do I make zsh the default shell?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#adding-a-nerd-font&quot;&gt;Adding a Nerd Font&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-in-my-zsh-config&quot;&gt;What is in my zsh config?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-do-i-enable-ssh&quot;&gt;How do I enable SSH?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-do-i-configure-git&quot;&gt;How do I configure Git?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#which-apps-do-i-install-from-flathub&quot;&gt;Which apps do I install from Flathub?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#which-gnome-tweaks-do-i-always-make&quot;&gt;Which GNOME tweaks do I always make?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-would-i-do-differently&quot;&gt;What would I do differently?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-changed-since&quot;&gt;What changed since?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;button type=&quot;button&quot; class=&quot;details-close&quot; aria-label=&quot;Close this section&quot;&gt;Close&lt;/button&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;which-desktop-did-i-run&quot;&gt;Which desktop did I run?&lt;/h2&gt;
&lt;p&gt;GNOME, because it is simple, fast, and needs almost no tweaking to do what I want. I would rather spend the effort on my terminal than on the window manager.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/arch-linux-gnome-desktop.avif&quot; alt=&quot;Arch Linux running the GNOME desktop&quot;&gt;&lt;figcaption&gt;Arch Linux running the GNOME desktop&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;why-yay-instead-of-plain-pacman&quot;&gt;Why yay instead of plain pacman?&lt;/h2&gt;
&lt;p&gt;yay installs packages straight from the AUR, the community-maintained collection that makes the official repos look small. It also updates your whole system in one command, so you stop babysitting mirror lists every two weeks.&lt;/p&gt;
&lt;p&gt;Start with git and the build tools:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pacman&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -S&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --needed&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; base-devel&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; git&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clone yay, build it, done:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://aur.archlinux.org/yay.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; yay&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;makepkg&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -si&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check it landed:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;yay&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then bring the whole system up to date:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;yay&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -u&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One habit worth keeping: update weekly, not once a quarter. A three-month backlog is how you end up with five apps breaking at the same time.&lt;/p&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;bug&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect width=&quot;8&quot; height=&quot;14&quot; x=&quot;8&quot; y=&quot;6&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;path d=&quot;m19 7-3 2&quot;&gt;&lt;/path&gt;&lt;path d=&quot;m5 7 3 2&quot;&gt;&lt;/path&gt;&lt;path d=&quot;m19 19-3-2&quot;&gt;&lt;/path&gt;&lt;path d=&quot;m5 19 3-2&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M20 13h-4&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4 13h4&quot;&gt;&lt;/path&gt;&lt;path d=&quot;m10 4 1 2&quot;&gt;&lt;/path&gt;&lt;path d=&quot;m14 4-1 2&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;Docker sulks after a system update&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;The fix is gloriously dumb: reboot and it behaves again.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/docker-daemon-error-before-reboot.webp&quot; alt=&quot;Docker daemon error after a system update, before a reboot&quot;&gt;&lt;figcaption&gt;Docker daemon error after a system update, before a reboot&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;which-packages-do-i-install-first&quot;&gt;Which packages do I install first?&lt;/h2&gt;
&lt;p&gt;These 19 packages go on every Linux machine I use, in one command:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; pacman&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -S&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; chromium&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; firefox&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; flatpak&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker-compose&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; zsh&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fzf&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; zoxide&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; neovim&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; npm&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; nodejs&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; gdu&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; lazygit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; earlyoom&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; rpi-imager&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; tmux&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; filezilla&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; gedit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is why each one earns its place:&lt;/p&gt;

















































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Package&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;chromium&lt;/td&gt;&lt;td&gt;My main browser, for the performance.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;firefox&lt;/td&gt;&lt;td&gt;Only to test my sites and tweak network requests.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;flatpak&lt;/td&gt;&lt;td&gt;Access &lt;a href=&quot;https://flathub.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Flathub&lt;/a&gt; apps from GNOME Software.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;docker&lt;/td&gt;&lt;td&gt;My dev and deploy tool of choice.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;docker-compose&lt;/td&gt;&lt;td&gt;Multi-container services on top of Docker.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;zsh&lt;/td&gt;&lt;td&gt;Highlighting, autocompletion and more in the terminal.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;fzf&lt;/td&gt;&lt;td&gt;Fuzzy finding for files and commands.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;zoxide&lt;/td&gt;&lt;td&gt;Jump to directories by history and frequency.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;neovim&lt;/td&gt;&lt;td&gt;My future editor, the day I finally find the courage to switch.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;npm&lt;/td&gt;&lt;td&gt;Installing Node packages and building locally.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nodejs&lt;/td&gt;&lt;td&gt;The language I build sites and APIs with.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;gdu&lt;/td&gt;&lt;td&gt;See which folders eat the most disk.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;lazygit&lt;/td&gt;&lt;td&gt;Reading commit history, since I never touch git outside the terminal.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;earlyoom&lt;/td&gt;&lt;td&gt;Watch RAM in real time and kill a runaway before the system freezes.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;rpi-imager&lt;/td&gt;&lt;td&gt;Flash bootable cards for my Raspberry Pi.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;tmux&lt;/td&gt;&lt;td&gt;Terminal sessions, essential alongside neovim.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;filezilla&lt;/td&gt;&lt;td&gt;SFTP client, for docs aimed at non-technical people.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;gedit&lt;/td&gt;&lt;td&gt;Because vim refuses to cooperate with FileZilla’s edit feature. vim &gt; everything else.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Most of the list explains itself. A few picks deserve a word, folded away so the walkthrough keeps moving.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;The reasoning behind a few of them&lt;/summary&gt;
&lt;p&gt;&lt;strong&gt;docker&lt;/strong&gt; is my single tool for install, development and production. One file pins a project to a specific Node version and puts its services on a private network, exposing only a separate external proxy network to the reverse proxy. The database sits on the private network alone, so nothing outside the VPS can reach it. That enforces good architecture for free: nothing stops a beginner from calling a database straight from React until the network does.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/docker-compose-from-real-project-in-production.avif&quot; alt=&quot;A production docker-compose file with an external proxy network and a private app network, keeping the database unreachable from outside the VPS&quot;&gt;&lt;figcaption&gt;A production docker-compose file with an external proxy network and a private app network, keeping the database unreachable from outside the VPS&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;chromium over firefox&lt;/strong&gt; for daily driving. Chromium is smoother and faster, and Firefox &lt;a href=&quot;https://github.com/mozilla/bedrock/commit/d459addab846d8144b61939b7f4310eb80c5470e&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;deleted its old promise never to sell your personal data&lt;/a&gt;. I keep Firefox for one niche move: replaying an XHR request to walk past a front-end form validation.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/firefox-xhr-edit-and-resend-feature.avif&quot; alt=&quot;Firefox devtools editing and resending an XHR request to walk past a front-end form validation&quot;&gt;&lt;figcaption&gt;Firefox devtools editing and resending an XHR request to walk past a front-end form validation&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;fzf&lt;/strong&gt; is a must-have, inside Neovim and on the command line both. It is the fastest way I know to find anything on a Linux system.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/fzf-system-search.avif&quot; alt=&quot;fzf narrowing a file search on the system&quot;&gt;&lt;figcaption&gt;fzf narrowing a file search on the system&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;lazygit&lt;/strong&gt; puts a UI on git without leaving the terminal. git is simple enough day to day that I only reach for it to read complex commit graphs and branches.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/lazygit-in-typovrak-tv-repository.avif&quot; alt=&quot;lazygit showing the branch graph of the typovrak.tv repository&quot;&gt;&lt;figcaption&gt;lazygit showing the branch graph of the typovrak.tv repository&lt;/figcaption&gt;&lt;/figure&gt;
&lt;button type=&quot;button&quot; class=&quot;details-close&quot; aria-label=&quot;Close this section&quot;&gt;Close&lt;/button&gt;&lt;/details&gt;
&lt;p&gt;One package needs the AUR, so it goes through yay instead of pacman:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;yay&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -S&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; nvm&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To finish this step, reboot so Flatpak wires itself into GNOME Software:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;reboot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rebooting from the terminal instead of the menu changes nothing, it just looks cooler.&lt;/p&gt;
&lt;p&gt;Once the machine is back, GNOME Software can install anything on &lt;a href=&quot;https://flathub.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Flathub&lt;/a&gt; directly.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/flathub-in-gnome-software.avif&quot; alt=&quot;Flathub linked into GNOME Software&quot;&gt;&lt;figcaption&gt;Flathub linked into GNOME Software&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;note&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;line x1=&quot;18&quot; y1=&quot;2&quot; x2=&quot;22&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;path d=&quot;M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;On NixOS today&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;In 2024 I barely opened Neovim. It is now my daily editor, and my whole &lt;a href=&quot;https://github.com/typovrak/nixos-nvim&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Neovim config&lt;/a&gt;, every custom plugin pinned, gets cloned on every &lt;code&gt;nixos-rebuild&lt;/code&gt;. 100% reproducible, zero interaction during install and build.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/declarative-nvim-configuration.avif&quot; alt=&quot;My declarative Neovim configuration running on NixOS&quot;&gt;&lt;figcaption&gt;My declarative Neovim configuration running on NixOS&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;note&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;line x1=&quot;18&quot; y1=&quot;2&quot; x2=&quot;22&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;path d=&quot;M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;On NixOS today&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;Installing an app like Insomnia &lt;a href=&quot;https://github.com/typovrak/nixos/blob/main/configuration.nix#L274&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;means adding its name&lt;/a&gt; to my NixOS config and nothing else. No pacman, no yay, no clicking through GNOME Software.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/installing-insomnia-with-nixos-in-one-line.avif&quot; alt=&quot;Installing Insomnia by adding one line to the NixOS configuration&quot;&gt;&lt;figcaption&gt;Installing Insomnia by adding one line to the NixOS configuration&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;how-do-i-run-docker-without-sudo&quot;&gt;How do I run Docker without sudo?&lt;/h2&gt;
&lt;p&gt;Add your user to the &lt;code&gt;docker&lt;/code&gt; group, and the daemon stops asking for root on every command. First start the service and make it survive reboots:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; start&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker.service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; enable&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add yourself to the group and refresh it in the current shell:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; usermod&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -aG&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; $USER&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;newgrp&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; docker&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout&quot; data-callout=&quot;note&quot; data-collapsible=&quot;false&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;line x1=&quot;18&quot; y1=&quot;2&quot; x2=&quot;22&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;path d=&quot;M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div class=&quot;callout-title-text&quot;&gt;On NixOS today&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;This whole dance is &lt;a href=&quot;https://github.com/typovrak/nixos/blob/main/configuration.nix#L228&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;one line in my config&lt;/a&gt;: &lt;code&gt;extraGroups = [ &quot;docker&quot; ];&lt;/code&gt;, written once and applied on every rebuild. No &lt;code&gt;usermod&lt;/code&gt;, no &lt;code&gt;newgrp&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/setup-docker-group-in-one-line-on-nixos.avif&quot; alt=&quot;Adding the docker group in one line of the NixOS configuration&quot;&gt;&lt;figcaption&gt;Adding the docker group in one line of the NixOS configuration&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Test it with the throwaway &lt;code&gt;hello-world&lt;/code&gt; image:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;docker&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hello-world&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/docker-run-hello-world-output.webp&quot; alt=&quot;docker run hello-world output&quot;&gt;&lt;figcaption&gt;docker run hello-world output&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;If the first line reads &lt;strong&gt;Hello from Docker!&lt;/strong&gt;, you are done.&lt;/p&gt;
&lt;h2 id=&quot;how-do-i-make-zsh-the-default-shell&quot;&gt;How do I make zsh the default shell?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;chsh&lt;/code&gt; changes the login shell. Point it at zsh, which is already installed. bash is fine, zsh is better, so:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;chsh&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; $USER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When it asks for the new shell, give it the zsh path:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;/usr/bin/zsh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm it took:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; $SHELL&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That should print &lt;code&gt;/usr/bin/zsh&lt;/code&gt;. The shell is switched, but it is bare until you give it a config.&lt;/p&gt;
&lt;h2 id=&quot;adding-a-nerd-font&quot;&gt;Adding a Nerd Font&lt;/h2&gt;
&lt;p&gt;A Nerd Font ships the extra glyphs and icons a good terminal theme needs, and Powerlevel10k will ask for one. Download one from &lt;a href=&quot;https://www.nerdfonts.com/font-downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;nerdfonts.com&lt;/a&gt;. I use JetBrains Mono for how it reads while coding, with Fira Code as my occasional favourite for a change of scenery.&lt;/p&gt;
&lt;p&gt;Unzip it, delete the &lt;code&gt;LICENSE.txt&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt; inside, then drop every font file into &lt;code&gt;~/.local/share/fonts&lt;/code&gt; (create it if missing, and keep it flat, no subfolders). Set that font in your terminal preferences, and if there is both a Mono and a non-Mono variant, pick the non-Mono one for a better terminal render.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/gnome-terminal-jetbrainsmono-nerd-font.webp&quot; alt=&quot;Choosing the JetBrainsMono Nerd Font in GNOME Terminal preferences&quot;&gt;&lt;figcaption&gt;Choosing the JetBrainsMono Nerd Font in GNOME Terminal preferences&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This is also where I set the size. With JetBrains Mono I use 12, to spare my eyes over a long day.&lt;/p&gt;
&lt;h2 id=&quot;what-is-in-my-zsh-config&quot;&gt;What is in my zsh config?&lt;/h2&gt;
&lt;p&gt;The config wires up zinit as the plugin manager, Powerlevel10k as the prompt, and a handful of plugins for highlighting, completion and fuzzy tab. Create &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;vim&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is mine, the one I actually ran:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# init ssh agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;ssh-agent&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# load custom ssh keys&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;#ssh-add ~/.ssh/github &amp;#x26;&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;#ssh-add ~/.ssh/gitlab &amp;#x26;&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; /usr/share/nvm/init-nvm.sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Initialization code that may require console input (password prompts, [y/n]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# confirmations, etc.) must go above this block; everything else may go below.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; [[&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; -r&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${XDG_CACHE_HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:-&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;$HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;cache}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/p10k-instant-prompt-&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:-%&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;n}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.zsh&quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; ]];&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;  source&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${XDG_CACHE_HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:-&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;$HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;cache}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/p10k-instant-prompt-&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:-%&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;n}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.zsh&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; [[&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;/opt/homebrew/bin/brew&quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; ]]&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;  # If you&apos;re using macOS, you&apos;ll want this enabled&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;  eval&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;/opt/homebrew/bin/brew&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; shellenv&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Set the directory we want to store zinit and plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;ZINIT_HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${XDG_DATA_HOME&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:-&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${HOME}&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;local&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;share}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/zinit/zinit.git&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Download Zinit, if it&apos;s not there yet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;$ZINIT_HOME&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; ];&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt; then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;   mkdir&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -p&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;dirname&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; $ZINIT_HOME&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;   git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; https://github.com/zdharma-continuum/zinit.git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;$ZINIT_HOME&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Source/Load zinit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${ZINIT_HOME}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;/zinit.zsh&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Add in Powerlevel10k&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ice&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; depth=&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; light&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; romkatv/powerlevel10k&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Add in zsh plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; light&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; zsh-users/zsh-syntax-highlighting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; light&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; zsh-users/zsh-completions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; light&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; zsh-users/zsh-autosuggestions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; light&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; Aloxaf/fzf-tab&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Add in snippets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::sudo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::archlinux&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::aws&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::kubectl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::kubectx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; snippet&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; OMZP::command-not-found&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Load completions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;autoload&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -Uz&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; compinit&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt; compinit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zinit&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; cdreplay&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -q&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;[[&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; ~&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;/.p10k.zsh &lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;]]&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt; source&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.p10k.zsh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Keybindings&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;bindkey&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; -v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# History&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;HISTSIZE&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;5000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;HISTFILE&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;~/.zsh_history&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;SAVEHIST&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;$HISTSIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;HISTDUP&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;erase&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; appendhistory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; sharehistory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hist_ignore_space&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hist_ignore_all_dups&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hist_save_no_dups&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hist_ignore_dups&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;setopt&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; hist_find_no_dups&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Completion styling&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zstyle&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;:completion:*&apos;&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; matcher-list&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;m:{a-z}={A-Za-z}&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zstyle&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;:completion:*&apos;&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; list-colors&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;s&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;.)&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt;LS_COLORS}&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zstyle&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;:completion:*&apos;&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; menu&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; no&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zstyle&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;:fzf-tab:complete:cd:*&apos;&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fzf-preview&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;ls --color $realpath&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zstyle&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;:fzf-tab:complete:__zoxide_z:*&apos;&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; fzf-preview&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;ls --color $realpath&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Aliases&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; ls&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&apos;ls --color&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; c&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;clear&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; e&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;exit&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; vim&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;nvim&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;nvim&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; vi&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;nvim&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; view&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;nvim -R&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4&quot;&gt; vimdiff&lt;/span&gt;&lt;span style=&quot;color:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;nvim -d&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;font-style:italic&quot;&gt;# Shell integrations&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;fzf&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --zsh&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;zoxide&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; init --cmd cd zsh&lt;/span&gt;&lt;span style=&quot;color:#9399B2&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload to apply it:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GitHub and GitLab SSH lines are commented out. Uncomment them if your keys share those names, or point them at yours. The aliases are the part I miss most on any machine that is not mine: &lt;code&gt;e&lt;/code&gt; to exit a terminal in one keystroke, &lt;code&gt;c&lt;/code&gt; to clear, and &lt;code&gt;v&lt;/code&gt;/&lt;code&gt;vim&lt;/code&gt;/&lt;code&gt;vi&lt;/code&gt; all pointing at Neovim.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/zsh-ls-colored-output.webp&quot; alt=&quot;Coloured ls output from the zsh config&quot;&gt;&lt;figcaption&gt;Coloured ls output from the zsh config&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The first time you open a terminal after this, Powerlevel10k walks you through building a prompt. Answer the questions to taste. The full options live in the &lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Powerlevel10k docs&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/zsh-powerlevel10k-prompt.webp&quot; alt=&quot;The finished zsh prompt with Powerlevel10k&quot;&gt;&lt;figcaption&gt;The finished zsh prompt with Powerlevel10k&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;how-do-i-enable-ssh&quot;&gt;How do I enable SSH?&lt;/h2&gt;
&lt;p&gt;The SSH service is off by default, which blocks cloning over SSH and connecting to a VPS. Same pattern as Docker, start it and enable it:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; start&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; sshd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; enable&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; sshd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a directory for your keys and lock down its permissions:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;mkdir&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;chmod&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; 700&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F38BA8;font-style:italic&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you drop keys in, fix their permissions or SSH will treat them as public and refuse them. Private keys need &lt;code&gt;600&lt;/code&gt;, public keys &lt;code&gt;644&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;chmod&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; 600&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; github&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; gitlab&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;chmod&lt;/span&gt;&lt;span style=&quot;color:#FAB387&quot;&gt; 644&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; github.pub&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; gitlab.pub&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;how-do-i-configure-git&quot;&gt;How do I configure Git?&lt;/h2&gt;
&lt;p&gt;Set your name, email and default branch, so new repos start on &lt;code&gt;main&lt;/code&gt; rather than &lt;code&gt;master&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --global&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; user.name&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &quot;First LAST&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --global&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; user.email&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; you@example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; --global&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; init.defaultBranch&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those three commands are the same as writing a &lt;code&gt;~/.gitconfig&lt;/code&gt; by hand. It is an &lt;a href=&quot;https://en.wikipedia.org/wiki/INI_file&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;INI file&lt;/a&gt;, one of the dozens of config formats you will meet, and you can read yours back with:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; ~/.gitconfig&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;which-apps-do-i-install-from-flathub&quot;&gt;Which apps do I install from Flathub?&lt;/h2&gt;
&lt;p&gt;The GUI apps that pacman and yay cannot give me come from &lt;a href=&quot;https://flathub.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Flathub&lt;/a&gt;, through GNOME Software now that Flatpak is wired in. My usual list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flathub.org/apps/com.slack.Slack&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Slack&lt;/a&gt;, for work chat.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flathub.org/apps/md.obsidian.Obsidian&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Obsidian&lt;/a&gt;, for personal notes.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flathub.org/apps/com.obsproject.Studio&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OBS&lt;/a&gt;, for recording with a webcam.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flathub.org/apps/org.videolan.VLC&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VLC&lt;/a&gt;, because GNOME Videos still trips over half my files.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flathub.org/apps/com.mattjakeman.ExtensionManager&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Extension Manager&lt;/a&gt;, to tweak GNOME.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;which-gnome-tweaks-do-i-always-make&quot;&gt;Which GNOME tweaks do I always make?&lt;/h2&gt;
&lt;p&gt;Three small ones, and I set all three on every machine.&lt;/p&gt;
&lt;p&gt;First, volume above 100%. GNOME caps output at 100% by default, and this one setting unlocks a 150% boost that a quiet laptop needs:&lt;/p&gt;
&lt;pre class=&quot;astro-code catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;color:#cdd6f4; overflow-x: auto;--file-name-offset: -0.75rem;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;font-style:italic&quot;&gt;gsettings&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; org.gnome.desktop.sound&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; allow-volume-above-100-percent&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1&quot;&gt; &apos;true&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/gnome-volume-over-100-percent.webp&quot; alt=&quot;GNOME volume boosted to 150 percent&quot;&gt;&lt;figcaption&gt;GNOME volume boosted to 150 percent&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Second, the minimize button. GNOME ships without one, so I turn it back on in GNOME Tweaks. Small thing, non-negotiable for me.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/gnome-tweaks-minimize-maximize-buttons.webp&quot; alt=&quot;Minimize and maximize buttons enabled in GNOME Tweaks&quot;&gt;&lt;figcaption&gt;Minimize and maximize buttons enabled in GNOME Tweaks&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Third, three extensions from Extension Manager:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Just Perfection&lt;/strong&gt;, to reshape the GNOME interface. I mostly toggle the battery percentage and the keyboard-layout indicator, since I switch layouts all day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick Settings Audio Panel&lt;/strong&gt;, to set the volume per app from the top-right menu.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tray Icons: Reloaded&lt;/strong&gt;, to show background apps like Discord or Slack in the tray.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/gnome-shell-extensions-list.webp&quot; alt=&quot;My GNOME Shell extensions&quot;&gt;&lt;figcaption&gt;My GNOME Shell extensions&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;what-would-i-do-differently&quot;&gt;What would I do differently?&lt;/h2&gt;
&lt;p&gt;Three calls I would change with two years of hindsight.&lt;/p&gt;
&lt;p&gt;GNOME was the right way to start, but I would go straight to i3 now. Everything runs from the keyboard: every app, window, workspace and command. One screen with i3 in the hands does the work of two, three, sometimes four screens under GNOME, and the whole application layout bends to how you actually work.&lt;/p&gt;
&lt;p&gt;zoxide is a small quality-of-life win. &lt;code&gt;cd typovrak.tv&lt;/code&gt; jumps to &lt;code&gt;~/projects/typovrak.tv&lt;/code&gt; from anywhere in the tree, and it is the kind of thing you should live with for a week to feel it. I dropped it anyway. It made me forget my own paths, which is backwards, and as a touch typist I lose almost no time typing a path in full.&lt;/p&gt;
&lt;figure style=&quot;max-width:400px&quot;&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/zoxide-in-action.avif&quot; alt=&quot;zoxide jumping straight to a project directory from anywhere in the tree&quot;&gt;&lt;figcaption&gt;zoxide jumping straight to a project directory from anywhere in the tree&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;FileZilla and gedit only survive for production work and the odd one-off. Given the choice, it is SSH and Vim every time.&lt;/p&gt;
&lt;p&gt;tmux is here for testing. I prefer zellij, the Rust terminal multiplexer, but it carries more than my daily work needs, so I never made the switch. My &lt;a href=&quot;https://github.com/typovrak/nixos-zellij&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;zellij config and theme&lt;/a&gt; are on GitHub if you want them. The setup adapts with or without NixOS.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://typovrak.tv/img/posts/arch-2024-setup/zellij-with-3-pane-and-catppuccin-mocha-green-theme.avif&quot; alt=&quot;zellij split into three panes with the Catppuccin Mocha green theme&quot;&gt;&lt;figcaption&gt;zellij split into three panes with the Catppuccin Mocha green theme&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;what-changed-since&quot;&gt;What changed since?&lt;/h2&gt;
&lt;p&gt;Everything above is imperative: run commands, click through GNOME, hope the next machine ends up the same. It worked, but it lived in my head and in screenshots like these, not in a file.&lt;/p&gt;
&lt;p&gt;Setting Arch up by hand took me about two hours of installing and clicking, and it never ran unattended. The same result on NixOS takes five minutes: I write the config once, start the build, and go make a coffee while the machine installs and configures everything with zero interaction. I never pulled that off on Arch, and it is the single biggest reason I moved.&lt;/p&gt;
&lt;p&gt;The setup is now a &lt;a href=&quot;https://typovrak.tv/nixos&quot;&gt;declarative NixOS config&lt;/a&gt;, one module per tool, so a fresh machine rebuilds to the same state from source. It has also grown well past this list, traded several tools for more modern and more secure ones, and picked up an end-to-end Catppuccin Mocha green theme. The 2024 list you just read is the foundation the rest was built on.&lt;/p&gt;
&lt;p&gt;If you are on Arch today, this still gets you a clean, developer-ready system. When you get tired of doing it by hand, you know where to look next.&lt;/p&gt;</content:encoded><category>Arch Linux</category><category>GNOME</category><category>zsh</category><category>Docker</category><category>CLI</category><author>typovrak@gmail.com (typovrak)</author></item></channel></rss>