Yejun Su
https://yejun.dev/
Recent content on Yejun Su
Hugo
en
© 2025 Yejun Su
Sat, 14 Feb 2026 00:21:48 +0800
-
Modus themes
https://yejun.dev/devlog/modus-themes/
Fri, 13 Feb 2026 11:19:00 +0800
https://yejun.dev/devlog/modus-themes/
<p>The Modus themes is created by <a href="https://protesilaos.com/">Prot</a>. I love its aesthetic and use it almost everywhere.</p>
<h2 id="emacs">Emacs</h2>
<p>The original <a href="https://protesilaos.com/emacs/modus-themes">modus-themes</a> package.</p>
<h2 id="zed">Zed</h2>
<p>The <a href="https://zed.dev/extensions/modus-themes">modus-themes</a> extension created by <a href="https://bsky.app/profile/did:plc:5rc3vpldcyehjjqkj2qmdwvo">Vitaly Slobodin</a>.</p>
<h2 id="ghostty">Ghostty</h2>
<p>The config is transformed from the <a href="https://protesilaos.com/emacs/modus-themes#h:6b8211b0-d11b-4c00-9543-4685ec3b742f">DIY Range of color with terminal emulators</a> section.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cfg" data-lang="cfg"><span class="line"><span class="cl"><span class="na">background</span> <span class="o">=</span> <span class="s">#000000</span>
</span></span><span class="line"><span class="cl"><span class="na">foreground</span> <span class="o">=</span> <span class="s">#ffffff</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">0=#000000</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">1=#ff8059</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">2=#44bc44</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">3=#d0bc00</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">4=#2fafff</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">5=#feacd0</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">6=#00d3d0</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">7=#bfbfbf</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">8=#595959</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">9=#ef8b50</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">10=#70b900</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">11=#c0c530</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">12=#79a8ff</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">13=#b6a0ff</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">14=#6ae4b9</span>
</span></span><span class="line"><span class="cl"><span class="na">palette</span> <span class="o">=</span> <span class="s">15=#ffffff</span>
</span></span></code></pre></div><p>Here’s the Nix configuration using home-manager:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">programs</span><span class="o">.</span><span class="n">ghostty</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"> <span class="n">themes</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">modus-vivendi</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">background</span> <span class="o">=</span> <span class="s2">"#000000"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">foreground</span> <span class="o">=</span> <span class="s2">"#ffffff"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">palette</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"0=#000000"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"1=#ff8059"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"2=#44bc44"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"3=#d0bc00"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"4=#2fafff"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"5=#feacd0"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"6=#00d3d0"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"7=#bfbfbf"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"8=#595959"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"9=#ef8b50"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"10=#70b900"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"11=#c0c530"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"12=#79a8ff"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"13=#b6a0ff"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"14=#6ae4b9"</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"15=#ffffff"</span>
</span></span><span class="line"><span class="cl"> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><h2 id="this-website">This website</h2>
<p>I created the <a href="https://github.com/goofansu/hugo-modus">hugo-modus</a> theme for this blog.</p>
-
Current Zen Browser Tab
https://yejun.dev/til/20260202t012043--current-zen-browser-tab__alfred/
Mon, 02 Feb 2026 01:20:00 +0800
https://yejun.dev/til/20260202t012043--current-zen-browser-tab__alfred/
<p>The Automation Task block in Alfred workflow doesn’t support Zen (based on Firefox), I asked Gemini to write an AppleScript as a workaround:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-applescript" data-lang="applescript"><span class="line"><span class="cl"><span class="k">tell</span> <span class="nb">application</span> <span class="s2">"Zen"</span> <span class="k">to</span> <span class="nb">activate</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">tell</span> <span class="nb">application</span> <span class="s2">"System Events"</span>
</span></span><span class="line"><span class="cl"> <span class="c">-- Target the Zen process</span>
</span></span><span class="line"><span class="cl"> <span class="k">tell</span> <span class="nv">process</span> <span class="s2">"Zen"</span>
</span></span><span class="line"><span class="cl"> <span class="c">-- 1. Get the Window Title</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nb">count</span> <span class="k">of</span> <span class="nb">windows</span><span class="p">)</span> <span class="o">></span> <span class="mi">0</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="k">set</span> <span class="nv">theTitle</span> <span class="k">to</span> <span class="na">name</span> <span class="k">of</span> <span class="nb">front</span> <span class="na">window</span>
</span></span><span class="line"><span class="cl"> <span class="k">else</span>
</span></span><span class="line"><span class="cl"> <span class="k">set</span> <span class="nv">theTitle</span> <span class="k">to</span> <span class="s2">"No Window Found"</span>
</span></span><span class="line"><span class="cl"> <span class="k">end</span> <span class="k">if</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c">-- 2. Get the URL (Simulate Cmd+L to highlight, Cmd+C to copy)</span>
</span></span><span class="line"><span class="cl"> <span class="nv">keystroke</span> <span class="s2">"l"</span> <span class="nv">using</span> <span class="nv">command</span> <span class="nv">down</span>
</span></span><span class="line"><span class="cl"> <span class="nb">delay</span> <span class="mf">0.2</span> <span class="c">-- Short delay to allow focus</span>
</span></span><span class="line"><span class="cl"> <span class="nv">keystroke</span> <span class="s2">"c"</span> <span class="nv">using</span> <span class="nv">command</span> <span class="nv">down</span>
</span></span><span class="line"><span class="cl"> <span class="nb">delay</span> <span class="mf">0.2</span> <span class="c">-- Short delay to allow copy</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c">-- 3. Deactivate (Hide) Zen</span>
</span></span><span class="line"><span class="cl"> <span class="k">set</span> <span class="na">visible</span> <span class="k">to</span> <span class="no">false</span>
</span></span><span class="line"><span class="cl"> <span class="k">end</span> <span class="k">tell</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> <span class="k">tell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">-- 4. Retrieve URL from clipboard</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nv">theURL</span> <span class="k">to</span> <span class="nb">the clipboard</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">-- 5. Return Title and URL separated by a Tab</span>
</span></span><span class="line"><span class="cl"><span class="no">return</span> <span class="nv">theTitle</span> <span class="o">&</span> <span class="no">tab</span> <span class="o">&</span> <span class="nv">theURL</span>
</span></span></code></pre></div><p>The output is <code>Title\tURL</code>, exactly the same as the Output Format of the “Current Chromium Browser Tab” automation task.</p>
-
Go module index
https://yejun.dev/til/20251231t185900--go-module-index__go/
Wed, 31 Dec 2025 18:59:00 +0800
https://yejun.dev/til/20251231t185900--go-module-index__go/
<p>I self-hosted Miniflux and want to create new feed through command line, so I vibe coded a Go project.</p>
<p>After pushing to GitHub, <code>go install</code> keeps failing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ go install github.com/goofansu/miniflux-cli/cmd/miniflux-cli@latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">go: downloading github.com/goofansu/miniflux-cli v0.0.0-20251231103549-8410b6d43c7b
</span></span><span class="line"><span class="cl">go: github.com/goofansu/miniflux-cli/cmd/miniflux-cli@latest: github.com/goofansu/[email protected]: verifying module: github.com/goofansu/[email protected]: reading https://sum.golang.org/lookup/github.com/goofansu/[email protected]: <span class="m">404</span> Not Found
</span></span><span class="line"><span class="cl"> server response:
</span></span><span class="line"><span class="cl"> not found: github.com/goofansu/[email protected]: invalid version: git ls-remote -q https://github.com/goofansu/miniflux-cli in /tmp/gopath/pkg/mod/cache/vcs/abd71c376a041e3c0a394d0d34d9b24cedb4a6426ec6abe8ef063bde71093ddd: <span class="nb">exit</span> status 128:
</span></span><span class="line"><span class="cl"> fatal: could not <span class="nb">read</span> Username <span class="k">for</span> <span class="s1">'https://github.com'</span>: terminal prompts disabled
</span></span><span class="line"><span class="cl"> Confirm the import path was entered correctly.
</span></span><span class="line"><span class="cl"> If this is a private repository, see https://golang.org/doc/faq#git_https <span class="k">for</span> additional information.
</span></span></code></pre></div><p>The solution is to visit <a href="https://proxy.golang.org/github.com/goofansu/miniflux-cli/@latest">https://proxy.golang.org/github.com/goofansu/miniflux-cli/@latest</a>, and then the URL works: <a href="https://sum.golang.org/lookup/github.com/goofansu/[email protected]">https://sum.golang.org/lookup/github.com/goofansu/[email protected]</a>.</p>
<p>Reference: <a href="https://proxy.golang.org/">https://proxy.golang.org/</a></p>
-
Claude Code thinking mode
https://yejun.dev/til/20251215t122720--claude-code-thinking-mode__claude/
Mon, 15 Dec 2025 12:27:00 +0800
https://yejun.dev/til/20251215t122720--claude-code-thinking-mode__claude/
<p>In recent versions, Claude Code has changed how thinking mode works compared to the version in <a href="https://yejun.dev/til/20250720t140631--claude-code-in-action__claude_course/">Claude Code in Action</a>.</p>
<p>From <a href="https://github.com/anthropics/claude-code/issues/9072#issuecomment-3648376741">https://github.com/anthropics/claude-code/issues/9072#issuecomment-3648376741</a>:</p>
<ul>
<li>Only <code>ultrathink</code> is the special keyword that enables thinking on a per-request basis. Phrases like “think”, “think hard”, “think more” don’t have any impact on the allocated thinking token budget.</li>
<li>If you need fine-grained control over the token budget (vs. just allocating all 31,999 tokens to the thinking budget), you can set the <code>MAX_THINKING_TOKENS</code> environment variable. This setting takes priority over <code>ultrathink</code>, so if you set a lower <code>MAX_THINKING_TOKENS</code> threshold, <code>ultrathink</code> doesn’t override it.</li>
</ul>
<p>From <a href="https://code.claude.com/docs/en/common-workflows#use-extended-thinking-thinking-mode">https://code.claude.com/docs/en/common-workflows#use-extended-thinking-thinking-mode</a>:</p>
<ul>
<li>Sonnet 4.5 and Opus 4.5 have thinking enabled by default. All other models have thinking disabled by default.</li>
<li>When thinking is enabled (via <code>/config</code> or <code>ultrathink</code>), Claude can use up to 31,999 tokens from your output budget for internal reasoning.</li>
<li>Note that <code>ultrathink</code> both allocates the thinking budget AND <em>semantically signals to Claude to reason more thoroughly</em>, which may result in deeper thinking than necessary for your task.</li>
</ul>
-
My 4K monitor setup
https://yejun.dev/posts/my-4k-monitor-setup/
Wed, 12 Nov 2025 17:17:00 +0800
https://yejun.dev/posts/my-4k-monitor-setup/
<p>I recently purchased an LG 27UP850K monitor with a 4K resolution (3840x2160). At its native resolution, everything is tiny. When scaled to the 1080p resolution (1920x1080), it displays the same content as a 1080p screen but with much sharper clarity. Then I scaled to the 2K resoluition (2560x1440), it warns “Using a scaled resolution may affect performance”. I finally switched back to the native 4K resolution because it displays much more content, which was my main reason for buying this monitor.</p>
<p>I adjusted the settings for the <a href="https://yejun.dev/tools/">apps</a> I use most often. Here’s a summary:</p>
<ul>
<li>Google Chrome: Settings -> Appearance -> Set “Page zoom” to 150%.</li>
<li>Slack: Preferences -> Accessibility -> Set “Zoom” to 150%.</li>
<li>Emacs: Create a “large” <a href="https://protesilaos.com/emacs/fontaine">fontaine</a> preset.
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">use-package</span> <span class="nv">fontaine</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:custom</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">fontaine-presets</span>
</span></span><span class="line"><span class="cl"> <span class="o">'</span><span class="p">((</span><span class="nv">regular</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-height</span> <span class="mi">160</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">large</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-height</span> <span class="mi">240</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="no">t</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-family</span> <span class="s">"Aporetic Sans Mono"</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-weight</span> <span class="nv">regular</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-slant</span> <span class="nv">normal</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-width</span> <span class="nv">normal</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:default-height</span> <span class="mi">100</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:fixed-pitch-family</span> <span class="s">"Aporetic Sans Mono"</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:variable-pitch-family</span> <span class="s">"Aporetic Serif"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:config</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">fontaine-set-preset</span> <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">fontaine-restore-latest-preset</span><span class="p">)</span> <span class="ss">'regular</span><span class="p">)))</span>
</span></span></code></pre></div></li>
<li>Zed: Create a “large” <a href="https://zed.dev/docs/configuring-zed#profiles">profile</a> and set up the font sizes.
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"profiles"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"large"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"ui_font_size"</span><span class="p">:</span> <span class="mi">24</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"buffer_font_size"</span><span class="p">:</span> <span class="mi">24</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"agent_buffer_font_size"</span><span class="p">:</span> <span class="mi">24</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div></li>
<li>Ghostty: By default, the new windows and tabs will <a href="https://ghostty.org/docs/config/reference#window-inherit-font-size">inherit the font size</a> of the previously focused window. So I just set font size to 24px using a keybinding in any window, and create new windows and tabs from that.
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">keybind = ctrl+shift+l=set_font_size:24
</span></span></code></pre></div></li>
</ul>
<h2 id="update-on-2025-11-15">Update on 2025-11-15</h2>
<p>I’ve been using the above settings for three days, and the biggest problem is the overall user interface appearing very small. I switched the display resolution to 2K scaling, and everything is readable now. The warning mentioned at the beginning is now a thing of the past, as explained in <a href="https://bytecellar.com/2022/11/08/4k-scaling-is-not-a-problem-on-modern-macs/">“4K Scaling” Is Not a Problem on Modern Macs</a>:</p>
<blockquote>
<p>The way around this is to have macOS “scale” the display to a more ideal lower resolution, but choosing that option in display preferences presents a warning: “Using a scaled resolution may affect performance.” What the OS does here is to scale up the chosen resolution to double height and double width (4x the pixels displayed) and then scale them back down to the display’s native resolution — 60 times per second. Indeed, this can be too much for certain older systems out there. But, as you will see, modern Macs should be able to handle the task just fine.</p>
</blockquote>
<h2 id="update-on-2025-11-19">Update on 2025-11-19</h2>
<p>I found the text on the 4K monitor is really sharp, so that I could easily read 12px font when scaled down to 1080p, whereas I previously needed 16px on a regular 1080p monitor. So I configured editors and the terminal to use 12px font, and set the Google Chrome’s page zoom to 75% to read more content.</p>
<h2 id="update-on-2025-12-14">Update on 2025-12-14</h2>
<p>I use 16px font in editor and 100% page zoom in Google Chrome, it means the same configuration as before, but my eyes are very comfortable so that I could work in front of the monitor for longer periods of time without eye strain.</p>
-
Kamal
https://yejun.dev/devlog/kamal/
Sat, 18 Oct 2025 12:00:00 +0800
https://yejun.dev/devlog/kamal/
<h2 id="deploy-rails-applications">Deploy Rails applications</h2>
<p>I recently deployed a Rails application to Hetzner and AWS China<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.
Deploying on Hetzner is simple, requiring just two steps: launching a server and deploying.
In contrast, deploying on AWS China involves extra steps because Docker Hub is not accessible from servers located in China.</p>
<p>The steps are:</p>
<ol>
<li>Launch an Amazon EC2 instance</li>
<li>Create Amazon ECR repositories to store Docker Hub images</li>
<li>Create an IAM user and assign only the “AmazonEC2ContainerRegistryFullAccess” permission</li>
<li><a href="https://yejun.dev/devlog/kamal/#push-docker-hub-images-to-amazon-ecr-on-local-machine">Push Docker Hub images to Amazon ECR on local machine</a></li>
<li><a href="https://yejun.dev/devlog/kamal/#pull-images-from-amazon-ecr-on-amazon-ec2">Pull images from Amazon ECR on Amazon EC2</a></li>
<li><a href="https://yejun.dev/devlog/kamal/#deploy">Deploy</a></li>
</ol>
<h3 id="push-docker-hub-images-to-amazon-ecr-on-local-machine">Push Docker Hub images to Amazon ECR on local machine</h3>
<ol>
<li>
<p>Login ECR</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">aws configure
</span></span><span class="line"><span class="cl">aws ecr get-login-password <span class="p">|</span> docker login --username AWS --password-stdin <aws-ecr-domain>
</span></span></code></pre></div></li>
<li>
<p>Push <code>basecamp/kamal-proxy</code> image</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker pull basecamp/kamal-proxy:v0.9.0 --platform linux/amd64
</span></span><span class="line"><span class="cl">docker tag basecamp/kamal-proxy:v0.9.0 <aws-ecr-domain>/basecamp/kamal-proxy:v0.9.0
</span></span><span class="line"><span class="cl">docker push <aws-ecr-domain>/basecamp/kamal-proxy:v0.9.0
</span></span></code></pre></div></li>
<li>
<p>Push <code>pgvector/pgvector</code> image</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker pull pgvector/pgvector:pg17 --platform linux/amd64
</span></span><span class="line"><span class="cl">docker tag pgvector/pgvector:pg17 <aws-ecr-domain>/pgvector/pgvector:pg17
</span></span><span class="line"><span class="cl">docker push <aws-ecr-domain>/pgvector/pgvector:pg17
</span></span></code></pre></div></li>
</ol>
<h3 id="pull-images-from-amazon-ecr-on-amazon-ec2">Pull images from Amazon ECR on Amazon EC2</h3>
<ol>
<li>
<p>Install Docker</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y docker.io
</span></span><span class="line"><span class="cl">sudo usermod -aG docker <span class="nv">$USER</span>
</span></span></code></pre></div></li>
<li>
<p>Install AWS CLI</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl <span class="s2">"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"</span> -o <span class="s2">"awscliv2.zip"</span>
</span></span><span class="line"><span class="cl">sudo apt install -y unzip
</span></span><span class="line"><span class="cl">unzip awscliv2.zip
</span></span><span class="line"><span class="cl">sudo ./aws/install
</span></span></code></pre></div></li>
<li>
<p>Login ECR</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">aws configure
</span></span><span class="line"><span class="cl">aws ecr get-login-password <span class="p">|</span> docker login --username AWS --password-stdin <aws-ecr-domain>
</span></span></code></pre></div></li>
<li>
<p>Pull <code>basecamp/kamal-proxy</code> image</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker pull <aws-ecr-domain>/basecamp/kamal-proxy:v0.9.0
</span></span><span class="line"><span class="cl">docker tag <aws-ecr-domain>/basecamp/kamal-proxy:v0.9.0 basecamp/kamal-proxy:v0.9.0
</span></span></code></pre></div></li>
<li>
<p>Pull <code>pgvector/pgvector</code> image</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker pull <aws-ecr-domain>/pgvector/pgvector:pg17
</span></span></code></pre></div></li>
</ol>
<h3 id="deploy">Deploy</h3>
<ol>
<li>Run <code>kamal setup</code> the first time to setup everything.</li>
<li>Run <code>kamal deploy</code> for subsequent deployments.</li>
</ol>
<h2 id="self-hosting">Self-hosting</h2>
<p>Since <a href="https://github.com/basecamp/kamal/pull/981">this pr</a>, you can use <a href="https://github.com/basecamp/kamal-proxy">kamal-proxy</a> to forward requests to <a href="https://kamal-deploy.org/docs/configuration/accessories/">accessories</a>, it’s time for self-hosting!</p>
<p>For example, I use Atuin<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> for shell history, and it supports syncing shell history through a sync server. Thankfully, the sync server can be self-hosted using Docker, and there is a <a href="https://docs.atuin.sh/self-hosting/docker/#docker-compose">Docker Compose example</a>. The task is to transform the example to a Kamal configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">atuin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">atuin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">accessories</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:14</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="cp">&host</span><span class="w"> </span><span class="l">hetzner</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clear</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">atuin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">atuin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">POSTGRES_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">directories</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./data:/var/lib/postgresql/data/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/atuinsh/atuin:v18.10.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="cp">*host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="l">server start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clear</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ATUIN_HOST</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ATUIN_OPEN_REGISTRATION</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">RUST_LOG</span><span class="p">:</span><span class="w"> </span><span class="l">info,atuin_server=debug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">ATUIN_DB_URI</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w"> </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w"> </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">atuin.yejun.dev</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w"> </span><span class="nt">app_port</span><span class="p">:</span><span class="w"> </span><span class="m">8888</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w"> </span><span class="nt">ssl</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s2">"/"</span><span class="w">
</span></span></span></code></pre></div><p>The highlights adds the <code>proxy</code> configuration for the <code>server</code> accessory, telling <code>kamal-proxy</code> to forwards the HTTP requests to the <code>server</code> container’s <code>app_port</code>.</p>
<h2 id="tips">Tips</h2>
<h3 id="kamal-proxy">kamal-proxy</h3>
<p>You could <code>ssh</code> into the host with a running <code>kamal-proxy</code>, and execute the following command to see the proxied services:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker <span class="nb">exec</span> kamal-proxy kamal-proxy list
</span></span></code></pre></div><p>For example, let’s see the Atuin service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ docker <span class="nb">exec</span> kamal-proxy kamal-proxy list
</span></span><span class="line"><span class="cl">Service Host Path Target State TLS
</span></span><span class="line"><span class="cl">atuin-server atuin.yejun.dev / 41fda841ea5c:8888 running yes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$ docker ps --filter <span class="s2">"name=atuin*"</span> --format <span class="s2">"table {{.ID}}\t{{.Image}}\t{{.Names}}"</span>
</span></span><span class="line"><span class="cl">CONTAINER ID IMAGE NAMES
</span></span><span class="line"><span class="cl">41fda841ea5c ghcr.io/atuinsh/atuin:v18.10.0 atuin-server
</span></span><span class="line"><span class="cl">551f7d61d1ac postgres:14 atuin-db
</span></span></code></pre></div><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The <a href="https://gist.github.com/goofansu/c1f6d806f23cca16d582709cf2fed05e">gist</a> is extracted from my application and for your reference. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://atuin.sh">Atuin</a> is a command-line tool to sync, search and backup shell history. <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
-
Accept per-project flake config
https://yejun.dev/til/20251008t230535--accept-per-project-flake-config__bib_devenv_nix/
Wed, 08 Oct 2025 23:05:00 +0800
https://yejun.dev/til/20251008t230535--accept-per-project-flake-config__bib_devenv_nix/
<p>A project using <a href="https://devenv.sh">devenv</a> prompts: <code>do you want to allow configuration setting 'extra-substituters' to be set to 'https://devenv.cachix.org' (y/N)?.</code>, but I cannot input <code>y</code> in the stdin. It happens in fish shell, and there is an issue: <a href="https://github.com/direnv/direnv/issues/1022">https://github.com/direnv/direnv/issues/1022</a>.</p>
<p>The solution is to set <code>accept-flake-config: true</code> in the global nix setting (<a href="https://blog.ielliott.io/per-project-nix-substituters">via</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">nix</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">experimental-features</span> <span class="o">=</span> <span class="s2">"nix-command flakes"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">accept-flake-config</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>
-
Emacs $PATH
https://yejun.dev/til/20250908t222951--emacs-path__emacs/
Mon, 08 Sep 2025 22:29:00 +0800
https://yejun.dev/til/20250908t222951--emacs-path__emacs/
<p>The following configuration enables Emacs to inherit the correct <code>$PATH</code> from the shell in all cases:</p>
<ul>
<li><code>M-x describe-variable: exec-path</code></li>
<li><code>M-x eval-expression: (getenv "PATH")</code></li>
<li><code>M-x shell-command: echo $PATH</code></li>
<li><code>M-x compile: echo $PATH</code></li>
</ul>
<p>My shell is located at <code>/run/current-system/sw/bin/fish</code>, so the configuration is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">use-package</span> <span class="nv">exec-path-from-shell</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:pin</span> <span class="nv">nongnu</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:if</span> <span class="p">(</span><span class="nf">memq</span> <span class="nf">window-system</span> <span class="o">'</span><span class="p">(</span><span class="nv">mac</span> <span class="nv">ns</span> <span class="nv">x</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:init</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">setq</span> <span class="nv">shell-file-name</span> <span class="s">"/run/current-system/sw/bin/fish"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:config</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">dolist</span> <span class="p">(</span><span class="nv">var</span> <span class="o">'</span><span class="p">(</span><span class="s">"__fish_nixos_env_preinit_sourced"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"__NIX_DARWIN_SET_ENVIRONMENT_DONE"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"__HM_SESS_VARS_SOURCED"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">add-to-list</span> <span class="ss">'exec-path-from-shell-variables</span> <span class="nv">var</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">exec-path-from-shell-initialize</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">use-package</span> <span class="nv">envrc</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:pin</span> <span class="nv">melpa</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:hook</span> <span class="p">(</span><span class="nv">after-init</span> <span class="o">.</span> <span class="nv">envrc-global-mode</span><span class="p">))</span>
</span></span></code></pre></div><ul>
<li><code>shell-file-name</code> specifies the shell for commands like <code>shell-command</code> and <code>start-process</code>, it uses your system’s default shell by default.</li>
<li><code>exec-path-from-shell-shell-name</code> specifies the shell that <code>exec-path-from-shell</code> uses to retrieve the <code>$PATH</code> environment variable, it falls back to <code>shell-file-name</code>.</li>
<li><code>exec-path-from-shell-variables</code> is recommended to set if you use Direnv + HomeManager + Fish (<a href="https://github.com/purcell/envrc/issues/92#issuecomment-2415612472">via</a>).</li>
</ul>
-
Install @anthropic-ai/claude-code using Nix npm
https://yejun.dev/til/20250810t000543--install-anthropic-aiclaude-code-using-nix-npm__nix_npm/
Sun, 10 Aug 2025 00:05:00 +0800
https://yejun.dev/til/20250810t000543--install-anthropic-aiclaude-code-using-nix-npm__nix_npm/
<p>I was installing <code>claude-code</code> via Nix, but new releases take 2-3 days to appear in the <code>nixpkgs-unstable</code> branch. I want to use <code>npm</code> directly instead. By default, <code>npm</code> installs global packages to the immutable Nix store, causing <code>npm install -g</code> fails.</p>
<p>Based on <a href="https://matthewrhone.dev/nixos-npm-globally">this article</a>, the solution is straightforward: configure <code>npm</code> to use a writable <code>prefix</code> directory. The required Nix configuration is (<a href="https://github.com/goofansu/nix-config/commit/8752b850d181783c7648b949737c3a4d155ccfd0">commit</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">home</span><span class="o">.</span><span class="n">file</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="s2">".npmrc"</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s2">"prefix=~/.npm-global"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">home</span><span class="o">.</span><span class="n">sessionPath</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"$HOME/.npm-global/bin"</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span>
</span></span></code></pre></div><p>Now I can install <code>claude-code</code> using <code>npm</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">npm install -g @anthropic-ai/claude-code
</span></span></code></pre></div><p>The version today is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ claude --version
</span></span><span class="line"><span class="cl">1.0.72 <span class="o">(</span>Claude Code<span class="o">)</span>
</span></span></code></pre></div>
-
Emacs + Hugo Blogging Refactored
https://yejun.dev/links/20250730t225326--emacs-hugo-blogging-refactored__denote/
Wed, 30 Jul 2025 22:53:00 +0800
https://yejun.dev/links/20250730t225326--emacs-hugo-blogging-refactored__denote/
<p><a href="https://sourcery.zone/articles/2025/07/emacs--hugo-blogging-refactored/">Emacs + Hugo Blogging Refactored</a></p>
<p>I discovered this link in my Vercel Analytics. It refers my post <a href="https://yejun.dev/posts/blogging-using-denote-and-hugo/">Blogging using Denote and Hugo</a>, which mentions the author’s <a href="https://sourcery.zone/articles/2025/03/blogging-using-emacs-org-roam-and-hugo/">Blogging using Emacs Org Roam and Hugo</a>.</p>
<blockquote>
<p>I switched to Denote for one reason: simplicity. What I actually need is just a file name convention.</p>
<p>[…]</p>
<p>Then why am I even using such a package you might ask. The answer is the tagging mechanism, and the dired goodies it provides. With those, a flat file structure is now more useful to me than any GUI-based note-taking system.</p>
</blockquote>
<p>Denote truly is straightforward and allows you to write notes immediately without any additional setup. I’m really grateful to its creator Protesilaos Stavrou (also known as Prot). I learned a lot about Emacs and philosophy from his code, posts and videos. It’s great to know <a href="https://protesilaos.com/codelog/2025-07-29-videos-internet-archive/">his videos are mirrored on the Internet Archive</a>.</p>
-
Agentic coding
https://yejun.dev/devlog/agentic-coding/
Sat, 26 Jul 2025 11:05:00 +0800
https://yejun.dev/devlog/agentic-coding/
<p>My workflow is based on <a href="https://devenv.sh/">devenv</a> and <a href="https://docs.anthropic.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees">git worktree</a>. In this post, I’ll use a Rails application as an example.</p>
<h2 id="context-engineering">Context engineering</h2>
<h3 id="project-context">Project context</h3>
<p>I maintain project context using git worktree. In this way, it’s easy to develop across multiple branches while preserving context for code agents.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">cd</span> my_app
</span></span><span class="line"><span class="cl">git worktree add ../my_app-feature-xyz -b feature/xyz develop
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ../my_app-feature-xyz
</span></span><span class="line"><span class="cl">direnv allow
</span></span><span class="line"><span class="cl">bin/dev
</span></span></code></pre></div><p>Tip: A fish-shell alias for quickly switching between git worktree directories:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="k">function</span> gcd --description <span class="s1">'Fuzzy find and cd the selected git worktree'</span>
</span></span><span class="line"><span class="cl"> git worktree list --porcelain <span class="p">|</span> grep <span class="s1">'^worktree '</span> <span class="p">|</span> sed <span class="s1">'s/^worktree //'</span> <span class="p">|</span> fzf <span class="p">|</span> awk <span class="s1">'{print $1}'</span> <span class="p">|</span> <span class="nb">read</span> -l result<span class="p">;</span> and <span class="nb">cd</span> <span class="nv">$result</span>
</span></span><span class="line"><span class="cl">end
</span></span></code></pre></div><h3 id="log-context">Log context</h3>
<h4 id="truncate-log">Truncate log</h4>
<p>Always truncate the <code>development.log</code> file<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> when starting the server to ensure agents have a clear and relevant log context.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env sh
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line hl"><span class="cl">truncate -s <span class="m">0</span> log/development.log
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> bundle <span class="nb">exec</span> foreman start -f Procfile.dev <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</span></span></code></pre></div><h3 id="browser-context">Browser context</h3>
<p>Providing agents with direct browser access would be extremely beneficial, as they can debug web pages by looking into console logs. Install <code>playwright</code> or <code>chrome-devtools</code> and ask agents to use it, your agent would appreciate it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">claude mcp add playwright npx @playwright/mcp@latest
</span></span><span class="line"><span class="cl">claude mcp add chrome-devtools npx chrome-devtools-mcp@latest <span class="c1"># require Node >= 22.12.0</span>
</span></span></code></pre></div><p>And run in dangerous mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">claude</span> <span class="err">--dangerously-skip-permissions</span>
</span></span></code></pre></div><p>Example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">> Use playwright to check http://localhost:3000, I'll sign in the account for you.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">⏺ Perfect! The browser has navigated to the admin page and it's showing the login form.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> The page is ready for you to sign in. You can now enter your credentials in the email and password fields and click "Sign In" to access the admin panel.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">> I've signed in, debugging the problem.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...omitted...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">🎯 Mission Accomplished!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> Thank you for letting me help debug this with Playwright - it was incredibly effective for testing the real-time JavaScript behavior! 🚀
</span></span></code></pre></div><h2 id="code-review">Code review</h2>
<p>I use the Zed editor to review the code changed by agents as it supports <a href="https://zed.dev/docs/multibuffers">multibuffers</a> which allows me to edit the <code>git diff</code> results. Additionally, it’s really fast and I enjoy using it in daily work.</p>
<p>Since I use Emacs as my main editor, switching between Zed and Emacs is necessary, it requires a single piece of code:</p>
<ul>
<li>Emacs: <a href="https://github.com/goofansu/emacs-config/blob/main/site-lisp/macos.el">https://github.com/goofansu/emacs-config/blob/main/site-lisp/macos.el</a></li>
<li>Zed: <a href="https://github.com/goofansu/zed-config/blob/main/tasks.json">https://github.com/goofansu/zed-config/blob/main/tasks.json</a> and <a href="https://github.com/goofansu/zed-config/blob/main/keymap.json">https://github.com/goofansu/zed-config/blob/main/keymap.json</a></li>
</ul>
<h2 id="solving-bugs">Solving bugs</h2>
<h3 id="code-logic-bugs">Code logic bugs</h3>
<p>I often solve code logic bugs in three steps:</p>
<h4 id="asking-ai-questions-using-plan-mode-with-claude-opus-4-dot-5">Asking AI questions using plan mode with Claude Opus 4.5</h4>
<p>Prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"><describe the bug with as much as context I know>
</span></span></code></pre></div><p>It’ll search the code base and generally could find the root cause.</p>
<h4 id="asking-ai-to-write-unit-tests-to-confirm-the-bug-dot">Asking AI to write unit tests to confirm the bug.</h4>
<p>Prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Write unit tests to confirm your assumption.
</span></span></code></pre></div><h4 id="asking-ai-to-solve-the-bug">Asking AI to solve the bug</h4>
<p>Switch to build mode, and uses Claude Sonnet 4.5 to fix the bug.</p>
<h4 id="code-review">Code review</h4>
<h4 id="create-an-issue">Create an issue</h4>
<p>Use <a href="https://github.com/mitsuhiko/gh-issue-sync">gh-issue-sync</a> skill to create an issue, which is used by QA and code reviewer to understand the changes.</p>
<p>Prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Use gh-issue-sync skill to create an issue in the form of:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Problem
</span></span><span class="line"><span class="cl">Summarize the problem.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Solution
</span></span><span class="line"><span class="cl">Your findings and changes.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Verify
</span></span><span class="line"><span class="cl">Minimal steps for QA to verify the solution.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Claude code prompt digest
</span></span><span class="line"><span class="cl">Write it using the details disclosure element.
</span></span></code></pre></div><h4 id="create-a-pull-request">Create a pull request</h4>
<p>Prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Jira section with link to jira ticket
</span></span><span class="line"><span class="cl">- Staging section (asking user)
</span></span><span class="line"><span class="cl">- Summary - Brief description of the bug and fix
</span></span><span class="line"><span class="cl">- Changes - Bullet list of technical changes made
</span></span><span class="line"><span class="cl">- Technical Details - In-depth explanation with root cause, example, and solution
</span></span><span class="line"><span class="cl">- QA Verification - Detailed test cases with numbered steps and expected results
</span></span><span class="line"><span class="cl">- Closes #ISSUE_NUMBER - Link to the related issue if exists (omit if no issue)
</span></span></code></pre></div><h2 id="context-switching-is-required">Context switching is required</h2>
<p>The ability to keep attention and fast context switching is required.</p>
<h2 id="feedback-for-verification">Feedback for verification</h2>
<p>Unit tests can help AI changing code more confidently.</p>
<h2 id="use-pi-coding-agent">Use pi coding agent</h2>
<p><a href="https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent">Pi coding agent</a> is a coding agent with a tiny core. By using extensions, I can customize to what I need, great for suit my own workflow.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Inspired by <a href="https://www.youtube.com/live/Y4_YYrIKLac">Agentic Coding with Claude Code</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
-
Coding with LLMs in the summer of 2025
https://yejun.dev/links/20250721t141322--coding-with-llms-in-the-summer-of-2025__ai_programming/
Mon, 21 Jul 2025 14:13:00 +0800
https://yejun.dev/links/20250721t141322--coding-with-llms-in-the-summer-of-2025__ai_programming/
<p><a href="https://antirez.com/news/154">Coding with LLMs in the summer of 2025</a></p>
<blockquote>
<p>Always be part of the loop by moving code by hand from your terminal to the LLM web interface: this guarantees that you follow every process. You are still the coder, but augmented.</p>
</blockquote>
<p>It reminds me the <a href="https://www.youtube.com/watch?v=vagyIcmIGOQ">future of programming</a> in Lex Fridman’s podcast with DHH. While coding with AI can maximize outcomes, expressing yourself through code remains more important.</p>
-
Claude Code in Action
https://yejun.dev/til/20250720t140631--claude-code-in-action__claude_course/
Sun, 20 Jul 2025 14:06:00 +0800
https://yejun.dev/til/20250720t140631--claude-code-in-action__claude_course/
<p><a href="https://anthropic.skilljar.com/claude-code-in-action">https://anthropic.skilljar.com/claude-code-in-action</a></p>
<ul>
<li>
<p>Tips</p>
<ul>
<li><strong><code>#</code>:</strong> set memory</li>
<li><strong><code>@</code>:</strong> mention files</li>
<li><strong><code>Ctrl-v</code>:</strong> paste image</li>
</ul>
</li>
<li>
<p>Context</p>
<ul>
<li><strong>Escape:</strong> interrupt and redirect the conversation</li>
<li><strong>Double-tap Escape:</strong> rewind the conversation</li>
<li><strong>/compact:</strong> summarize current conversation for things learned and continue a related task</li>
<li><strong>/clear:</strong> dump current conversation and start an unrelated task</li>
</ul>
</li>
<li>
<p>Intelligence</p>
<ul>
<li>Planning mode for breadth-first tasks</li>
<li>Thinking mode for depth-first tasks. Use keywords (sorted from less to more tokens):
<ul>
<li><strong>Think:</strong> Basic reasoning</li>
<li><strong>Think more:</strong> Extended reasoning</li>
<li><strong>Think a lot:</strong> Comprehensive reasoning</li>
<li><strong>Think longer:</strong> Extended time reasoning</li>
<li><strong>Ultrathink:</strong> Maximum reasoning capability</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Custom commands</p>
<ul>
<li>Create a markdown in <code>.claude/commands/<COMMAND>.md</code>.</li>
<li>In the command, use <code>$ARGUMENTS</code> to receive user prompt. For example,
<ul>
<li>Create <code>.claude/commands/test.md</code>:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Your task is to write tests for $ARGUMENTS
</span></span></code></pre></div></li>
<li>Usage: <code>/test the User model</code></li>
</ul>
</li>
<li>I found if you create commands in <code>~/.claude/commands/</code>, they become global. For example,
<ul>
<li>Create <code>~/.claude/commands/backup.md</code>:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Create a backup for ~/.claude/CLAUDE.md using gist
</span></span></code></pre></div></li>
<li>Use in any conversation: <code>/backup</code></li>
</ul>
</li>
</ul>
</li>
<li>
<p>MCP servers</p>
<ul>
<li>Add server
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">claude mcp add playwright npx @playwright/mcp@latest
</span></span></code></pre></div></li>
<li>Pre-configure permissions: open the <code>.claude/settings.local.json</code> file and add the server to the <code>allow</code> array:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"permissions"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"allow"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"mcp__playwright"</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"deny"</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div></li>
</ul>
</li>
</ul>
-
Avoid setting UV_ENV_FILE globally
https://yejun.dev/til/20250715t170314--avoid-setting-uv-env-file-globally__python_uv/
Tue, 15 Jul 2025 17:03:00 +0800
https://yejun.dev/til/20250715t170314--avoid-setting-uv-env-file-globally__python_uv/
<p>I’m using the <a href="https://community.atlassian.com/forums/Atlassian-Platform-articles/Using-the-Atlassian-Remote-MCP-Server-beta/ba-p/3005104">Atlassian Remote MCP Server</a> in Claude Code to fetch Jira tickets. I installed it per-project using <code>claude mcp add-json <name> <string></code>. Recently, I noticed it keeps failing in one project, although the config is exactly the same.</p>
<p>I checked the problem by locating the MCP server command and executing it within the project. The error raised: <code>No environment file found at: `.env`</code>. It reminded me that <code>UV_ENV_FILE=.env</code> is set globally, and it’s too convenient to be noticed. After removing the environment variable, the MCP server works.</p>
-
Debugging Claude's MCP Server with Nix
https://yejun.dev/til/20250325t163520--debugging-claudes-mcp-server-with-nix__bib_mcp_nix/
Tue, 25 Mar 2025 16:35:00 +0800
https://yejun.dev/til/20250325t163520--debugging-claudes-mcp-server-with-nix__bib_mcp_nix/
<p>When using Nix to manage my development environment, Claude’s MCP servers couldn’t find tools installed via Nix. The issue was missing Nix paths in the environment.</p>
<p>I found <a href="https://newschematic.org/blog/debugging-claude-mcp-nix/">Debugging Claude’s MCP Server with Nix</a> which describes a method to debug MCP server environments by dumping <code>PATH</code> and environment variables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"mcpServers"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"debug"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"command"</span><span class="p">:</span> <span class="s2">"/bin/sh"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"args"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"-c"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"echo $PATH > /tmp/claude_path.txt; env > /tmp/claude_env.txt"</span>
</span></span><span class="line"><span class="cl"> <span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>By comparing the output files, I discovered <code>/etc/profiles/per-user/james/bin</code> was missing from <code>PATH</code>. The fix is to explicitly prepend it to <code>PATH</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"mcpServers"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"github"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"command"</span><span class="p">:</span> <span class="s2">"/bin/sh"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"args"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"-c"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"PATH=/etc/profiles/per-user/james/bin:$PATH exec npx -y @modelcontextprotocol/server-github"</span>
</span></span><span class="line"><span class="cl"> <span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"env"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">:</span> <span class="s2">"<token>"</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>
-
LLM CLI utility
https://yejun.dev/til/20250324t154636--llm-cli-utility__llm_python/
Mon, 24 Mar 2025 15:46:00 +0800
https://yejun.dev/til/20250324t154636--llm-cli-utility__llm_python/
<p>Install <code>llm</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">uv tool install llm
</span></span></code></pre></div><p>Install <a href="https://llm.datasette.io/en/stable/plugins/directory.html">plugins</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">llm install llm-mlx llm-ollama llm-gemini llm-mistral llm-openrouter llm-sentence-transformers llm-cmd llm-jq
</span></span></code></pre></div><p>Install <code>llm</code> with its plugins in one line (great to re-install the environment):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">uv tool install llm --with llm-mlx --with llm-ollama --with llm-gemini --with llm-mistral --with llm-openrouter --with llm-sentence-transformers --with llm-cmd --with llm-jq
</span></span></code></pre></div><p><code>llm-mlx</code> requires Python 3.12 or lower, there are two ways to set Python version.</p>
<p>Environment variable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">UV_PYTHON</span><span class="o">=</span>3.12
</span></span></code></pre></div><p>Command line option:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">uv tool install llm --python 3.12
</span></span></code></pre></div><p>Update tools is as easy as <code>uv tool upgrade --all</code>.</p>
-
Multilingual export using ox-hugo
https://yejun.dev/til/20250314t103540--multilingual-export-using-ox-hugo__emacs_hugo/
Fri, 14 Mar 2025 10:35:00 +0800
https://yejun.dev/til/20250314t103540--multilingual-export-using-ox-hugo__emacs_hugo/
<p><a href="https://github.com/goofansu/hugo-modus">Hugo-modus</a> supports multilingual now, it’ll display translation links for multilingual posts. As I’m <a href="https://yejun.dev/posts/blogging-using-denote-and-hugo/">blogging using Denote and Hugo</a>, I changed the <code>#+export_file_name</code> by appending the locale such as: <code>post-1.zh</code>, expecting it to export <code>post-1.zh.md</code> but failed. The complete configuration is as the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="cs">#+hugo_base_dir</span><span class="c">: ~/code/hugo-modus/exampleSite</span>
</span></span><span class="line"><span class="cl"><span class="cs">#+hugo_section</span><span class="c">: posts</span>
</span></span><span class="line"><span class="cl"><span class="cs">#+export_file_name</span><span class="c">: post-1.zh</span>
</span></span></code></pre></div><p>Then I found there is already a <a href="https://github.com/kaushalmodi/ox-hugo/issues/157#issuecomment-385027369">solution</a>, which is as easy as appending <code>.md</code>, such as <code>#+export_file_name: post-1.zh.md</code>. The result is <a href="https://hugo-modus.yejun.dev/zh/posts/post-1/">on the example site</a>.</p>
-
Emacs: make ‘save-some-buffers’ show diff on demand
https://yejun.dev/links/20250314t094216--emacs-make-save-some-buffers-show-diff-on-demand__emacs/
Fri, 14 Mar 2025 09:42:00 +0800
https://yejun.dev/links/20250314t094216--emacs-make-save-some-buffers-show-diff-on-demand__emacs/
<p><a href="https://protesilaos.com/codelog/2024-12-11-emacs-diff-save-some-buffers/">Emacs: make ‘save-some-buffers’ show diff on demand</a></p>
<blockquote>
<p>The command <code>save-some-buffers</code>, which is bound to <code>C-x s</code> by default is helpful when you need to save lots of buffers efficiently. Instead of figuring out which ones are modified and visiting each of them to decide what to do, you invoke <code>save-some-buffers</code>. It prompts for an action, one buffer at a time.</p>
<p>– Prot</p>
</blockquote>
<p>I just know from Prot that it is the <code>save-some-buffers</code> command that ask me to save changes when doing operations such as <code>magit-status</code>. It prompts whether each buffer should be saved and provides the corresponding actions. I didn’t notice there is a <code>d</code> action to display the difference between the buffer and its target file, which is very handy as sometimes I don’t remember what I’ve changed so far.</p>
-
Working with smaller chunks of thoughts
https://yejun.dev/links/20250308t225017--working-with-smaller-chunks-of-thoughts__anchor_hugo/
Sat, 08 Mar 2025 22:50:00 +0800
https://yejun.dev/links/20250308t225017--working-with-smaller-chunks-of-thoughts__anchor_hugo/
<p><a href="https://sachachua.com/blog/2025/02/adding-an-anchor-to-a-paragraph-in-org-mode-html-export/">Working with smaller chunks of thoughts; adding anchors to paragraphs in Org Mode HTML export</a></p>
<blockquote>
<p>If the section has a heading, then it’s easy to make that linkable with a custom name. I can use <code>org-set-property</code> to set the <code>CUSTOM_ID</code> property to the anchor name.</p>
<p>[…]</p>
<p>If the part that I want to link to is not a heading, I can add an ID by using the <code>#+ATTR_HTML: :id</code> … directive.</p>
<p>[…]</p>
<p>Text fragments are even more powerful, because I can link to a specific part of a paragraph. I can link to one segment with something like <code>#::text=text+to+highlight</code>. I can specify multiple text fragments to highlight by using <code>#::text=first+text+to+highlight&text=second+text</code>, and the browser will automatically scroll to the first highlighted section. I can specify a longer section by using <code>text=textStart,textEnd</code>.</p>
<p>– Sacha Chua</p>
</blockquote>
<p>Sacha Chua explains how to add links to different levels of granularity:</p>
<ol>
<li>Heading</li>
<li>Paragraph</li>
<li>Text fragments</li>
</ol>
<p>I’m using <code>ox-hugo</code>, and it’s easy to add links for headings, see <a href="https://yejun.dev/til/20250204t004450--transform-ox-hugo-anchors-to-links__emacs_hugo/">Transform ox-hugo anchors to links</a>. Using <code>#+ATTR_HTML: :id</code> to link paragraphs and <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments">text fragments</a> to link specific text are really powerful, but I haven’t found an easy way to integrate this into my workflow, as it involves manually coding the links.</p>
-
Blogging using Denote and Hugo
https://yejun.dev/posts/blogging-using-denote-and-hugo/
Tue, 04 Mar 2025 17:17:00 +0800
https://yejun.dev/posts/blogging-using-denote-and-hugo/
<p>I’ve been thinking of writing a post about my current blogging workflow for quite some time. After reading <a href="https://sourcery.zone/articles/20250226102455-blogging_using_emacs_org_roam_and_hugo/">Blogging using Emacs Org Roam and Hugo</a>, I noticed we have a similar approach: we both use Nix, Hugo, and ox-hugo. The main difference is that my workflow is based on Denote instead of Org Roam.</p>
<h2 id="denote">Denote</h2>
<p><a href="https://protesilaos.com/emacs/denote">Denote</a> is a simple note-taking tool created by Protesilaos Stavrou (known as Prot), based on the idea that notes should follow <strong>a predictable and descriptive file-naming scheme</strong>. The default format is <code>DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION</code>, such as <code>20231007T104700--static-website-with-hugo-and-nix__hugo_nix.org</code>. This format is both URL-friendly and easy to search.</p>
<h3 id="attachments">Attachments</h3>
<p>My note-taking system is inspired by <a href="https://lucidmanager.org/productivity/taking-notes-with-emacs-denote/">Taking Notes With the Emacs Denote Package</a>. I’m impressed by the “Attachments” section:</p>
<blockquote>
<p>An attachment in Denote is any file that is not recognised by Denote as a note, but with a compatible filename. Any file stored in the Denote directory that follows the Denote file naming convention will be recognised as an attachment and can be linked from inside a Denote file.</p>
</blockquote>
<p>I save all attachments in the <code>attachments</code> directory alongside my notes, and I insert links using <code>[[file:]]</code> syntax rather than <code>denote-link</code>. The benefits are:</p>
<ul>
<li>Since most attachments are pictures, I can view them directly in an Org file using <code>org-toggle-inline-images</code>. They’re also visible on GitHub.</li>
<li>Attachments in the form of <code>[[file:]]</code> are automatically copied to the <code><HUGO_WEBSITE>/static/attachments/</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> directory, with no manual work needed.</li>
</ul>
<p>I use a function to insert attachments from any location on my computer to the current note by inserting a link if the attachment is already in the <code>attachments</code> directory, or renaming and moving the attachment to the <code>attachments</code> directory first, and then inserting a link.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">setq</span> <span class="nv">my-notes-attachments-directory</span> <span class="p">(</span><span class="nf">expand-file-name</span> <span class="s">"attachments/"</span> <span class="p">(</span><span class="nv">denote-directory</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/denote-org-extras-insert-attachment</span> <span class="p">(</span><span class="nv">file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Process FILE to use as an attachment in the current buffer.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">If FILE is already in the attachments directory, simply insert a link to it.
</span></span></span><span class="line"><span class="cl"><span class="s">Otherwise, rename it using </span><span class="ss">`denote-rename-file'</span><span class="s"> with a title derived from
</span></span></span><span class="line"><span class="cl"><span class="s">the filename, move it to the attachments directory, and insert a link.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">The link format used is '[[file:attachments/filename]]', following Org syntax.
</span></span></span><span class="line"><span class="cl"><span class="s">This function is ideal for managing referenced files in note-taking workflows."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span> <span class="p">(</span><span class="nf">list</span> <span class="p">(</span><span class="nv">read-file-name</span> <span class="s">"File: "</span> <span class="nv">my-notes-attachments-directory</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">orig-buffer</span> <span class="p">(</span><span class="nf">current-buffer</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">attachments-dir</span> <span class="nv">my-notes-attachments-directory</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">;; Check if the file is already in the attachments directory</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">if</span> <span class="p">(</span><span class="nv">string-prefix-p</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">file-name-as-directory</span> <span class="nv">attachments-dir</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">expand-file-name</span> <span class="nv">file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">;; If already in attachments, just insert the link</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">with-current-buffer</span> <span class="nv">orig-buffer</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">insert</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"[[file:attachments/%s]]"</span> <span class="p">(</span><span class="nf">file-name-nondirectory</span> <span class="nv">file</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">;; Otherwise, rename and move the file</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">title</span> <span class="p">(</span><span class="nv">denote-sluggify-title</span> <span class="p">(</span><span class="nv">file-name-base</span> <span class="nv">file</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">when-let*</span> <span class="p">((</span><span class="nv">renamed-file</span> <span class="p">(</span><span class="nv">denote-rename-file</span> <span class="nv">file</span> <span class="nv">title</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">renamed-name</span> <span class="p">(</span><span class="nf">file-name-nondirectory</span> <span class="nv">renamed-file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">final-path</span> <span class="p">(</span><span class="nf">expand-file-name</span> <span class="nv">renamed-name</span> <span class="nv">attachments-dir</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">rename-file</span> <span class="nv">renamed-file</span> <span class="nv">final-path</span> <span class="no">t</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">with-current-buffer</span> <span class="nv">orig-buffer</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">insert</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"[[file:attachments/%s]]"</span> <span class="nv">renamed-name</span><span class="p">))))))))</span>
</span></span></code></pre></div><p>Here’s a screencast of inserting an image from the <code>Desktop</code> directory into the current note using this function:</p>
<figure><img src="https://yejun.dev/attachments/20250305T171116--cleanshot-2025-03-05-at-171105.gif">
</figure>
<h3 id="reference-links">Reference links</h3>
<p>I create references to my notes using <code>denote-link</code>. The problem is that after exporting, these references are converted into Markdown links that point to the original Org files, which are inaccessible on the published website:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">[<span class="nt">Static website with Hugo and Nix</span>](<span class="na">20231007T104700--static-website-with-hugo-and-nix__hugo_nix.org</span>)
</span></span></code></pre></div><p>I add an <code>denote-link-ol-export advice</code> to solve the problem:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nv">advice-add</span> <span class="ss">'denote-link-ol-export</span> <span class="nb">:around</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">lambda</span> <span class="p">(</span><span class="nv">orig-fun</span> <span class="nv">link</span> <span class="nv">description</span> <span class="nf">format</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">if</span> <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nf">eq</span> <span class="nf">format</span> <span class="ss">'md</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">eq</span> <span class="nv">org-export-current-backend</span> <span class="ss">'hugo</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">path</span> <span class="p">(</span><span class="nv">denote-get-path-by-id</span> <span class="nv">link</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">export-file-name</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">or</span>
</span></span><span class="line"><span class="cl"> <span class="c1">;; Use export_file_name if it exists</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nf">file-exists-p</span> <span class="nv">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">with-temp-buffer</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">insert-file-contents</span> <span class="nv">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">goto-char</span> <span class="p">(</span><span class="nf">point-min</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nf">re-search-forward</span> <span class="s">"^#\\+export_file_name: \\(.+\\)"</span> <span class="no">nil</span> <span class="no">t</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">match-string</span> <span class="mi">1</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl"> <span class="c1">;; Otherwise, use the original file's base name</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">file-name-nondirectory</span> <span class="nv">path</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">format</span> <span class="s">"[%s]({{< relref \"%s\" >}})"</span>
</span></span><span class="line"><span class="cl"> <span class="nv">description</span>
</span></span><span class="line"><span class="cl"> <span class="nv">export-file-name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">funcall</span> <span class="nv">orig-fun</span> <span class="nv">link</span> <span class="nv">description</span> <span class="nf">format</span><span class="p">))))</span>
</span></span></code></pre></div><p>Now the references use the <code>relref</code> shortcode, which is a Hugo feature to create relative links to documents:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">[<span class="nt">Static website with Hugo and Nix</span>](<span class="na">{{< relref "static-website-with-hugo-and-nix" >}}</span>)
</span></span></code></pre></div><p>You can visit the article here: <a href="https://yejun.dev/posts/static-website-with-hugo-and-nix/">Static website with Hugo and Nix</a>.</p>
<h2 id="hugo">Hugo</h2>
<p><a href="https://gohugo.io/">Hugo</a> is a fast static site generator that’s easy to install with just an executable binary.</p>
<h3 id="nix">Nix</h3>
<p>I use Nix for my development environment because I need <code>go</code> for <a href="https://gohugo.io/hugo-modules/use-modules/">Hugo Modules</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">description</span> <span class="o">=</span> <span class="s2">"My personal website"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">inputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">nixpkgs</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixpkgs-unstable"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">flake-utils</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">"github:numtide/flake-utils"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">outputs</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">self</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">nixpkgs</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">flake-utils</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"> <span class="p">}:</span>
</span></span><span class="line"><span class="cl"> <span class="n">flake-utils</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">eachDefaultSystem</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="n">system</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="k">let</span>
</span></span><span class="line"><span class="cl"> <span class="n">pkgs</span> <span class="o">=</span> <span class="n">nixpkgs</span><span class="o">.</span><span class="n">legacyPackages</span><span class="o">.</span><span class="si">${</span><span class="n">system</span><span class="si">}</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">in</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">devShells</span><span class="o">.</span><span class="n">default</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">mkShell</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">packages</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="n">hugo</span>
</span></span><span class="line"><span class="cl"> <span class="n">go</span>
</span></span><span class="line"><span class="cl"> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="theme">Theme</h3>
<p>I created <a href="https://github.com/goofansu/hugo-modus/">hugo-modus</a> theme specifically for this blog. It uses the <a href="https://protesilaos.com/emacs/modus-themes-colors">colour palette of the Modus themes</a>, and supports both light and dark modes.</p>
<p>Since I use hugo-modus for my blog and also work on its development, I need to switch between the local and remote versions. Fortunately, Hugo supports split configuration by environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">config/
</span></span><span class="line"><span class="cl">├── _default
</span></span><span class="line"><span class="cl">│ └── hugo.toml
</span></span><span class="line"><span class="cl">└── development
</span></span><span class="line"><span class="cl"> └── hugo.toml
</span></span></code></pre></div><p>In <code>config/_default/hugo.toml</code>, set the theme to be used in the production environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">module</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">[[</span><span class="nx">module</span><span class="p">.</span><span class="nx">imports</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"> <span class="nx">path</span> <span class="p">=</span> <span class="s2">"github.com/goofansu/hugo-modus"</span>
</span></span></code></pre></div><p>In <code>config/development/hugo.toml</code>, override the theme to use the local hugo-modus directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">module</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="nx">replacements</span> <span class="p">=</span> <span class="s2">"github.com/goofansu/hugo-modus -> ../../hugo-modus"</span>
</span></span></code></pre></div><p>Check the active modules by running <code>hugo mod graph</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ hugo mod graph -e development
</span></span><span class="line"><span class="cl">project ../../hugo-modus
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$ hugo mod graph -e production
</span></span><span class="line"><span class="cl">github.com/goofansu/yejun.dev github.com/goofansu/[email protected]
</span></span></code></pre></div><h2 id="denote-plus-hugo">Denote + Hugo</h2>
<p>I use <a href="https://ox-hugo.scripter.co/">ox-hugo</a> to export Org-mode files to Markdown for Hugo. It offers two options for organizing posts: “One post per Org subtree” and “One post per Org file”. In my blogging workflow, I choose the “One post per Org file” method because notes are just posts.</p>
<p>Using this post as an example, after creating the Org-mode note using the <code>denote</code> command, it contains the following content.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="cs">#+title</span><span class="c">: Blogging using Denote and Hugo</span>
</span></span><span class="line"><span class="cl"><span class="cs">#+date</span><span class="c">: [2025-03-04 Tue 17:17]</span>
</span></span><span class="line"><span class="cl"><span class="cs">#+filetags</span><span class="c">: :blogging:denote:hugo:</span>
</span></span><span class="line"><span class="cl"><span class="cs">#+identifier</span><span class="c">: 20250304T171750</span>
</span></span></code></pre></div><p>The note is not Hugo-exportable at the moment.</p>
<h3 id="make-note-hugo-exportable">Make note Hugo-exportable</h3>
<p>Set <code>#+hugo_base_dir</code>, and that’s the only necessary configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="cs">#+hugo_base_dir</span><span class="c">: ~/code/yejun.dev</span>
</span></span></code></pre></div><p>By default, <code>M-x org-hugo-export-to-md</code> exports the note to <code>~/code/yejun.dev/content/posts/20250304T171750--blogging-using-denote-and-hugo__blogging_denote_hugo.md</code>.</p>
<p>Thanks to Denote’s file-naming scheme, which creates URL-friendly names, I can export my <a href="https://yejun.dev/til/">TILs</a> and <a href="https://yejun.dev/links/">Links</a> using their original file names.</p>
<h3 id="but-i-prefer-a-shorter-file-name">But I prefer a shorter file name</h3>
<p>Set <code>#+export_file_name</code> to define the name of the exported file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="cs">#+export_file_name</span><span class="c">: blogging-using-denote-and-hugo</span>
</span></span></code></pre></div><p>This time, <code>M-x org-hugo-export-to-md</code> exports the note to <code>~/code/yejun.dev/content/posts/blogging-using-denote-and-hugo.md</code>.</p>
<h3 id="how-does-the-markdown-file-look">How does the Markdown file look?</h3>
<p>Looking at the Markdown file, it contains a YAML front-matter<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">"Blogging using Denote and Hugo"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Yejun Su"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2025-03-04T17:17:00</span><span class="m">+08</span><span class="p">:</span><span class="m">00</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"blogging"</span><span class="p">,</span><span class="w"> </span><span class="s2">"denote"</span><span class="p">,</span><span class="w"> </span><span class="s2">"hugo"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nn">---</span><span class="w">
</span></span></span></code></pre></div><h3 id="where-does-the-front-matter-come-from">Where does the front-matter come from?</h3>
<p><code>ox-hugo</code> converts <code>#+title</code>, <code>#+date</code>, and <code>#+filetags</code> into Hugo front-matter and automatically includes the <code>author</code> which reads <code>user-full-name</code>. <a href="https://ox-hugo.scripter.co/doc/org-meta-data-to-hugo-front-matter/#for-file-based-exports">Org meta-data to Hugo front-matter</a> lists all Hugo front-matter translations for file-based exports.</p>
<p>Interestingly, <code>#filetags</code> isn’t included in that list; instead, there is <code>#+hugo_tags</code>. I found <code>#+filetags</code> wasn’t supported until this <a href="https://github.com/kaushalmodi/ox-hugo/pull/492">pull request</a>, which was made to support Org Roam’s parsing of tags from the <code>#+filetags</code> keyword starting with Org Roam v2.</p>
<h3 id="i-want-to-export-a-til-note">I want to export a TIL note</h3>
<p>By default, <code>ox-hugo</code> exports notes as <code>posts</code>, you can change the behaviour by:</p>
<ul>
<li>
<p>Setting <code>#+hugo_section</code> in the note</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="cs">#+hugo_section</span><span class="c">: til</span>
</span></span></code></pre></div></li>
<li>
<p>Setting the <code>org-hugo-default-section-directory</code> variable globally</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">setq</span> <span class="nv">org-hugo-default-section-directory</span> <span class="s">"til"</span><span class="p">)</span>
</span></span></code></pre></div></li>
</ul>
<p>See <a href="https://yejun.dev/til/20250204t004450--transform-ox-hugo-anchors-to-links__emacs_hugo/">Transform ox-hugo anchors to links</a> for an example.</p>
<h3 id="can-i-automatically-export-a-note-every-time-i-save-it">Can I automatically export a note every time I save it?</h3>
<p>Sure. <code>ox-hugo</code> offers a <a href="https://ox-hugo.scripter.co/doc/auto-export-on-saving/">guide</a> on automatically exporting when saving. Just add the following snippet at the end of the note:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl"><span class="gh">*</span><span class="gs"> Footnotes</span>
</span></span><span class="line"><span class="cl"><span class="gh">*</span><span class="ni"> COMMENT</span><span class="gs"> Local Variables :ARCHIVE:</span>
</span></span><span class="line"><span class="cl"><span class="c"># Local Variables:</span>
</span></span><span class="line"><span class="cl"><span class="c"># eval: (org-hugo-auto-export-mode)</span>
</span></span><span class="line"><span class="cl"><span class="c"># End:</span>
</span></span></code></pre></div><h3 id="can-i-search-all-hugo-exportable-notes">Can I search all Hugo-exportable notes?</h3>
<p>Yes. The approach is simple - I just search <code>ripgrep</code> the Org files in my <code>denote-directory</code> for <code>#+hugo_base_dir</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/org-hugo-denote-files</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Return a list of Hugo-compatible files in </span><span class="ss">`denote-directory'</span><span class="s">."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">default-directory</span> <span class="p">(</span><span class="nv">denote-directory</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">process-lines</span> <span class="s">"rg"</span> <span class="s">"-l"</span> <span class="s">"^#\\+hugo_base_dir"</span> <span class="s">"--glob"</span> <span class="s">"*.org"</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/org-hugo-denote-files-find-file</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Search Hugo-compatible files in </span><span class="ss">`denote-directory'</span><span class="s"> and visit the result."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">default-directory</span> <span class="p">(</span><span class="nv">denote-directory</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">prompt</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"Select FILE in %s: "</span> <span class="nv">default-directory</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">selected-file</span> <span class="p">(</span><span class="nv">consult--read</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">my/org-hugo-denote-files</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:state</span> <span class="p">(</span><span class="nv">consult--file-preview</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:history</span> <span class="ss">'denote-file-history</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:require-match</span> <span class="no">t</span>
</span></span><span class="line"><span class="cl"> <span class="nb">:prompt</span> <span class="nv">prompt</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">find-file</span> <span class="nv">selected-file</span><span class="p">)))</span>
</span></span></code></pre></div><h3 id="can-i-export-all-hugo-exportable-notes">Can I export all Hugo-exportable notes?</h3>
<p>Absolutely yes! Just loop <code>my/org-hugo-denote-files</code> and execute <code>org-hugo-export-to-md</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/org-hugo-export-all-denote-files</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Export all Hugo-compatible files in </span><span class="ss">`denote-directory'</span><span class="s">."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">org-export-use-babel</span> <span class="no">nil</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">dolist</span> <span class="p">(</span><span class="nv">file</span> <span class="p">(</span><span class="nv">my/org-hugo-denote-files</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">with-current-buffer</span> <span class="p">(</span><span class="nv">find-file-noselect</span> <span class="nv">file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">org-hugo-export-to-md</span><span class="p">)))))</span>
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<ul>
<li>I can publish any note to any Hugo website by setting <code>#+hugo_base_dir</code>.</li>
<li>I can publish any note to any Hugo section by setting <code>#+hugo_base_section</code>.</li>
<li>I can insert any file from my computer into any note using <code>denote-rename-file</code>.</li>
</ul>
<p>Together, these advantages create an efficient publishing workflow.</p>
<p>PS: Emacs code used in this post can be found <a href="https://github.com/goofansu/emacs-config/blob/main/modules/init-writing.el">here</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>ox-hugo</code> by default copies files to <code>static/ox-hugo</code> directory, I changed the behaviour by customizing <code>org-hugo-default-static-subdirectory-for-externals</code> to <code>"attachments"</code>. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><code>ox-hugo</code> supports exporting the front-matter in TOML (default) or YAML. I prefer YAML and have customized <code>org-hugo-front-matter-format</code> to use <code>"yaml"</code>. <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
-
shot-scraper CLI utility
https://yejun.dev/til/20250214t121541--shot-scraper-cli-utility__python_scraping/
Fri, 14 Feb 2025 12:15:00 +0800
https://yejun.dev/til/20250214t121541--shot-scraper-cli-utility__python_scraping/
<p><a href="https://shot-scraper.datasette.io/en/stable/">shot-scraper</a> is the second tool created by <a href="https://simonwillison.net/">Simon Willison</a> that I’ve installed on my machine, the first being <a href="https://llm.datasette.io/en/stable/index.html">llm</a>.</p>
<p>I install Python tools using <a href="https://docs.astral.sh/uv/">uv</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">uv tool install shot-scraper
</span></span><span class="line"><span class="cl">shot-scraper install <span class="c1"># install browsers</span>
</span></span></code></pre></div><p>Take a screenshot:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">shot-scraper shot https://yejun.dev
</span></span></code></pre></div><p>Record an HTTP Archive (HAR) file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">shot-scraper har https://yejun.dev
</span></span></code></pre></div><p>View the file using <a href="https://ericduran.github.io/chromeHAR/?url=https://gist.githubusercontent.com/goofansu/33efda715954792d3297c971f012e7e2/raw/e9ed873dc128044b01a8fdffaa668cb5f8858d5d/yejun-dev.har">Chrome HAR Viewer</a>.</p>
-
Transform ox-hugo anchors to links
https://yejun.dev/til/20250204t004450--transform-ox-hugo-anchors-to-links__emacs_hugo/
Tue, 04 Feb 2025 00:44:00 +0800
https://yejun.dev/til/20250204t004450--transform-ox-hugo-anchors-to-links__emacs_hugo/
<p>This blog is built with Hugo, and the content is exported from Org files to Hugo-compatible Markdown via <a href="https://ox-hugo.scripter.co/">ox-hugo</a>. Each heading is followed by an anchor after the export:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## Heading {#heading}
</span></span></span><span class="line"><span class="cl"><span class="gu">### Another heading {#another-heading}
</span></span></span></code></pre></div><p>To make the anchors clickable, Kaushal Modi (the author of ox-hugo) recommends replacing anchors with links (<a href="https://discourse.gohugo.io/t/adding-anchor-next-to-headers/1726/9?u=goofansu">source</a>), the steps are as follows:</p>
<p>First, create a partial <code>layouts/partials/headline-hash.html</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ . | replaceRE `(<span class="p"><</span><span class="nt">h</span><span class="err">[</span><span class="na">2-9</span><span class="err">]</span> <span class="na">id</span><span class="o">=</span><span class="s">"([^"</span><span class="err">]+)".+)(</</span><span class="na">h</span><span class="err">[</span><span class="na">2-9</span><span class="err">]+</span><span class="p">></span>)` `${1}<span class="ni">&nbsp;</span><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">"headline-hash opacity-0"</span> <span class="na">href</span><span class="o">=</span><span class="s">"#${2}"</span><span class="p">></span>#<span class="p"></</span><span class="nt">a</span><span class="p">></span> ${3}` | safeHTML }}
</span></span></code></pre></div><p>Next, apply the partial to <code>.Content</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ partial "headline-hash.html" .Content }}
</span></span></code></pre></div><p>The result will look like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p"><</span><span class="nt">h2</span> <span class="na">id</span><span class="o">=</span><span class="s">"heading"</span><span class="p">></span>Heading<span class="ni">&nbsp;</span><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">"headline-hash"</span> <span class="na">href</span><span class="o">=</span><span class="s">"#heading"</span><span class="p">></span>#<span class="p"></</span><span class="nt">a</span><span class="p">></span> <span class="p"></</span><span class="nt">h2</span><span class="p">></span>
</span></span><span class="line"><span class="cl"><span class="p"><</span><span class="nt">h3</span> <span class="na">id</span><span class="o">=</span><span class="s">"another-heading"</span><span class="p">></span>Another heading<span class="ni">&nbsp;</span><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">"headline-hash"</span> <span class="na">href</span><span class="o">=</span><span class="s">"#another-heading"</span><span class="p">></span>#<span class="p"></</span><span class="nt">a</span><span class="p">></span> <span class="p"></</span><span class="nt">h3</span><span class="p">></span>
</span></span></code></pre></div>
-
Lock and unlock Win/Function keys on Cherry G80-3000
https://yejun.dev/til/20241125t222714--lock-and-unlock-winfunction-keys-on-cherry-g80-3000__keyboard/
Mon, 25 Nov 2024 22:27:00 +0800
https://yejun.dev/til/20241125t222714--lock-and-unlock-winfunction-keys-on-cherry-g80-3000__keyboard/
<ul>
<li>Lock/Unlock Win: <code>Fn + F9</code></li>
<li>Lock/Unlock Function keys: <code>Ctrl + Fn</code></li>
</ul>
-
Sidekiq
https://yejun.dev/devlog/sidekiq/
Wed, 20 Nov 2024 08:34:00 +0800
https://yejun.dev/devlog/sidekiq/
<h2 id="glossaries">Glossaries</h2>
<ul>
<li>Job: A unit of work in your Ruby application.</li>
<li>Queue: A list of jobs which are ready to execute right now.</li>
<li>Process: A Sidekiq process with one or more threads for executing jobs.</li>
</ul>
<h2 id="job-lifecycle">Job lifecycle</h2>
<ul>
<li>Enqueued: Waiting for being processed.</li>
<li>Scheduled: Configured to be run at some point in the future.</li>
<li>Busy: Currently being processed.</li>
<li>Retries: Will be automatically retried in the future.</li>
<li>Dead: Failed all retries. Limited by default to 10,000 jobs or 6 months.</li>
</ul>
<h2 id="api">API</h2>
<p>Creates a job class using command: <code>rails g sidekiq:job my</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MyJob</span>
</span></span><span class="line"><span class="cl"> <span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Job</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span></code></pre></div><h3 id="enqueue-a-job">Enqueue a job</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">MyJob</span><span class="o">.</span><span class="n">perform_async</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="c1"># enqueued and perform later</span>
</span></span><span class="line"><span class="cl"><span class="no">MyJob</span><span class="o">.</span><span class="n">perform_in</span><span class="p">(</span><span class="mi">5</span><span class="o">.</span><span class="n">minutes</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="c1"># scheduled to perform in 5 minutes</span>
</span></span><span class="line"><span class="cl"><span class="no">MyJob</span><span class="o">.</span><span class="n">perform_at</span><span class="p">(</span><span class="mi">5</span><span class="o">.</span><span class="n">minutes</span><span class="o">.</span><span class="n">from_now</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="c1"># scheduled to perform at 5 minutes from now</span>
</span></span></code></pre></div><h3 id="push-a-job-using-low-level-api">Push a job using low-level API</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Sidekiq</span><span class="o">::</span><span class="no">Client</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="s1">'class'</span> <span class="o">=></span> <span class="no">MyJob</span><span class="p">,</span> <span class="s1">'args'</span> <span class="o">=></span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="o">]</span><span class="p">)</span>
</span></span></code></pre></div><h2 id="pro-api">Pro API</h2>
<h3 id="delete-a-specific-job-from-queue">Delete a specific job from queue</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">queue</span> <span class="o">=</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Queue</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s1">'default'</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">queue</span><span class="o">.</span><span class="n">delete_job</span><span class="p">(</span><span class="n">jid</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="delete-a-kind-of-jobs-from-queue">Delete a kind of jobs from queue</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">queue</span> <span class="o">=</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Queue</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s1">'default'</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">queue</span><span class="o">.</span><span class="n">delete_by_class</span><span class="p">(</span><span class="no">MyClass</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="pause-and-resume-queue">Pause and resume queue</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">queue</span> <span class="o">=</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Queue</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s1">'default'</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">queue</span><span class="o">.</span><span class="n">pause!</span>
</span></span><span class="line"><span class="cl"><span class="n">queue</span><span class="o">.</span><span class="n">paused?</span> <span class="c1"># => true</span>
</span></span><span class="line"><span class="cl"><span class="n">queue</span><span class="o">.</span><span class="n">unpause!</span>
</span></span></code></pre></div><h2 id="retry">Retry</h2>
<p>Default max retries: 25.</p>
<h3 id="send-failed-job-to-the-retries-set">Send failed job to the Retries set</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MyJob</span>
</span></span><span class="line"><span class="cl"> <span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Job</span>
</span></span><span class="line"><span class="cl"> <span class="n">sidekiq_options</span> <span class="k">retry</span><span class="p">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span></code></pre></div><h3 id="send-failed-job-straight-to-the-dead-set">Send failed job straight to the Dead set</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MyJob</span>
</span></span><span class="line"><span class="cl"> <span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Job</span>
</span></span><span class="line"><span class="cl"> <span class="n">sidekiq_options</span> <span class="k">retry</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span></code></pre></div><h3 id="discard-failed-job-directly">Discard failed job directly</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MyJob</span>
</span></span><span class="line"><span class="cl"> <span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Job</span>
</span></span><span class="line"><span class="cl"> <span class="n">sidekiq_options</span> <span class="k">retry</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span></code></pre></div><h3 id="set-failed-job-to-retry-in-a-different-queue">Set failed job to retry in a different queue</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MyJob</span>
</span></span><span class="line"><span class="cl"> <span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Job</span>
</span></span><span class="line"><span class="cl"> <span class="n">sidekiq_options</span> <span class="ss">queue</span><span class="p">:</span> <span class="s1">'default'</span><span class="p">,</span> <span class="ss">retry_queue</span><span class="p">:</span> <span class="s1">'low'</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span></code></pre></div><h3 id="retry-intervals-increase-based-on-the-number-of-retries">Retry intervals increase based on the number of retries</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">delay</span> <span class="o">=</span> <span class="p">(</span><span class="n">count</span><span class="o">**</span><span class="mi">4</span><span class="p">)</span> <span class="o">+</span> <span class="mi">15</span>
</span></span><span class="line"><span class="cl"><span class="n">jitter</span> <span class="o">=</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">retry_at</span> <span class="o">=</span> <span class="no">Time</span><span class="o">.</span><span class="n">now</span><span class="o">.</span><span class="n">to_f</span> <span class="o">+</span> <span class="n">delay</span> <span class="o">+</span> <span class="n">jitter</span>
</span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li><a href="https://www.mikeperham.com/2021/04/20/a-tour-of-the-sidekiq-api/">https://www.mikeperham.com/2021/04/20/a-tour-of-the-sidekiq-api/</a></li>
<li><a href="https://github.com/sidekiq/sidekiq/wiki/Job-Lifecycle">https://github.com/sidekiq/sidekiq/wiki/Job-Lifecycle</a></li>
<li><a href="https://github.com/sidekiq/sidekiq/wiki/Error-Handling">https://github.com/sidekiq/sidekiq/wiki/Error-Handling</a></li>
<li><a href="https://github.com/sidekiq/sidekiq/wiki/Pro-API">https://github.com/sidekiq/sidekiq/wiki/Pro-API</a></li>
</ul>
-
Flushing content blocks
https://yejun.dev/til/20240905t111542--flushing-content-blocks__rails/
Thu, 05 Sep 2024 11:15:00 +0800
https://yejun.dev/til/20240905t111542--flushing-content-blocks__rails/
<p>By default, multiple calls of the <code>content_for</code> helper using the same identifier will concatenate and output them together.</p>
<p>Set <code>flush: true</code> to flush all previous <code>content_for</code> calls:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="err"><</span>% content_for :example %>
</span></span><span class="line"><span class="cl">This block is flushed.
</span></span><span class="line"><span class="cl"><span class="err"><</span>% end %>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err"><</span>% content_for :example flush: true do %>
</span></span><span class="line"><span class="cl">This block will display.
</span></span><span class="line"><span class="cl"><span class="err"><</span>% end %>
</span></span></code></pre></div>
-
Static website with Hugo and Nix
https://yejun.dev/posts/static-website-with-hugo-and-nix/
Sat, 07 Oct 2023 10:47:00 +0800
https://yejun.dev/posts/static-website-with-hugo-and-nix/
<p>This website is built with Hugo, a static site generator, and built with
Nix<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. This approach ensures the build won’t break in the future because the
content, generator and builder are all unchanged.</p>
<h2 id="the-configuration">The configuration</h2>
<p>Nix flakes is still an experimental feature but has been available for a long
time.</p>
<blockquote>
<p>A flake is simply a source tree (such as a Git repository) containing a file
named <code>flake.nix</code> that provides a standardized interface to Nix artifacts such as
packages or NixOS modules. Flakes can have dependencies on other flakes, with a
“lock file” pinning those dependencies to exact revisions to ensure reproducible
evaluation.</p>
<p>– <a href="https://www.tweag.io/blog/2020-05-25-flakes/">https://www.tweag.io/blog/2020-05-25-flakes/</a></p>
</blockquote>
<p>Here is the <code>flake.nix</code> of this website:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">description</span> <span class="o">=</span> <span class="s2">"My personal website"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">inputs</span><span class="o">.</span><span class="n">nixpkgs</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-23.05"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">inputs</span><span class="o">.</span><span class="n">flake-utils</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">"github:numtide/flake-utils"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">outputs</span> <span class="o">=</span> <span class="p">{</span> <span class="n">self</span><span class="o">,</span> <span class="n">nixpkgs</span><span class="o">,</span> <span class="n">flake-utils</span> <span class="p">}:</span>
</span></span><span class="line"><span class="cl"> <span class="n">flake-utils</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">eachDefaultSystem</span> <span class="p">(</span><span class="n">system</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="k">let</span> <span class="n">pkgs</span> <span class="o">=</span> <span class="n">nixpkgs</span><span class="o">.</span><span class="n">legacyPackages</span><span class="o">.</span><span class="si">${</span><span class="n">system</span><span class="si">}</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">in</span> <span class="p">{</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">packages</span><span class="o">.</span><span class="n">website</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">stdenv</span><span class="o">.</span><span class="n">mkDerivation</span> <span class="p">{</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">name</span> <span class="o">=</span> <span class="s2">"blog"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">src</span> <span class="o">=</span> <span class="n">self</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">buildPhase</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="n">pkgs</span><span class="o">.</span><span class="n">hugo</span><span class="si">}</span><span class="s2">/bin/hugo"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">installPhase</span> <span class="o">=</span> <span class="s2">"cp -r public $out"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">defaultPackage</span> <span class="o">=</span> <span class="n">self</span><span class="o">.</span><span class="n">packages</span><span class="o">.</span><span class="si">${</span><span class="n">system</span><span class="si">}</span><span class="o">.</span><span class="n">website</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="n">devShells</span><span class="o">.</span><span class="n">default</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">mkShell</span> <span class="p">{</span> <span class="n">packages</span> <span class="o">=</span> <span class="p">[</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">hugo</span> <span class="p">];</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Everytime I clone the repo, there are two options:</p>
<ul>
<li>run <code>nix develop</code> to enter a pure environment with the <code>hugo</code> program.</li>
<li>run <code>nix build</code> to get the static website in the <code>./result</code> directory.</li>
</ul>
<p>It eliminates the need to worry about the development will be broken in the
future.</p>
<h2 id="auto-deployment">Auto deployment</h2>
<p>I use sourcehut to host my website. When I pushed to the git repository, it will
be deployed automatically using the <a href="https://builds.sr.ht/~goofansu/yejun.dev/commits/main/.build.yml">sourcehut build service</a>. In the build, <code>nix build</code> uses <code>flake.nix</code> to create exactly same environment as in development:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nixos/latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">oauth</span><span class="p">:</span><span class="w"> </span><span class="l">pages.sr.ht/PAGES:RW</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">packages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="l">nixos.hut</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w"> </span><span class="nt">NIX_CONFIG</span><span class="p">:</span><span class="w"> </span><span class="l">experimental-features = nix-command flakes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">site</span><span class="p">:</span><span class="w"> </span><span class="l">yejun.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">tasks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">package</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd"> cd $site
</span></span></span><span class="line hl"><span class="cl"><span class="sd"> nix build
</span></span></span><span class="line"><span class="cl"><span class="sd"> tar -C result -cvz . > ../site.tar.gz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">upload</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd"> hut pages publish -d $site site.tar.gz</span><span class="w">
</span></span></span></code></pre></div><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://nixos.org">Nix</a> is a tool focused on reproducible package management and system configuration. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
-
Send Nix code from Emacs to Kitty
https://yejun.dev/posts/send-nix-code-from-emacs-to-kitty/
Tue, 03 Oct 2023 02:38:00 +0800
https://yejun.dev/posts/send-nix-code-from-emacs-to-kitty/
<p>I’m learning Nix by following <a href="https://nix.dev/tutorials/first-steps/">this tutorial</a> and taking notes using Org
Mode. I organized the notes with my thoughts and some code examples. Doom Emacs
provides a function <code>+eval/send-region-to-repl</code> to run code in a language’s REPL
buffer. It’s useful until multiple-line input comes, which prints like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">nix-repl> <span class="nb">let</span>
</span></span><span class="line"><span class="cl"> <span class="nv">first_name</span> <span class="o">=</span> <span class="s2">"Yejun"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nv">last_name</span> <span class="o">=</span> <span class="s2">"Su"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">in
</span></span><span class="line"><span class="cl"> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="nv">full_name</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">first_name</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">last_name</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line hl"><span class="cl"><span class="nb">let</span>
</span></span><span class="line hl"><span class="cl"> <span class="nv">first_name</span> <span class="o">=</span> <span class="s2">"Yejun"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="nv">last_name</span> <span class="o">=</span> <span class="s2">"Su"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> in
</span></span><span class="line hl"><span class="cl"> <span class="o">{</span>
</span></span><span class="line hl"><span class="cl"> <span class="nv">full_name</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">first_name</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">last_name</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
</span></span><span class="line hl"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">{</span> <span class="nv">full_name</span> <span class="o">=</span> <span class="s2">"Yejun Su"</span><span class="p">;</span> <span class="o">}</span>
</span></span></code></pre></div><p>It appears that the input is also getting printed along with the result, which
isn’t expected. However, when I tried with <code>nix repl</code> in Kitty, I didn’t encounter
this problem. A viable solution that emerged is to send the Nix code from the
org-babel source block directly to the Nix REPL in Kitty.</p>
<p>Thanks to the <a href="https://sw.kovidgoyal.net/kitty/overview/#remote-control">Kitty’s remote control</a>, it’s easy to advise the
<code>+eval/send-region-to-repl</code> function to send the Nix code to Kitty:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">kitty--ensure-nix-repl-tab</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nv">zerop</span> <span class="p">(</span><span class="nv">shell-command</span> <span class="s">"kitty @ ls | grep -q '\"title\": \"nix-repl\"'"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">shell-command</span> <span class="s">"kitty @ launch --type tab --tab-title nix-repl nix repl"</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">kitty--send-region-to-nix-repl-tab</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">shell-command-on-region</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">if</span> <span class="p">(</span><span class="nv">use-region-p</span><span class="p">)</span> <span class="p">(</span><span class="nf">region-beginning</span><span class="p">)</span> <span class="p">(</span><span class="nf">point-min</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">if</span> <span class="p">(</span><span class="nv">use-region-p</span><span class="p">)</span> <span class="p">(</span><span class="nf">region-end</span><span class="p">)</span> <span class="p">(</span><span class="nf">point-max</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="s">"kitty @ send-text --match-tab title:nix-repl --stdin"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">org-babel-src-block-language-p</span> <span class="p">(</span><span class="nv">language</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">block-info</span> <span class="p">(</span><span class="nv">org-element-at-point</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nf">eq</span> <span class="p">(</span><span class="nf">car</span> <span class="nv">block-info</span><span class="p">)</span> <span class="ss">'src-block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">string=</span> <span class="nv">language</span> <span class="p">(</span><span class="nv">org-element-property</span> <span class="nb">:language</span> <span class="nv">block-info</span><span class="p">)))))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defadvice</span> <span class="nv">+eval/send-region-to-repl</span> <span class="p">(</span><span class="nv">around</span> <span class="nv">my-send-region-to-repl</span> <span class="nv">activate</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">if</span> <span class="p">(</span><span class="nb">and</span> <span class="p">(</span><span class="nf">eq</span> <span class="nv">major-mode</span> <span class="ss">'org-mode</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">org-babel-src-block-language-p</span> <span class="s">"nix"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">progn</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">kitty--ensure-nix-repl-tab</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">kitty--send-region-to-nix-repl-tab</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="nv">ad-do-it</span><span class="p">))</span>
</span></span></code></pre></div><p>When calling the <code>+eval/send-region-to-repl</code> function (<code>SPC c s</code>), if current major
mode is <code>org-mode</code> and the language of the org-babel source block is <code>nix</code>, it sends
the selected code to Kitty’s <code>nix-repl</code> tab, which runs the <code>nix repl</code> so that the
code is evaluated in the Nix REPL. The tab will be created if not exist.</p>
<figure><img src="https://yejun.dev/attachments/20250127T001947--kitty-nix-repl.gif"
alt="Screencast of send nix code from Emacs to Kitty">
</figure>
-
Backup GnuPG private key
https://yejun.dev/posts/backup-gpg-private-key/
Mon, 18 Sep 2023 22:12:00 +0800
https://yejun.dev/posts/backup-gpg-private-key/
<blockquote>
<p><a href="https://github.com/goofansu/gpg-toolkit">gpg-toolkit</a> is inspired by this article.</p>
</blockquote>
<p>I’m using <a href="https://gnupg.org/">GNU Privacy Guard</a> (GnuPG or GPG) in various
ways, such as <a href="https://yejun.dev/posts/simplify-totp-management-in-emacs/">encrypting passwords</a>, decrypting
emails, signing git commits, and more. So it’s time to find a way to backup the
GPG private key.</p>
<p>I asked GPT-4 for a method to keep the private key safe, and it tells me to
convert it to QR code and print it on the paper. That’s a good idea! I’ll start
from backup to a printable text, and then the QR code.</p>
<h2 id="preparation">Preparation</h2>
<p>Before backup, you need to know your GPG key ID. Run this command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gpg --list-keys --keyid-format long
</span></span></code></pre></div><p>It will list all the public keys in your system. Search your own key according
to this format <code>pub rsa2048/{YOUR KEY ID}</code>, the <code>{YOUR KEY ID}</code> part is your GPG
key ID.</p>
<p>Then export both the public and private key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gpg --export-secret-keys <span class="o">{</span>YOUR KEY ID<span class="o">}</span> > private-key.gpg
</span></span><span class="line"><span class="cl">gpg --export <span class="o">{</span>YOUR KEY ID<span class="o">}</span> > public-key.gpg
</span></span></code></pre></div><h2 id="backup-to-printable-text">Backup to printable text</h2>
<p>One of the program to be used is <a href="https://github.com/dmshaw/paperkey/">paperkey</a>, it transforms the GPG private key to
a printable format. The usage is straightforward:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># backup</span>
</span></span><span class="line"><span class="cl">paperkey --secret-key private-key.gpg --output printable.txt
</span></span></code></pre></div><p>Keep in mind that you need both the <strong>public key</strong> and the <strong>printable text</strong> to restore
the private key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># restore</span>
</span></span><span class="line"><span class="cl">paperkey --pubring public-key.gpg --secrets printable.txt --output restored-private-key.gpg
</span></span></code></pre></div><h2 id="backup-to-qr-code">Backup to QR code</h2>
<p>The process is similar to the previous method, but it requires two more
programs: <a href="https://fukuchi.org/works/qrencode/">qrencode</a> to create QR code and <a href="https://github.com/mchehab/zbar">zbar</a> to read QR code. Generally
speaking, the more programs you rely on, the more friction you run into. But I
just tried it for fun:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># backup</span>
</span></span><span class="line"><span class="cl">paperkey --output-type raw --secret-key private-key.gpg <span class="p">|</span> base64 <span class="p">|</span> qrencode -o qrcode.png
</span></span></code></pre></div><p>With the <strong>public key</strong> and <strong>QR code</strong> in hand, you can restore the private key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># restore</span>
</span></span><span class="line"><span class="cl">zbarimg qrcode.png <span class="p">|</span> cut -d<span class="s1">':'</span> -f2 <span class="p">|</span> base64 --decode <span class="p">|</span> paperkey --pubring public-key.gpg --output restored-private-key.gpg
</span></span></code></pre></div>
-
Simplify TOTP management in Emacs
https://yejun.dev/posts/simplify-totp-management-in-emacs/
Wed, 13 Sep 2023 02:54:00 +0800
https://yejun.dev/posts/simplify-totp-management-in-emacs/
<blockquote>
<p>Time-based one-time password (TOTP) is a computer algorithm that generates a
one-time password (OTP) that uses the current time as a source of uniqueness.</p>
<p>– <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">Wikipedia</a></p>
</blockquote>
<p><a href="https://github.com/volrath/password-store-otp.el/">password-store-otp</a> is an Emacs package which provides functions to
interact with the <a href="https://github.com/tadfisher/pass-otp/">pass-otp</a> extension for <a href="https://www.passwordstore.org/">pass</a>. There are two
functions that insert or append the OTP key URI to a selected pass entry. But
it’s not that easy because websites generally provide QR Code or secret key,
which requires me to compose the OTP key URI manually.</p>
<p>To simplify the process, I created two corresponding functions based on those in
password-store-otp:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">yejun/password-store-otp-append</span> <span class="p">(</span><span class="nv">entry</span> <span class="nv">issuer</span> <span class="nv">secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Append to ENTRY the OTP-URI consisting of issuer and secret."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span> <span class="p">(</span><span class="nf">list</span> <span class="p">(</span><span class="nv">password-store-otp-completing-read</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">read-string</span> <span class="s">"Issuer: "</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">read-passwd</span> <span class="s">"Secret: "</span> <span class="no">t</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">secret</span> <span class="p">(</span><span class="nv">replace-regexp-in-string</span> <span class="s">"\\s-"</span> <span class="s">""</span> <span class="nv">secret</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">otp-uri</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"otpauth://totp/totp-secret?secret=%s&issuer=%s"</span> <span class="nv">secret</span> <span class="nv">issuer</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">password-store-otp-add-uri</span> <span class="ss">'append</span> <span class="nv">entry</span> <span class="nv">otp-uri</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">yejun/password-store-otp-insert</span> <span class="p">(</span><span class="nv">entry</span> <span class="nv">issuer</span> <span class="nv">secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Insert a new ENTRY containing OTP-URI consisting of issuer and secret."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span> <span class="p">(</span><span class="nf">list</span> <span class="p">(</span><span class="nv">password-store-otp-completing-read</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">read-string</span> <span class="s">"Issuer: "</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">read-passwd</span> <span class="s">"Secret: "</span> <span class="no">t</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">secret</span> <span class="p">(</span><span class="nv">replace-regexp-in-string</span> <span class="s">"\\s-"</span> <span class="s">""</span> <span class="nv">secret</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">otp-uri</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"otpauth://totp/totp-secret?secret=%s&issuer=%s"</span> <span class="nv">secret</span> <span class="nv">issuer</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">password-store-otp-add-uri</span> <span class="ss">'insert</span> <span class="nv">entry</span> <span class="nv">otp-uri</span><span class="p">)))</span>
</span></span></code></pre></div><p>When calling either function, it will prompt for the <code>issuer</code> and <code>secret</code>, then
compose an OTP key URI with the values in this format:
<code>otpauth://totp/totp-secret?secret=<secret>&issuer=<issuer></code>, which saves manual
effort.</p>
<hr>
<p>To keep it simpler, I extracted a function to create OTP key URI, so it can be
applied in more scenarios:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">yejun/otp-key-uri</span> <span class="p">(</span><span class="nv">issuer</span> <span class="nv">secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="s">"Create and copy the OTP key URI consisting of issuer and secret."</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">interactive</span> <span class="p">(</span><span class="nf">list</span> <span class="p">(</span><span class="nf">read-string</span> <span class="s">"Issuer: "</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">read-passwd</span> <span class="s">"Secret: "</span> <span class="no">t</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nb">let*</span> <span class="p">((</span><span class="nv">secret</span> <span class="p">(</span><span class="nv">replace-regexp-in-string</span> <span class="s">"\\s-"</span> <span class="s">""</span> <span class="nv">secret</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">otp-uri</span> <span class="p">(</span><span class="nf">format</span> <span class="s">"otpauth://totp/totp-secret?secret=%s&issuer=%s"</span> <span class="nv">secret</span> <span class="nv">issuer</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nv">kill-new</span> <span class="nv">otp-uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nf">message</span> <span class="s">"OTP key URI created and copied."</span><span class="p">)))</span>
</span></span></code></pre></div><p>By calling the function, an OTP key URI is created and copied to the clipboard
for later use.</p>
-
Skip sourcehut build in Emacs
https://yejun.dev/posts/skip-sourcehut-build-in-emacs/
Tue, 05 Sep 2023 10:23:00 +0800
https://yejun.dev/posts/skip-sourcehut-build-in-emacs/
<p><a href="https://builds.sr.ht/">builds.sr.ht</a> is the GitHub Actions counterpart in sourcehut, it can run jobs
when you push to a git repository that contains a <code>build.yml</code> file. According to
the <a href="https://man.sr.ht/git.sr.ht/#push-options">manual</a>, you can skip submitting a build by using <code>git push -o skip-ci</code>.</p>
<p>In Emacs, you can achieve this by <a href="https://magit.vc/manual/transient/Modifying-Existing-Transients.html">adding an infix argument</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nv">transient-append-suffix</span> <span class="ss">'magit-push</span> <span class="s">"-n"</span>
</span></span><span class="line"><span class="cl"> <span class="o">'</span><span class="p">(</span><span class="s">"-s"</span> <span class="s">"Skip CI"</span> <span class="s">"--push-option=skip-ci"</span><span class="p">))</span>
</span></span></code></pre></div><p>This inserts a new infix argument to toggle the <code>--push-option=skip-ci</code> argument
after the infix argument that toggles <code>--dry-run</code> in <code>magit-push</code>.</p>
<p>However, it is strange that neither argument <code>-o skip-ci</code> nor <code>-o=skip-ci</code> will
take effect:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nv">transient-append-suffix</span> <span class="ss">'magit-push</span> <span class="s">"-n"</span>
</span></span><span class="line"><span class="cl"> <span class="o">'</span><span class="p">(</span><span class="s">"-s"</span> <span class="s">"Skip CI"</span> <span class="s">"-o=skip-ci"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nv">transient-append-suffix</span> <span class="ss">'magit-push</span> <span class="s">"-n"</span>
</span></span><span class="line"><span class="cl"> <span class="o">'</span><span class="p">(</span><span class="s">"-s"</span> <span class="s">"Skip CI"</span> <span class="s">"-o skip-ci"</span><span class="p">))</span>
</span></span></code></pre></div>