<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Filipe Fortes</title><link>https://fortes.com/</link><description>Personal website for Filipe Fortes, a software engineer living in Miami</description><language>en-us</language><lastBuildDate>Tue, 05 May 2026 15:32:08 +0000</lastBuildDate><atom:link href="https://fortes.com/index.xml" rel="self" type="application/rss+xml"/><item><title>2026 Personal Tech Stack</title><link>https://fortes.com/2026/personal-tech-stack/</link><guid>https://fortes.com/2026/personal-tech-stack/</guid><pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate><description>My yearly tradition no one sane cares about</description><content:encoded>&lt;p&gt;At the end of 2024, I &lt;a href="https://fortes.com/2024/personal-tech-stack"&gt;documented my personal tech stack&lt;/a&gt; for the first time. I meant to do a 2025 update, but due to supply-chain issues we&amp;rsquo;re exactly a month past due here. I&amp;rsquo;ve made sure to post this update after market hours so I don&amp;rsquo;t disrupt the financial markets too much.&lt;/p&gt;
&lt;h2 id="so-many-agents"&gt;So Many Agents&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2026/personal-tech-stack/2026-nested-tmux.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_85b333a04d5c7301.webp 400w, https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_8e0b1ea3d578e298.webp 800w, https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_698c9b501c96ecd.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_65e390ef442cfa86.png" srcset="https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_4014012d4d5311b7.png 400w, https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_65e390ef442cfa86.png 800w, https://fortes.com/2026/personal-tech-stack/2026-nested-tmux_hu_8a0e159a6e156d85.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Multiple coding agents inside a nested tmux session, more agents hiding in local tmux windows. Got agents coming out my ears here" width="800" height="450" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Multiple coding agents inside a nested tmux session, more agents hiding in local tmux windows. Got agents coming out my ears here&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It&amp;rsquo;s a bit surreal that Claude Code is less than a year old. A year ago, I was playing around with &lt;code&gt;aider&lt;/code&gt; and was ecstatic when (once in a while) I was able to get an initial rough draft of a code change. OpenAI&amp;rsquo;s &lt;code&gt;o1-preview&lt;/code&gt; was expensive, but with a steady hand you could get some stuff done with it.&lt;/p&gt;
&lt;p&gt;When I first &lt;a href="https://github.com/fortes/dotfiles/commit/22861f7a82384e81325f49cd78290afe0180e6ed"&gt;started playing with Claude Code in early February&lt;/a&gt;, it was a slightly nicer experience than aider. Less futzing with manually including files, but nothing revolutionary. But new, better models kept on getting released, and everything changed so quickly.&lt;/p&gt;
&lt;p&gt;Once Claude Code started picking up steam, &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt;, and a deluge of other coding agents joined the party. At the end of 2024, I was genuinely worried I&amp;rsquo;d have to abandon my terminal setup and give in to a GUI IDE. Instead, the opposite happened: &lt;em&gt;suddenly being fluent in &lt;code&gt;tmux&lt;/code&gt; and &lt;code&gt;git worktree&lt;/code&gt; is a huge advantage&lt;/em&gt;! I&amp;rsquo;m routinely running three or four agents in parallel across different &lt;code&gt;tmux&lt;/code&gt; panes with more running on a home server via SSH.&lt;/p&gt;
&lt;p&gt;Currently, I mostly rely on Claude for the main planning (Opus) and execution (Sonnet), then loop in Codex and Gemini for code reviews. It&amp;rsquo;s easy to hit limits when multi-Clauding with Opus, which forces me to switch to Codex/Gemini. It&amp;rsquo;s important to try driving with other models once in a while, which is how I discovered that Gemini&amp;rsquo;s Flash family can be &lt;em&gt;an absolute beast&lt;/em&gt; if you give it a clearly defined task. It&amp;rsquo;s my go-to for straightforward, easily verified tasks.&lt;/p&gt;
&lt;p&gt;My model choices in 2025 changed at least monthly based on the latest release, and I have no doubt 2026 will be similar. Frankly, if you&amp;rsquo;re not constantly trying the latest you&amp;rsquo;re falling behind.&lt;/p&gt;
&lt;p&gt;I also used web agents a fair amount, specifically &lt;a href="https://jules.google.com/"&gt;Jules&lt;/a&gt; (Google) and &lt;a href="https://chatgpt.com/codex"&gt;Codex&lt;/a&gt; (OpenAI). Takes a bit of investment to get your environment set up, but it&amp;rsquo;s absolutely worthwhile to be able to kick off a bunch of tasks on your phone. Claude released an agent in late 2025, but the environment setup &lt;a href="https://code.claude.com/docs/en/claude-code-on-the-web#dependency-management"&gt;involves hooks&lt;/a&gt; which I haven&amp;rsquo;t invested time into figuring out yet. This is mostly since Jules and Codex have been good enough for the simpler tasks, and the more complex ones I typically go local.&lt;/p&gt;
&lt;p&gt;I also keep trying the IDE-based agents - VS Code, Cursor, and the new-ish &lt;a href="https://antigravity.google/"&gt;Antigravity&lt;/a&gt;. Aside from the traditional code-editing-centric view, they&amp;rsquo;ve all added an agent-centric mode that is functionally identical to Jules or Codex Web.&lt;/p&gt;
&lt;p&gt;But I kept dropping back to the terminal. Old habits, yes, but it&amp;rsquo;s really hard to beat the ergonomics of &lt;code&gt;tmux&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, and &lt;code&gt;vim&lt;/code&gt; once you git gud.&lt;/p&gt;
&lt;h2 id="hardware-consolidation"&gt;Hardware Consolidation&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2026/personal-tech-stack/2025-desk-setup.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_30a1c00464375fac.webp 400w, https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_dd14b71eb3e8ec73.webp 800w, https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_f3781e7675fc24b1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_3efc472c3da9c759.jpg" srcset="https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_8625e03c9bfca38f.jpg 400w, https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_3efc472c3da9c759.jpg 800w, https://fortes.com/2026/personal-tech-stack/2025-desk-setup_hu_348feb182f9c8664.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Not pictured: Vitamin D deficiency" width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Not pictured: Vitamin D deficiency&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Last year I was juggling an M3 MacBook, a Windows desktop, a Framework laptop, a Chromebook, and a headless Linux server. I&amp;rsquo;ve collapsed this down to almost entirely Mac (one for work, one personal) with a little bit of Chromebook use when traveling. The Linux server persists, now running Debian Trixie with persistent tmux sessions for agent minions.&lt;/p&gt;
&lt;p&gt;I still mourn the loss of a good tiling window manager. i3/Sway on Linux was nearly perfect, and nothing on macOS comes close despite valiant efforts from &lt;a href="https://rectangleapp.com/"&gt;Rectangle&lt;/a&gt; (the best of the options I&amp;rsquo;ve tried). I&amp;rsquo;ve persevered through this untold hardship, but I want it on record that I&amp;rsquo;m suffering an absolutely inhumane amount here.&lt;/p&gt;
&lt;p&gt;I bought some cheap Anker Soundcore noise-cancelling headphones after going without for over a decade. Turns out the technology has gotten dramatically better and I was an idiot for waiting. Still walking on the treadmill desk during meetings (typically four or five miles a day).&lt;/p&gt;
&lt;h2 id="other-terminal-tools"&gt;Other Terminal Tools&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_7a1c19c345ee274d.webp 400w, https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_95e5cd46a8b8a407.webp 800w, https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_d101a983fce14fad.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_36db2a52a77cbf0f.png" srcset="https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_cabb3f8a329801cb.png 400w, https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_36db2a52a77cbf0f.png 800w, https://fortes.com/2026/personal-tech-stack/2026-ghostty-yazi_hu_1fa8af3cf94ceaa8.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Yazi displaying images within a tmux pane while writing this post" width="800" height="450" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Yazi displaying images within a tmux pane while writing this post&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I&amp;rsquo;ve fully moved from WezTerm to &lt;a href="https://ghostty.org/"&gt;Ghostty&lt;/a&gt;. It&amp;rsquo;s made a ton of progress, and it definitely helps to have a &lt;a href="https://news.ycombinator.com/item?id=46139922"&gt;billionaire primary developer&lt;/a&gt;. The image viewing in Ghostty + yazi + tmux is genuinely nice.&lt;/p&gt;
&lt;p&gt;Speaking of which, &lt;a href="https://yazi-rs.github.io/"&gt;yazi&lt;/a&gt; has replaced &lt;a href="https://github.com/vifm/vifm"&gt;vifm&lt;/a&gt; exactly as I predicted last year. Worthwhile for the image display alone.&lt;/p&gt;
&lt;p&gt;Other changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pnpm&lt;/strong&gt;: Replaced npm. Faster, more disk-efficient, build steps must be manually approved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uv&lt;/strong&gt;: Replaced pip for Python. Much faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gh&lt;/strong&gt;: Coding agents love it, so I&amp;rsquo;m trying to use it more. Best use is &lt;code&gt;gh pr create&lt;/code&gt;, but otherwise I often forget.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tsgo&lt;/strong&gt;: Microsoft&amp;rsquo;s new TypeScript language server in Go is so much faster. I&amp;rsquo;m writing a lot less code by hand these days, so it doesn&amp;rsquo;t make as much of a difference in my day-to-day.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="non-terminal-tools"&gt;Non-Terminal Tools&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2026/personal-tech-stack/2026-obsidian.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_4d5b91ec22224b25.webp 400w, https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_edc9cf83d8f9aaff.webp 800w, https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_67f5e6198d5224a9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_a197856090b5e347.png" srcset="https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_ed9f8f76dfcc4052.png 400w, https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_a197856090b5e347.png 800w, https://fortes.com/2026/personal-tech-stack/2026-obsidian_hu_b7bcc6229b12c884.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Visualizing personal data in Obsidian" width="800" height="451" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Visualizing personal data in Obsidian&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Obsidian&lt;/strong&gt;: Moved my personal and work journals from Coda to the filesystem using a combination of Neovim and Obsidian for editing. It&amp;rsquo;s really nice to have all this data sitting in Markdown and letting the LLMs party on top. I&amp;rsquo;m really enjoying this system, so I&amp;rsquo;ll break it out into a separate post &amp;hellip; some day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spotify&lt;/strong&gt;: cmus kept crashing on Mac, so I tried &lt;a href="https://github.com/hrkfdn/ncspot"&gt;ncspot&lt;/a&gt; which also had issues. Then I gave up and just went to the full Spotify app, but I need to find a better solution here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Video playback&lt;/strong&gt;: I used to use &lt;code&gt;yt-dlp&lt;/code&gt; to download videos and &lt;code&gt;mpv&lt;/code&gt; to watch things at 2.5x+ speed. I still do sometimes, but now I usually open DevTools and type &lt;code&gt;document.querySelector('video').playbackRate = 2.5&lt;/code&gt; which works great.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else is unchanged: Signal, 1Password, Firefox for personal, Chrome for work. Some of the &lt;a href="https://blog.google/products-and-platforms/products/chrome/gemini-3-auto-browse"&gt;new Gemini integrations in Chrome&lt;/a&gt; look pretty nice, so I may move back to Chrome once vertical tabs come out of beta.&lt;/p&gt;
&lt;h2 id="whats-next-in-2026"&gt;What&amp;rsquo;s Next in 2026?&lt;/h2&gt;
&lt;p&gt;Honestly: No clue. I mean, I can try to predict some things but given how 2025 went I&amp;rsquo;d likely just make myself embarrassed next year.&lt;/p&gt;
&lt;p&gt;At risk of embarrassment, I think it&amp;rsquo;s pretty safe to say I&amp;rsquo;ll still be terminal-heavy in a year. That old habit dies hard. Regardless of habit, there are pretty nice advantages to working within a local terminal for agents, so I&amp;rsquo;d be surprised if &lt;em&gt;everything&lt;/em&gt; moved to the cloud with a nice GUI in front.&lt;/p&gt;
&lt;p&gt;Moving my personal journal/logging systems to use formats that agents can work with worked out very well, so I can imagine doing that for a few systems. I worry that platform providers will increasingly start locking down their data, so I&amp;rsquo;ve been proactively archiving just in case.&lt;/p&gt;
&lt;p&gt;Regardless of whatever new models come out, it&amp;rsquo;s still a safe bet my computer skills will remain superior to my social skills.&lt;/p&gt;
&lt;h2 id="pointless-aside-screenshot-workflow"&gt;Pointless Aside: Screenshot Workflow&lt;/h2&gt;
&lt;p&gt;I got a few million complaints that my 2024 post didn&amp;rsquo;t have screenshots, so this year I tried to sprinkle a few in. Took me a little to find a reasonable capture flow (I really miss Linux for this type of thing), so I&amp;rsquo;m documenting it here for my future self.&lt;/p&gt;
&lt;p&gt;First, resize the terminal to 1600px by 900px:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;osascript -e &lt;span style="color:#e6db74"&gt;&amp;#39;tell application &amp;#34;System Events&amp;#34; to tell process &amp;#34;Ghostty&amp;#34; to set size of window 1 to {1600, 900}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;del&gt;Now use the native Mac window capture mode via &lt;code&gt;Cmd+Shift+4&lt;/code&gt;, then &lt;code&gt;Spacebar&lt;/code&gt;, then click the window. This captures the window without the desktop background, but annoyingly includes a window shadow which I&amp;rsquo;m too much of a control freak to live with. (Well-adjusted individuals can skip this next part and move on with their lives)&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;&lt;del&gt;Open the screenshot in &lt;code&gt;Preview&lt;/code&gt;, use the Markup Toolbar and the magic wand tool, click and drag on the area outside of the window until you&amp;rsquo;ve selected everything outside of the window. Delete the selection, then reselect the area outside again (sadly, no saved selection in &lt;code&gt;Preview&lt;/code&gt;), then invert the selection and crop.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mastodon.social/@djstarr@mas.to/1159941109457284510"&gt;Thanks to DJ Starr&lt;/a&gt; a far simpler method is available: Cmd+Shift+4 then press space, hold option before clicking the window to capture without shadow&lt;/p&gt;
&lt;p&gt;Finally, save out the file, touch the &lt;code&gt;f&lt;/code&gt; key of your keyboard exactly 3 times with your left ring finger, close both eyes for 1.5 seconds, and then lift your right foot at a 14.3 degree angle. Now simply go and touch all the doorknobs in your house exactly seven times and you&amp;rsquo;re all set!&lt;/p&gt;</content:encoded></item><item><title>Custom Kids’ Podcasts With NotebookLM</title><link>https://fortes.com/2025/kids-podcasts-with-notebooklm/</link><guid>https://fortes.com/2025/kids-podcasts-with-notebooklm/</guid><pubDate>Sun, 02 Feb 2025 00:00:00 +0000</pubDate><description>Using NotebookLM to create personalized podcasts for kids</description><content:encoded>&lt;p&gt;A few years back, I took the family on a road trip from Florida up to Charleston, SC. My original plan to rely on audiobooks failed spectacularly, and in a desperate play for some sanity we introduced the children to a few kid-friendly podcasts. It took some searching, but we found some options that made the drive tolerable, at the cost of introducing a few I never knew existed. Even though that road trip was years back, the cursed podcasts have lingered; there&amp;rsquo;s one specific podcast that covers niche content my son loves, but it&amp;rsquo;s intolerable for adults.&lt;/p&gt;
&lt;p&gt;In my desperation, I started looking into creating a podcast that could serve as a substitute. I found Google&amp;rsquo;s &lt;a href="https://notebooklm.google.com/"&gt;NotebookLM&lt;/a&gt;, an AI-powered tool with a neat feature that can generate a podcast-style overview of the sources saved into the notebook.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_98261bcfee001b2e.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_d85a696d368fea0b.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_7e4f4a8ed0e8c96d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_f5539ac9fd816aa0.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_2590749fcf0f08c4.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_f5539ac9fd816aa0.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-kids-podcast_hu_6e4a7e8f79dba6e8.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Imagine explaining this screen to someone 30 years ago" width="800" height="434" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Imagine explaining this screen to someone 30 years ago&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The &lt;a href="https://open.spotify.com/show/2upilh1nDHni8ao6TyMqye?si=cd3ad03fc034441e"&gt;AI-generated content and voices&lt;/a&gt; aren&amp;rsquo;t going to win an award any time soon, but they&amp;rsquo;re surprisingly decent and of higher quality than the majority of kid-friendly podcasts out there.&lt;/p&gt;
&lt;p&gt;The tool is currently free, and creating a podcast tailored to your interests only takes a few minutes, here are the steps:&lt;/p&gt;
&lt;h2 id="creating-a-new-notebook"&gt;Creating a New Notebook&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_b126a77051532e8d.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_244c22b07dbed16.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_2acba94df2621bd.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_76b6aa544c018bd1.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_b105366893cb94b5.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_76b6aa544c018bd1.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-1_hu_9851edb027366bc1.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Once you&amp;#39;ve created a notebook, add sources for the topic" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Once you&amp;#39;ve created a notebook, add sources for the topic&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;To get started, head over to &lt;a href="https://notebooklm.google.com/"&gt;NotebookLM&lt;/a&gt; and sign in with your Google account. Once you&amp;rsquo;re in, click on the &amp;ldquo;Create a notebook&amp;rdquo; button and you&amp;rsquo;ll be prompted to begin adding some sources.&lt;/p&gt;
&lt;h2 id="adding-sources"&gt;Adding Sources&lt;/h2&gt;
&lt;p&gt;This is the most important step in the process. You need to pick a compelling topic and feed information into the notebook for it to synthesize.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_5ba0bf3c0a3eba8a.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_ea33fc9c6a01dee.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_677ec327fa1cc3f4.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_52b32623de4150c2.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_e2be2bbe14465210.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_52b32623de4150c2.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-2_hu_144daaa42a9c3fb2.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Adding sources" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Adding sources&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Here are some things I&amp;rsquo;ve found work well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I recommend starting with the Wikipedia page for the topic you&amp;rsquo;ve chosen. NotebookLM has very good support for Wikipedia, and it serves as a good general overview of the topic. Depending on the topic, I often add a few more Wikipedia pages to provide more depth and related content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&amp;rsquo;ll then do a quick search on the topic, sometimes adding &amp;ldquo;for kids&amp;rdquo; to the search query. Results here will vary, but as long as something looks fairly reasonable, I&amp;rsquo;ll dump it in as a source. I&amp;rsquo;ll admit I don&amp;rsquo;t always invest a lot of time vetting each source, so there&amp;rsquo;s a slight risk of including something less than factual about the diet of a Parasaurolophus. It&amp;rsquo;s a risk I&amp;rsquo;m willing to take.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For certain links, you might get an error saying it is unable to be imported. Usually, this isn&amp;rsquo;t a big deal since you likely have a lot of sources with overlapping information. If it&amp;rsquo;s something really important for your topic, you can add it manually by copying the text and pasting it in as a source.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;YouTube videos can be a great option as well, I assume that&amp;rsquo;s a huge advantage of this tool being created by Google.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As a last resort if I&amp;rsquo;m having issues finding good sources, I&amp;rsquo;ll have ChatGPT or Claude generate a summary of the topic and copy-paste that in as a source.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&amp;rsquo;ve added a bunch of sources, you&amp;rsquo;ll see them all listed and you can use the chat interface to ask questions about the topic.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_7c20283824912fc2.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_8fe83e2465e92601.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_efd4fb6c947d1a76.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_3343d883572b408a.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_e2cd753cc79e6d09.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_3343d883572b408a.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-3_hu_1f8a52b0f7ea927.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="A notebook populated with sources" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;A notebook populated with sources&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id="generating-the-podcast"&gt;Generating the Podcast&lt;/h2&gt;
&lt;p&gt;Now that you&amp;rsquo;ve got your sources, we can work on generating the actual podcast.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_a47671f7e7bd7ac8.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_ee243789b516abeb.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_21196ea7e3d637b4.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_979b173b54d598b3.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_78d186fe0f3fabc.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_979b173b54d598b3.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-4_hu_61a34050713d50e7.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Customize the podcast with your own prompt" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Customize the podcast with your own prompt&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Without custom instructions, you&amp;rsquo;ll get a generic podcast, which will likely be tailored for adults and may not capture your kids&amp;rsquo; attention. It&amp;rsquo;s way more fun to hit the &amp;ldquo;Customize&amp;rdquo; button in order to add your own prompt here.&lt;/p&gt;
&lt;p&gt;The custom instructions are limited to around 500 characters, so you can&amp;rsquo;t go too crazy here. I like to do a mix of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Include the kids&amp;rsquo; names and ages. This helps the AI tailor the content to their level of understanding, also with the ability for their names to be mentioned once in a while.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add some facts about them, or the family in general, to help keep things personal. For example, I might mention specific interests or experiences with the topic that can be woven into the podcast.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If there&amp;rsquo;s a specific aspect of the topic that I want to make sure gets covered (e.g. the temperament of Hinoxes in Zelda), I&amp;rsquo;ll add that into the prompt as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For some of the podcasts, I&amp;rsquo;ll include instructions to have a short quiz at the end, where the hosts will prompt the kids with questions and pause for a few seconds to give them time to answer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can also just have ChatGPT / Claude generate the prompt for you, they&amp;rsquo;re usually pretty good at keeping within the character limit.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&amp;rsquo;ve got your prompt set, hit the &amp;ldquo;Create podcast&amp;rdquo; button and wait a few minutes for it to generate.&lt;/p&gt;
&lt;h2 id="generating-the-episode-listing"&gt;Generating the Episode Listing&lt;/h2&gt;
&lt;p&gt;While you&amp;rsquo;re waiting for the audio to generate, you can use this time to generate a title and description for the episode. I like to use the chat interface to ask for a title and description, for later use when uploading the episode.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_b27c68fb8a4f175e.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_7d7e98dfb78fb845.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_74e65d2a6850a4f0.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_5bc82b93c08356d0.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_4cf526419057605f.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_5bc82b93c08356d0.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-notebooklm-podcast-creation-5_hu_8be264f3d84ee860.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Use the Chat interface to generate a text intro" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Use the Chat interface to generate a text intro&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I also use this time to create a cover image for the episode using &lt;a href="https://chatgpt.com/"&gt;ChatGPT&lt;/a&gt;. I&amp;rsquo;ll either prompt directly or use a custom GPT like &lt;a href="https://chatgpt.com/g/g-gFFsdkfMC-cartoonize-yourself"&gt;Cartoonize Yourself GPT&lt;/a&gt;. Generally, I ask for a cartoon image of the kids with the topic in the background, it usually takes a couple of revisions to get something decent.&lt;/p&gt;
&lt;p&gt;Once the audio has generated, download it locally to your computer.&lt;/p&gt;
&lt;h2 id="publishing"&gt;Publishing&lt;/h2&gt;
&lt;p&gt;Now that you&amp;rsquo;ve got the audio, text intro, and image you&amp;rsquo;re ready to publish the podcast. There are a ton of solutions out there, but I found the quickest and easiest way to get this done was via &lt;a href="https://creators.spotify.com/"&gt;Spotify for Creators&lt;/a&gt;, which is free if you&amp;rsquo;re already a Spotify user.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_376203b82650190b.webp 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_80a1e11941b883f3.webp 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_91962447b32f37c2.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_a1cab98348162598.png" srcset="https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_930064e0b1bbb680.png 400w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_a1cab98348162598.png 800w, https://fortes.com/2025/kids-podcasts-with-notebooklm/2025-spotify-creators-episodes_hu_67d2d3b20d949c73.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Generate episodes in bulk and leave them scheduled" width="800" height="434" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Generate episodes in bulk and leave them scheduled&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This part is even easier than the rest, so figure it out on your own. One recommendation I do have is to generate episodes in bulk, and then schedule them out. Given your (likely) advanced age, you&amp;rsquo;ll probably forget what you created so it&amp;rsquo;ll be a fun surprise for the whole family.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;C&amp;rsquo;mon, you don&amp;rsquo;t really need help with this, do you?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;PS: If you&amp;rsquo;re looking for non-AI-generated podcasts for kids, &lt;a href="https://strangeanimalspodcast.blubrry.net/"&gt;Strange Animals&lt;/a&gt; and &lt;a href="https://open.spotify.com/show/0KvuZJgXfOGanBRuq6B9dm"&gt;Extinct Zoo&lt;/a&gt; are the best of the ones that resonated with my children.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>2024 Personal Tech Stack</title><link>https://fortes.com/2024/personal-tech-stack/</link><guid>https://fortes.com/2024/personal-tech-stack/</guid><pubDate>Tue, 31 Dec 2024 00:00:00 +0000</pubDate><description>An on-spectrum overview of the tools and services I use regularly</description><content:encoded>&lt;p&gt;As a neckbeard who started coding in the 1900s, I&amp;rsquo;ve steadfastly held on to my terminal-centered workflow. You might think that using tools that are decades old would mean things are nice and stable, but due to childhood brain trauma I keep on tweaking and adjusting my setup.&lt;/p&gt;
&lt;p&gt;So, in order to help the thousands of future historians dedicated to studying my life in the future, here&amp;rsquo;s a snapshot of my personal tech stack as of the end of 2024.&lt;/p&gt;
&lt;h2 id="too-many-machines-too-many-platforms"&gt;Too many machines, too many platforms&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2024/personal-tech-stack/2024-office-setup.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_88764e66198456fc.webp 400w, https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_f8b341361cce4fa4.webp 800w, https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_f86b4bbb9e28bee9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_563026bf8788bd54.jpg" srcset="https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_b7254a1cbd0fd5b6.jpg 400w, https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_563026bf8788bd54.jpg 800w, https://fortes.com/2024/personal-tech-stack/2024-office-setup_hu_cc11c0306b6d699f.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="A very reasonable number of computers (not visible: desktop that is a deskbelow and home server)" width="800" height="533" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;A very reasonable number of computers (not visible: desktop that is a deskbelow and home server)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;After over five years of not using macOS, I&amp;rsquo;m now on an M3 for my day-to-day development at &lt;a href="https://coda.io"&gt;Coda&lt;/a&gt; due to reasons beyond my control. This means I currently use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;M3 MacBook Pro&lt;/strong&gt;: My primary work driver. It&amp;rsquo;s hefty for a laptop, but is mostly plugged in, driving three monitors with ease.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desktop&lt;/strong&gt;: A six-year-old random Dell with 64GB of RAM and plenty of drive space. Dual-boots Windows 11 and Debian Bookworm, but stays in Debian except for some &lt;a href="https://en.wikipedia.org/wiki/StarCraft_II"&gt;occasional Windows gaming&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework laptop&lt;/strong&gt;: Runs Windows 11, with heavy use of WSL2 for CLI for testing and some personal use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chromebook Pixel&lt;/strong&gt;: For personal use and travel, it&amp;rsquo;s lightweight and has great battery life. Crostini Linux support is pretty great, so I keep my terminal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headless server&lt;/strong&gt;: For my home lab, runs Debian Bookworm. Gets ssh&amp;rsquo;d into and has a few long-running &lt;code&gt;tmux&lt;/code&gt; sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Swapping between these mostly works fine since I&amp;rsquo;ve invested a ton of time in order to normalize as much as possible (see below), but there are plenty of paper cuts especially with macOS since it&amp;rsquo;s the only BSD and doesn&amp;rsquo;t use &lt;code&gt;apt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In 2025, I&amp;rsquo;m hoping to simplify this a bit. I&amp;rsquo;d love to return to desktop Linux as my primary work environment.&lt;/p&gt;
&lt;h2 id="dotfiles"&gt;Dotfiles&lt;/h2&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_5b7dc2caf5348451.webp 400w, https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_c4ad074c90b628fc.webp 800w, https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_694c3af412cb89f9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_80542a89dbf7978f.png" srcset="https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_2ac0b3734c80776f.png 400w, https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_80542a89dbf7978f.png 800w, https://fortes.com/2024/personal-tech-stack/2024-terminal-screenshot_hu_7b7f9d54b5147218.png 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Neovim and tmux in action" width="800" height="399" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Neovim and tmux in action&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Over a decade ago, inspired by a &lt;a href="https://github.com/dcreemer/dotfiles"&gt;coworker&lt;/a&gt;, I moved all my configuration files into a &lt;a href="https://github.com/fortes/dotfiles"&gt;git repository&lt;/a&gt; and created some simple installation scripts.&lt;/p&gt;
&lt;p&gt;Years later, I&amp;rsquo;ve changed pretty much everything about the setup. It started off as macOS only, then added Linux, followed by Chromebook via Crouton (IYKYK) and later Crostini, then added Windows via WSL2, removed macOS, and now macOS has returned again.&lt;/p&gt;
&lt;p&gt;The core pieces are mostly unchanged throughout the years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash&lt;/code&gt; + &lt;code&gt;tmux&lt;/code&gt;: I&amp;rsquo;ve done &lt;code&gt;zsh&lt;/code&gt; for a while, but came back to bash due to incompatibilities. I&amp;rsquo;ve debated trying &lt;code&gt;zsh&lt;/code&gt; again but haven&amp;rsquo;t felt the need. Originally I started on &lt;code&gt;screen&lt;/code&gt;, since I learned that in the 1900s, but &lt;code&gt;tmux&lt;/code&gt; is actively developed and has been great.&lt;/li&gt;
&lt;li&gt;Neovim: Moved from Vim a while back, and the ecosystem continues to mature very quickly. I try to use IDEs every once in a while, but thankfully the combination of LSP and Copilot being available in Neovim means I&amp;rsquo;m not missing out on much.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fzf&lt;/code&gt;, &lt;code&gt;fd&lt;/code&gt;, &lt;code&gt;ripgrep&lt;/code&gt;, &lt;code&gt;eza&lt;/code&gt;, &lt;code&gt;bat&lt;/code&gt;, &lt;code&gt;zoxide&lt;/code&gt;: These next gen CLI tools are awesome and you should use them as well.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmus&lt;/code&gt;: Terminal-based music player that uses minimal resources and can be scripted. I love it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My installation scripts set up all these tools and their configuration, so I can hop through different machines and keep a consistent experience.&lt;/p&gt;
&lt;p&gt;Window management ends up being the biggest difference, with i3/Sway tiling window managers on Linux being far and away my favorite. Chromebook and Windows both have some decent built-in shortcuts, but macOS absolutely requires installing &lt;a href="https://rectangleapp.com/"&gt;Rectangle&lt;/a&gt; in order to maintain any kind of efficiency.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve tried switching to VS Code / Cursor, but it never sticks for more than a few days. There are still too many things that I do that are just much faster in a terminal.&lt;/p&gt;
&lt;h2 id="llm-tools"&gt;LLM Tools&lt;/h2&gt;
&lt;p&gt;Although I&amp;rsquo;ve &lt;a href="https://coda.io/blog/about-coda/introducing-coda-brain"&gt;been working on an AI-powered product&lt;/a&gt;, only recently have I started to integrate LLMs into my terminal workflow (other than code completion via &lt;a href="https://github.com/github/copilot.vim"&gt;Copilot in Neovim&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;My two favorites are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/llm"&gt;llm&lt;/a&gt;: Fantastic CLI utility that connects to pretty much every LLM provider out there. I find myself using this all the time, some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick questions where I don&amp;rsquo;t want to open a browser&lt;/li&gt;
&lt;li&gt;Piping files / command output into it for summarization or transformation&lt;/li&gt;
&lt;li&gt;Drafting commit messages based on git diffs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ve done some experimenting with local models via &lt;a href="https://ollama.com/"&gt;ollama&lt;/a&gt;, it&amp;rsquo;s pretty neat to be able to run models locally, but it&amp;rsquo;s significantly slower than the cloud models and my use cases aren&amp;rsquo;t crazy so the monthly price is trivial anyway.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://aider.chat/"&gt;aider&lt;/a&gt;: This is a nice terminal-based AI coding assistant that can read your codebase, understand git history, and work iteratively on changes. It can use various models based on your API keys, and they have a &lt;a href="https://aider.chat/2024/12/21/polyglot.html"&gt;very cool benchmark&lt;/a&gt; for new models as they come out. The pace of improvement here has been fast, and I&amp;rsquo;ve been able to partially outsource some small fixes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, &lt;a href="https://github.com/olimorris/codecompanion.nvim"&gt;codecompanion.nvim&lt;/a&gt; is a nice Neovim plugin that also supports the major providers and lets you chat within Neovim. Very useful to be able to quickly move text between the editor and the chat (obviously, also possible via tmux, but vim registers are far more powerful).&lt;/p&gt;
&lt;h2 id="other-2024-changes"&gt;Other 2024 Changes&lt;/h2&gt;
&lt;p&gt;I kept on getting nerd sniped into trying out some new tools or fixing things. Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I pulled the trigger and switched my Linux desktop from X11/i3 to Wayland/Sway. This took a ton of work and in the end it&amp;rsquo;s maybe 5% better and a few things are worse.&lt;/li&gt;
&lt;li&gt;I moved from &lt;a href="https://alacritty.org/"&gt;Alacritty&lt;/a&gt; to &lt;a href="https://codeberg.org/dnkl/foot"&gt;Foot&lt;/a&gt; and then finally to &lt;a href="https://wezterm.org"&gt;WezTerm&lt;/a&gt;. Looking back, this was mostly a distraction, as they&amp;rsquo;re all pretty similar these days. &lt;a href="https://ghostty.org/"&gt;Ghostty&lt;/a&gt; looks promising but isn&amp;rsquo;t quite there yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are probably other examples, but my brain is likely blocking them as a self-defense mechanism.&lt;/p&gt;
&lt;h2 id="non-terminal-tools"&gt;Non-Terminal Tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signal Desktop&lt;/strong&gt;: About half of my messaging is via Signal, maybe a third is WhatsApp, and the remainder are SMS (not including automated alerts, of course). Would love to have everyone on Signal, but it&amp;rsquo;s not realistic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1Password&lt;/strong&gt;: Moved over from LastPass many years ago and no regrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firefox&lt;/strong&gt; &amp;amp; &lt;strong&gt;Chrome&lt;/strong&gt;: I switched to Firefox as my primary browser for personal use, but have kept Chrome as my primary for work. Firefox on Android is awesome because you can run an ad blocker on it, which just makes the web actually somewhat usable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coda&lt;/strong&gt;: Unsurprisingly, I run a fair amount of my life via Coda docs, including a personal tracker / journal that probably deserves its own post.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spotify&lt;/strong&gt;: Not perfect, but it works fine for me.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="misc-tools--devices"&gt;Misc Tools / Devices&lt;/h2&gt;
&lt;p&gt;They don&amp;rsquo;t live on my computer, but a few related things that I use (and enjoy) regularly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treadmill desk&lt;/strong&gt;: I&amp;rsquo;ve had a sit-stand desk for years, but this year I finally added a treadmill. I try to do most of my meetings while walking, and can comfortably do about 2.5 mph (4 km/h). For light coding, I can pull off around 1.5 mph (2.5 km/h), but generally for tougher things I sit down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kindle&lt;/strong&gt;: I&amp;rsquo;ve had a Kindle since the first one came out in 2007, and cannot imagine going back to paper books for reading. The portability (and built-in lighting) is too difficult to give up.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gl-inet.com/products/gl-mt3000/"&gt;&lt;strong&gt;Beryl AX Travel Router&lt;/strong&gt;&lt;/a&gt;: Spent a fair amount of time on the road with family this year, and having a consistent private WiFi network at each location saves a ton of time. You only need to authenticate once per location, then all devices just connect automatically after that. This lets me also travel with a Chromecast and have it work everywhere. WireGuard is supported on the router, making it easy to access my network back home.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Pixel 7&lt;/strong&gt;: I moved over to Android some years ago when the cameras were (briefly) better than iOS and have stayed mostly due to inertia. The Pixel 7 is getting a bit old, so I&amp;rsquo;ll likely switch next year.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="whats-next-in-2025"&gt;What&amp;rsquo;s next in 2025?&lt;/h2&gt;
&lt;p&gt;Everything&amp;rsquo;s changing so quickly, I won&amp;rsquo;t pretend to know. My biggest concern is that the Neovim ecosystem may not be able to keep up with the polish and integration of AI tools in VS Code and friends. The vim bindings aren&amp;rsquo;t bad, but I&amp;rsquo;ve optimized so much in tmux / Neovim that I always feel slower in VS Code.&lt;/p&gt;
&lt;p&gt;Or maybe I&amp;rsquo;ll finally realize that tweaking my vim setup for the hundredth time is squandering the precious little time I have left on this earth and if I just spent a fraction of that effort into my mental and physical health I could be so much happier.&lt;/p&gt;
&lt;p&gt;Oh, and also &lt;a href="https://yazi-rs.github.io/"&gt;yazi&lt;/a&gt; looks like a potential replacement for &lt;a href="https://github.com/vifm/vifm"&gt;vifm&lt;/a&gt;!&lt;/p&gt;</content:encoded></item><item><title>Hierarchical Tags in Hugo</title><link>https://fortes.com/2023/hierarchical-tags-in-hugo/</link><guid>https://fortes.com/2023/hierarchical-tags-in-hugo/</guid><pubDate>Thu, 13 Jul 2023 00:00:00 +0000</pubDate><description>Making your posts a dessert topping &lt;em&gt;and&lt;/em&gt; a floor wax</description><content:encoded>&lt;p&gt;Once again, if you&amp;rsquo;re not &lt;a href="https://fortes.com/2023/hugo-debug-meta-tags/"&gt;using Hugo to build a static site&lt;/a&gt;, your time is better spent elsewhere. Or maybe it&amp;rsquo;s not? I won&amp;rsquo;t pretend to know what&amp;rsquo;s going on with your life.&lt;/p&gt;
&lt;p&gt;Hugo has a &lt;a href="https://gohugo.io/content-management/taxonomies/"&gt;taxonomy system&lt;/a&gt; that is quite flexible. By default, the &lt;code&gt;tag&lt;/code&gt; field is already enabled, so you can quickly organize posts by adding tag values into the post frontmatter like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Edan - Beauty and the Beat&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;tags&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;experimental-hip-hop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;psychedelic-rock&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now that post will show up in both &lt;code&gt;/tags/experimental-hip-hop&lt;/code&gt; and &lt;code&gt;/tags/psychedelic-rock&lt;/code&gt;. But what if we want it to also appear in &lt;code&gt;tags/hip-hop&lt;/code&gt;? One option is to create the following directory structure:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;content/
├── tags/
 ├── hip-hop/
 ├── experimental-hip-hop/
 ├── _index.md
 ├── rock/
 ├── psychedelic-rock/
 ├── _index.md
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But then, instead of tagging with just &lt;code&gt;experimental-hip-hop&lt;/code&gt;, you&amp;rsquo;ll have to use &lt;code&gt;hip-hop/experimental-hip-hop&lt;/code&gt; which is wordy and error-prone. Also, each tag will only be able to have a single parent.&lt;/p&gt;
&lt;p&gt;Instead, we&amp;rsquo;ll try something different: we&amp;rsquo;ll add tags to the tag pages themselves. For example, create a file at &lt;code&gt;content/tags/experimental-hip-hop/_index.md&lt;/code&gt; and add the following frontmatter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Experimental Hip-Hop&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;tags&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;hip-hop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This by itself won&amp;rsquo;t do much, we still won&amp;rsquo;t see &lt;i&gt;Beauty and the Beat&lt;/i&gt; when visiting &lt;code&gt;/tags/hip-hop&lt;/code&gt;. The next step is to change the layout of our tag pages in order to also pull all pages from any child tags. Typically, the template for a tag page will look something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Pages&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{/* Post rendering logic goes here... */}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.Pages&lt;/code&gt; collection will only grab pages with the specific tag (e.g. &lt;code&gt;hip-hop&lt;/code&gt;), it does not include any child tags. We can adjust the code to include child tags by first adding a new &lt;code&gt;partial&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; Returns a slice of pages matching the given tag, including all pages from child tags.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; Usage:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; {{ $pages := partial &amp;#34;partials/get_descendant_tag_pages&amp;#34; (dict &amp;#34;Page&amp;#34; .) }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;*/}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;define&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;partials/get_descendant_tag_pages&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;or&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Matches&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;slice&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$all_tag_pages&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;site&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Pages&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;.Kind&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;eq&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;term&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;.Type&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;eq&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;where&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$all_tag_pages&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;.Params.Tags&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;intersect&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;slice&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page.Data.Term&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;in&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;append&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$child_tag_pages&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;partials/get_descendant_tag_pages&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dict&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Page&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Matches&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;union&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$child_tag_pages&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$matches&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, update the template for the tag page to use the result of this partial instead of &lt;code&gt;.Pages&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$pages&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;partials/get_descendant_tag_pages&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dict&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Page&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$pages&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{/* Post rendering logic goes here... */}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, when you visit &lt;code&gt;/tags/hip-hop&lt;/code&gt; you&amp;rsquo;ll see &lt;i&gt;Beauty and the Beat&lt;/i&gt; listed along with any other pages tagged with &lt;code&gt;experimental-hip-hop&lt;/code&gt;. One nice thing about this technique is that you can add multiple parent tags to a single tag page, so you can make &lt;code&gt;nursery-rhymes&lt;/code&gt; a child of both &lt;code&gt;children&lt;/code&gt; and &lt;code&gt;death-metal&lt;/code&gt; in order to confuse your headbanging friends.&lt;/p&gt;</content:encoded></item><item><title>Using FZF in a Deno Script</title><link>https://fortes.com/2023/using-fzf-in-deno-scripts/</link><guid>https://fortes.com/2023/using-fzf-in-deno-scripts/</guid><pubDate>Tue, 23 May 2023 00:00:00 +0000</pubDate><description>Piping to FZF, with extra steps</description><content:encoded>&lt;p&gt;At some point, all of my Bash scripts become incomprehensible enough that I&amp;rsquo;m forced to re-write them in another language. Usually, I&amp;rsquo;d switch into Python (which I&amp;rsquo;m also terrible at) since it&amp;rsquo;s broadly available or trivial to install. Lately, I&amp;rsquo;ve started using &lt;a href="https://deno.com/"&gt;Deno&lt;/a&gt; instead, since it means I can use TypeScript which I actually (kinda) know. Deno is &lt;a href="https://github.com/fortes/dotfiles/blob/7ef0d6bd903693d58174c5f769d5b4925a4ae46c/scripts/setup_github_packages#L157-L163"&gt;a little more work to install&lt;/a&gt;, but otherwise has some nice ergonomics for CLI tools for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deno supports TypeScript natively, so you get type safety without needing to compile the code&lt;/li&gt;
&lt;li&gt;No need to install or run a package manager like &lt;code&gt;pip&lt;/code&gt; or &lt;code&gt;npm&lt;/code&gt; to install dependencies, &lt;code&gt;deno&lt;/code&gt; takes care of it automatically when running the script&lt;/li&gt;
&lt;li&gt;The Standard Library has a &lt;a href="https://deno.land/std@0.187.0/flags/mod.ts"&gt;solid command line argument parser&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Startup time is faster than Node, so it&amp;rsquo;s not a huge pain to run a script for a one-off task&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you know, &lt;a href="https://fortes.com/2022/make-git-better-with-fzf/"&gt;I&amp;rsquo;m a big fan of FZF&lt;/a&gt;, and I use it in my bash scripts all the time. There&amp;rsquo;s a &lt;a href="https://github.com/ajitid/fzf-for-js"&gt;library that mimics&lt;/a&gt; the fuzzy-find algorithm, but it&amp;rsquo;s mostly meant for the browser so it doesn&amp;rsquo;t handle terminal input and display (especially nice touches like honoring &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Readline-Interaction.html"&gt;readline keyboard shortcuts&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Fortunately, Deno has some nice APIs for spawning external commands, so we can just use the real &lt;code&gt;fzf&lt;/code&gt; binary instead. Here&amp;rsquo;s some code I&amp;rsquo;ve used in a few places in order to spawn FZF in order to do some interactive selection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FzfSelection&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/** Value displayed to the user, must not contain newlines */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/** Unique identifier for this selection, must not contain spaces */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FzfOptions&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/** Allow multiple selections */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;allowMultiple?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/** Automatically select if only one item */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;autoSelectSingle?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BASE_FZF_ARGUMENTS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;--cycle&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;--no-sort&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;--bind&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ctrl-a:select-all,ctrl-d:deselect-all&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;--with-nth&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;2..&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Let the user make selections interactively via FZF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getUserSelections&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;T&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;extends&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FzfSelection&lt;/span&gt;&amp;gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;T&lt;/span&gt;[],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &lt;span style="color:#a6e22e"&gt;allowMultiple&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;autoSelectSingle&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; }&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FzfOptions&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;T&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;[]&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;length&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;length&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;autoSelectSingle&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fzf&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Deno&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Command&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`fzf`&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;args&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [...&lt;span style="color:#a6e22e"&gt;BASE_FZF_ARGUMENTS&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;allowMultiple&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;--multi&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;].&lt;span style="color:#a6e22e"&gt;filter&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Boolean,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;stdin&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;piped&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;stdout&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;piped&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;stderr&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;inherit&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fzf&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;spawn&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;choiceList&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;line&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;line&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;line&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Write the choices to stdin
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;encoder&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TextEncoder&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;writer&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stdin&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getWriter&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Must use trailing newline, otherwise last item won&amp;#39;t appear
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writer&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;write&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;encoder&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;encode&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;choiceList&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// User can now interact with fzf to filter and select
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// This will now wait until the process exits
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;code&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;success&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;stdout&lt;/span&gt; } &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;output&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;success&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;code&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#75715e"&gt;// No match
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;130&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#75715e"&gt;// Command terminated by user
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Error(&lt;span style="color:#e6db74"&gt;`fzf exited with status &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;code&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lines&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TextDecoder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;decode&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;stdout&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;trim&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;selectedIds&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;split&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;line&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;line&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;split&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34; &amp;#34;&lt;/span&gt;)[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;items&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;filter&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;item&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;selectedIds&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;includes&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;item&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here&amp;rsquo;s an example of how you&amp;rsquo;d use it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;KombuchaFlavors&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;1&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Gingerade ($3.99)&amp;#39;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;2&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Multi-Green ($3.49)&amp;#39;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Guava Goddess ($3.99)&amp;#39;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;4&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Island Bliss ($2.99)&amp;#39;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;5&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Strawberry Serenity ($4.99)&amp;#39;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;flavorsToOrder&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getUserSelections&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;KombuchaFlavors&lt;/span&gt;, {&lt;span style="color:#a6e22e"&gt;allowMultiple&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;You ordered: &amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;flavorsToOrder&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;flavor&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;flavor&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;, &amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And here it is in action:&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2023/using-fzf-in-deno-scripts/deno-fzf-demo.gif"&gt;&lt;img src="https://fortes.com/2023/using-fzf-in-deno-scripts/deno-fzf-demo.gif" alt="Deno script using FZF interactively" width="400" height="209" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Deno script using FZF interactively&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Note that there are a few edge cases that aren&amp;rsquo;t handled here: specifically, you need to make sure the &lt;code&gt;id&lt;/code&gt; does not have any spaces, and there cannot be any newlines in the &lt;code&gt;display&lt;/code&gt; value. If you want to get fancy, you could stream the options into FZF as they&amp;rsquo;re generated, but that will be left as an exercise to the reader.&lt;/p&gt;</content:encoded></item><item><title>Debugging Hugo via meta tags</title><link>https://fortes.com/2023/hugo-debug-meta-tags/</link><guid>https://fortes.com/2023/hugo-debug-meta-tags/</guid><pubDate>Tue, 10 Jan 2023 00:00:00 +0000</pubDate><description>Start using your &lt;code&gt;&lt;head&gt;&lt;/code&gt; when working</description><content:encoded>&lt;p&gt;If you don&amp;rsquo;t use &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; to build a website, then you&amp;rsquo;re gonna be real bored here. Go ahead and skip this page, take advantage of your time savings and do a quick stretch or maybe a few push-ups.&lt;/p&gt;
&lt;p&gt;It can be tricky to understand Hugo&amp;rsquo;s &lt;a href="https://gohugo.io/templates/lookup-order/"&gt;template lookup order&lt;/a&gt;, as well as the properties available on the &lt;code&gt;Page&lt;/code&gt; object. After a bunch of frustration and semi-permanent hair loss, I came up with the following helper partial:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; Output a bunch of `&amp;lt;meta&amp;gt;` tags for the given page
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; Expects a Page object
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;*/}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;define&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;partials/debug_meta_tags.html&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-bundle-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.BundleType&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-categories&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.categories&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-fuzzy-word-count&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.FuzzyWordCount&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-home&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.IsHome&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-node&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.IsNode&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-page&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.IsPage&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-keywords&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Keywords&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-kind&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Kind&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-lastmod&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Lastmod&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.PrevInSection&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-prev-in-section&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.NextInSection&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-next-in-section&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.File&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-file-path&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Path&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Aliases&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-alias&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-section&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Section&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Type&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.tags&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-tag&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Ancestors&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-ancestor&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Parent&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Parent.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent-kind&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Parent.Kind&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Parent.Type&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Pages&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-child-page-path&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Take that code and put it in a file called &lt;code&gt;debug_meta_tags.tmpl&lt;/code&gt; in your &lt;code&gt;layouts/partials&lt;/code&gt; directory. Now in your &lt;code&gt;baseof.html&lt;/code&gt; layout (or wherever you&amp;rsquo;re defining your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;) add the following within the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;site&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.IsServer&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;print&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;!-- Debug info only available when running a local server --&amp;gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeHTML&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;debug_meta_tags&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;print&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;!-- End debug info --&amp;gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeHTML&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The nice thing about using &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags is that they&amp;rsquo;re not displayed in the browser, so it doesn&amp;rsquo;t affect the design of your site. Also, the &lt;code&gt;site.IsServer&lt;/code&gt; conditional means these tags will only be output when you&amp;rsquo;re running your local development server (i.e. &lt;code&gt;hugo server&lt;/code&gt;), so they won&amp;rsquo;t be part of your deployed site. Here&amp;rsquo;s an example of what the output looks like for this post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- Debug info only available when running a local server --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-bundle-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;leaf&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-categories&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-fuzzy-word-count&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;400&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-home&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;false&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-node&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;false&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-is-page&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-keywords&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;[]&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-kind&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;page&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-lastmod&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;2023-01-10 00:00:00 &amp;amp;#43;0000 UTC&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-prev-in-section&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/2022/make-git-better-with-fzf/&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-file-path&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts/2023-hugo-debug-meta-tags/index.md&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-section&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-ancestor&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/posts/&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-ancestor&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/posts/&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent-kind&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;section&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-parent-type&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- End debug info --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These are also easily viewed from the browser&amp;rsquo;s developer tools in the element inspector, so you don&amp;rsquo;t even need to view source.&lt;/p&gt;
&lt;p&gt;Of course, for this stupid site over half these fields are not of much interest. But maybe it&amp;rsquo;s useful for your erotic, yet tasteful, Paw Patrol fanfic site!&lt;/p&gt;</content:encoded></item><item><title>Unlock SSH keys with the 1Password CLI</title><link>https://fortes.com/2022/unlock-ssh-keys-with-the-1password-cli/</link><guid>https://fortes.com/2022/unlock-ssh-keys-with-the-1password-cli/</guid><pubDate>Wed, 28 Sep 2022 00:00:00 +0000</pubDate><description>Use the 1Password CLI and a small shell script to unlock SSH keys without copy-pasting passphrases</description><content:encoded>&lt;p&gt;Like any self-respecting, somewhat security conscious nerd I make sure my SSH keys have a stupidly-long passphrase. Obviously, I use a &lt;a href="https://1password.com"&gt;password manager&lt;/a&gt; to store my passwords. But instead of copying and pasting the passphrase from 1Password into the terminal like some kind of 12th-century peasant, let&amp;rsquo;s walk through how to grab the passphrase out of 1Password automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you&amp;rsquo;re running the desktop application, 1Password can actually &lt;a href="https://developer.1password.com/docs/ssh/agent/"&gt;serve as an SSH Agent&lt;/a&gt;. If that setup works for you, then I recommend going that direction instead and stop reading immediately. I spent precious time I&amp;rsquo;ll never get back figuring out how to do this because I can&amp;rsquo;t run the desktop application as an SSH agent due to reasons so unimportant you&amp;rsquo;d instantly die of boredom if I told you.&lt;/p&gt;
&lt;h2 id="how-ssh_askpass-works"&gt;How &lt;code&gt;SSH_ASKPASS&lt;/code&gt; works&lt;/h2&gt;
&lt;p&gt;Normally &lt;code&gt;ssh-add&lt;/code&gt; reads your passphrase straight from the terminal. But pipe something into it and it thinks there&amp;rsquo;s no terminal, and if &lt;code&gt;DISPLAY&lt;/code&gt; and &lt;code&gt;SSH_ASKPASS&lt;/code&gt; are set it&amp;rsquo;ll run whatever program &lt;code&gt;SSH_ASKPASS&lt;/code&gt; points to, using the output as the passphrase. This was designed for GUI password dialogs, but nothing stops us from abusing it in the terminal.&lt;/p&gt;
&lt;p&gt;The trick is an empty pipe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo | SSH_ASKPASS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;op_ask_pass ssh-add
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ssh-add&lt;/code&gt; calls &lt;code&gt;op_ask_pass&lt;/code&gt;, reads its output as the passphrase, and we never have to touch the clipboard.&lt;/p&gt;
&lt;p&gt;I use two helper scripts for the workflow. &lt;code&gt;op_ask_pass&lt;/code&gt; calls &lt;code&gt;op_get_pass&lt;/code&gt; with &lt;code&gt;--categories &amp;quot;SSH Key&amp;quot;&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Use 1Password to unlock SSH keys&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Usage:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# echo | SSH_ASKPASS=op_ask_pass ssh-add&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IFS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;$&amp;#39;\n\t&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; op_get_pass --categories &lt;span style="color:#e6db74"&gt;&amp;#34;SSH Key&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;@&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;op_get_pass&lt;/code&gt; handles signing in to 1Password, uses &lt;a href="https://github.com/junegunn/fzf"&gt;fzf&lt;/a&gt; so you can fuzzy-search through your items, then outputs the password:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Fuzzy find a password from 1Password and output to stdout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IFS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;$&amp;#39;\n\t&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local -r user_id&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;op account list --format json | jq -r &lt;span style="color:#e6db74"&gt;&amp;#39;.[].user_uuid&amp;#39;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local -r variable_name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;OP_SESSION_&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;user_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local op_session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; op_session&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;tmux show-environment -g &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;variable_name&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | cut -d&lt;span style="color:#f92672"&gt;=&lt;/span&gt; -f2-&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; ! op whoami --session &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;op_session&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &amp;amp;&amp;gt; /dev/null; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Sign in to 1Password first&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; op_session&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;op signin --raw&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tmux set-environment -g &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;variable_name&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;op_session&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Select item via FZF and output password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# shellcheck disable=SC2068&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; op item list --session &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;op_session&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;@&lt;span style="color:#e6db74"&gt;}&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | fzf --no-multi --header-lines&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; --nth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1.. --with-nth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;2.. --exit-0 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | cut -d&lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt; -f1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | xargs --no-run-if-empty &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; op item get --fields&lt;span style="color:#f92672"&gt;=&lt;/span&gt;password --session &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;op_session&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;@&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The 1password session token gets added to the tmux global environment, so you only have to sign in once per tmux session.&lt;/p&gt;
&lt;h2 id="copy-any-password-within-tmux"&gt;Copy any password within tmux&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;op_get_pass&lt;/code&gt; isn&amp;rsquo;t just for SSH keys, I have &lt;code&gt;&amp;lt;prefix&amp;gt; .&lt;/code&gt; open a popup to put a password into the tmux paste buffer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Copy password from 1Password to tmux paste buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt;-shell &lt;span style="color:#e6db74"&gt;&amp;#39;command -v op&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bind-key -N &lt;span style="color:#e6db74"&gt;&amp;#34;Copy password to paste buffer&amp;#34;&lt;/span&gt; . &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display-popup -T &lt;span style="color:#e6db74"&gt;&amp;#34;#[fg=cyan] 1Password&amp;#34;&lt;/span&gt; -w 80% -h 80% -E -E &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;bash -c &amp;#34;set -o pipefail; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; op_get_pass | tmux load-buffer - \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;amp;&amp;amp; tmux display-message -N \&amp;#34;Copied to paste buffer\&amp;#34;&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yet another super-brittle hacky workflow with obvious security risks all just to avoid using a GUI, success!&lt;/p&gt;</content:encoded></item><item><title>Make Git better with FZF</title><link>https://fortes.com/2022/make-git-better-with-fzf/</link><guid>https://fortes.com/2022/make-git-better-with-fzf/</guid><pubDate>Mon, 13 Jun 2022 00:00:00 +0000</pubDate><description>How to look like a cool movie hacker while using Git</description><content:encoded>&lt;p&gt;Like any nerd who spends way too much time in the command-line, I absolutely love &lt;a href="https://github.com/junegunn/fzf"&gt;FZF&lt;/a&gt;, a fantastic fuzzy finder that&amp;rsquo;s been an essential part of my workflow for years now.&lt;/p&gt;
&lt;p&gt;By default, FZF doesn&amp;rsquo;t do much but it really shines when you use it in conjunction with other tools. I use it on a near-daily basis with Git, here&amp;rsquo;s the relevant section of my &lt;code&gt;.gitconfig&lt;/code&gt; file, the full file is available in my &lt;a href="https://github.com/fortes/dotfiles"&gt;dotfiles repo&lt;/a&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-gitconfig" data-lang="gitconfig"&gt;# Place in `~/.gitconfig` or `~/.config/git/config`
[alias]
 addm = &amp;#34;!git ls-files --deleted --modified --other --exclude-standard | fzf -0 -m --preview &amp;#39;git diff --color=always {-1}&amp;#39; | xargs -r git add&amp;#34;
 addmp = &amp;#34;!git ls-files --deleted --modified --exclude-standard | fzf -0 -m --preview &amp;#39;git diff --color=always {-1}&amp;#39; | xargs -r -o git add -p&amp;#34;
 cb = &amp;#34;!git branch --all | grep -v &amp;#39;^[*+]&amp;#39; | awk &amp;#39;{print $1}&amp;#39; | fzf -0 --preview &amp;#39;git show --color=always {-1}&amp;#39; | sed &amp;#39;s/remotes\\/origin\\///g&amp;#39; | xargs -r git checkout&amp;#34;
 cs = &amp;#34;!git stash list | fzf -0 --preview &amp;#39;git show --pretty=oneline --color=always --patch \&amp;#34;$(echo {} | cut -d: -f1)\&amp;#34;&amp;#39; | cut -d: -f1 | xargs -r git stash pop&amp;#34;
 db = &amp;#34;!git branch | grep -v &amp;#39;^[*+]&amp;#39; | awk &amp;#39;{print $1}&amp;#39; | fzf -0 --multi --preview &amp;#39;git show --color=always {-1}&amp;#39; | xargs -r git branch --delete&amp;#34;
 Db = &amp;#34;!git branch | grep -v &amp;#39;^[*+]&amp;#39; | awk &amp;#39;{print $1}&amp;#39; | fzf -0 --multi --preview &amp;#39;git show --color=always {-1}&amp;#39; | xargs -r git branch --delete --force&amp;#34;
 ds = &amp;#34;!git stash list | fzf -0 --preview &amp;#39;git show --pretty=oneline --color=always --patch \&amp;#34;$(echo {} | cut -d: -f1)\&amp;#34;&amp;#39; | cut -d: -f1 | xargs -r git stash drop&amp;#34;
 edit = &amp;#34;!git ls-files --modified --other --exclude-standard | sort -u | fzf -0 --multi --preview &amp;#39;git diff --color {}&amp;#39; | xargs -r $EDITOR -p&amp;#34;
 fixup = &amp;#34;!git log --oneline --no-decorate --no-merges | fzf -0 --preview &amp;#39;git show --color=always --format=oneline {1}&amp;#39; | awk &amp;#39;{print $1}&amp;#39; | xargs -r git commit --fixup&amp;#34;
 resetm = &amp;#34;!git diff --name-only --cached | fzf -0 -m --preview &amp;#39;git diff --color=always {-1}&amp;#39; | xargs -r git reset&amp;#34;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;rsquo;s what each command does, along with a demo for a few of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;addm&lt;/code&gt;: Add new or modified files, use &lt;code&gt;&amp;lt;tab&amp;gt;&lt;/code&gt; to select multiple, patch mode via &lt;code&gt;addmp&lt;/code&gt;. Preview shows the file diff.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cb&lt;/code&gt;: Select branch to switch to, preview shows last commit&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2022/make-git-better-with-fzf/2022-git-cb.gif"&gt;&lt;img src="https://fortes.com/2022/make-git-better-with-fzf/2022-git-cb.gif" alt="Using FZF to switch git branches" width="400" height="277" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Using FZF to switch git branches&lt;/figcaption&gt;
 &lt;/figure&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cs&lt;/code&gt;: Choose stash to apply, preview shows diff&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;db&lt;/code&gt;: Delete branches, use &lt;code&gt;&amp;lt;tab&amp;gt;&lt;/code&gt; to select multiple, force delete via &lt;code&gt;Db&lt;/code&gt;. Preview shows last commit&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2022/make-git-better-with-fzf/2022-git-db.gif"&gt;&lt;img src="https://fortes.com/2022/make-git-better-with-fzf/2022-git-db.gif" alt="Using FZF to delete git branches" width="400" height="277" loading="lazy" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Using FZF to delete git branches&lt;/figcaption&gt;
 &lt;/figure&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ds&lt;/code&gt;: Delete a stash, preview shows diff&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;edit&lt;/code&gt;: Choose modified files to edit, use &lt;code&gt;&amp;lt;tab&amp;gt;&lt;/code&gt; to select multiple (one tab per file). Preview shows diff&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2022/make-git-better-with-fzf/2022-git-edit.gif"&gt;&lt;img src="https://fortes.com/2022/make-git-better-with-fzf/2022-git-edit.gif" alt="Using FZF to edit modified files" width="400" height="277" loading="lazy" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Using FZF to edit modified files&lt;/figcaption&gt;
 &lt;/figure&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fixup&lt;/code&gt;: Commit staged files as a fixup, fuzzy-finding the original commit. Preview shows commit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resetm&lt;/code&gt;: Choose staged files to reset, use &lt;code&gt;&amp;lt;tab&amp;gt;&lt;/code&gt; to select multiple. Preview shows cached diff&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href="https://github.com/junegunn/fzf/wiki/examples#git"&gt;FZF wiki&lt;/a&gt; has a few more examples if you&amp;rsquo;re looking for further inspiration. Hopefully, this will change your life as much as it did mine. Just make sure you don&amp;rsquo;t scream out &lt;em&gt;&amp;ldquo;I love you, FZF!&amp;rdquo;&lt;/em&gt; during an intimate moment just like &amp;hellip; a friend of mine did. He lives in Canada, you wouldn&amp;rsquo;t know him.&lt;/p&gt;</content:encoded></item><item><title>Extracting FitBit Weight Data</title><link>https://fortes.com/2022/extracting-fitbit-weight-data/</link><guid>https://fortes.com/2022/extracting-fitbit-weight-data/</guid><pubDate>Fri, 18 Feb 2022 00:00:00 +0000</pubDate><description>Stealing your history back from FitBit, minus the janky interpolated values</description><content:encoded>&lt;p&gt;Being just barely off the spectrum, I&amp;rsquo;m inexorably drawn to measuring and recording all sorts of things. Without going into the (boring) details, I&amp;rsquo;ve been weighing myself on a near-daily basis since my mid-twenties. In 2013, I gave into temptation and got an internet-connected scale that automatically uploads to FitBit, depriving me of the joy of manual data entry every morning.&lt;/p&gt;
&lt;p&gt;Like any normal, well-adjusted person, I want to run esoteric multivariate analysis on my weight data. FitBit provides basic export functionality, but it&amp;rsquo;s not made for my &lt;em&gt;serious business™&lt;/em&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Only weight is provided, no body fat percentage&lt;/li&gt;
&lt;li&gt;If there is no measurement in a given day, the weight value is silently interpolated between the nearest measurements&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Though I can be a bit obsessive about data, I&amp;rsquo;m fine living without the body fat percentage, since these cheap scales aren&amp;rsquo;t particularly accurate anyway. However, interpolating weight data across days is simply unacceptable, and I&amp;rsquo;m &lt;a href="https://community.fitbit.com/t5/Fitbit-com-Dashboard/Exporting-weight-loss-and-interpolated-data/td-p/1861996"&gt;not the only one incredibly offended&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After waiting a few years for FitBit developers to fix this, today I decided to take matters into my own hands and get the data myself. I&amp;rsquo;ve documented the steps here for my future self along with other weight-conscious obsessives.&lt;/p&gt;
&lt;p&gt;If you go to the &lt;a href="https://www.fitbit.com/weight"&gt;FitBit Weight Page&lt;/a&gt;, there&amp;rsquo;s a nice little chart you can use to see all your historical measurements without any interpolated values. Poking around in the browser developer tools, you&amp;rsquo;ll see that this chart is generated on the browser-side which means that at some point in time the raw data is downloaded by the browser. This is fantastic news.&lt;/p&gt;
&lt;p&gt;Fire up the &lt;a href="https://developer.chrome.com/docs/devtools/network/"&gt;Network Panel&lt;/a&gt; in the browser web tools and keep an eye out for a request to the following URL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://web-api.fitbit.com/1.1/user/-/body/log/weight/graph/all.json?durationType=all
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you take a look at the server response, you&amp;rsquo;ll see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;graphValues&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bmi&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;23.9&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;dateTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;2013-09-05T12:00:00&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;fat&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;19.358999252319336&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;weight&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;80900&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bmi&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;23.83&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;dateTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;2013-09-06T12:00:00&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;fat&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;19.68600082397461&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;weight&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;80670&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// &amp;lt;thousands of entries omitted&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://youtu.be/O5s3Oj2cPgc?t=77"&gt;That&amp;rsquo;s a bingo!&lt;/a&gt; The weight value is provided in grams, and it even includes the body fat percentage, which is missing from FitBit&amp;rsquo;s export.&lt;/p&gt;
&lt;p&gt;Save this huge JSON blob to its own file called &lt;code&gt;all.json&lt;/code&gt;. Now you&amp;rsquo;re gonna have to open up the command line and use &lt;a href="https://stedolan.github.io/jq/"&gt;jq&lt;/a&gt; to convert this JSON into something easier to use. We&amp;rsquo;ll start by running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jq &lt;span style="color:#e6db74"&gt;&amp;#39;.graphValues[] | [ .dateTime, .weight, .fat ]&amp;#39;&lt;/span&gt; all.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I don&amp;rsquo;t bother grabbing the &lt;abbr title="Body Mass Index"&gt;BMI&lt;/abbr&gt; data here because it&amp;rsquo;s easily derived later. That &lt;code&gt;jq&lt;/code&gt; command above will output thousands of lines that look something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;2013-09-05T12:00:00&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;80900&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;19.358999252319336&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;2013-09-06T12:00:00&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;80670&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;19.68600082397461&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isn&amp;rsquo;t quite usable yet, so let&amp;rsquo;s ask &lt;code&gt;jq&lt;/code&gt; to convert it to &lt;abbr title="Comma Separated Values"&gt;CSV&lt;/abbr&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jq -r &lt;span style="color:#e6db74"&gt;&amp;#39;.graphValues[] | [ .dateTime, .weight, .fat ] | @csv&amp;#39;&lt;/span&gt; all.json &amp;gt; all.csv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hooray, now you have everything in a &lt;abbr title="Comma Separated Values"&gt;CSV&lt;/abbr&gt; file which you can use to import into Excel, Sheets, or (my biased choice) &lt;a href="https://coda.io"&gt;Coda&lt;/a&gt;. Don&amp;rsquo;t forget to convert grams into your preferred unit, which is either dram, mercantile stone, or pennyweight, depending on which 19th century British village you hail from. Now it&amp;rsquo;s time for you to touch grass and burn some calories, nerd.&lt;/p&gt;</content:encoded></item><item><title>Letterbox Blur with ImageMagick</title><link>https://fortes.com/2021/letterbox-blur-with-imagemagick/</link><guid>https://fortes.com/2021/letterbox-blur-with-imagemagick/</guid><pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate><description>Putting round pegs into square image holes</description><content:encoded>&lt;p&gt;Web designers (and &lt;a href="https://www.wired.com/2015/08/instagram-says-goodbye-square-photos/"&gt;pre-2015 Instagram users&lt;/a&gt;) have all experienced tyrannical enforcement of square crops for images. If you&amp;rsquo;ve got an image with a non-square aspect ratio, you have two reasonable choices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Crop the image to a 1:1 aspect ratio&lt;/em&gt;: Fairly easy to do manually. When doing this automatically, the naïve technique is to crop from the center, but there are some clever heuristics out there to get better results.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Letterbox/Pillarbox&lt;/em&gt;: Keep the entire image visible, filling the excess space by surrounding the image with (typically) black bars.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(Of course, you could stretch the image to force a new aspect ratio, but it&amp;rsquo;s pretty much guaranteed to look bad)&lt;/p&gt;
&lt;p&gt;The terms &amp;ldquo;Letterbox&amp;rdquo; and &amp;ldquo;Pillarbox&amp;rdquo; come from the film world, as they&amp;rsquo;ve been &lt;a href="https://en.wikipedia.org/wiki/Letterboxing_(filming)"&gt;dealing with the issue forever&lt;/a&gt;. With the increased frequency of vertical video, a recent technique has been to replace the traditional black bars with a stretched and blurred version of the video, which creates a neat effect. There doesn&amp;rsquo;t seem to be a standard name for the blurred letterbox effect, though some people call it &amp;ldquo;blanking fill&amp;rdquo; since that&amp;rsquo;s the name used in &lt;a href="https://www.blackmagicdesign.com/products/davinciresolve"&gt;DaVinci Resolve&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to do this effect purely with CSS (an exercise left to the reader), but due to reasons beyond my control, I recently had a situation where using CSS wasn&amp;rsquo;t an option so once again &lt;a href="https://imagemagick.org"&gt;ImageMagick&lt;/a&gt; comes to the rescue. Here&amp;rsquo;s the command I used:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;convert image.jpg &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;\(&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -clone &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -blur 0x20 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -resize 800x800&lt;span style="color:#ae81ff"&gt;\!&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -modulate 80,50,100 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;\)&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +swap &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -gravity center -compose over -composite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output.jpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s no good way to put comments into a multi-line bash command, so here&amp;rsquo;s what the command does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make a copy of the original image&lt;/li&gt;
&lt;li&gt;Blur by 20 pixels&lt;/li&gt;
&lt;li&gt;Stretch to force a square aspect ratio&lt;/li&gt;
&lt;li&gt;80% brightness, 50% saturation&lt;/li&gt;
&lt;li&gt;Center the original image on top of the blurred copy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s the output for a landscape and a portrait image&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_5ac13cb43937226e.webp 400w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_482c232ba86af4f4.webp 800w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_26af62647cfa9332.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_fdff9172eb233413.jpg" srcset="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_1d3de95e6a4b1eea.jpg 400w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_fdff9172eb233413.jpg 800w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-portrait_hu_fdf789c49567ac43.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Pillarbox blur on a portrait image" width="800" height="800" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Pillarbox blur on a portrait image&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_22c6648bb66d204.webp 400w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_fe357cb468fce7fe.webp 800w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_18b47c9f10078670.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_fc918ba6928c5287.jpg" srcset="https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_8e654ec8314ae141.jpg 400w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_fc918ba6928c5287.jpg 800w, https://fortes.com/2021/letterbox-blur-with-imagemagick/2021-letterbox-blur-with-imagemagick-landscape_hu_c6e50b3fa158aefa.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Letterbox blur on a landscape image" width="800" height="800" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Letterbox blur on a landscape image&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;To convert a bunch of images in bulk, feel free to use this bash script which is, as always, of &lt;a href="https://fortes.com/2019/bash-script-args-and-stdin/"&gt;questionable quality&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Create a square image, letter/pillarbox blur effect if aspect ratio not already square&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;get_largest_dimension&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local dimensions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dimensions&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;identify -format &lt;span style="color:#e6db74"&gt;&amp;#34;%w\n%h&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | sort --numeric-sort --reverse --unique&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dimensions&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | wc -l&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#39;square&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$dimensions&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | head -n &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; filepath in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; ! -r &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Can&amp;#39;t read file &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;, skipping&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local dim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dim&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;get_largest_dimension &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local dest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local ext
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local filename
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ext&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filename##*.&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dest&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;dirname &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename -s &lt;span style="color:#e6db74"&gt;&amp;#34;.&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;ext&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-square.&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;ext&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$dim&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;square&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; already square, linking to &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dest&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Link file so we don&amp;#39;t take up excess disk space&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ln &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dest&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Converting &lt;/span&gt;$filepath&lt;span style="color:#e6db74"&gt; into &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dim&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;x&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dim&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; square&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; convert &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;\(&lt;/span&gt; -clone &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; -blur 0x20 -resize &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dim&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;x&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;dim&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\!&lt;/span&gt; -modulate 80,50,100 &lt;span style="color:#ae81ff"&gt;\)&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +swap -gravity center -compose over -composite &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$dest&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Saved &lt;/span&gt;$dest&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title>Scripting Photo Collages with ImageMagick</title><link>https://fortes.com/2021/image-collage-via-imagemagick/</link><guid>https://fortes.com/2021/image-collage-via-imagemagick/</guid><pubDate>Sun, 17 Jan 2021 00:00:00 +0000</pubDate><description>Automating something I don’t even do anymore</description><content:encoded>&lt;p&gt;Back before everyone was working from home, I wrote about my &lt;a href="https://fortes.com/2019/working-from-home-green-screen/"&gt;Green Screen setup&lt;/a&gt; and shared a selection of daily screenshots from meetings. Now that everyone works from home, the joke is a bit tired (and I got lazy) so I no longer bother with the green screen. My co-workers can now see my office filled with three crates of pandemic-panic-purchased toilet paper that I&amp;rsquo;m still working my way through.&lt;/p&gt;
&lt;p&gt;I never got around to automating the collection of action shots from that post. Zoom has an API but my feeble attention span didn&amp;rsquo;t last long enough to find a good way to programmatically capture a daily image. Manual screenshots were simple enough, though I did often forget due to the fact that I was born in the 1900s and it&amp;rsquo;s probably already past my bedtime.&lt;/p&gt;
&lt;p&gt;I did, however, have a simple script for generating the 3x3 images from screenshots using &lt;a href="https://imagemagick.org/"&gt;ImageMagick&lt;/a&gt;. Much like exiftool, which I use extensively for &lt;a href="https://fortes.com/2015/command-line-photo-organization/"&gt;photo management&lt;/a&gt;, ImageMagick is one of those programs that have been around forever and can be coaxed to do just about anything via arcane command-line flags.&lt;/p&gt;
&lt;p&gt;Though it&amp;rsquo;s been years, I &lt;a href="https://fortes.com/2019/bash-script-args-and-stdin/"&gt;still don&amp;rsquo;t know what I&amp;rsquo;m doing&lt;/a&gt; when writing these things so I&amp;rsquo;ll share here in case I ever need it again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Creates 3x3 montages of given images, saved to current directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Usage:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# $ ./create_montage.sh [files...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local resized_dir
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resized_dir&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;mktemp -d&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local iter
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iter&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; filename in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iter&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;expr $iter + 1&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Resizing &lt;/span&gt;$filename&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; convert &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$filename&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; -resize x400 -gravity center -crop 600x400+0+0 +repage &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$resized_dir&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$iter&lt;span style="color:#e6db74"&gt;.png&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;expr $iter % 9&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Creating montage &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;expr $iter / 9&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; montage &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$resized_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;/*.png -mode Concatenate &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;montage-&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;expr $iter / 9&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rm &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$resized_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;/*.png
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;It&amp;#39;s time &lt;/span&gt;$iter&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Processed &lt;/span&gt;$iter&lt;span style="color:#e6db74"&gt; collages&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rm -rf &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$resized_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It&amp;rsquo;s not the prettiest, but it did the job. Or at least it used to. Maybe it still does? I guess we&amp;rsquo;ll never know.&lt;/p&gt;</content:encoded></item><item><title>Getting Debian to send emails that actually get delivered</title><link>https://fortes.com/2020/debian-configure-email-sending/</link><guid>https://fortes.com/2020/debian-configure-email-sending/</guid><pubDate>Tue, 02 Jun 2020 00:00:00 +0000</pubDate><description>Making sure your algorithmic love letters don’t get lost</description><content:encoded>&lt;p&gt;As &lt;a href="https://fortes.com/2020/linux-disk-encryption-luks-and-lvm/"&gt;I&amp;rsquo;ve warned&lt;/a&gt; you &lt;a href="https://fortes.com/2019/bash-script-args-and-stdin/"&gt;before&lt;/a&gt; I barely know how to turn on a computer, let alone operate it securely. Given enough time, I do occasionally manage to get things working after spending an unreasonable amount of time (and often losing data). Sometimes, only sometimes, I&amp;rsquo;m smart enough to write down a few steps so my future forgetful self can blindly repeat them in the future.&lt;/p&gt;
&lt;p&gt;In today&amp;rsquo;s episode, we&amp;rsquo;ll get a Debian machine to be able to send out emails. This should be simple, and probably is if you know what you&amp;rsquo;re doing, but took me a while to get right and actually ensure that emails arrived.&lt;/p&gt;
&lt;h2 id="get-an-smtp-provider"&gt;Get an SMTP Provider&lt;/h2&gt;
&lt;p&gt;Back in &lt;em&gt;ye olde golden era&lt;/em&gt; of the Internet, just about any computer could send an email. Thanks to a bunch of jackasses who spammed the entire Internet over the past few decades, sending email that actually gets delivered is best left to the professionals these days.&lt;/p&gt;
&lt;p&gt;So the first step is to find some service that provides an SMTP gateway for your machine to send emails through. In theory, you could use any old GMail account here, but I&amp;rsquo;m slightly paranoid and don&amp;rsquo;t want Google account credentials sitting on whatever old computer I have laying around.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href="https://sendgrid.com/free/"&gt;SendGrid has a free tier&lt;/a&gt; that lets you send 100 emails a day for free. This is more than enough for my needs, and probably yours unless you&amp;rsquo;re trying to help some foreign prince transfer funds, in which case I suggest you jump off a cliff.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve set up a SendGrid account, go and create a &lt;a href="https://app.sendgrid.com/settings/api_keys"&gt;dedicated API key&lt;/a&gt; for your computer. You should only need the &amp;ldquo;Mail Send&amp;rdquo; access details. I&amp;rsquo;d recommend a unique key per machine you&amp;rsquo;re using, but you do you.&lt;/p&gt;
&lt;h2 id="ssmtp"&gt;SSMTP&lt;/h2&gt;
&lt;p&gt;Next we&amp;rsquo;ll install &lt;a href="https://wiki.debian.org/sSMTP"&gt;SSMTP&lt;/a&gt; in order to send emails. Why SSMTP? Because it&amp;rsquo;s the first one I found, so obviously it&amp;rsquo;s the best.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install ssmtp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now edit the &lt;code&gt;/etc/ssmtp/ssmtp.conf&lt;/code&gt; file in order to have the SendGrid credentials (obviously, you should replace &lt;code&gt;example.com&lt;/code&gt;, the &lt;code&gt;AuthPass&lt;/code&gt;, etc with your own values). If you&amp;rsquo;re using some other SMTP provider, then you gotta figure out what to put here on your own:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# The person who gets all mail for userids &amp;lt; 1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;root&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;postmaster&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# SMTP info&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;mailhub&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;smtp.sendgrid.net:587&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AuthUser&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;apikey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AuthPass&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;INSERT_GENERATED_PASSWORD_HERE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;UseSTARTTLS&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Where will the mail seem to come from?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;rewriteDomain&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;hostname&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;my-computer.example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Don&amp;#39;t let users specify their own `From:` address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;FromLineOverride&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;NO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can now test if this works locally via:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;To: Lovely Human &amp;lt;you@example.com&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;From: Your Computer &amp;lt;you@example.com&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;Subject: Hello, from your computer
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;Greetings from your computer, hopefully you see this!&amp;#39;&lt;/span&gt; | sudo ssmtp you@example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assuming you&amp;rsquo;ve done everything correctly, this email should arrive in your inbox. If not, check the &lt;a href="https://app.sendgrid.com/email_activity"&gt;SendGrid Activity Feed&lt;/a&gt; and see if there is an error message. If you still can&amp;rsquo;t figure it out, you should reconsider your life choices and think about writing that novel you&amp;rsquo;ve been putting off.&lt;/p&gt;
&lt;h2 id="sending-mail-externally"&gt;Sending Mail Externally&lt;/h2&gt;
&lt;p&gt;There are a bunch of programs that run on your machine that will try to send email from a local user, so we need to make sure those don&amp;rsquo;t get lost due to being invalid. We do that by editing the &lt;code&gt;/etc/ssmtp/revaliases&lt;/code&gt; (create it if it doesn&amp;rsquo;t exist):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# location_account:delivery_address:smtp_server:smtp_port
root:you@example.com:smtp.sendgrid.net:587
postmaster:you@example.com:smtp.sendgrid.net:587
you:you@example.com:smtp.sendgrid.net:587
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a mapping such that mail &lt;em&gt;from&lt;/em&gt; the given user is rewritten to be from an external address instead of something like &lt;code&gt;you@your-machine&lt;/code&gt; which will probably get rejected by most email providers.&lt;/p&gt;
&lt;p&gt;Now test by sending an email to yourself via the command line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;Hello Sysadmin!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;This message was sent via the command line&amp;#39;&lt;/span&gt; | mail -s &lt;span style="color:#e6db74"&gt;&amp;#34;Message From the Command Line&amp;#34;&lt;/span&gt; you@example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once again, check the &lt;a href="https://app.sendgrid.com/email_activity"&gt;SendGrid Activity Feed&lt;/a&gt; if you don&amp;rsquo;t see the email and consider painting or some other more productive hobby if things still aren&amp;rsquo;t working.&lt;/p&gt;
&lt;h2 id="loose-ends"&gt;Loose ends&lt;/h2&gt;
&lt;p&gt;In theory, you should be all set to receive emails. That didn&amp;rsquo;t seem to be the case for me though, so there are a few places I had to go through and make more changes that I don&amp;rsquo;t really understand.&lt;/p&gt;
&lt;h3 id="unattended-upgrades"&gt;Unattended Upgrades&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re running &lt;a href="https://wiki.debian.org/UnattendedUpgrades"&gt;unattended upgrades&lt;/a&gt; to automatically update your machine, you&amp;rsquo;ll probably want to change how those emails get sent. Edit &lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Replace the line with `Unattended-Upgrade::Mail &amp;#34;&amp;#34;;`
Unattended-Upgrade::Mail &amp;#34;you@example.com&amp;#34;;
Unattended-Upgrade::Sender &amp;#34;Machine Upgrades &amp;lt;you@example.com&amp;gt;&amp;#34;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Assuming things worked, you&amp;rsquo;ll now get an email whenever any automatic updates are installed.&lt;/p&gt;
&lt;h3 id="sudo-authentication-failures"&gt;&lt;code&gt;Sudo&lt;/code&gt; Authentication Failures&lt;/h3&gt;
&lt;p&gt;By default, &lt;code&gt;sudo&lt;/code&gt; will try to email when someone attempts to call &lt;code&gt;sudo&lt;/code&gt; but doesn&amp;rsquo;t enter the correct password. In order to get those emails delivered, I had to add the following into &lt;code&gt;/etc/sudoers&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Defaults mailto=&amp;#34;you@example.com&amp;#34;
Defaults mailfrom=&amp;#34;you@example.com&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To test, try to run any command via &lt;code&gt;sudo&lt;/code&gt; without entering the correct password.&lt;/p&gt;
&lt;h3 id="cron"&gt;Cron&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re using &lt;a href="https://wiki.debian.org/cron"&gt;Cron&lt;/a&gt; to run some periodic tasks, you might not know that your system emails you the output of each command. At least on my machine, this was going into some dark place never to be seen again. To fix this, run &lt;code&gt;crontab -e&lt;/code&gt; and add the following at the top:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-crontab" data-lang="crontab"&gt;MAILFROM=you@example.com
MAILTO=you@example.com

# Cron jobs go below
0 5 * * * echo &amp;#34;It&amp;#39;s 5am, did you know your computer is on?&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By this point, you&amp;rsquo;re old enough to test this on your own.&lt;/p&gt;
&lt;h2 id="other-places"&gt;Other Places&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m pretty certain I&amp;rsquo;ve missed others, I&amp;rsquo;ll add those whenever I discover them.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Hopefully your life is slightly better now that your computer can send emails. Mine isn&amp;rsquo;t, but I like to pretend.&lt;/p&gt;</content:encoded></item><item><title>Linux disk encryption with LUKS and LVM</title><link>https://fortes.com/2020/linux-disk-encryption-luks-and-lvm/</link><guid>https://fortes.com/2020/linux-disk-encryption-luks-and-lvm/</guid><pubDate>Fri, 28 Feb 2020 00:00:00 +0000</pubDate><description>An unreasonable number of steps to get reasonable disk security on Linux</description><content:encoded>&lt;p&gt;I don&amp;rsquo;t let lack of knowledge stop me from trying to run and maintain a few Linux machines. Just like &lt;a href="https://fortes.com/2019/bash-script-args-and-stdin/"&gt;writing bash scripts&lt;/a&gt;, I&amp;rsquo;m always forgetting how I did things previously so I&amp;rsquo;m writing it all down this time so I can copy and paste it again later in a few years.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t reach tinfoil hat level paranoia, but I do like having some level of data security so thieves can&amp;rsquo;t read my journal and figure out which BTS member I&amp;rsquo;m currently crushing on (Jungkook). Full disk encryption is pretty easy on Windows or Mac, and it seems easy on Linux, but you always have to be on your toes in the land of the penguin. Specifically, the following two requirements are typically at odds:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Full disk encryption&lt;/li&gt;
&lt;li&gt;Ability to boot without being physically present&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;ve only ever used &lt;abbr title="Linux Unified Key Setup"&gt;&lt;a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup"&gt;LUKS&lt;/a&gt;&lt;/abbr&gt; for disk encryption because it&amp;rsquo;s the default with Debian and I&amp;rsquo;m too lazy to research alternatives. The default setup has a tiny unencrypted boot partition that prompts you to unlock the main disk so you can actually use the computer. This is fine for a desktop or laptop, but not really tenable for situations where you can&amp;rsquo;t physically type in the password on the machine.&lt;/p&gt;
&lt;p&gt;Fortunately, there&amp;rsquo;s a nifty package called &lt;a href="https://packages.debian.org/buster/dropbear-initramfs"&gt;&lt;code&gt;dropbear-initramfs&lt;/code&gt;&lt;/a&gt; which adds a barebones SSH server to that tiny boot partition which allows you to unlock the machine remotely. Installation is pretty straightforward, you&amp;rsquo;ll need to set up your SSH key by creating a &lt;code&gt;/etc/dropbear-initramfs/authorized_keys&lt;/code&gt; file and adding your public key:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command=&amp;#34;/bin/cryptroot-unlock&amp;#34; ssh-rsa &amp;lt;SSH public key goes here&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;command=/bin/cryptroot-unlock&lt;/code&gt; part is optional, but very convenient since it means you&amp;rsquo;ll automatically be prompted to unlock the drive whenever you connect to the server. It&amp;rsquo;s also a way to lock down the machine a bit more. Once you&amp;rsquo;ve added your key, make sure to set the correct permissions and make another offering to the boot gods:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# File is ignored if permissions aren&amp;#39;t strict&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt; /etc/dropbear-initramfs/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Changes don&amp;#39;t take effect if you don&amp;#39;t update!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo update-initramfs -u
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can now reboot your machine and (assuming it can get network access) it will open up an SSH server so you can connect and input the encryption passphrase. Note that the server key for this miniserver is gonna be different than your server once booted. So I recommend doing the following in your &lt;code&gt;~/.ssh/config&lt;/code&gt; file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-ssh_config" data-lang="ssh_config"&gt;Host *_unlock
 UserKnownHostsFile /home/fortes/.ssh/known_hosts_unlock
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will make sure SSH doesn&amp;rsquo;t freak out and worry that your server is being hijacked all the time.&lt;/p&gt;
&lt;p&gt;So now the machine can boot without a sweet human caress, congratulations you might be done!&lt;/p&gt;
&lt;h2 id="adding-encrypted-drives"&gt;Adding encrypted drives&lt;/h2&gt;
&lt;p&gt;If you have more than one drive, you still have a little more work left. This will set up what is apparently called &lt;a href="https://wiki.archlinux.org/index.php/Dm-crypt/Encrypting_an_entire_system#LVM_on_LUKS"&gt;&lt;abbr title="Logical Volume Manager"&gt;LVM&lt;/abbr&gt; on LUKS&lt;/a&gt;, a popular alternative is called LUKS on LVM and it might be better for you? The &lt;a href="https://wiki.archlinux.org/index.php/Dm-crypt/Encrypting_an_entire_system#LVM_on_LUKS"&gt;Arch Wiki&lt;/a&gt; has a lot of information that I definitely did not read.&lt;/p&gt;
&lt;p&gt;Make sure your drive is connected, and then you&amp;rsquo;ll need to figure out what device it&amp;rsquo;s been assigned. This is typically something like &lt;code&gt;/dev/sdb&lt;/code&gt; or whatever. The best way to find out which disk you want is via &lt;code&gt;lsblk&lt;/code&gt; (or possibly &lt;code&gt;sudo blkid&lt;/code&gt;, I can never remember). If the drive was previously formatted, you may need wipe out the existing partitions via &lt;code&gt;fdisk /dev/sdb&lt;/code&gt;. The interface is (of course) a bit cryptic, you&amp;rsquo;ll need to keep on hitting &lt;code&gt;d&lt;/code&gt; at the prompt to delete all the partitions. I &lt;em&gt;highly&lt;/em&gt; recommend double and triple checking which drive you&amp;rsquo;re operating on, since recovering from deleting all those partitions is not very fun. Or you can just YOLO, whatever I&amp;rsquo;m not your dad.&lt;/p&gt;
&lt;p&gt;Now that the drive is wiped, it&amp;rsquo;s time to set up encryption. You should generate a nice long passphrase and save it in your password manager. The partition can also be unlocked with the contents of a static file, and we&amp;rsquo;ll use that for the automatic unlocking:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# You should be _very_ confident about this choice&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export DRIVE_TO_ADD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/dev/sdc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Create a file full of random noise used&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dd &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/dev/random bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; count&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; of&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/root/.data-keyfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Unless you trust everyone/everything with access to your machine, you should&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# lock down permissions for the keyfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo chmod &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt; /root/.data-keyfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# This will prompt you for a passphrase, pick a secure one and store it in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# your password manager (you should rarely, if ever, need to type it in)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cryptsetup luksFormat &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;DRIVE_TO_ADD&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Add the generated file as a second encryption key (will prompt you for the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# passphrase you just generated)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cryptsetup luksAddKey &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;DRIVE_TO_ADD&lt;span style="color:#e6db74"&gt;}&lt;/span&gt; /root/.data-keyfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The disk is now encrypted. We still need to do a few things to get it mounted automatically though. First, add an entry to &lt;code&gt;/etc/crypttab&lt;/code&gt; in order to make the disk decrypt on boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Append to /etc/crypttab&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Find the disk UUID via `sudo blkid`, which will output something like:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /dev/sdc: UUID=&amp;#34;YOUR-DISK-UUID&amp;#34; TYPE=&amp;#34;crypto_LUKS&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sdc_crypt UUID&lt;span style="color:#f92672"&gt;=&lt;/span&gt;YOUR-DISK-UUID /root/.data-keyfile luks,discard
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This file gets used by the system to automatically decrypt drives at boot. Although the decryption key is a file on the system, it&amp;rsquo;s sitting the main (encrypted) drive that&amp;rsquo;s only accessible after unlocking the entire machine at boot.&lt;/p&gt;
&lt;p&gt;Now test to make sure it gets automatically decrypted via:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cryptdisks_start sdc_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assuming no errors, you should have the unencrypted disk available at &lt;code&gt;/dev/mapper/sdc_crypt&lt;/code&gt;. In theory, you should now be able to format this directly via &lt;code&gt;mkfs.ext4&lt;/code&gt; or your favorite disk format.&lt;/p&gt;
&lt;p&gt;On my machines, I still go ahead and set up my partitions via LVM since that&amp;rsquo;s what Debian does by default and I&amp;rsquo;m just cargo culting my way through this whole thing. In theory there is a bunch of flexibility to be gained via LVM, but I&amp;rsquo;ve never actually learned how it all works.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# You might need to do this, LVM will complain about device filter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo wipefs -a &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;DRIVE_TO_ADD&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Create a bunch of LVM stuff that I don&amp;#39;t understand&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo pvcreate /dev/mapper/sdc_crypt &lt;span style="color:#75715e"&gt;# confirm with `pvdisplay /dev/mapper/sdc_crypt`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo vgcreate data-vg /dev/mapper/sdc_crypt &lt;span style="color:#75715e"&gt;# confirm with `vgdisplay data-vg`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo lvcreate -n data -l100%FREE data-vg &lt;span style="color:#75715e"&gt;# confirm with `lvdisplay /dev/data-vg/data`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we&amp;rsquo;re ready to actually format the drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkfs.ext4 -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; /dev/data-vg/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In order to automatically mount the drive, we need to edit &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Add into /etc/fstab&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/dev/mapper/data--vg-data /mnt/data ext4 defaults,user,noatime,nofail &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test that the &lt;code&gt;/etc/fstab&lt;/code&gt; setup worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Voilà, you should now see your disk mounted at &lt;code&gt;/mnt/data&lt;/code&gt;, and all it took was a crazy number of error-prone steps written by a total stranger with, at best, a passing understanding of how it all works.&lt;/p&gt;</content:encoded></item><item><title>Working From Everywhere</title><link>https://fortes.com/2019/working-from-home-green-screen/</link><guid>https://fortes.com/2019/working-from-home-green-screen/</guid><pubDate>Wed, 17 Jul 2019 00:00:00 +0000</pubDate><description>How to make your co-workers think you’re working from Mordor</description><content:encoded>&lt;p&gt;For the past year and a half, I&amp;rsquo;ve been working remotely for &lt;a href="https://coda.io"&gt;Coda&lt;/a&gt;. Fortunately, this has worked well since the company was already distributed with offices in California and Washington State.&lt;/p&gt;
&lt;p&gt;Coda relies heavily on video conferencing, we use Zoom which has a &lt;a href="https://support.zoom.us/hc/en-us/articles/210707503-Virtual-Background"&gt;virtual background feature&lt;/a&gt; that lets you choose a custom image as your backdrop, absolutely fooling all your co-workers into thinking that you&amp;rsquo;re not actually working from home (assuming your co-workers are as gullible as mine).&lt;/p&gt;
&lt;p&gt;Zoom uses chroma key compositing to add a virtual background, which works best with a green backdrop since it doesn&amp;rsquo;t match non-Martian skin tones. I don&amp;rsquo;t like doing things halfway, so I decided to go all out and spend about $70 to buy a &lt;a href="https://amzn.to/2DGnIf8"&gt;green screen&lt;/a&gt; and &lt;a href="https://amzn.to/2qcHnQv"&gt;stand&lt;/a&gt; in order to make the most realistic virtual scene possible.&lt;/p&gt;
&lt;p&gt;The set up is relatively painless and can be done in a couple of minutes, as shown here by a handsome anonymous model:&lt;/p&gt;
&lt;figure class="video"&gt;
 &lt;iframe src="https://www.youtube.com/embed/x0DWXTz-Kew" width="560" height="315" loading="lazy" allowfullscreen allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="no-referrer-when-downgrade" title="YouTube video"&gt;&lt;/iframe&gt;
&lt;/figure&gt;

&lt;p&gt;Every once in a while, I remember to take screen captures of the backgrounds I&amp;rsquo;ve used. I&amp;rsquo;ve collected a few here (I&amp;rsquo;ll try to update these, but no promises):&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_37a9051da6b9beed.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_67aa63068205153.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_5d6c19fac73dda9b.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_fb7d255d5f8d9057.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_8db8430d2d72993a.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_fb7d255d5f8d9057.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-2_hu_448297b909dfe97.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Dogs and Cats live together at the US Open before the Benfica match in a Black Hole within the White House Sauna which arrived via NYC subway to the Indy 500" width="800" height="533" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Dogs and Cats live together at the US Open before the Benfica match in a Black Hole within the White House Sauna which arrived via NYC subway to the Indy 500&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_8e8f51597a0c9675.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_b3bb40370b964f18.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_b7ef361d7109cc04.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_7f08ba7442791d4.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_2fd187f390b6bfa6.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_7f08ba7442791d4.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-3_hu_e01948aeb30a1d08.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="English countryside in Bellevue / West Village with Winterfell&amp;#39;s armies supporting Wreck-It Ralph in the Spider-Verse in Bliss on Mars in King&amp;#39;s Landing." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;English countryside in Bellevue / West Village with Winterfell&amp;#39;s armies supporting Wreck-It Ralph in the Spider-Verse in Bliss on Mars in King&amp;#39;s Landing.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_75c2f4629453706.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_7a4c8a6e4cd7181c.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_4941e43be5b8a582.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_15bf914c23446a0c.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_4e75f8c0ce592bb8.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_15bf914c23446a0c.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-4_hu_ad523295b1801762.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="King&amp;#39;s Landing gets Hamilton with source code in a doctor&amp;#39;s office on the NYSE candle-lit floor inside an airplane hosting the Democratic debates with E.T. in the background." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;King&amp;#39;s Landing gets Hamilton with source code in a doctor&amp;#39;s office on the NYSE candle-lit floor inside an airplane hosting the Democratic debates with E.T. in the background.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_bc2312cd1a445802.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_611d00eb5422e53e.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_ec4d3dec37384f6f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_4661aa38969d8cea.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_d7578ef3c972f5a9.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_4661aa38969d8cea.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-5_hu_9c07074ce5946272.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Tennis and Cricket in the Matrix, Paw Patrol goes for graffiti while Coda&amp;#39;s band eats sushi in the everglades before floods." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Tennis and Cricket in the Matrix, Paw Patrol goes for graffiti while Coda&amp;#39;s band eats sushi in the everglades before floods.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_23313d8398642948.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_e2db4a7cd7df1ebb.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_1a17a1ddcc585751.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_d67a5918211c7453.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_8f0c8d43335c5d73.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_d67a5918211c7453.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-6_hu_fd82f29238df5d63.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Greenlandic Hong Kong protestors visit the Sahara via TWA after a stop at the NYSE maze, followed by the G7 meeting before returning to a Floridian hurricane." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Greenlandic Hong Kong protestors visit the Sahara via TWA after a stop at the NYSE maze, followed by the G7 meeting before returning to a Floridian hurricane.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_ab38ae55f33482.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_f9e566826b71b830.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_d298a90e6d3fbf11.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_b85890c15f936ec0.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_fc1101e9dbfe3f42.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_b85890c15f936ec0.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-7_hu_4994c9b7b23b127c.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Hurricane Dorian goes to Vermont with a lion cub at the Paris Fashion Week with Democratic candidates in a submarine on a Tel Aviv rooftop party full of bugs from Dinoco." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Hurricane Dorian goes to Vermont with a lion cub at the Paris Fashion Week with Democratic candidates in a submarine on a Tel Aviv rooftop party full of bugs from Dinoco.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_986a6892668c43fc.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_8e97adaeff9ed6bd.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_def8723f7857e61c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_94bee2b7a7221fbe.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_806d7d88e95fc6f1.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_94bee2b7a7221fbe.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-8_hu_c08681092fdffe15.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="The Coda team together in Lisbon via ancient boat that got upgraded in the SF Bay before the launch party on Halloween which also happened to be voting day for revolutionary war veterans." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;The Coda team together in Lisbon via ancient boat that got upgraded in the SF Bay before the launch party on Halloween which also happened to be voting day for revolutionary war veterans.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;&lt;a href="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9.jpg"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_d64693fabd1250ed.webp 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_f6e4dcd3fe9a1e6.webp 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_c510d67300e9e5eb.webp 1600w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_befd233718f45ec0.jpg" srcset="https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_ce4f06bd8b69a3a.jpg 400w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_befd233718f45ec0.jpg 800w, https://fortes.com/2019/working-from-home-green-screen/2019-green-screen-montage-9_hu_8f46be27ab88cfd6.jpg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Abbey road intersects with Penny Lane in the Antarctic glaciers during Thanksgiving aftermath in a library near the airport with the royal family and Jumanji." width="800" height="533" loading="lazy" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Abbey road intersects with Penny Lane in the Antarctic glaciers during Thanksgiving aftermath in a library near the airport with the royal family and Jumanji.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Have suggestions for an upcoming background? &lt;a href="https://twitter.com/fortes"&gt;Let me know on Twitter&lt;/a&gt;!&lt;/p&gt;</content:encoded></item><item><title>Merging piped stdin with command-line arguments within Bash</title><link>https://fortes.com/2019/bash-script-args-and-stdin/</link><guid>https://fortes.com/2019/bash-script-args-and-stdin/</guid><pubDate>Tue, 02 Apr 2019 00:00:00 +0000</pubDate><description>How to avoid using &lt;code&gt;xargs&lt;/code&gt; at all costs</description><content:encoded>&lt;p&gt;I am computer nerd who is easily seduced by &lt;a href="https://xkcd.com/1319/"&gt;automation&lt;/a&gt;, which means I end up writing a fair number of shell scripts. Even though I&amp;rsquo;ve written my fair share of Bash, my small primate brain never quite grasps the language and I always copy-paste my way through the task at hand, absorb nothing, and then pat myself on the back for being such a &lt;em&gt;1337 h4x0r&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Typically my needs are quite pedestrian, and I can immediately find an answer to plagiarize. My latest script, however, wasn&amp;rsquo;t solved within seconds so I&amp;rsquo;ve written up the solution for my very handsome future self.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The &amp;ldquo;Problem&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;My script takes one or more command-line parameters, which is straightforward to invoke when you know the values:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Called via command-line arguments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;my_script item_one item_two
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In other cases, the values I&amp;rsquo;m passing in are the output of some other command. As a &lt;a href="https://linux.die.net/Linux-CLI/c1089.htm"&gt;godly Unix user&lt;/a&gt;, my instinct is to pipe the output (&lt;code&gt;printf&lt;/code&gt; just for illustration):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Called with piped input, one argument per line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#34;item_one\nitem_two&amp;#34;&lt;/span&gt; | my_script
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The argument-handling Bash code I copy-paste every time only handles command-line arguments, and completely ignores any piped input. Here&amp;rsquo;s code that will read in the piped input and convert to command-line arguments, one per line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Copy command-line arguments over to new array&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ARGS&lt;span style="color:#f92672"&gt;=(&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Read in from piped input, if present, and append to newly-created array&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; ! -t &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; readarray STDIN_ARGS &amp;lt; /dev/stdin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ARGS&lt;span style="color:#f92672"&gt;=(&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;STDIN_ARGS[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Single loop to process all arguments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ARG in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;ARGS[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$ARG&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve already forgotten I wrote this code, so don&amp;rsquo;t ask me what it does. I&amp;rsquo;m just gonna copy-paste it again in a few months, leave me alone!&lt;/p&gt;
&lt;p&gt;Note that you can get around supporting piped input if you&amp;rsquo;re just willing to use &lt;code&gt;xargs&lt;/code&gt; (which I also make sure to regularly forget how to use):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# This is way easier than what I just showed you&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#34;item_one\nitem_two&amp;#34;&lt;/span&gt; | xargs my_script
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Farewell, future self. Be glad that your tiny brain retains the vital lyrics to &lt;em&gt;Ice Ice Baby&lt;/em&gt; instead of something as useless as &lt;code&gt;xargs&lt;/code&gt; and Bash arrays.&lt;/p&gt;</content:encoded></item><item><title>Mixing React and DOM events</title><link>https://fortes.com/2018/react-and-dom-events/</link><guid>https://fortes.com/2018/react-and-dom-events/</guid><pubDate>Thu, 07 Jun 2018 00:00:00 +0000</pubDate><description>Figuring out why your foot got blown off after you inadvertently shot it</description><content:encoded>&lt;p&gt;When possible, avoid mixing React event handlers with the native DOM event handlers. Using React&amp;rsquo;s &lt;a href="https://reactjs.org/docs/handling-events.html"&gt;event handling&lt;/a&gt; is unequivocally the way to go (less code, better for performance, avoids memory leaks, etc). Unfortunately, since you&amp;rsquo;re reading this, you&amp;rsquo;re probably in some situation where you can&amp;rsquo;t just use a React listener. This typically happens when using a non-React component within the React tree. Another scenario is when a component needs to listen to events that fire outside of the scope of the DOM owned by that component, for example listening &lt;code&gt;mousemove&lt;/code&gt; event when the mouse leaves the element&amp;rsquo;s bounds (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events#Pointer_capture"&gt;Pointer Capture&lt;/a&gt; solves this scenario, but it&amp;rsquo;s not universally available yet).&lt;/p&gt;
&lt;p&gt;Code nerds should read on, but all others can get by with the following TL;DR:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Avoid using &lt;code&gt;addEventListener&lt;/code&gt; and rely on React event handlers&lt;/li&gt;
&lt;li&gt;Listen on &lt;code&gt;document&lt;/code&gt; (or &lt;code&gt;window&lt;/code&gt;) if you want to receive events &lt;em&gt;after&lt;/em&gt; all React handlers. Listen anywhere else in order to receive &lt;em&gt;before&lt;/em&gt; React handlers&lt;/li&gt;
&lt;li&gt;React event handlers will &lt;em&gt;always&lt;/em&gt; execute after native capture handlers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s work with the following (simplified) structure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;!-- Container element for the React tree, target of ReactDOM.render() --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;!-- Root React component --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;App&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onClick&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{onAppClick}&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;Button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onClick&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{onButtonClick}&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;App&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you likely know, if you call &lt;code&gt;event.stopPropagation()&lt;/code&gt; in your &lt;code&gt;onButtonClick&lt;/code&gt; handler, then the &lt;code&gt;onAppClick&lt;/code&gt; handler will never be called. Now let&amp;rsquo;s add a DOM event listener on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;document.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;click&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stopPropagation&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now which event handlers will be called after clicking the &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;? You might (quite reasonably!) assume that, since &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; is an ancestor of &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;, the &lt;code&gt;onButtonClick&lt;/code&gt; and &lt;code&gt;onAppClick&lt;/code&gt; handlers get called first. Unfortunately, in this case neither React handler is called! Only the DOM handler on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; gets called, so what gives?&lt;/p&gt;
&lt;p&gt;React has its &lt;a href="https://reactjs.org/docs/events.html"&gt;own event system&lt;/a&gt;. The &lt;code&gt;Event&lt;/code&gt; argument received by DOM event handler is &lt;em&gt;not&lt;/em&gt; the same as the &lt;code&gt;Event&lt;/code&gt; argument the React handler receives. This isn&amp;rsquo;t a problem in most cases, but if your code depends on specific handler order, or any of the handlers calls &lt;code&gt;event.stopPropagation()&lt;/code&gt; then we run into situations like the contrived scenario above.&lt;/p&gt;
&lt;p&gt;React&amp;rsquo;s event system works by &lt;a href="https://github.com/facebook/react/blob/e8857918422b5ce8505ba5ce4a2d153e509c17a1/packages/react-dom/src/events/ReactBrowserEventEmitter.js#L105-L173"&gt;attaching native event listeners on the &lt;code&gt;document&lt;/code&gt; object&lt;/a&gt;. Once an event bubbles up to &lt;code&gt;document&lt;/code&gt;, React dispatches its own &lt;code&gt;SyntheticEvent&lt;/code&gt; that bubbles through the React component tree. If you&amp;rsquo;re using &lt;a href="https://reactjs.org/docs/portals.html"&gt;Portals&lt;/a&gt;, React is smart enough to route the event through the Portal for you (unlike the DOM event, which can only follow the DOM tree).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: React 17 &lt;a href="https://reactjs.org/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation"&gt;changed how event listeners are set up&lt;/a&gt;. Listeners are now attached at the root of the React tree, instead of the &lt;code&gt;document&lt;/code&gt; object. This post was written years before React 17 release, but the general concept here is still applicable.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This means that any native event handler that calls &lt;code&gt;stopPropagation()&lt;/code&gt; before the event reaches &lt;code&gt;document&lt;/code&gt; will cause React to &lt;em&gt;never see that event at all&lt;/em&gt;. In (most?) scenarios this is probably the desired result, since the native event handlers are probably there in order to completely override default behavior.&lt;/p&gt;
&lt;p&gt;What if you only want to let React handlers get called &lt;em&gt;before&lt;/em&gt; your native handler? Since event handlers are called in order of registration, the answer is to listen on the &lt;code&gt;document&lt;/code&gt; object (or &lt;code&gt;window&lt;/code&gt;, depending on the event). Note that this will not work if you add the handler before the React tree mounts (i.e. before calling &lt;code&gt;ReactDOM.render()&lt;/code&gt;)! In that case, it&amp;rsquo;s best to listen on &lt;code&gt;window&lt;/code&gt; (or wait until React has mounted before adding the handler).&lt;/p&gt;
&lt;p&gt;Things get a bit trickier during the &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Bubbling_and_capturing_explained"&gt;capture phase&lt;/a&gt;. Since React is listening to the bubble phase on &lt;code&gt;document&lt;/code&gt;, &lt;strong&gt;any&lt;/strong&gt; handler that calls &lt;code&gt;stopPropagation()&lt;/code&gt; during the capture phase will prevent React from ever seeing the event (this issue is what inspired me to write this post). I hacked up a &lt;a href="https://jsfiddle.net/fortesdotcom/n5u2wwjg/37487/"&gt;simple test&lt;/a&gt; to visualize scenarios (feel free to play around at home).&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://jsfiddle.net/fortesdotcom/n5u2wwjg/37487/"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2018/react-and-dom-events/2018-react-event-bubbling_hu_38ea4be5e13e2309.webp 400w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2018/react-and-dom-events/2018-react-event-bubbling.png" srcset="https://fortes.com/2018/react-and-dom-events/2018-react-event-bubbling_hu_22dc7ff40b031add.png 400w" sizes="(max-width: 800px) 100vw, 800px" alt="Stopping propagation during capture phase prevents React from firing events" width="601" height="330" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Stopping propagation during capture phase prevents React from firing events&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This behavior is fairly understandable once you dig into the details, but it&amp;rsquo;s completely counterintuitive to anyone unfamiliar with the difference between the React and native event systems. Unless you&amp;rsquo;re paid per bug fixed, I highly recommend avoiding mixing the two systems. I learned this lesson too late. My penance was to write this post, yours to read it, and now we&amp;rsquo;re both done.&lt;/p&gt;</content:encoded></item><item><title>Using Rclone and MergerFS together across drives</title><link>https://fortes.com/2018/rclone-and-mergerfs/</link><guid>https://fortes.com/2018/rclone-and-mergerfs/</guid><pubDate>Tue, 20 Feb 2018 00:00:00 +0000</pubDate><description>Create an encrypted drive pool in a hundred steps or less</description><content:encoded>&lt;p&gt;Due to a combination of hoarding (digital and physical), frugality, and laziness I have a large number of external hard drives and cloud services that I use for general storage and backup. I also have an annoying tendency to physically misplace things (adds to the backup paranoia), so I like having some level of basic encryption for removable devices for privacy reasons (my threat model is identity theft, not government agencies).&lt;/p&gt;
&lt;p&gt;My (grossly over-engineered) solution involves two fantastic pieces of userland software:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/trapexit/mergerfs"&gt;MergerFS&lt;/a&gt;: FUSE filesystem that pools drives into a single mount&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rclone.org/"&gt;Rclone&lt;/a&gt;: Versatile cloud backup and sync tool, also supports local files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The workflow here is roughly as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plug in one or more external drives and mount locally&lt;/li&gt;
&lt;li&gt;(optional) Use &lt;code&gt;rclone&lt;/code&gt; to mount a readonly cloud drive (i.e. Dropbox, Google Drive, etc)&lt;/li&gt;
&lt;li&gt;Pool the drives into a unified folder using &lt;code&gt;mergerfs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Decrypt and mount the pooled content using &lt;code&gt;rclone&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After jumping through these hoops, I now have a single folder structure with my files across local and cloud storage. Once mounted, &lt;a href="https://github.com/trapexit/mergerfs-tools"&gt;MergerFS tools&lt;/a&gt; provides some nice scripts for duplicating (or de-duplicating) content across your drive pools.&lt;/p&gt;
&lt;p&gt;This type of thing isn&amp;rsquo;t useful for many people, but if you&amp;rsquo;re interested in replicating this arcane setup, here&amp;rsquo;s how to go about it (instructions assume Linux, &lt;del&gt;should work with minor modifications on MacOS&lt;/del&gt;):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Choose one or more external drives you&amp;rsquo;d like to use. There&amp;rsquo;s no need to clean format the drive (other files can live on the drive as well). Create a folder called &lt;code&gt;encrypted&lt;/code&gt; in the root of each drive.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(Optional) If you&amp;rsquo;d like to use Cloud Storage as well, setup one or more providers by following the &lt;a href="https://rclone.org/docs/#configure"&gt;rclone config instructions&lt;/a&gt;. Once setup, use &lt;code&gt;rclone&lt;/code&gt; to mount it locally:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Replace `cloud` with whatever you named the remote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# NOTE: Must create `/media/cloud` folder beforehand&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# NOTE: Must create `cloud:/encrypted` folder beforehand via `rclone mkdir`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# `allow-other` and `read-only` flags are optional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone mount cloud:/encrypted /media/cloud --allow-other --read-only
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;mergerfs&lt;/code&gt; to mount the drives together in a pool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# NOTE: Must create `/media/encrypted_pool` if it doesn&amp;#39;t exist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Replace `/media/driveX` with paths to the removable/cloud drives&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# `allow_other` setting is optional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mergerfs /media/drive1/encrypted:/media/drive2/encrypted:/media/cloud /media/encrypted_pool &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -o defaults,fsname&lt;span style="color:#f92672"&gt;=&lt;/span&gt;encrypted_pool,allow_other &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -o moveonenospc&lt;span style="color:#f92672"&gt;=&lt;/span&gt;true,category.create&lt;span style="color:#f92672"&gt;=&lt;/span&gt;epmfs,func.getattr&lt;span style="color:#f92672"&gt;=&lt;/span&gt;newest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;rclone&lt;/code&gt; to &lt;a href="https://rclone.org/crypt/"&gt;setup the encrypted storage&lt;/a&gt;. Use &lt;code&gt;/media/encrypted_pool&lt;/code&gt; as the remote path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mount the decrypted drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# NOTE: Must create `/media/decrypted_pool` if it doesn&amp;#39;t exist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Replace `pool` with whatever you named the encrypted storage in step 4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# `allow-other` setting is optional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone mount pool:/ /media/decrypted_pool --no-modtime --allow-other
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You now have a pooled view of your encrypted folders, congratulations! Of course, they&amp;rsquo;re completely empty so it is kinda useless.&lt;/p&gt;
&lt;p&gt;At this point, you can start copying data into this merged view but I wouldn&amp;rsquo;t recommend it for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It won&amp;rsquo;t be obvious what data is stored on each pooled drive. If you&amp;rsquo;re always using the same drives (or never disconnect), then maybe this isn&amp;rsquo;t a concern for you.&lt;/li&gt;
&lt;li&gt;Filesystem tools like &lt;code&gt;cp&lt;/code&gt; and &lt;code&gt;rsync&lt;/code&gt; are going to have to go through two FUSE layers, which isn&amp;rsquo;t great for performance.&lt;/li&gt;
&lt;li&gt;For cloud storage, &lt;a href="https://rclone.org/commands/rclone_mount/#rclone-mount-vs-rclone-sync-copy"&gt;the &lt;code&gt;rclone&lt;/code&gt; documentation&lt;/a&gt; explains that using &lt;code&gt;rclone sync&lt;/code&gt; is the best practice for copying into the cloud (if you mounted your cloud drive with &lt;code&gt;--read-only&lt;/code&gt;, this isn&amp;rsquo;t an issue since you&amp;rsquo;ll never upload anything).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In order to workaround these issues, I manually add data to individual drives via &lt;code&gt;rclone sync&lt;/code&gt;. In order to do this, you need to setup rclone storage for each drive you&amp;rsquo;re using. You can do this manually via &lt;a href="https://rclone.org/crypt/"&gt;&lt;code&gt;rclone config&lt;/code&gt;&lt;/a&gt; (use the same passwords you used for the merged drive). Alternatively, it&amp;rsquo;s easier to manually edit the &lt;code&gt;rclone.conf&lt;/code&gt; file and copy the config over per drive.&lt;/p&gt;
&lt;p&gt;At this point, your &lt;code&gt;rclone.conf&lt;/code&gt; file looks something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[cloud]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# `type` varies depending on storage provider&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;type&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;s3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;client_id&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;client_secret&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;token&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[pool]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;type&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;crypt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;remote&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/media/encrypted_pool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;filename_encryption&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;password2&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead of running &lt;code&gt;rclone config&lt;/code&gt; a bunch of times, you can copy the &lt;code&gt;[pool]&lt;/code&gt; section and just change the &lt;code&gt;remote&lt;/code&gt; path, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Create one of these per drive&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[drive1]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;type&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;crypt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;remote&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/media/drive1/encrypted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;filename_encryption&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;password2&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since the encryption parameters are identical, anything you store in this rclone remote can be read by the pooled mount. To add data to the pool, you can now &lt;code&gt;rclone sync stable_genius_files drive1:/&lt;/code&gt;. Do the same setup for your cloud remotes as well.&lt;/p&gt;
&lt;p&gt;Running these commands manually is a pain, so if you&amp;rsquo;re running Linux &lt;del&gt;(and maybe MacOS, I haven&amp;rsquo;t tested)&lt;/del&gt;, I&amp;rsquo;ve created a helper script that automatically takes care of mounting and pooling the drives (also &lt;a href="https://gist.github.com/fortes/03846f4e896e5d86562c527387e82ec3"&gt;posted as a gist&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;set -euf -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Removable drive names go here, separated by spaces&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DRIVE_NAMES&lt;span style="color:#f92672"&gt;=(&lt;/span&gt;drive1 drive2 drive3&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Removable and cloud drives are mounted in this local directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DRIVE_MOUNT_ROOT&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/media&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Name of folder for root of encrypted storage on drives&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENCRYPTED_DIR_NAME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;encrypted&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Rclone remote name for pooled drives&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RCLONE_POOL_NAME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;encrypted_pool&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Local path for mounting decrypted drive pool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DECRYPTED_MOUNT_PATH&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/media/decrypted_pool&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Helper for joining array items with `:`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; colon_join &lt;span style="color:#f92672"&gt;{&lt;/span&gt; local IFS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$*&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Helper for unmounting on exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; finish &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Cleaning up and unmounting ...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; mount | grep -q &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DECRYPTED_MOUNT_PATH&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Unmounting merged rclone at &lt;/span&gt;$DECRYPTED_MOUNT_PATH&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fusermount -u &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DECRYPTED_MOUNT_PATH&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &amp;gt; /dev/null 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; mount | grep -q &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/merged&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Unmounting mergerfs at &lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/merged&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fusermount -u &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/merged&amp;#34;&lt;/span&gt; &amp;gt; /dev/null 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; name in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;CLOUD_DRIVE_NAMES[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mount_path&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; mount | grep -q &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Unmounting cloud remote &lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fusermount -u &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &amp;gt; /dev/null 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;trap finish EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VALID_MOUNTS&lt;span style="color:#f92672"&gt;=()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Checking removable drives&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; name in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;DRIVE_NAMES[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mount_path&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$ENCRYPTED_DIR_NAME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VALID_MOUNTS&lt;span style="color:#f92672"&gt;+=(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Mount cloud drives, if any&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CLOUD_DRIVE_NAMES&lt;span style="color:#f92672"&gt;=(&lt;/span&gt;gdrive&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Checking cloud drives&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; name in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;CLOUD_DRIVE_NAMES[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mount_path&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; ! mount | grep -q &lt;span style="color:#e6db74"&gt;&amp;#34;rclone_&lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Mounting rclone remote &lt;/span&gt;$name&lt;span style="color:#e6db74"&gt; ...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rclone mount &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$name&lt;span style="color:#e6db74"&gt;:/&lt;/span&gt;$ENCRYPTED_DIR_NAME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-other --read-only &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VALID_MOUNTS&lt;span style="color:#f92672"&gt;+=(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$mount_path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Found drives: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;VALID_MOUNTS[*]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Mounting pool via mergerfs&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mergerfs &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;colon_join &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;VALID_MOUNTS[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DRIVE_MOUNT_ROOT&lt;span style="color:#e6db74"&gt;/merged&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -o defaults,allow_other,moveonenospc&lt;span style="color:#f92672"&gt;=&lt;/span&gt;true &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -o fsname&lt;span style="color:#f92672"&gt;=&lt;/span&gt;encrypted_merged,category.create&lt;span style="color:#f92672"&gt;=&lt;/span&gt;epmfs,func.getattr&lt;span style="color:#f92672"&gt;=&lt;/span&gt;newest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Mounting decrypted pool&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone mount &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$RCLONE_POOL_NAME&lt;span style="color:#e6db74"&gt;:/&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$DECRYPTED_MOUNT_PATH&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-other --no-modtime &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Mounting complete, hit Control-C to unmount and exit&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wait
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve made it this far, congratulations for creating a brittle, complex storage system instead of just buying a bigger drive! Now I assume you can get back to growing your own vegetables, sewing your own clothing, and other inefficient uses of the precious little time you have left before dying alone.&lt;/p&gt;</content:encoded></item><item><title>One Good Tweet</title><link>https://fortes.com/2017/one-good-tweet/</link><guid>https://fortes.com/2017/one-good-tweet/</guid><pubDate>Wed, 08 Nov 2017 00:00:00 +0000</pubDate><description>I’m a one-hit wonder on Twitter; this is life with a marginally-popular tweet</description><content:encoded>&lt;p&gt;My road to being a huge Twitter celebrity (almost 2,000 followers!) started on a
typical fall day in 2013. My wife was watching &lt;a href="https://en.wikipedia.org/wiki/The_Killing_(U.S._TV_series)"&gt;some rainy crime
drama&lt;/a&gt;, while I
was trying to fix a tricky bug that was due to some terrible code I had written
while drunk. The same creative juices that conceived my indecipherable code gave birth to what are, without a doubt, the most important 87 characters in
modern history:&lt;/p&gt;
&lt;blockquote class="twitter-tweet" data-dnt="true"&gt;&lt;p lang="en" dir="ltr"&gt;Debugging is like being the detective in a crime movie where you are also the murderer.&lt;/p&gt;&amp;mdash; Filipe Fortes (@fortes) &lt;a href="https://twitter.com/fortes/status/399339918213652480?ref_src=twsrc%5Etfw"&gt;November 10, 2013&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;


&lt;p&gt;Within a few hours, my timeline (previously solely populated by Nigerian royalty
and local singles) started to blow up. Hollywood messaged me non-stop, hoping to
purchase the movie rights to my incredibly-popular tweets. I started dreaming of
leaving the code mines and entering the star-studded world of mediocre comedy.&lt;/p&gt;
&lt;p&gt;Unfortunately, lightning only struck once, and none of those lucrative contracts
panned out. But, even years later, my timeline is still completely dominated by
a tweet that is over four years old.&lt;/p&gt;
&lt;p&gt;Given the unprecedented popularity, it&amp;rsquo;s worth sharing the statistics Twitter
reports for my absolutely brilliant tweet:&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2017/one-good-tweet/2017-tweet-stats.png"&gt;&lt;picture&gt;
 &lt;source type="image/webp" srcset="https://fortes.com/2017/one-good-tweet/2017-tweet-stats_hu_d95f30e11f61ac67.webp 400w, https://fortes.com/2017/one-good-tweet/2017-tweet-stats_hu_18eb9a763bb7dba2.webp 800w" sizes="(max-width: 800px) 100vw, 800px"&gt;
 &lt;img src="https://fortes.com/2017/one-good-tweet/2017-tweet-stats_hu_17df806da02b5566.png" srcset="https://fortes.com/2017/one-good-tweet/2017-tweet-stats_hu_593c5cb7d9385d5f.png 400w, https://fortes.com/2017/one-good-tweet/2017-tweet-stats_hu_17df806da02b5566.png 800w" sizes="(max-width: 800px) 100vw, 800px" alt="Twitter&amp;#39;s statistics for the greatest tweet of all time, as of November 2017" width="800" height="393" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;/picture&gt;&lt;/a&gt;
 &lt;figcaption&gt;Twitter&amp;#39;s statistics for the greatest tweet of all time, as of November 2017&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Despite over 700 thousand impressions, and almost 20 thousand &amp;ldquo;engagements&amp;rdquo;
(whatever that means), my super-popular tweet generated a measly 57 new
followers. Twitter&amp;rsquo;s offer to &amp;ldquo;Reach a bigger audience&amp;rdquo; isn&amp;rsquo;t very compelling
given a 0.008% conversion rate.&lt;/p&gt;
&lt;p&gt;Despite the lack of life-changing fame, it&amp;rsquo;s been interesting to see how my
(astoundingly insightful) creative work has taken a life of its own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Tweets without attribution are &lt;a href="https://twitter.com/search?f=tweets&amp;amp;q=debugging%20crime%20movie%20-fortes&amp;amp;src=typd"&gt;very
common&lt;/a&gt;,
but joke stealing on Twitter is old news and shouldn&amp;rsquo;t come as a shock. What I
found interesting is how commonly a joke is stolen with half-hearted
attribution:&lt;/p&gt;
&lt;blockquote class="twitter-tweet" data-dnt="true"&gt;&lt;p lang="en" dir="ltr"&gt;&amp;quot;Debugging is like being the detective in a crime movie where you are also the murderer.&amp;quot; - Filipe Fortes&lt;/p&gt;&amp;mdash; Programming Wisdom (@CodeWisdom) &lt;a href="https://twitter.com/CodeWisdom/status/897911593878511617?ref_src=twsrc%5Etfw"&gt;August 16, 2017&lt;/a&gt;&lt;/blockquote&gt;
 &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;
 

&lt;p&gt;An account with 100 thousand followers made sure to strip out the username and
quotes me by name instead (alongside
&lt;a href="https://en.wikipedia.org/wiki/Edsger_W._Dijkstra"&gt;Dijkstra&lt;/a&gt; and
&lt;a href="https://en.wikipedia.org/wiki/Douglas_Engelbart"&gt;Engelbart&lt;/a&gt;, who lack my
universal name recognition). Here&amp;rsquo;s &lt;a href="https://twitter.com/Bill_Gross/status/400008237724213248"&gt;another
example&lt;/a&gt;, this time
with over 200 thousand followers.&lt;/p&gt;
&lt;p&gt;Someone decided that 87 characters read better
&lt;a href="https://twitter.com/shirleyalmosni/status/840179669488005121"&gt;as an image&lt;/a&gt;,
and managed to get over two thousand likes as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The brain trust over at 9gag &lt;a href="https://9gag.com/gag/aP985Dg/debugging-means-being-the-detective-in-a-crime-where-you-are-also-the-murderer"&gt;mashed it into an unrelated
joke&lt;/a&gt;,
where I guess it went over well? I can&amp;rsquo;t bear to keep the tab open long enough
to find out.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Someone &lt;a href="https://henrikwarne.com/2016/04/17/more-good-programming-quotes"&gt;aggregated it into a list of programmer
quotes&lt;/a&gt;,
which later had a &lt;a href="https://news.ycombinator.com/item?id=14825811"&gt;good showing on Hacker
News&lt;/a&gt;. Once more, I feel sorry
for nobodies like &lt;a href="https://en.wikipedia.org/wiki/Linus_Torvalds"&gt;Linus
Torvalds&lt;/a&gt; who get overshadowed
by my celebrity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Someone &lt;a href="https://www.redbubble.com/people/farhanhafeez/works/28832185-debugger-detective-in-a-crime-movie?p=classic-tee"&gt;made a T-shirt based on my
tweet&lt;/a&gt;
as well. (Thanks to &lt;a href="https://twitter.com/peajai"&gt;@peajai&lt;/a&gt; for sending me a
pointer!). &lt;strong&gt;Update&lt;/strong&gt;: &lt;a href="https://twitter.com/mcbarron"&gt;Mike Barron&lt;/a&gt; found
&lt;a href="https://pro.teechip.com/coder-debug"&gt;another T-shirt&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A couple of days after I wrote this post, someone &lt;a href="https://www.reddit.com/r/ProgrammerHumor/comments/7bwzuj/we_are_all_sherlock_holmes_and_moriarty/"&gt;posted a screenshot of the
tweet to
/r/ProgrammerHumor&lt;/a&gt;
where it got to the number one spot and wasted a few cumulative years of
programmer productivity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hopefully my example serves as a cautionary tale. Think twice before composing
that super-popular tweet!&lt;/p&gt;</content:encoded></item><item><title>Using Language Servers in Neovim</title><link>https://fortes.com/2017/language-server-neovim/</link><guid>https://fortes.com/2017/language-server-neovim/</guid><pubDate>Tue, 24 Oct 2017 00:00:00 +0000</pubDate><description>Adding IDE-like features into Neovim via the Language Server Protocol</description><content:encoded>&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; A lot has changed since the writing of this post, and I&amp;rsquo;ve &lt;a href="https://github.com/fortes/dotfiles/tree/master/stowed-files/nvim"&gt;changed my config to use coc.nvim&lt;/a&gt;. I&amp;rsquo;ve preserved the instructions as written in 2017, but you should find the latest instructions for one of the &lt;a href="https://langserver.org/"&gt;many LSP options for vim&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been fascinating to watch how Microsoft has changed its outlook on open source now that they&amp;rsquo;re no longer the crushingly dominant force they used to be (I caught the tail end of that phase while I worked there in the early 2000s). Last summer, the team behind Visual Studio Code &lt;a href="https://code.visualstudio.com/blogs/2016/06/27/common-language-protocol"&gt;introduced the Language Server Protocol&lt;/a&gt;, which is used to power syntax highlighting, code completion, and other advanced editing features in Visual Studio. What&amp;rsquo;s exciting about the Language Server Protocol (LSP) is that it is editor neutral, so it&amp;rsquo;s not limited to a single editor.&lt;/p&gt;
&lt;p&gt;As an ancient neckbeard, this is exciting since I use &lt;a href="https://neovim.io/"&gt;an offshoot of an editor that is older than I am&lt;/a&gt;. Although Neovim does many things well, IDE-like features such as code completion have always been kludgey hacks that compare poorly to GUI environments like Visual Studio. There is an effort to &lt;a href="https://github.com/neovim/neovim/issues/5522"&gt;add support to mainline Neovim&lt;/a&gt;, but integrating LSP into Neovim today is still a bit tricky, so I decided to document the process so others don&amp;rsquo;t have to go through the same pain I did.&lt;/p&gt;
&lt;p&gt;First, some caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;m using Neovim. If you use plain Vim, these instructions may or may not work for you.&lt;/li&gt;
&lt;li&gt;You should use a plugin manager for Neovim, I use &lt;a href="https://github.com/junegunn/vim-plug"&gt;vim-plug&lt;/a&gt;, but the syntax is similar across the alternatives (the &lt;a href="https://github.com/autozimu/LanguageClient-neovim/blob/master/INSTALL.md"&gt;LanguageClient-neovim installation instructions&lt;/a&gt; may be of use for you).&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;m assuming basic knowledge of Neovim and the command line. If you&amp;rsquo;re comfortable in the terminal, you&amp;rsquo;re probably pretty bored by now anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s begin with a minimal configuration that loads the &lt;a href="https://github.com/autozimu/LanguageClient-neovim"&gt;LanguageClient-neovim&lt;/a&gt; plugin and enables it for JavaScript files.&lt;/p&gt;
&lt;p&gt;First, we need to install a Language Server for JavaScript, we&amp;rsquo;ll use &lt;a href="https://github.com/sourcegraph/javascript-typescript-langserver"&gt;javascript-typescript-langserver&lt;/a&gt; which you can install via &lt;a href="https://yarnpkg.com/en/"&gt;Yarn&lt;/a&gt; by running &lt;code&gt;yarn global add javascript-typescript-langserver&lt;/code&gt; (or &lt;code&gt;npm install -g javascript-typescript-langserver&lt;/code&gt; if you&amp;rsquo;re still on &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Now we create a minimal configuration file, which you should save to &lt;code&gt;$HOME/.config/nvim/init.vim&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-vim" data-lang="vim"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plug&lt;/span&gt;#&lt;span style="color:#a6e22e"&gt;begin&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; LanguageClient plugin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Plug&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;autozimu/LanguageClient-neovim&amp;#39;&lt;/span&gt;, { &lt;span style="color:#e6db74"&gt;&amp;#39;do&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;:UpdateRemotePlugins&amp;#39;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plug&lt;/span&gt;#&lt;span style="color:#a6e22e"&gt;end&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Automatically start language servers.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;g&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;LanguageClient_autoStart&lt;/span&gt; = &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Minimal LSP configuration for JavaScript&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;g&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;LanguageClient_serverCommands&lt;/span&gt; = {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;executable&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;javascript-typescript-stdio&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;g&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;LanguageClient_serverCommands&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; = [&lt;span style="color:#e6db74"&gt;&amp;#39;javascript-typescript-stdio&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; &amp;#34; Use LanguageServer for omnifunc completion&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setlocal&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;omnifunc&lt;/span&gt;=&lt;span style="color:#a6e22e"&gt;LanguageClient&lt;/span&gt;#&lt;span style="color:#a6e22e"&gt;complete&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;echo&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;javascript-typescript-stdio not installed!\n&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :&lt;span style="color:#a6e22e"&gt;cq&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;endif&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you load up a JavaScript file in vim now (make sure you ran &lt;code&gt;:PlugInstall&lt;/code&gt; to install the plugins) you&amp;rsquo;ll see &amp;hellip; nothing special. However, if you invoke &lt;a href="http://vim.wikia.com/wiki/Omni_completion"&gt;omni completion&lt;/a&gt; (via &lt;code&gt;&amp;lt;C-x&amp;gt;&amp;lt;C-u&amp;gt;&lt;/code&gt;), you&amp;rsquo;ll see the completion is much more intelligent than the default:&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-omni-complete.gif"&gt;&lt;img src="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-omni-complete.gif" alt="Smart omni completion in vim via language server" width="400" height="334" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Smart omni completion in vim via language server&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Other than smarter omni completion, this minimal setup also provides an error marker in the gutter indicating invalid syntax. These are both pretty nice, but really only scratch the surface of what we can do here. There is almost nothing enabled by default, so let&amp;rsquo;s setup a few convenience mappings by adding the following to the end of &lt;code&gt;init.vim&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-vim" data-lang="vim"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; &amp;lt;leader&amp;gt;ld to go to definition&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;nnoremap&lt;/span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; \ &amp;lt;&lt;span style="color:#a6e22e"&gt;leader&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;ld&lt;/span&gt; :&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LanguageClient_textDocument_definition&lt;/span&gt;()&amp;lt;&lt;span style="color:#a6e22e"&gt;cr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; &amp;lt;leader&amp;gt;lh for type info under cursor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;nnoremap&lt;/span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; \ &amp;lt;&lt;span style="color:#a6e22e"&gt;leader&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;lh&lt;/span&gt; :&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LanguageClient_textDocument_hover&lt;/span&gt;()&amp;lt;&lt;span style="color:#a6e22e"&gt;cr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; &amp;lt;leader&amp;gt;lr to rename variable under cursor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;nnoremap&lt;/span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; \ &amp;lt;&lt;span style="color:#a6e22e"&gt;leader&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;lr&lt;/span&gt; :&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LanguageClient_textDocument_rename&lt;/span&gt;()&amp;lt;&lt;span style="color:#a6e22e"&gt;cr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can use the Language Server to find object definitions, types, and intelligently rename things. The rename is my personal favorite, since it is smarter than a normal find and replace, and respects variable scope:&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-rename.gif"&gt;&lt;img src="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-rename.gif" alt="Renaming a variable in vim via language server" width="400" height="334" loading="lazy" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Renaming a variable in vim via language server&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This is pretty awesome, but so far it&amp;rsquo;s not much different than what was possible with other plugins like &lt;a href="https://github.com/ternjs/tern_for_vim"&gt;Tern for Vim&lt;/a&gt;. One of the exciting things about the Language Server Protocol is that it&amp;rsquo;s language agnostic. There are already implementations for &lt;a href="https://langserver.org/"&gt;several programming languages&lt;/a&gt;, and we get to leverage the same configuration once we&amp;rsquo;ve got our initial setup.&lt;/p&gt;
&lt;p&gt;If I get tricked into coding python again, I can add support into Neovim by installing the &lt;a href="https://github.com/palantir/python-language-server"&gt;Python Language Server&lt;/a&gt; (via &lt;code&gt;pip install python-language-server&lt;/code&gt;), and adding the following to &lt;code&gt;init.vim&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-vim" data-lang="vim"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;g&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;LanguageClient_serverCommands&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;python&lt;/span&gt; = [&lt;span style="color:#e6db74"&gt;&amp;#39;pyls&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Map renaming in python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;python&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;nnoremap&lt;/span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; \ &amp;lt;&lt;span style="color:#a6e22e"&gt;leader&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;lr&lt;/span&gt; :&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LanguageClient_textDocument_rename&lt;/span&gt;()&amp;lt;&lt;span style="color:#a6e22e"&gt;cr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now Neovim will load up the language server when opening python files, so I have access to the same features as when I edit JavaScript. Feature support varies by Language Server, but most support syntax checking, autocompletion, and renaming. A few even support &lt;a href="https://github.com/autozimu/LanguageClient-neovim/issues/35#issuecomment-324497559"&gt;code formatting&lt;/a&gt; as well.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/autozimu/LanguageClient-neovim/"&gt;LanguageClient-neovim&lt;/a&gt; is a pretty awesome plugin, there are two integrations that are worth setting up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/autozimu/LanguageClient-neovim/issues/35#issuecomment-288731839"&gt;Fuzzy finding
symbols&lt;/a&gt;
via &lt;a href="https://github.com/junegunn/fzf"&gt;FZF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;IDE-like completion via
&lt;a href="https://github.com/roxma/nvim-completion-manager"&gt;nvim-completion-manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Adding this to your &lt;code&gt;init.vim&lt;/code&gt; is pretty easy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-vim" data-lang="vim"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Put this in your plugin section:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Fuzzy selection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Plug&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;junegunn/fzf&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; IDE-like autocompletion without&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Plug&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;roxma/nvim-completion-manager&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; Put this outside of the plugin section&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;#34; &amp;lt;leader&amp;gt;lf to fuzzy find the symbols in the current document&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;autocmd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileType&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;javascript&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;nnoremap&lt;/span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;buffer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; \ &amp;lt;&lt;span style="color:#a6e22e"&gt;leader&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;lf&lt;/span&gt; :&lt;span style="color:#a6e22e"&gt;call&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LanguageClient_textDocument_documentSymbol&lt;/span&gt;()&amp;lt;&lt;span style="color:#a6e22e"&gt;cr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Using fuzzy find to search through symbols is especially nice when &lt;a href="https://fortes.com/filipe/"&gt;some idiot&lt;/a&gt; decides it&amp;rsquo;s a good idea to have thousands of lines of code in a single file:&lt;/p&gt;
&lt;figure class="terminal"&gt;&lt;a href="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-fuzzy-find.gif"&gt;&lt;img src="https://fortes.com/2017/language-server-neovim/2017-neovim-language-server-fuzzy-find.gif" alt="Fuzzy-finding a symbol in vim via language server" width="400" height="334" loading="lazy" decoding="async"&gt;&lt;/a&gt;
 &lt;figcaption&gt;Fuzzy-finding a symbol in vim via language server&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If you&amp;rsquo;re curious, I keep all &lt;a href="https://github.com/fortes/dotfiles"&gt;my configuration files&lt;/a&gt; on GitHub. Let me know if you come up with any improvements!&lt;/p&gt;</content:encoded></item><item><title>Coda</title><link>https://fortes.com/2017/coda/</link><guid>https://fortes.com/2017/coda/</guid><pubDate>Thu, 19 Oct 2017 00:00:00 +0000</pubDate><description>Talking about what I’ve been working on for the last three years.</description><content:encoded>&lt;p&gt;A little over three years ago, I decided to join up with some friends and go on
a crazy mission to re-think design decisions made &lt;a href="https://en.wikipedia.org/wiki/VisiCalc"&gt;almost
40&lt;/a&gt; &lt;a href="https://en.wikipedia.org/wiki/WordStar"&gt;years
ago&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Today, I&amp;rsquo;m happy to finally be able to talk about what I&amp;rsquo;ve been doing all this time!
With Coda, we&amp;rsquo;re building a new type of doc that blends the power of
spreadsheets and the flexibility of documents into a single new canvas. Here&amp;rsquo;s a
sneak peek:&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://fortes.com/2017/coda/2017-coda-walkthrough.gif"&gt;&lt;img src="https://fortes.com/2017/coda/2017-coda-walkthrough.gif" alt="Coda in Action" width="357" height="233" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/a&gt;
&lt;/figure&gt;

&lt;p&gt;If you&amp;rsquo;re curious, here are some places you can learn a bit more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shishir wrote about &lt;a href="https://blog.coda.io/its-a-new-day-for-docs-2643fb16f05a"&gt;the motivation behind
Coda&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The Verge has &lt;a href="https://www.theverge.com/2017/10/19/16497444/coda-spreadsheet-krypton-shishir-mehrotra"&gt;a detailed article about the Coda
launch&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Our lead investors, &lt;a href="https://news.greylock.com/our-investment-in-coda-449e0ab944d5"&gt;Reid Hoffman at
Greylock&lt;/a&gt; and
&lt;a href="https://medium.com/@gcvp/the-making-of-the-more-than-a-document-document-432237352816"&gt;Hemant Taneja at General
Catalyst&lt;/a&gt;
wrote a bit about their investments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although we&amp;rsquo;ve started to talk about things publicly, Coda isn&amp;rsquo;t quite ready for
everyone just yet. If you&amp;rsquo;re willing to put up with a few rough edges and give
us feedback, &lt;a href="https://coda.io/welcome"&gt;add yourself to our invite list&lt;/a&gt;. For
now, we&amp;rsquo;re mostly focused on Product / Project Managers, but we&amp;rsquo;re excited to
help all sorts of people build their own tools to get their work done.&lt;/p&gt;</content:encoded></item></channel></rss>