<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Nathan's Blog]]></title><description><![CDATA[Code. Languages. Music.]]></description><link>https://blog.nathanwillson.com/</link><image><url>https://blog.nathanwillson.com/favicon.png</url><title>Nathan&apos;s Blog</title><link>https://blog.nathanwillson.com/</link></image><generator>Ghost 5.81</generator><lastBuildDate>Thu, 25 Jun 2026 05:07:18 GMT</lastBuildDate><atom:link href="https://blog.nathanwillson.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Vibe coding an iPhone app — from an iPhone]]></title><description><![CDATA[<h2 id="good-bye-computer">Good-bye computer </h2><p>There&apos;s a newborn &#x1F476; in the house &#x1F3E0;, which means my time at a desk has mostly evaporated. These days I&apos;m more likely to be on a walk, at a park, or waiting around at the hospital than sitting in front of my laptop.</p>]]></description><link>https://blog.nathanwillson.com/coding-from-my-phone/</link><guid isPermaLink="false">6a2df7f76f6e30027c865b23</guid><category><![CDATA[ios]]></category><category><![CDATA[mobile]]></category><category><![CDATA[ai]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sun, 14 Jun 2026 14:35:24 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/monster-afar.png" medium="image"/><content:encoded><![CDATA[<h2 id="good-bye-computer">Good-bye computer </h2><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/monster-afar.png" alt="Vibe coding an iPhone app &#x2014; from an iPhone"><p>There&apos;s a newborn &#x1F476; in the house &#x1F3E0;, which means my time at a desk has mostly evaporated. These days I&apos;m more likely to be on a walk, at a park, or waiting around at the hospital than sitting in front of my laptop.</p><p>So I went looking for a way to keep building my iOS app <em>without</em> the laptop in front of me &#x2014; ideally hands-free, driven by voice, from the one computer I always have on me: my phone.</p><h2 id="but-my-app-needs-a-real-computer">But my app needs a real computer</h2><p>This turned out to be harder than the usual &quot;code from your phone&quot; setup, because I&apos;m not editing a web app on a cloud server. I&apos;m building a <strong>native iOS app</strong>, and that means Xcode &#x2014; which only runs on Apple hardware and can&apos;t be spun up on some Linux VPS. Whatever I came up with had to reach back to a real Mac to compile, run the simulator, and deploy.</p><p>That one constraint shaped everything below. Whatever I came up with had to let my phone <em>drive</em> a Mac, not replace it.</p><p>Here&apos;s the setup I landed on, what each piece does, and the one problem I still haven&apos;t cracked.</p><h2 id="what-im-aiming-for">What I&apos;m aiming for</h2><p>Three things had to be true for this to work:</p><ol><li><strong>Control the Claude Code session running on my Mac, from my phone.</strong></li><li><strong>Reliable, hands-free voice-to-text on iOS</strong> &#x2014; so I can actually talk to it instead of thumb-typing.</li><li><strong>Remotely deploy a build to my phone from anywhere.</strong> (Spoiler: still unsolved &#x2014; more on that at the end.)</li></ol><p>Here&apos;s the flow I was aiming for. The dotted line &#x2014; pushing a build back to the phone &#x2014; is the part I haven&apos;t cracked yet:<br></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/Screenshot-2026-06-14-at-9.53.29.png" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="1894" height="796"><figcaption><span style="white-space: pre-wrap;">My ideal flow for iOS development: Me &#x2192; iPhone &#x2192; Claude Code + Xcode on a MacBook Air</span></figcaption></figure><p>And here&apos;s what I actually ended up with:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/Screenshot-2026-06-14-at-17.51.06.png" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="2000" height="464"><figcaption><span style="white-space: pre-wrap;">Phone to Laptop coding flowLocalWhisper &#x2192; Happy app on phone &#x2192; Happy client on laptop &#x2192; Claude &#x2192; Xcode</span></figcaption></figure><p>The rest of this post walks through each piece. I&apos;m driving all of it with <a href="https://www.anthropic.com/claude-code?ref=blog.nathanwillson.com">Claude Code</a>, Anthropic&apos;s CLI agent, running on my Mac.</p><h2 id="happy-controlling-claude-code-from-your-phone">Happy: Controlling Claude Code from your phone</h2><p>The piece that makes this possible is <a href="https://happy.engineering/?ref=blog.nathanwillson.com"><strong>Happy</strong></a> (<a href="https://github.com/slopus/happy?ref=blog.nathanwillson.com">source on GitHub</a>) &#x2014; an open-source mobile and web client for Claude Code and Codex.<br></p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/slopus/happy?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - slopus/happy: Mobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured</div><div class="kg-bookmark-description">Mobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured - slopus/happy</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Vibe coding an iPhone app &#x2014; from an iPhone"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">slopus</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/6871b96f741d8b637b106d75b58651c8202c8455c17f25779f933d1eb1940405/slopus/happy" alt="Vibe coding an iPhone app &#x2014; from an iPhone"></div></a></figure><figure class="kg-card kg-image-card"><img src="https://github.com/slopus/happy/blob/main/.github/mascot.png?raw=true" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="1024" height="1024"></figure><p><br>You run it on your Mac instead of <code>claude</code>, and it wraps a normal Claude Code session so you can pick it up from the Happy app on your phone, desktop, or browser. The bits I care about:</p><ul><li><strong>Remote access</strong> &#x2014; continue the same session from my phone, away from my desk.</li><li><strong>Voice control</strong> &#x2014; &quot;voice-to-action,&quot; not just dictation into a text box.</li><li><strong>Push notifications</strong> &#x2014; it pings me when Claude needs input or finishes a task.</li><li><strong>End-to-end encrypted</strong> &#x2014; the relay only passes encrypted blobs; it can&apos;t read your session. (Worth knowing, since you are effectively letting a phone drive a shell on your laptop.)</li><li><strong>Multi-session</strong> &#x2014; run several Claude Code instances in parallel.</li><li><strong>Free and self-hostable</strong> (MIT licensed) if you&apos;d rather not use their relay. </li></ul><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/IMG_4846-combined.PNG" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="3618" height="2622"></figure><h2 id="localwhisper-reliable-free-voice-to-text-on-ios">LocalWhisper: Reliable, free Voice-to-text on iOS</h2><p>Happy gets my phone talking to my Mac. But to be truly hands-free I needed good voice-to-text, and that&apos;s its own rabbit hole.</p><p>iPhones come with text-to-speech, but it&apos;s super unreliable &#x1F614; and not very configurable. Maybe it will get better with the next version of Siri, but right now it&apos;s &#x1F4A9; &#x1F44E;. <br><br>The popular paid options &#x2014; Wispr Flow, SuperWhisper &#x2014; work well but are pricey for what I wanted. I also just prefer models that run <strong>on-device</strong>.&#x1F4B0; &#x1F44E;</p><p>What I landed on is <a href="https://apps.apple.com/us/app/localwhisper/id6760680371?ref=blog.nathanwillson.com"><strong>LocalWhisper</strong></a>:</p><ul><li><strong>Free</strong>, and runs Whisper models entirely on-device.</li><li><strong>No subscription</strong> &#x2014; there&apos;s a one-time $5 Pro unlock, and that&apos;s it.</li><li><strong>Model choice</strong>, optional AI text cleanup, a system keyboard so it works in any app, and self-hosting support.</li><li><strong>Very customizable,</strong> you can specify rules and keywords and even post-grammar checks with another model<br></li></ul><p></p><figure class="kg-card kg-image-card"><img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/c6/3a/84/c63a8492-da08-1b1d-9d00-893fbaeb0f2a/Placeholder.mill/400x400bb-75.webp" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="400" height="400"></figure><p></p><p>Here are some screenshots:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/IMG_4842-combined.PNG" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="2000" height="1449"><figcaption><span style="white-space: pre-wrap;">Notes app using the LocalWhisper keyboard, the app&apos;s settings pages</span></figcaption></figure><p></p><h2 id="putting-it-together">Putting it together</h2><p>With both pieces in place, the loop looks like this:</p><ol><li>I dictate with <strong>LocalWhisper</strong>, which types straight into the <strong>Happy</strong> app.</li><li><strong>Happy</strong> relays that to the Claude Code session running on my Mac.</li><li><strong>Claude Code</strong> does the work &#x2014; and for the iOS-specific parts (building, running the simulator, deploying) it uses <a href="https://github.com/getsentry/XcodeBuildMCP?ref=blog.nathanwillson.com"><strong>XcodeBuildMCP</strong></a>, which gives the agent a clean set of tools to drive Xcode.</li></ol><p>So I can be on a walk, talk to my phone, and have an agent compiling my app on a Mac sitting at home.</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2026/06/Screenshot-2026-06-14-at-17.51.06-1.png" class="kg-image" alt="Vibe coding an iPhone app &#x2014; from an iPhone" loading="lazy" width="2000" height="464"></figure><h2 id="how-im-actually-using-it">How I&apos;m actually using it</h2><p>Honestly? Mostly for <strong>planning</strong>. That&apos;s where the bulk of my time goes anyway &#x2014; thinking through a feature, breaking it down, arguing with the agent about the approach &#x2014; and all of that works great from a phone.</p><p>I can also compile and build remotely. What I <em>can&apos;t</em> do yet is the satisfying part: manual testing on a real device still waits until I&apos;m home in front of the Mac.</p><h2 id="the-missing-piece-getting-a-build-onto-my-phone-instantly">The missing piece: getting a build onto my phone instantly</h2><p>This is the one thing I haven&apos;t solved, and it&apos;s the dotted line in that first diagram.</p><p>The problem: there&apos;s no quick way to push a fresh build to my physical phone unless the phone is <strong>cabled to the Mac or on the same Wi-Fi</strong>. The moment I&apos;m out of the house, that breaks.</p><p>What I&apos;ve tried or considered:</p><ul><li><strong>Xcode Cloud + TestFlight</strong> &#x2014; works, but a build takes 10&#x2013;15+ minutes round-trip, which kills the fast feedback loop I&apos;m after.<ul><li>use <a href="https://fastlane.tools/?ref=blog.nathanwillson.com">FastLane</a> running locally would work fine in my case</li><li>10-15 minutes is not the worst and maybe what I&apos;ll end up with anyways</li></ul></li><li><strong>Tailscale</strong> &#x2014; I tried putting the phone and Mac on the same tailnet so they&apos;d look &quot;local&quot; to each other, but couldn&apos;t get a wireless deploy to go through. <em>(If you&apos;ve made this work, I&apos;d genuinely love to hear how.)</em></li><li><strong>Xcode&apos;s old &quot;deploy to IP address&quot;</strong> feature &#x2014; discontinued.</li></ul><p>There are still a couple things I want to try, but TBD!</p><h2 id="conclusion">Conclusion</h2><p>The setup I&apos;ve got &#x2014; LocalWhisper into Happy into Claude Code into Xcode &#x2014; lets me plan and build my iOS app from basically anywhere, hands-free, which is exactly what I needed right now.</p><p>The last mile is getting a build onto my phone when I&apos;m off my home network. If you&apos;ve solved that for native iOS, <a href="https://github.com/nbw?ref=blog.nathanwillson.com">come tell me</a> &#x2014; I&apos;ll update this post.</p>]]></content:encoded></item><item><title><![CDATA[Image Shadow: Curve-balls]]></title><description><![CDATA[<p>I take a lot of photos and I edit them afterwards. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, shadows, etc. actually work.</p><p>In this post I define investigate how to raise the &quot;shadows&quot; of an image. I&apos;ll walk through</p>]]></description><link>https://blog.nathanwillson.com/untitled/</link><guid isPermaLink="false">684393f87ee4d9013b56c3e9</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Wed, 18 Feb 2026 07:09:00 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadows.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadows.png" alt="Image Shadow: Curve-balls"><p>I take a lot of photos and I edit them afterwards. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, shadows, etc. actually work.</p><p>In this post I define investigate how to raise the &quot;shadows&quot; of an image. I&apos;ll walk through some programming approaches how to adjust shadows with all the trade-offs.</p><p><br></p><hr><h1 id="wdym-shadow-of-an-image">WDYM &quot;shadow&quot; of an image?</h1><p>Here&apos;s a before/after of an image with the shadows <em>raised </em>using Adobe Lightroom: </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadow_comaprison_sm.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">Original (left), adjusted with Lightroom (right)</span></figcaption></figure><p>When adjusting the &quot;shadows&quot; of an image, we&apos;re really trying to bump up the tones on the lower end of the spectrum between the darks and midtones. </p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadows_hist.png" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="1100" height="636"></figure><p>We can use <strong><em>LUMINANCE</em></strong> is a weighted average of RGB values to determine &quot;tone&quot; or brightness. </p><blockquote><strong>L = 0.2126 * R + 0.7152 * G + 0.0722 * B<br><em>(</em>L</strong>uminance is between 0 and 1)</blockquote><hr><h2 id="attempt-1-hard-threshold">Attempt #1: Hard threshold</h2><p>The most naive approach would be to adjust the luminance values only within shadow luminance range without any smoothing. Here&apos;s what some pseudo code might look like:</p><pre><code>if (luminance &gt; 0.1 &amp;&amp; luminance &lt;= 0.4) {
  # increase the luminance in the shadow range
  r_new = amount * r
  g_new = amount * g
  b_new = amount * b
}</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadow_hard-1.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">hard threshold: before after</span></figcaption></figure><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/output_shadow_hard-1.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="1333"><figcaption><span style="white-space: pre-wrap;">hard threshold adjusted image</span></figcaption></figure><blockquote>&#x274C; this does brighten the shadows, but results in <strong>harsh boundary clipping</strong> at the threshold edges (e.g., 0.1 and 0.4). You&apos;ll notice visible breaks in the tonal gradient.</blockquote><hr><h2 id="attempt-2-smooth-blending-with-a-bell-curve">Attempt #2: Smooth Blending with a Bell Curve</h2><p>To solve the boundary clipping at the threshold boundaries, adjusting the shadows with a bell-curve instead should help smooth out the transition. </p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/250608_11h39m00s_screenshot.png" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="1238" height="578"></figure><p>Pseudo-code:<br></p><pre><code>if (luminance &gt; 0.1 &amp;&amp; luminance &lt;= 0.5) {
  # increase the luminance in the shadow range
  r_new = amount * weighted_curve(L) * r
  g_new = amount * weighted_curve(L) * g 
  b_new = amount * weighted_curve(L) * b
}</code></pre><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/shadow_curve-1.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="444"><figcaption><span style="white-space: pre-wrap;">left to right: original, medium strength, extreme strength</span></figcaption></figure><p>Using a weighted curve does raise the shadows of the image with less clipping, but there&apos;s a flaw this approach: </p><p>&#x2705; threshold edges are better (kinda)</p><p><strong>&#x274C; darker regions of the image may end up brighter than lighter ones, resulting in an unnatural reversal of contrast. </strong></p><p>Pixels that were originally in deep shadow can become brighter than nearby midtones, creating unnatural halos or flattening the depth of the image<strong>. </strong>Said another way:<strong> tonal order is not preserved.</strong></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/output_3.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="1333"><figcaption><span style="white-space: pre-wrap;">shadow adjusted image</span></figcaption></figure><p><strong>&#x1F4A1;New requirement: raising the shadows must preserve the <u>tonal order</u>. </strong></p><blockquote>If two values start with <code>x&#x2081; &lt; x&#x2082;</code>, then after adjustment, their output should still follow: <code>f(x&#x2081;) &lt; f(x&#x2082;)</code></blockquote><p>In mathematics terms,  this is called <strong>monotonicity. </strong></p><hr><h2 id="attempt-3-preserving-tone-order">Attempt #3: Preserving tone order</h2><p>Our goal for raising the shadows is now:</p><ul><li>boost the brightness of pixels within the shadow range</li><li>have smooth blending between at the boundaries of shadow range</li><li>ensure that tonal order is maintained</li></ul><p>The solution is to have a <em>target</em> tone line that we use as a reference or anchor for our adjustments. The easiest way to do this is with a tone curve, specifically a <strong>gamma curve.</strong>  </p><h3 id="%F0%9F%93%88-what%E2%80%99s-a-gamma-curve">&#x1F4C8; What&#x2019;s a Gamma Curve?</h3><p>Gamma curves are widely used in image encoding, tone mapping, and display correction. For our purposes, a gamma curve defines an <strong>ideal, perceptually-correct brightness</strong> for a given input.</p><blockquote><strong>Gamma</strong> is an exponent (<code>&#x3B3;</code>) used to map linear brightness values to nonlinear ones &#x2014; and vice versa &#x2014; to match human perception or display behavior.</blockquote><p>The equation for a gamma curve is simple:</p><p><strong><code>y=x^&#x3B3;</code></strong></p>
<!--kg-card-begin: html-->
<table><thead data-start="1699" data-end="1742"><tr data-start="1699" data-end="1742"><th data-start="1699" data-end="1709" data-col-size="sm">&#x3B3;</th><th data-start="1709" data-end="1742" data-col-size="sm">Effect</th></tr></thead></table>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<table><tbody><tr data-start="1787" data-end="1830"><td data-start="1787" data-end="1797" data-col-size="sm">1.0</td><td data-start="1797" data-end="1830" data-col-size="sm">Linear (no change)</td></tr></tbody></table>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<table><tbody><tr data-start="1831" data-end="1874"><td data-start="1831" data-end="1841" data-col-size="sm">&lt; 1.0</td><td data-start="1841" data-end="1874" data-col-size="sm">Lifts shadows (brightens)</td></tr></tbody></table>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<table><tbody><tr data-start="1875" data-end="1919"><td data-start="1875" data-end="1885" data-col-size="sm">&gt; 1.0</td><td data-start="1885" data-end="1919" data-col-size="sm">Darkens shadows (more contrast)</td></tr></tbody></table>
<!--kg-card-end: html-->
<p>When &#x3B3; &lt; 1, the curve lifts the shadows while keeping the highlights mostly unchanged.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/250608_14h55m05s_screenshot.png" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="739" height="731"><figcaption><span style="white-space: pre-wrap;">gamma curves</span></figcaption></figure><h3 id="adjusting-shadows-with-a-target-curve">Adjusting shadows with a target curve</h3><p>Our shadow equation becomes:</p><pre><code class="language- ">output = x + amount* bell_curve(x) * (gamma_curve(x) - x)</code></pre><ul><li><code>x</code> = original pixel brightness (linear or gamma-encoded)</li><li>gamma_curve<code>(x)</code> = your tone curve (e.g., gamma 0.6)</li><li>bell_curve<code>(x)</code> = how much you want to apply the effect</li><li><code>strength</code> = global multiplier</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/250608_15h03m33s_screenshot.png" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="1196" height="742"><figcaption><span style="white-space: pre-wrap;">adjustment curve preserving tonal order</span></figcaption></figure><p>We can look at the shadow region and see that the tonal order is preserved. Or in mathematical terms, the slope is always greater than zero:</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/contrast_2-1-.png" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="769" height="523"></figure><p>Here&apos;s the Jupyter code for the graph &#x2B07;&#xFE0F;</p>
<!--kg-card-begin: html-->
<script src="https://gist.github.com/nbw/c281bfaa41e1095e5a89fc0ac883e32d.js"></script>
<!--kg-card-end: html-->
<h3 id="final-image-results">Final image results:</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/compare-3.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">before and after</span></figcaption></figure><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/output_sh-3.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="1333"></figure><p>&#x2705; This result is an improvement now that we&apos;re able to maintain tone order.<br>&#x274C; The color has changed; the final image is de-saturated compared to the original</p><p>&#x274C; There&apos;s considerable grey clipping in the trees </p><h2 id="attempt-4">Attempt #4: ...</h2><p>At this point I was ready to throw in the towel, but</p><p></p><h2 id="bonus-attempt-5-slow-but-better-details">Bonus Attempt #5: Slow but better details </h2><p>My research lead me down one more path that was better at preserving contrast and fine details: <strong>a</strong> <strong>bilateral filter for shadow lifting.</strong></p><p>This approach is more computationally intensive and thus much slower.</p><blockquote>The bilateral filter approach is particularly effective for shadow lifting because it can separate large-scale illumination changes from fine details, allowing you to boost shadows without amplifying noise or losing texture information.<br>- Claude.ai</blockquote><p>There&apos;s a whole <a href="https://en.wikipedia.org/wiki/Bilateral_filter?ref=blog.nathanwillson.com">wiki article on bilateral filters</a>, but the TLDR is that a bilateral filter considers both <strong>spatial distance</strong> and <strong>intensity similarity.</strong> </p><p>We can calculate a weight that is a combination of both: <code>weight = spatial_weight &#xD7; range_weight</code></p><p>The details of the implementation are quite intense. It&apos;s a bunch of matrix logic, but the results are impressive. I leave it to you to generate the code with AI tools, but here&apos;s the result:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/compare-4.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="444"><figcaption><span style="white-space: pre-wrap;">left: Lightroom; middle: gamma curves; right: bilateral filter</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/06/output_shb.jpg" class="kg-image" alt="Image Shadow: Curve-balls" loading="lazy" width="2000" height="1333"><figcaption><span style="white-space: pre-wrap;">bilateral filter</span></figcaption></figure><p>The contrast and edges are much better preserved. This is the closest to Lightroom quality that I was able to achieve. Unfortunately, it&apos;s quite a slow calculation. </p><p></p><h2 id="conclusion">Conclusion</h2><p>When I first tried to understand how &quot;shadow&quot; adjustment works, I found very little clear guidance. Different approaches used various tone curves, masks, and blending models &#x2014; some even considered neighboring pixels.</p><p>The gamma-target approach described here struck a good balance between <strong>simplicity</strong>, <strong>control</strong>, and <strong>perceptual correctness</strong>. It avoids tonal inversion and creates a more natural look.<br><br>Bilateral filters are cool, but slow. There are obviously ways to optimize and it would be interesting to see how far one could take it.</p><p>Whatever Lightroom is doing under the hood, it&#x2019;s probably more refined and adaptive &#x2014; but this is a solid starting point for understanding and building your own tone mapping tools.</p>]]></content:encoded></item><item><title><![CDATA[Omarchy customizations]]></title><description><![CDATA[<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://omarchy.org/?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Omarchy</div><div class="kg-bookmark-description">An opinionated Arch + Hyprland Setup</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://omarchy.org/assets/images/favicon.png" alt><span class="kg-bookmark-author">Omarchy</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://omarchy.org/assets/images/opengraph.png" alt></div></a></figure><p>Omarchy is DHH&apos;s Hyperland Arch config. I&apos;m enjoying a lot more than <a href="https://github.com/HyDE-Project/HyDE?ref=blog.nathanwillson.com">Hyde already</a> because it has (for me) less insane config and defaults.</p><p><strong>This is a working list of the things I&apos;m customizing after installing Omarchy.</strong></p>]]></description><link>https://blog.nathanwillson.com/omarchy-customizations/</link><guid isPermaLink="false">6899ec667ee4d9013b56c62e</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Mon, 11 Aug 2025 13:20:30 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/08/omarchy.png" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://omarchy.org/?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Omarchy</div><div class="kg-bookmark-description">An opinionated Arch + Hyprland Setup</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://omarchy.org/assets/images/favicon.png" alt="Omarchy customizations"><span class="kg-bookmark-author">Omarchy</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://omarchy.org/assets/images/opengraph.png" alt="Omarchy customizations"></div></a></figure><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/08/omarchy.png" alt="Omarchy customizations"><p>Omarchy is DHH&apos;s Hyperland Arch config. I&apos;m enjoying a lot more than <a href="https://github.com/HyDE-Project/HyDE?ref=blog.nathanwillson.com">Hyde already</a> because it has (for me) less insane config and defaults.</p><p><strong>This is a working list of the things I&apos;m customizing after installing Omarchy.</strong></p><h1 id="ctrl-instead-of-capslock">Ctrl instead of Capslock </h1><p>Capslock out of the box calls xcompose for emojis. I prefer to have it by a ctrl key for my dev/tmux/vim workflows.</p><p><code>nvim ~/.config/hyprland/input.conf</code></p><p>change the kb_options value:</p><pre><code>kb_options = ctrl:nocaps</code></pre><h1 id="claude-not-chatgpt">Claude, not ChatGPT</h1><p>Open claude instead of chatgpt</p><p><code>nvim ~/.config/hyprland/bindings.conf</code></p><p>Change the ChatGPT bind to:</p><pre><code>bindd = SUPER, A, Claude, exec, $webapp=&quot;https://claude.ai&quot;</code></pre><h1 id="zed-code-ide">Zed (Code IDE)</h1><pre><code>curl -f https://zed.dev/install.sh | sh</code></pre><h1 id="elixir">Elixir</h1><p>EDIT: You can now install with the menu: <code>Super Alt Space &gt; Install &gt; Development &gt; Elixir</code> </p><pre><code>mise list-all erlang

mise install erlang@[VERSION]

sh ~/.local/share/mise/installs/erlang/[VERSION]/activate

mise list-all elixir

mise install elixir@[VERSION]

mise use -g erlang@[VERSION]

mise use -g elixir@[VERSION]
</code></pre><h2 id="screensaver">Screensaver</h2><p>Instead of the default Omarchy logo.  To edit the logo: <code>Super Alt Space &gt; Style &gt; Screen Saver</code></p><pre><code>&#x2584;          &#x2584;          &#x2584;          &#x2584;&#x2584;
&#x2588;&#x2588;&#x2584;        &#x2588;&#x2588;&#x2584;        &#x2588;&#x2588;&#x2584;        &#x2588;&#x2588;
&#x2588;&#x2588;&#x2588;&#x2588;&#x2584;      &#x2588;&#x2588;&#x2588;&#x2588;&#x2584;      &#x2588;&#x2588;&#x2588;&#x2588;&#x2584;      &#x2588;&#x2588;
&#x2588;&#x2588; &#x2580;&#x2588;&#x2588;&#x2584;    &#x2588;&#x2588; &#x2580;&#x2588;&#x2588;&#x2584;    &#x2588;&#x2588; &#x2580;&#x2588;&#x2588;&#x2584;    &#x2588;&#x2588;
&#x2588;&#x2588;   &#x2580;&#x2588;&#x2588;&#x2584;  &#x2588;&#x2588;   &#x2580;&#x2588;&#x2588;&#x2584;  &#x2588;&#x2588;   &#x2580;&#x2588;&#x2588;&#x2584;  &#x2588;&#x2588;
&#x2588;&#x2588;     &#x2580;&#x2588;&#x2588;&#x2584;&#x2588;&#x2588;     &#x2580;&#x2588;&#x2588;&#x2584;&#x2588;&#x2588;     &#x2580;&#x2588;&#x2588;&#x2584;&#x2588;&#x2588;
&#x2588;&#x2588;       &#x2580;&#x2588;&#x2588;&#x2588;       &#x2580;&#x2588;&#x2588;&#x2588;       &#x2580;&#x2588;&#x2588;&#x2588;
&#x2588;&#x2588;         &#x2580;&#x2588;         &#x2580;&#x2588;         &#x2580;&#x2588;</code></pre><p></p><h2 id="mouse-cursor-speed">Mouse Cursor speed</h2><pre><code>gsettings set org.gnome.desktop.peripherals.mouse speed 0.5</code></pre><p>This is for Wayland, which Hyprland uses.</p>]]></content:encoded></item><item><title><![CDATA[Marc Levoy's Lectures on Digital Photography]]></title><description><![CDATA[<p>If you want to learn about photography, Marc Levoy&apos;s lectures are some of the best content I&apos;ve seen. For the mildly experienced photographer (like myself) with an engineering background, his explanations fill in so many gaps with mathematics and physics. <br><br>Topics like:</p><ul><li>how do lenses work</li></ul>]]></description><link>https://blog.nathanwillson.com/marc-levoys-lectures-on-digital-photography/</link><guid isPermaLink="false">68736fa97ee4d9013b56c611</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sun, 13 Jul 2025 08:42:10 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/media_13ea26bc8b17270883c3fc69ba8ffdc1886b313ec.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/media_13ea26bc8b17270883c3fc69ba8ffdc1886b313ec.jpeg" alt="Marc Levoy&apos;s Lectures on Digital Photography"><p>If you want to learn about photography, Marc Levoy&apos;s lectures are some of the best content I&apos;ve seen. For the mildly experienced photographer (like myself) with an engineering background, his explanations fill in so many gaps with mathematics and physics. <br><br>Topics like:</p><ul><li>how do lenses work and what are the limitations?</li><li>how does perspective and depth of field work?</li><li>the history of photography</li><li>and much more</li></ul><p>He&apos;s a big deal in the field of digital photography, especially with HDR: <a href="https://en.wikipedia.org/wiki/Marc_Levoy?ref=blog.nathanwillson.com">https://en.wikipedia.org/wiki/Marc_Levoy</a><br><br>There are 18 lectures, and they&apos;re all excellent.<br><br>Youtube Playlist: <a href="https://www.youtube.com/watch?v=y7HrM-fk_Rc&amp;list=PL7ddpXYvFXspUN0N-gObF1GXoCA-DA-7i&amp;ref=blog.nathanwillson.com">https://www.youtube.com/watch?v=y7HrM-fk_Rc&amp;list=PL7ddpXYvFXspUN0N-gObF1GXoCA-DA-7i</a><br></p>
<!--kg-card-begin: html-->
<iframe width="560" height="315" src="https://www.youtube.com/embed/y7HrM-fk_Rc?si=zjac_REWoA0HtC4P" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Automatic vs Manual Focus Lens Comparison with Sony]]></title><description><![CDATA[Comparing a Sony 40mm auto focus lens with a Voigtlander 35mm manual focus lens using Sony's A7Cii camera. ]]></description><link>https://blog.nathanwillson.com/automatic-focus-vs-manual-focus-comparison/</link><guid isPermaLink="false">687256f27ee4d9013b56c59f</guid><category><![CDATA[photography]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sun, 13 Jul 2025 03:06:46 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/cd.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/cd.jpg" alt="Automatic vs Manual Focus Lens Comparison with Sony"><p>This post compares two camera lenses, one with auto focus and one manual focus:</p><ul><li>Sony  FE 40mm F2.5G G Lens&#xA0; (SEL40F25G) with autofocus</li><li>Voigtlander 35mm f/2 Ultron VM manual focus lens (M mount)</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/lenses.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="1102" height="498"><figcaption><span style="white-space: pre-wrap;">left: Sony FE 40mm, right: Voigtlander 35mm</span></figcaption></figure><p>The camera I used was Sony&apos;s A7Cii.</p><figure class="kg-card kg-image-card"><img src="https://item-shopping.c.yimg.jp/i/n/camera-no-ohbayashi_4548736154292" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="900" height="900"></figure><h2 id="why-use-a-manual-focus-lens">Why use a manual focus lens?</h2><p>This is the big question I&apos;ve been asking myself since Sony cameras are praised for their auto focus capabilities. Ultimately, it boiled down to wanting to try a different way of taking photos. </p><p>There are a number of drawbacks to shooting with the manual lens I chose:</p><ul><li>no access to auto focus, which includes auto tracking and a myriad of other features</li><li>shutter priority mode (S) is not useful since the camera doesn&apos;t have the ability to auto-control the aperture of the lens. </li><li>no meta information about the lens or focal length is saved in the image</li><li>an M-mount to E-mount adapter is required</li><li>and more</li></ul><p>Despite the list of drawbacks, I&apos;ve loved using a manual focus. The way it forces me to compose photos is really fun. Especially if you take photos where there are objects in the foreground (like through a fence), auto-focus can get in your way unless you know what you&apos;re doing. </p><p>The lens itself has really warm saturated colors. It&apos;s smaller in size (though equivalent in weight). </p><h2 id="comparison">Comparison</h2><p>I will compare the <strong>raw photos unedited except for cropping/rotating, and lens correction</strong>. In both cases, I&apos;ve <strong>used a form of aperture priority (A) mode, where the shutter speed and ISO are automatically decided by the camera</strong>. This is how I shoot photos rather than manual to control everything to a precise setting. </p><p><strong>My hypothesis</strong> going into this was that the final edited photos will look the same, but perhaps feel different. That distinction is subtle, but the approach to taking a photo with manual focus requires more focus and intention, which I&apos;ve learned to enjoy. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/1h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/2h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/3h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/4h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="1366" height="1024"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/5h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/6h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/7h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/8h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/9h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/10h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/11h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="1366" height="1024"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/13h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/14h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/15h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/16h.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">&#x2B05;&#xFE0F; Sony 40mm AF; &#x27A1;&#xFE0F; Voigtlander 35mm MF</span></figcaption></figure><h2 id="comparison-takeaways">Comparison: Takeaways</h2><ul><li>the images are more blue on the Sony (cold), more orange on the Voigtlander (warm)</li><li>vignetting is much more prominent with the Voigtlander lens</li><li>colors more more saturated in the Voigtlander lens</li><li>images with the Sony are more balanced, but also flat</li><li>sharpness is about the same</li><li>the Voigtlander feel more vintage without being edited</li></ul><h2 id="comparison-edited-photos">Comparison: Edited Photos</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/NWK09795.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="1333"><figcaption><span style="white-space: pre-wrap;">Sony edited</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/07/NWK09791.jpg" class="kg-image" alt="Automatic vs Manual Focus Lens Comparison with Sony" loading="lazy" width="2000" height="1333"><figcaption><span style="white-space: pre-wrap;">Voigtlander edited</span></figcaption></figure><p>Comparing images with editing applied (coloring, lighting, etc) it really starts to come down to preference. The Voigtlander images definitely feel warmer, but that could be corrected in post if desired.</p><h2 id="so-auto-focus-or-manual-focus">So, auto focus or Manual focus?</h2><p>I think this really comes down to preference, but the main difference is experiential. It&apos;s hard to quantify in a side by side comparison. </p><p>In my opinion, the final product doesn&apos;t vary substantially. Personally I&apos;m enjoying the Voigtlander MF lens, but maybe that&apos;s because it&apos;s something different and forces me to pay more attention to the process.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Image Saturation: Color Intensity]]></title><description><![CDATA[<p></p><p>I take a lot of photos and I edit the ones I like in post. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, exposure, etc. are actually work.</p><p>In this post I define image &quot;saturation.&quot; Then I&apos;ll walk through some</p>]]></description><link>https://blog.nathanwillson.com/image-saturation-slow-and-accurate-or-fast-by-dirty/</link><guid isPermaLink="false">683067a07ee4d9013b56c30b</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sat, 24 May 2025 04:51:53 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-23--2025--11_14_01-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-23--2025--11_14_01-PM.png" alt="Image Saturation: Color Intensity"><p></p><p>I take a lot of photos and I edit the ones I like in post. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, exposure, etc. are actually work.</p><p>In this post I define image &quot;saturation.&quot; Then I&apos;ll walk through some programming approaches how to adjust saturation with all the trade-offs. </p><hr><h1 id="image-saturation">Image Saturation</h1><p>The saturation of an image is how vivid or intense the colors are. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output-horiz.jpg" class="kg-image" alt="Image Saturation: Color Intensity" loading="lazy" width="2000" height="667"><figcaption><span style="white-space: pre-wrap;">saturated image on the right</span></figcaption></figure><h1 id="ways-to-edit-image-saturation">Ways to edit image saturation</h1><p>There are 2 main approaches to adjusting the saturation of an image. </p><p>All the code is on Github: <a href="https://github.com/nbw/foto/blob/main/src/cmds/saturation.rs?ref=blog.nathanwillson.com">https://github.com/nbw/foto/blob/main/src/cmds/saturation.rs</a></p><h2 id="1-hsv-approach-slower-but-accurate">[1] HSV approach:  slower but accurate </h2><p><strong>How it works:</strong></p><p>Convert the image from RGB to its HSV (Hue, Saturation, Value) representation. <a href="https://blog.nathanwillson.com/hsv/">Here&apos;s my blog post about what HSV is and how it works.</a><br><br>Once you have an image in HSV value of a pixel, adjust the S value appropriately, then convert back to an RGB pixel. </p><p><strong>Pros:</strong></p><ul><li>generally accurate way of adjusting saturation</li><li>easy to rationalize</li><li>preserves hue</li></ul><p><strong>Cons: </strong></p><ul><li>compatibly CPU intensive to other approaches</li><li>the conversion from RBG to HSV is a non-linear operation so you can&apos;t use things like SIMD to parallelize it at the CPU level</li></ul><p><strong>Rust code:</strong></p><pre><code class="language-rust">fn apply_hsv_saturation(img: DynamicImage, saturation: f32) -&gt; Result&lt;DynamicImage&gt; {
    let mut out_img: RgbImage = ImageBuffer::new(img.width(), img.height());

    for (x, y, pixel) in img.to_rgb8().enumerate_pixels() {
        let [r, g, b] = pixel.0;
        let (h, s_base, v) = color::rgb_to_hsv(r, g, b);
        let s = (s_base * saturation).clamp(0.0, 1.0);

        let (r, g, b) = color::hsv_to_rgb(h, s, v);
        let new_pixel = image::Rgb([r, g, b]);
        out_img.put_pixel(x, y, new_pixel);
    }

    Ok(DynamicImage::ImageRgb8(out_img))
}</code></pre><h2 id="2-via-luminance-approach-fast-but-has-issues">[2] Via Luminance approach: fast but has issues</h2><p><strong>How it works: </strong></p><p>Luminance (L) and Saturation are related concepts; rather than HSV we can use HSL. </p><p>Conveniently, luminance can be approximated using the following equation (the <strong>ITU-R BT.601 standard</strong>, used to calculate the <strong>perceived brightness</strong> of a color): </p><pre><code>L = 0.299 * R + 0.587 * G + 0.114 * B;</code></pre><p>then we can calculate the new R&apos;, G&apos;, B&apos; values using:</p><pre><code>R&apos; = L + (R - L) * saturation
G&apos; = L + (G - L) * saturation
B&apos; = L + (B - L) * saturation
</code></pre><p><strong>Pros:</strong></p><ul><li>fast and good enough for many cases</li><li>linear operation that can be parallelized easily at the CPU level with tools like SIMD. </li></ul><p><strong>Cons</strong></p><ul><li>With extreme adjustments can shift the hue in ways that are hard to control</li></ul><p><strong>Rust code using luminance approach:</strong></p><pre><code>fn apply_luminance_saturation(img: DynamicImage, saturation: f32) -&gt; Result&lt;DynamicImage&gt; {
    let mut out_img: RgbImage = ImageBuffer::new(img.width(), img.height());

    for (x, y, pixel) in img.to_rgb8().enumerate_pixels() {
        let [r, g, b] = pixel.0;
        let r_f = r as f32;
        let g_f = g as f32;
        let b_f = b as f32;
        let luminance = 0.299 * r_f + 0.587 * g_f + 0.114 * b_f;

        let new_pixel = image::Rgb([
            (luminance + (r_f - luminance) * saturation) as u8,
            (luminance + (g_f - luminance) * saturation) as u8,
            (luminance + (b_f - luminance) * saturation) as u8,
        ]);
        out_img.put_pixel(x, y, new_pixel);
    }

    Ok(DynamicImage::ImageRgb8(out_img))
}</code></pre><p><strong>Rust code using luminance approach with Simd for performance:</strong></p><pre><code class="language-rust">fn apply_luminance_saturation_simd(img: DynamicImage, saturation: f32) -&gt; Result&lt;DynamicImage&gt; {
    let mut out_img_raw = vec![0u8; img.to_rgb8().into_raw().len()];

    // NOTE:assumes Simd lane factor of 4,
    // but this value depends on your hardware!
    let lanes = 4; 

    for (chunk_idx, rgb_chunk) in img
        .to_rgb32f()
        .into_raw()
        .chunks_exact(lanes * 3)
        .enumerate()
    {
        let chunk_start = chunk_idx * lanes * 3;
        let mut r_vals = [0f32; 4];
        let mut g_vals = [0f32; 4];
        let mut b_vals = [0f32; 4];

        for i in 0..lanes {
            r_vals[i] = rgb_chunk[i * 3] as f32;
            g_vals[i] = rgb_chunk[i * 3 + 1] as f32;
            b_vals[i] = rgb_chunk[i * 3 + 2] as f32;
        }

        let r_f = Simd::from_array(r_vals);
        let g_f = Simd::from_array(g_vals);
        let b_f = Simd::from_array(b_vals);

        let luminance =
            Simd::splat(0.299) * r_f + Simd::splat(0.587) * g_f + Simd::splat(0.114) * b_f;

        let sat: Simd&lt;f32, 4&gt; = Simd::splat(saturation);

        let r_out = luminance + (r_f - luminance) * sat;
        let g_out = luminance + (g_f - luminance) * sat;
        let b_out = luminance + (b_f - luminance) * sat;

        for i in 0..lanes {
            out_img_raw[chunk_start + i * 3] = (r_out[i].clamp(0.0, 1.0) * 255.0) as u8;
            out_img_raw[chunk_start + i * 3 + 1] = (g_out[i].clamp(0.0, 1.0) * 255.0) as u8;
            out_img_raw[chunk_start + i * 3 + 2] = (b_out[i].clamp(0.0, 1.0) * 255.0) as u8;
        }
    }

    let out_img = image::RgbImage::from_raw(img.width(), img.height(), out_img_raw)
        .expect(&quot;Failed to create image&quot;);

    Ok(image::DynamicImage::ImageRgb8(out_img))
}</code></pre><p></p><h1 id="comparison-of-approaches">Comparison of approaches:</h1><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/vietname.jpg" class="kg-image" alt="Image Saturation: Color Intensity" loading="lazy" width="1024" height="683"><figcaption><span style="white-space: pre-wrap;">Original Image</span></figcaption></figure><p>Applying a <strong>saturation factor of 2.5</strong> with each approach:</p>
<!--kg-card-begin: html-->
<div class="image-grid">
  <div class="image-pair">
    <div class="image-cell header">HSV</div>
    <div class="image-cell">
      <img alt="Image Saturation: Color Intensity" class="mx-auto block" src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output_hsv-1.jpg">
    </div>
  </div>
  <div class="image-pair">
    <div class="image-cell header">Luminance</div>
    <div class="image-cell">
      <img alt="Image Saturation: Color Intensity" class="mx-auto block" src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output_lum.jpg">
    </div>
  </div>
  <div class="image-pair">
    <div class="image-cell header">Luminance with SIMD</div>
    <div class="image-cell">
      <img alt="Image Saturation: Color Intensity" class="mx-auto block" src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output_lumsimd.jpg">
    </div>
  </div>
</div>

<style>
  .image-grid {
    display: flex;
    flex-direction: column;
    gap: 2rem;
    max-width: 960px;
    margin: auto;
  }

  .image-pair {
    display: flex;
    flex-direction: column;
    text-align: center;
  }

  .image-cell img {
    max-width: 100%;
    max-height: 300px;
    object-fit: contain;
    border: 1px solid #ccc;
    border-radius: 8px;
  }

  .header {
    font-weight: bold;
    font-size: 1.2em;
    margin-bottom: 0.5rem;
    margin-top: 1.5rem;
  }

  @media (min-width: 700px) {
    .image-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 1rem;
    }

    .image-pair {
      flex-direction: column;
    }
  }
</style>

<!--kg-card-end: html-->
<p></p><p></p><p>Performance using a 3mb image wise we get:</p><pre><code>. ./benchmarks/saturation                             rs &#xE68B;  &#xF43A; 12:27 

hsv     1.71s user 0.12s system 107% cpu 1.704 total

lum     1.02s user 0.11s system 112% cpu 1.010 total

lumsimd 1.11s user 0.15s system 111% cpu 1.121 total</code></pre><p>The luminance simd approach is actually <em>slower</em> than without in this instance &#x1F914; so it would be worth spending time to make some optimizations there. For video processing video files, using SIMD should be a 3-10x speed increase.</p><p>I verified the same results on a 25mb image as well.</p><h1 id="conclusion">Conclusion</h1><p>There are a bunch of way to edit an image&apos;s saturation. Each has it&apos;s tradeoffs or side-effects. </p><p>In the case of editing photos, the photographer&apos;s eye is the final judge; it&apos;s probably not super important which technique you use if you&apos;re adjusting saturation within reasonable limits. In that case, I&apos;d probably opt for whatever is fastest. </p><p>Personally, I find saturation to be less useful compared to <em>vibrance</em>, which I&apos;ll look at in another post!</p>]]></content:encoded></item><item><title><![CDATA[Image HSV: RBG for humans]]></title><description><![CDATA[<p>This post is about what the HSV (Hue, Saturation, Value) representation of an image is.</p><hr><p><strong>If I told you to visualize the color (125, 75, 200), what color do you see?</strong> &#x1F914;</p><p>&#x1F9CD;When describing a color, as a human of course, you might say things like: &#x201C;<em>deep red</em></p>]]></description><link>https://blog.nathanwillson.com/hsv/</link><guid isPermaLink="false">68305a497ee4d9013b56c2a0</guid><category><![CDATA[image]]></category><category><![CDATA[rust]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Fri, 23 May 2025 12:08:02 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-23--2025--08_54_35-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-23--2025--08_54_35-PM.png" alt="Image HSV: RBG for humans"><p>This post is about what the HSV (Hue, Saturation, Value) representation of an image is.</p><hr><p><strong>If I told you to visualize the color (125, 75, 200), what color do you see?</strong> &#x1F914;</p><p>&#x1F9CD;When describing a color, as a human of course, you might say things like: &#x201C;<em>deep red</em>&#x201D; or &#x201C;<em>pastel purple</em>&#x201D; or &#x201C;<em>bright and muted orange</em>.&quot;</p><p>&#x1F916; Whereas a computer (and hardware) would say something like: RED: 125, GREEN: 75, BLUE:&#xA0;&#xA0;200.</p><p>What is intuitive for us is different than what is logical for hardware. So in the 1970&#x2019;s&#xA0;the <strong>H</strong>ue <strong>S</strong>aturation <strong>V</strong>alue system (HSL, too) was invented to represent RGB color values in terms more intuitive to humans.</p><ul><li><strong> Hue (H)</strong>: <em>the color (red, purple, green, ..). </em>It&#x2019;s a spectrum represented in degrees (red: 0&#xB0;, green: 120&#xB0;, blue 240&#xB0;).&#xA0;</li><li><strong>Saturation (S)</strong>: <em>the intensity of the color.</em> When describing a colors, it&#x2019;s often the adjective.</li><li><strong>Value (V)</strong>: <em>the brightness.</em><ul><li> Imagine that you were <a href="https://images.squarespace-cdn.com/content/v1/5a26c41ef09ca4da1420a6b4/1555622481520-CEYJNZQM060XXS8LD92F/image-asset.jpeg?ref=blog.nathanwillson.com">mixing paint</a> &#x1F3A8; to make a color, the &#x201C;value&#x201D; would be how much white or black you mixed in (to brighten or darken it).</li></ul></li></ul><h2 id="consider-the-following">Consider the following...</h2><ul><li>&#x201C;Light sky blue&#x201D;: light (V), sky (S), blue (H)</li><li>&#x201C;Muted deep red&#x201D;: muted (V), deep (S), red (H)]</li></ul><p>A HSV cone is a common way to visualize it:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.researchgate.net/publication/266462481/figure/fig8/AS:669424390516744@1536614620338/HSV-color-model-single-hex-cone-1014.ppm" class="kg-image" alt="Image HSV: RBG for humans" loading="lazy" width="328" height="289"><figcaption><span style="white-space: pre-wrap;">HSV Color Cone</span></figcaption></figure><p></p><h1 id="converting-from-rbg-to-hsv">Converting from RBG to HSV</h1><p>Let&apos;s look at the RBG to HSV equations and try to understand what they&apos;re describing. They look complicated at first glance:</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/250523_20h35m40s_screenshot.png" class="kg-image" alt="Image HSV: RBG for humans" loading="lazy" width="824" height="843"></figure><p>(<a href="https://math.stackexchange.com/questions/556341/rgb-to-hsv-color-conversion-algorithm?ref=blog.nathanwillson.com">equations reference</a>)</p><p>Starting with the easiest:<strong> </strong></p><p><strong>VALUE: V = Cmax,</strong> is the most dominant color, i.e: the highest value (R, G, or B) of an RGB pixel. So if R = 200, and G and B are 100, then V is 200. <br><br><strong>SATURATION: S = (Cmax - Cmin)/ Cmax,</strong> is the ratio between the highest and lowest RGB value divided by Cmax to normalize the value between 0 to 1. The exception if the pixel is black, in which case we assume 0 saturation. </p><p><strong>HUE: H, returns an angle value between 0 to 360. </strong></p><ul><li>If the brightest of the RGB values is Red, we expect the angle to be between 0&#xB0; to 120&#xB0;</li><li>If the brightest of the RGB values is Green, we expect the angle to be between 120&#xB0; to 240&#xB0;</li><li>If the brightest of the RGB values is Blue, we expect the angle to be between 240&#xB0; to 360&#xB0;</li></ul><h1 id="code-rbg-to-hsv">Code: RBG to HSV</h1><p>Here&apos;s an implementation of RGB to HSV using rust: </p><pre><code class="language-rust">pub fn rgb_to_hsv(r: u8, g: u8, b: u8) -&gt; (f32, f32, f32) {
    let r = r as f32 / 255.0;
    let g = g as f32 / 255.0;
    let b = b as f32 / 255.0;

    let max = r.max(g).max(b);
    let min = r.min(g).min(b);
    let delta = max - min;

    let h = if delta == 0.0 {
        0.0
    } else if max == r {
        60.0 * (((g - b) / delta) % 6.0)
    } else if max == g {
        60.0 * (((b - r) / delta) + 2.0)
    } else {
        60.0 * (((r - g) / delta) + 4.0)
    };

    let h = if h &lt; 0.0 { h + 360.0 } else { h };
    let s = if max == 0.0 { 0.0 } else { delta / max };
    let v = max;

    (h, s, v)
}</code></pre><p><a href="https://github.com/nbw/foto/blob/main/src/utils/color.rs?ref=blog.nathanwillson.com#L17C1-L41C2">Code on github</a>.</p><h1 id="next-time">Next time...</h1><p>In my next post, I&apos;ll walk through how to adjust the &quot;saturation&quot; of an image, which is easy if you already have the HSV representation of the image. </p>]]></content:encoded></item><item><title><![CDATA[Rust CLI: Add CLI docs to your README.md]]></title><description><![CDATA[Generate CLI docs in your README for a Clap-based Rust CLI.]]></description><link>https://blog.nathanwillson.com/rust-cli-docs-readme/</link><guid isPermaLink="false">682f07a97ee4d9013b56c252</guid><category><![CDATA[rust]]></category><category><![CDATA[cli]]></category><category><![CDATA[clap]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Thu, 22 May 2025 11:39:49 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-22--2025--08_50_11-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/ChatGPT-Image-May-22--2025--08_50_11-PM.png" alt="Rust CLI: Add CLI docs to your README.md"><p></p><p>So you&apos;ve written a Rust CLI with <a href="https://github.com/clap-rs/clap?ref=blog.nathanwillson.com">Clap</a> and you want to update your <em>README.md</em> with details on how to use it. Well, ConnorGray has written a library that does just that: </p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/ConnorGray/clap-markdown/tree/main?tab=readme-ov-file&amp;ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - ConnorGray/clap-markdown: Autogenerate Markdown documentation for clap command-line tools</div><div class="kg-bookmark-description">Autogenerate Markdown documentation for clap command-line tools - ConnorGray/clap-markdown</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Rust CLI: Add CLI docs to your README.md"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">ConnorGray</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/7ee7adcd517e6a5cafe03f9353598a64aae653633cbbfc979f74d540abbbedb8/ConnorGray/clap-markdown" alt="Rust CLI: Add CLI docs to your README.md"></div></a></figure><p><strong>Problem</strong>: clap-markdown generates markdown just fine, but we want to add the output to our <em>README.md</em> without overwriting the entire contents of the file..<br><br><strong>Solution: </strong>use hidden comments in the README as a placemarker.<br><br>1. Add something like the following to your <em>README.md</em> file:</p><pre><code class="language-markdown">&lt;!-- start: CLI USAGE --&gt;

&lt;!-- end: CLI USAGE --&gt;</code></pre><ol start="2"><li>Add a hidden command to your CLI <a href="https://github.com/ConnorGray/clap-markdown/tree/main?tab=readme-ov-file&amp;ref=blog.nathanwillson.com#usage-convention-commandlinehelpmd">as suggested by clap-markdown here</a>, which should call a function that looks something like:</li></ol><pre><code class="language-rust">pub fn add_cli_cmd_to_readme() -&gt; Result&lt;()&gt; {
    let md = clap_markdown::help_markdown::&lt;Cli&gt;();

    // Read the README.md file
    let mut content = std::fs::read_to_string(&quot;README.md&quot;)?;

    // Find the start and end markers
    let start = &quot;&lt;!-- start: CLI USAGE --&gt;&quot;;
    let end = &quot;&lt;!-- end: CLI USAGE --&gt;&quot;;

    // Replace content between markers
    let start_idx = content
        .find(start)
        .ok_or_else(|| anyhow::anyhow!(&quot;Could not find start marker&quot;))?;
    let end_idx = content
        .find(end)
        .ok_or_else(|| anyhow::anyhow!(&quot;Could not find end marker&quot;))?;

    content.replace_range((start_idx + start.len())..end_idx, &amp;format!(&quot;\n\n{}\n&quot;, md));

    // Write back to README.md
    std::fs::write(&quot;README.md&quot;, content)?;

    println!(&quot;Markdown help written to README.md&quot;);

    Ok(())
}</code></pre><p>This function will add the output of  <code>clap_markdown</code> your between the place markers in your <em>README.md.</em><br></p><h1 id="example">Example</h1><p><br><br>Here&apos;s a repo of mine using it: <a href="https://github.com/nbw/foto?ref=blog.nathanwillson.com">https://github.com/nbw/foto</a><br><br>I run <code>cargo run &#x2013; cli-readme</code> to generate a new README.md with updated docs. <br><br></p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/250522_20h35m37s_screenshot-1.png" class="kg-image" alt="Rust CLI: Add CLI docs to your README.md" loading="lazy" width="1260" height="1399"></figure>]]></content:encoded></item><item><title><![CDATA[Image Contrast: the distance from middle-gray]]></title><description><![CDATA[<p>I take a lot of photos and I edit the ones I like in post. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, exposure, etc. are actually work.</p><p>In this post I define &quot;contrast&quot; in programmatic/mathematical terms (i.e: the raw</p>]]></description><link>https://blog.nathanwillson.com/contrast/</link><guid isPermaLink="false">68287ac67ee4d9013b56c1d7</guid><category><![CDATA[image]]></category><category><![CDATA[photography]]></category><category><![CDATA[rust]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sat, 17 May 2025 12:46:16 GMT</pubDate><content:encoded><![CDATA[<p>I take a lot of photos and I edit the ones I like in post. Lately I&apos;ve been wondering how basic photo editing operations like contrast, saturation, exposure, etc. are actually work.</p><p>In this post I define &quot;contrast&quot; in programmatic/mathematical terms (i.e: the raw pixel value), show a quick example, and finally provide a simple rust script to illustrate how it works. </p><h1 id="what-is-image-contrast">What is image contrast?</h1><p><strong>Adjusting the &quot;contrast&quot; of an image means stretching pixel values away or towards <em>middle gray</em>.</strong></p><p>What is <em>middle gray</em>? If black is on one end of the spectrum (0) and white is on the other (255) then middle gray could be in the middle (128).</p><p>If we were to take the histogram of an image, which is a graph of the spread of pixel values of an image, we could draw a line down the middle and call it &quot;middle gray&quot;. </p><p>Increasing (+) the contrast of an image would push all the values left of the line even further left (and oppositely those right of the line further right). I.e: stretch pixel values to the edges of the graph.</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/contrast_1.png" class="kg-image" alt loading="lazy" width="1100" height="796"></figure><p>Decreasing the contrast pushes pixel values closer to middle gray. I.e: crush pixel values towards the middle of the graph.</p><p></p><h1 id="example">Example </h1><h2 id="original-image">Original Image:</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/input.jpg" class="kg-image" alt loading="lazy" width="1752" height="1168"><figcaption><span style="white-space: pre-wrap;">Original Image</span></figcaption></figure><h2 id="increased-contrast-20">Increased contrast (2.0)</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output.jpg" class="kg-image" alt loading="lazy" width="1752" height="1168"><figcaption><span style="white-space: pre-wrap;">Increased contrast by factor of 2</span></figcaption></figure><h2 id="decreased-contrast-05">Decreased contrast (0.5)</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/output-1.jpg" class="kg-image" alt loading="lazy" width="1752" height="1168"><figcaption><span style="white-space: pre-wrap;">Decrease contrast by a factor of 0.5</span></figcaption></figure><h1 id="adjusting-contrast-with-code">Adjusting contrast with code</h1><p>Here&apos;s an code example using <code>rust</code> to adjust the contrast of an image.</p><p>Usage:</p><ul><li>requires <code>image</code> library as a dependency</li><li>assumes an image file <code>input.jpg</code> in the project folder</li></ul><pre><code class="language-rust">use image::{ImageBuffer, RgbImage};

/*
Adjusts the contrast of a pixel.

How it works:
- subtract 128 to moves the value range of 0 - 256 to -128 to 128
- multiplies the result by a &quot;factor&quot;
- adds 128 to return the value range between 0 - 256
*/
fn adjust_contrast(value: u8, factor: f32) -&gt; u8 {
    let val = (value as f32 - 128.0) * factor + 128.0;
    val.clamp(0.0, 255.0) as u8
}

fn main() {
    // read the image
    let img = image::open(&quot;input.jpg&quot;).expect(&quot;Failed to open image&quot;);
    let mut out_img: RgbImage = ImageBuffer::new(img.width(), img.height());

    // iterate over each pixel of the image
    for (x, y, pixel) in img.to_rgb8().enumerate_pixels() {
        // in this example the contrast is being adjusted by
        // a factor of 0.5
        let [r, g, b] = pixel.0;
        let new_pixel = image::Rgb([
            adjust_contrast(r, 0.5),
            adjust_contrast(g, 0.5),
            adjust_contrast(b, 0.5),
        ]);
        out_img.put_pixel(x, y, new_pixel);
    }

    out_img.save(&quot;output.jpg&quot;).expect(&quot;Failed to save image&quot;);
}
</code></pre><p>That&apos;s it!</p><h1 id="things-to-think-about">Things to think about</h1><ul><li>In this example I&apos;ve assumed anchoring to &quot;<em>middle gray</em>&quot; as the threshold (value of 128). What if we used a different anchor value instead? What if we used a different anchor for each color (R, G, B)?</li><li>Adjusting contrast linearly with a simple multiplication by a &quot;factor&quot; is easy, but what if we used a more complex equation instead (logarithmic for example).</li></ul>]]></content:encoded></item><item><title><![CDATA[Easy mode: Arch + Hyprland + Hyde]]></title><description><![CDATA[<p>This is mostly a PSA because things are much easier than they once were before. <br></p><ol><li>&#x274C;<strong> ARCH is hard to install and configure!</strong>  <strong>WRONG</strong> &#x2013; Arch is no longer hard to install. There&apos;s a nice install wizard that you can now use: Run <code>archinstall</code> when you&apos;re</li></ol>]]></description><link>https://blog.nathanwillson.com/easy-mode-arch-hyperland-hyde/</link><guid isPermaLink="false">681de7ce7ee4d9013b56c1b0</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Fri, 09 May 2025 11:44:12 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/sddefault.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2025/05/sddefault.jpg" alt="Easy mode: Arch + Hyprland + Hyde"><p>This is mostly a PSA because things are much easier than they once were before. <br></p><ol><li>&#x274C;<strong> ARCH is hard to install and configure!</strong>  <strong>WRONG</strong> &#x2013; Arch is no longer hard to install. There&apos;s a nice install wizard that you can now use: Run <code>archinstall</code> when you&apos;re installing the OS.<ol><li>You can install Hyprland as part of the installer (easy!)</li><li>to log into Hyprland, you need to start in TTY mode (ctrl + alt + F4) if you&apos;ve configured a UI login (maybe you installed Plasma too like I did)</li><li>FYI: I installed Arch by putting the disk image on a USB stick and loading it via the bootloader</li></ol></li><li><strong>&#x274C; HYPERLAND is hard to install configure: WRONG</strong> &#x2013; if you want a nice setup to try Hyprland already configured then install the [Hyde project](<a href="https://github.com/HyDE-Project/HyDE?ref=blog.nathanwillson.com">https://github.com/HyDE-Project/HyDE</a>)<ol><li>once you&apos;ve installed Hyde, CMD + / shows you shortcuts. </li><li>You can configure hotkeys in ~/.config/hypr/keybindings.conf</li><li>run <code>hydectl reload</code> to refresh stuff if needed</li><li>there&apos;s also <a href="https://wiki.archcraft.io/docs/wayland-compositors/hyprland/?ref=blog.nathanwillson.com">Archcraft Hyprland</a> that is worth looking at if you don&apos;t like Hyde</li></ol></li></ol><h1 id="heres-a-good-install-guide-for-arch">Here&apos;s a good install guide for Arch:</h1><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/FxeriGuJKTM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="How to Install Arch Linux: Step-by-Step Guide"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Linux: Framework 13 NixOS, sleep but screen still on 🪫 (how to fix it)]]></title><description><![CDATA[<p>I got a Framework laptop (yay) and I put NixOS on it. But when I put the computer to sleep the screen is still on &#x1F44E;, the computer is still warm &#x1F44E;, and it drains battery real fast &#x1F480;. </p><p>&#x1FAAB;</p><p>So what gives?</p><p>Well, <a href="https://community.frame.work/t/resolved-laptop-stays-on-in-sleep/17168/3?ref=blog.nathanwillson.com" rel="noreferrer">there are numerous people reporting similar</a></p>]]></description><link>https://blog.nathanwillson.com/linux-framework-13-nixos-sleep-screen-still-on/</link><guid isPermaLink="false">672a210d4c6c7c013be2ef38</guid><category><![CDATA[linux]]></category><category><![CDATA[nixos]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Tue, 05 Nov 2024 13:59:10 GMT</pubDate><content:encoded><![CDATA[<p>I got a Framework laptop (yay) and I put NixOS on it. But when I put the computer to sleep the screen is still on &#x1F44E;, the computer is still warm &#x1F44E;, and it drains battery real fast &#x1F480;. </p><p>&#x1FAAB;</p><p>So what gives?</p><p>Well, <a href="https://community.frame.work/t/resolved-laptop-stays-on-in-sleep/17168/3?ref=blog.nathanwillson.com" rel="noreferrer">there are numerous people reporting similar issues with other distributions of Linux</a> and what ended up being the issue was the Linux Kernel version. There are some background processes that stop the computer from sleeping properly. <strong>If you upgrade the Kernel then it should be fixed.</strong></p><h2 id="linux-kernel-after-installing-nixos">Linux Kernel after installing NixOS</h2><p>After installing NixOS the Linux Kernel I had was:</p><pre><code>$ uname -r
6.6.36
</code></pre><h2 id="upgrade-the-kernel">Upgrade the Kernel</h2><p>Using <a href="https://www.reddit.com/r/framework/comments/1eychnp/13_amd_nixos_will_not_suspend_and_lid_close_does/?rdt=56034&amp;ref=blog.nathanwillson.com" rel="noreferrer">a tip from this post</a> add the line:</p><pre><code>boot.kernelPackages = pkgs.linuxPackages_latest;</code></pre><p>to your <code>configuration.nix</code> file. &#x1F91E;Hope nothing breaks!</p><p>Rebuild, restart the computer and:</p><pre><code>$ uname -r
6.9.7</code></pre><p>And voila, at least my Framework laptop now properly &quot;sleeps.&quot; The screen actually turns off and I&apos;m not draining tons of battery when it&apos;s idle. </p><p>&#x1F389;</p><hr><h2 id="aside-hibernation">Aside: Hibernation</h2><p>By default &quot;sleep&quot; will simply suspend Linux and that means the computer is still sort of running (everything is still in RAM). For real power savings you want to hibernate where RAM is dumped into swap memory and loaded again on boot up.</p><p><a href="https://askubuntu.com/questions/3369/what-is-the-difference-between-hibernate-and-suspend?ref=blog.nathanwillson.com" rel="noreferrer">Hibernate vs Suspend</a>.</p><p> I&apos;ll have to figure out how to do that with NixOS at some point.</p>]]></content:encoded></item><item><title><![CDATA[Linux: Hyprland + NixOS: Fun, but pausing for now]]></title><description><![CDATA[<p>I <a href="https://blog.nathanwillson.com/linux-becoming-a-linux-user/" rel="noreferrer">started my Linux journey on a cheap ThinkPad X1 Carbon Gen 7</a> and I opted for NixOS as my Linux distro. It&apos;s been great, but to spice things up I also tried Hyprland. </p><figure class="kg-card kg-image-card"><img src="https://preview.redd.it/font-used-in-hyprland-logo-v0-43ddnec24hnb1.png?width=640&amp;crop=smart&amp;auto=webp&amp;s=a447fdf5dc17fe56b797ea653846bb0f60c2d1d3" class="kg-image" alt="r/identifythisfont - Font used in Hyprland logo?" loading="lazy" width="640" height="360"></figure><p>Hyprland is a window manager that automatically organizes new windows for you. At the</p>]]></description><link>https://blog.nathanwillson.com/nixos-hyperland-pause/</link><guid isPermaLink="false">6726b9854c6c7c013be2eec5</guid><category><![CDATA[linux]]></category><category><![CDATA[nixos]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sun, 03 Nov 2024 00:46:20 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2024/11/2024-11-03T09-00-56-272951506-09-00-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/11/2024-11-03T09-00-56-272951506-09-00-3.png" alt="Linux: Hyprland + NixOS: Fun, but pausing for now"><p>I <a href="https://blog.nathanwillson.com/linux-becoming-a-linux-user/" rel="noreferrer">started my Linux journey on a cheap ThinkPad X1 Carbon Gen 7</a> and I opted for NixOS as my Linux distro. It&apos;s been great, but to spice things up I also tried Hyprland. </p><figure class="kg-card kg-image-card"><img src="https://preview.redd.it/font-used-in-hyprland-logo-v0-43ddnec24hnb1.png?width=640&amp;crop=smart&amp;auto=webp&amp;s=a447fdf5dc17fe56b797ea653846bb0f60c2d1d3" class="kg-image" alt="Linux: Hyprland + NixOS: Fun, but pausing for now" loading="lazy" width="640" height="360"></figure><p>Hyprland is a window manager that automatically organizes new windows for you. At the end of it, you basically have a no-mouse setup&#x2013; or at least you&apos;re not dragging around windows. There are other alternatives (like Sway) out there and they all run a bit differently. I&apos;m not an expert on how Hyprland works (and honestly it&apos;s not that important), but there are <em>many</em> people out there who&apos;ve already written articles on the topic. </p><p>If you search &quot;<a href="https://www.youtube.com/watch?v=TZMfpNfRKUQ&amp;ref=blog.nathanwillson.com" rel="noreferrer">hyprland rice</a>&quot; on Youtube, you&apos;ll get endless cool setups. </p><h3 id="sidenote-hyprland-has-some-problematic-history">Sidenote: Hyprland has some problematic history</h3><p>The creator of Hyprland, Vaxxry, has been banned from a number of Linux communities. I leave it to read to search why, but it&apos;s not great.</p><p>I willingly tried Hyprland knowing a bit about the drama, but that&apos;s not an endorsement of the creator. </p><h2 id="my-setup">My Setup</h2><p>Hyprland looks cool. That is maybe the one great thing about it. It looks and feels super minimalist. </p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/11/2024-11-03T09-00-56-272951506-09-00-1.png" class="kg-image" alt="Linux: Hyprland + NixOS: Fun, but pausing for now" loading="lazy" width="1920" height="1080"></figure><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/11/2024-11-03T08-58-43-772588199-09-00.png" class="kg-image" alt="Linux: Hyprland + NixOS: Fun, but pausing for now" loading="lazy" width="1920" height="1080"></figure><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/11/2024-11-03T09-01-13-919488884-09-00.png" class="kg-image" alt="Linux: Hyprland + NixOS: Fun, but pausing for now" loading="lazy" width="1920" height="1080"></figure><h2 id="hyprland-is-hard-but-cool">Hyprland is hard, but cool</h2><p>Part of me wanted to believe I could make it my daily driver, but the learning curve and required setup is a lot to take on (you setup basically everything). For now, I&apos;m pausing on Hyprland &#x1F6D1;, but maybe I&apos;ll revisit it once I&apos;m ready again.  I still have my configs, everything is NixOS based, so I can rebuild my existing setup in a single command. </p><p>Here are some of the things you have to setup that you&apos;d otherwise get for free with other distros:</p><ul><li>a task bar</li><li>volume control, brightness control</li><li>search (programs, files, etc.)</li><li>lock screen</li><li>network setup</li><li>file viewer</li><li>power on/off</li><li>auto-sleep the computer</li><li><em>many</em> other things, but basically everything</li></ul><p>The other upside/downside of Hyprland is that it&apos;s basically runs on hotkeys. You set those hotkeys so that&apos;s nice, but nevertheless you have to remember them. If you step away from your Hyprland setup for a bit, it can feel pretty alien when you come back.</p><h2 id="getting-started">Getting Started</h2><p>There are a number of tutorials out there, but this is the one that got me started:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/61wGzIv12Ds?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="Nixos and Hyprland - Best Match Ever"></iframe></figure><p>For my navigation bar, I ended up finding someone else&apos;s implementation online then grabbing what I wanted from it:</p><ul><li>Github: <a href="https://github.com/SolDoesTech/HyprV4?ref=blog.nathanwillson.com">https://github.com/SolDoesTech/HyprV4</a></li></ul><p>Unfortunately I spent countless hours watching videos, reading blogs, etc. to figure out a working setup. It just takes time and I can&apos;t remember all the resources I used.</p>]]></content:encoded></item><item><title><![CDATA[Rust Nix flake for Mac]]></title><description><![CDATA[<p>I have a working Nix flake that I use to have a Rust dev environment for Linux, but doesn&apos;t work with Mac. If I run <code>cargo install rustlings</code> I get compile errors like:</p><pre><code>  = note: ld: framework not found CoreServices
          clang-16: error: linker command failed with exit code 1</code></pre>]]></description><link>https://blog.nathanwillson.com/rust-nix-flake-for-mac/</link><guid isPermaLink="false">66cdcb354c6c7c013be2ee87</guid><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Tue, 27 Aug 2024 13:01:01 GMT</pubDate><content:encoded><![CDATA[<p>I have a working Nix flake that I use to have a Rust dev environment for Linux, but doesn&apos;t work with Mac. If I run <code>cargo install rustlings</code> I get compile errors like:</p><pre><code>  = note: ld: framework not found CoreServices
          clang-16: error: linker command failed with exit code 1 (use -v to see invocation)
</code></pre><p>and like:</p><pre><code>  = note: ld: framework not found CoreServices
          clang-16: error: linker command failed with exit code 1 (use -v to see invocation)
</code></pre><p>&#x1F4A1; There are some mac-specific (and darwin) libraries that need to be present it would seem.</p><h2 id="my-working-rust-nixflake">My working Rust nix.flake</h2><p>I&apos;m sure it could be leaner (I&apos;m working off my Linux flake that I used for a project):</p><figure class="kg-card kg-code-card"><pre><code class="language-nix">{
  inputs = {
    flake-utils.url = &quot;github:numtide/flake-utils&quot;;
    naersk.url = &quot;github:nix-community/naersk&quot;;
    nixpkgs.url = &quot;github:NixOS/nixpkgs/nixpkgs-unstable&quot;;
    fenix.url = &quot;github:nix-community/fenix&quot;;
  };

  outputs = {
    self,
    flake-utils,
    naersk,
    nixpkgs,
    fenix,
  }:
    flake-utils.lib.eachDefaultSystem (
      system: let
        pkgs = (import nixpkgs) {
          inherit system;
          overlays = [fenix.overlays.default];
        };

        PKG_CONFIG_PATH = &quot;${pkgs.openssl.dev}/lib/pkgconfig&quot;;

        naersk&apos; = pkgs.callPackage naersk {};
      in rec {
        defaultPackage = naersk&apos;.buildPackage {
          src = ./.;
        };

        inherit (pkgs.lib) optionals;
        inherit (pkgs.stdenv) isDarwin;
        inherit (pkgs.darwin.apple_sdk.frameworks);

        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.openssl
            pkgs.pkg-config
          ] ++ optionals pkgs.stdenv.isDarwin [
            # Additional darwin specific inputs can be set here
            pkgs.libiconv
            pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
          ] ++ (with pkgs.darwin.apple_sdk; [
            frameworks.CoreFoundation
            frameworks.CoreServices
            frameworks.SystemConfiguration
          ])
;
          env = {
            PKG_CONFIG_PATH = PKG_CONFIG_PATH;
          };
          nativeBuildInputs = with pkgs; [
            alejandra
            rust-analyzer
            (pkgs.fenix.stable.withComponents [
              &quot;cargo&quot;
              &quot;clippy&quot;
              &quot;rust-src&quot;
              &quot;rustc&quot;
              &quot;rustfmt&quot;
            ])
            openssl
          ];
        };
      }
    );
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">full flake.nix file that I&apos;m using for mac</span></p></figcaption></figure><p>The Mac specific lines I had to add were:</p><figure class="kg-card kg-code-card"><pre><code>        inherit (pkgs.lib) optionals;
        inherit (pkgs.stdenv) isDarwin;
        inherit (pkgs.darwin.apple_sdk.frameworks);
        </code></pre><figcaption><p><span style="white-space: pre-wrap;">mac specific packages</span></p></figcaption></figure><p>and </p><figure class="kg-card kg-code-card"><pre><code>++ optionals pkgs.stdenv.isDarwin [
            # Additional darwin specific inputs can be set here
            pkgs.libiconv
            pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
          ] ++ (with pkgs.darwin.apple_sdk; [
            frameworks.CoreFoundation
            frameworks.CoreServices
            frameworks.SystemConfiguration
          ])
        </code></pre><figcaption><p><span style="white-space: pre-wrap;">mac specific libraries</span></p></figcaption></figure><p>I use <a href="https://direnv.net/?ref=blog.nathanwillson.com">direnv</a> so I have a <code>.envrc</code> file in the same folder that simply looks like:</p><figure class="kg-card kg-code-card"><pre><code>use flake</code></pre><figcaption><p><span style="white-space: pre-wrap;">.envrc</span></p></figcaption></figure><p>and then run <code>direnv allow</code> </p><p>Every time you <code>cd</code> into the folder, it runs the nix.flake and you have your Rust environment!</p><hr><h2 id="some-helpful-references">Some helpful references</h2><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://discourse.nixos.org/t/ld-framework-not-found-system/15096/6?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Ld: framework not found System</div><div class="kg-bookmark-description">Facing similar issue = note: ld: framework not found SystemConfiguration clang-16: error: linker command failed with exit code 1 (use -v to see invocation) error: could not compile `prisma-cli` (bin &#x201C;prisma-cli&#x201D;) due to 1 previous error</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://discourse.nixos.org/uploads/default/optimized/1X/06a3745850c8afd669372d01ee3ac98ab5d9fdc9_2_180x180.png" alt><span class="kg-bookmark-author">NixOS Discourse</span><span class="kg-bookmark-publisher">cideM</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://discourse.nixos.org/uploads/default/original/1X/9d278415294337993f871f958d77b080670f494b.png" alt></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://discourse.nixos.org/t/cannot-build-rust-binary-on-macos-m1/27630?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Cannot build rust binary on MacOS M1</div><div class="kg-bookmark-description">Introduction I&#x2019;m not being able to compile the toml crate on a MacOS M1. The output error below is from nix-shell but I can reproduce this error on native compilation, with xcode installed. Versions XCode: 1.3.0 Rust: 1.64.0 toml: 0.7.3 Extra informations With nix I also add some apple_sdk frameworks: packages.aarch64-darwin = rec { default = pescarte-desafios; pescarte-desafios = pkgs.rustPlatform.buildRustPackage { pname = &#x201C;pescarte-desafios&#x201D;; versi&#x2026;</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://discourse.nixos.org/uploads/default/optimized/1X/06a3745850c8afd669372d01ee3ac98ab5d9fdc9_2_180x180.png" alt><span class="kg-bookmark-author">NixOS Discourse</span><span class="kg-bookmark-publisher">Mdsp9070</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://discourse.nixos.org/uploads/default/original/1X/9d278415294337993f871f958d77b080670f494b.png" alt></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Linux: Virtual Block Devices]]></title><description><![CDATA[How hard is it to make a virtual block device? Turns out, easy.]]></description><link>https://blog.nathanwillson.com/block-devices/</link><guid isPermaLink="false">66aad7284c6c7c013be2ee2e</guid><category><![CDATA[linux]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Thu, 01 Aug 2024 01:37:02 GMT</pubDate><content:encoded><![CDATA[<p>I was reading Fly.io post <a href="https://fly.io/blog/machine-migrations/?ref=blog.nathanwillson.com" rel="noreferrer">Making Machines Move</a> and I was wondering how hard it is to create a virtual block device. Turns out, not too bad at all.</p><ol><li>Create an image</li></ol><pre><code>dd if=/dev/zero of=/path/to/virtual-disk.img bs=1M count=1024</code></pre><ol start="2"><li>Setup a loop device</li></ol><pre><code>losetup /dev/loop0 /path/to/virtual-disk.img</code></pre><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">losetup </strong></b>is really saying &quot;<b><strong style="white-space: pre-wrap;">lo</strong></b>op <b><strong style="white-space: pre-wrap;">setup</strong></b>&quot;. <br><br>A loop device (or loopback device) is a pseudo-device in Unix-like operating systems (aka Linux) that <i><em class="italic" style="white-space: pre-wrap;">allows a file to be accessed as if it were a block device. </em></i><br><br>Here are some common use cases (thanks ChatGPT):<br><br><b><strong style="white-space: pre-wrap;">- Mounting Disk Images</strong></b>: A common use is to mount ISO files, filesystem images, or other disk images without needing to burn them to physical media.<br><br>- <b><strong style="white-space: pre-wrap;">Testing Filesystems</strong></b>: Developers can create a filesystem within a file and mount it to test its behavior without using a physical disk.<br><br>- <b><strong style="white-space: pre-wrap;">Creating Virtual Storage</strong></b>: Useful in virtual environments to provide storage for virtual machines or containers. <br><br>(&#x261D;&#xFE0F; we&apos;re using it for the 3rd point)</div></div><ol start="3"><li>Confirm the loop device exists with:</li></ol><pre><code class="language-sh">losetup -a</code></pre><ol><li>Create a Filesystem on the device</li></ol><pre><code class="language-sh">mkfs.ext4 /dev/loop0</code></pre><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F44B;</div><div class="kg-callout-text"><code spellcheck="false" style="white-space: pre-wrap;">loop0</code> is coming from the output of <code spellcheck="false" style="white-space: pre-wrap;">losetup -a</code> and depending on your setup your device might be <code spellcheck="false" style="white-space: pre-wrap;">loop1</code>, etc.. Make sure to check! <br></div></div><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">mkfs</strong></b>: &quot;make filesystem.&quot; usually paired with a filesystem type like ext4. Without a filesystem, the operating system won&apos;t be able to manage files with the loop device.</div></div><ol start="4"><li>Mount the device</li></ol><pre><code class="language-sh">mkdir /mnt/myblockdevice
mount /dev/loop0 /mnt/myblockdevice</code></pre><ol start="5"><li>Confirm the drive is mounted with the <code>df</code> command<br></li><li>Unmount when you&apos;re done</li></ol><pre><code class="language-sh">umount /mnt/myblockdevice
losetup -d /dev/loop0</code></pre><p>(you can delete the image at this point too if you want)</p><hr><h2 id="bonus-encrypted-block-device">Bonus: Encrypted block device</h2><p>Mostly the same as above with a few extra steps:</p><pre><code class="language-sh"># Create a file for the loop device
dd if=/dev/zero of=/path/to/encrypted.img bs=1M count=1024

# Set up a loop device
losetup /dev/loop0 /path/to/encrypted.img

# Encrypt the loop device
cryptsetup luksFormat /dev/loop0

# Open the encrypted loop device
cryptsetup luksOpen /dev/loop0 encrypted

# Create a filesystem on the encrypted device
mkfs.ext4 /dev/mapper/encrypted

# Mount the encrypted filesystem
mount /dev/mapper/encrypted /mnt/encrypted</code></pre><hr><p>The next step is to explore the <a href="https://en.wikipedia.org/wiki/Device_mapper?ref=blog.nathanwillson.com">device mapper</a> framework and see what it takes to manage logical volumes. </p>]]></content:encoded></item><item><title><![CDATA[Linux: Becoming a Linux User for $175]]></title><description><![CDATA[Starting my journey into Linux]]></description><link>https://blog.nathanwillson.com/linux-becoming-a-linux-user/</link><guid isPermaLink="false">66948a43ff5944013b3cf681</guid><category><![CDATA[linux]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Mon, 15 Jul 2024 05:06:00 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/Screenshot-2024-07-15-at-12.19.02-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/Screenshot-2024-07-15-at-12.19.02-1.png" alt="Linux: Becoming a Linux User for $175"><p>I bought a used Thinkpad X1 Carbon Gen 7 laptop to try Linux and find out if it&apos;s right for me. </p><p>At the time of writing I&apos;m about a month or more into it.</p><figure class="kg-card kg-image-card"><img src="https://www.wallpaperflare.com/static/893/596/940/tux-linux-foxyriot-logo-wallpaper.jpg" class="kg-image" alt="Linux: Becoming a Linux User for $175" loading="lazy" width="4128" height="2322"></figure><h2 id="why-linux">Why Linux? </h2><p>For a long time I&apos;ve been a Mac user. For many reasons, it&apos;s been a great choice for me as a developer, as someone who makes music, etc.. I still think Mac is a great option, but I&apos;ve also always wanted to explore Linux. </p><p>I think it started off as wanting to understand computers more, but that wasn&apos;t really enough. I hardly ever actually do something <em>because it might be good for me</em>. Certainly not if it requires a ton of effort. </p><p>For a  while now I&apos;ve been wanting to decouple my dependency from Apple.  My experience being a developer with Mac is to keep installing and adding tools until the thing you&apos;re working on just works without cleaning up any messes you made a long the way. What I&apos;m left with is a computer that has so many things installed on it that I can&apos;t keep track of it all anymore. It just becomes bloated until I either reset my computer or buy a new one. </p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/laptop_trash.jpeg" class="kg-image" alt="Linux: Becoming a Linux User for $175" loading="lazy" width="1536" height="1536"></figure><p>Also there are some tools that I want to try that work better (or only work) with Linux. Tools like <a href="https://firecracker-microvm.github.io/?ref=blog.nathanwillson.com">Firecracker Micro VMs</a>. The developer experience overall just seems to work better on Linux (or matches Mac for the most part).</p><p>Ultimately, I want to have more control over what my computer is doing. I want the leanest setup for my needs or at least some better record keeping of what tools I&apos;ve installed.</p><p>I can say already that keeping a lean and organized setup has been empowering. </p><h2 id="this-isnt-the-first-time-ive-tried-to-switch">This isn&apos;t the first time I&apos;ve tried to switch</h2><p>I actually tried switching to Linux last year when I build a desktop computer for the first time (dual boot Linux and Windows). I started with Manjaro, briefly tried Ubuntu, considered Arch, then ended up on Fedora. </p><p>For games (Apex, etc) the Linux setup was great. I rarely had any issues, but in the end my Window was just easier when I wanted to use bluetooth, use an Xbox controller, and use some software that I couldn&apos;t get on Linux (I&apos;m looking at you Adobe). So although I have a working Linux desktop, I mostly boot it in Windows these days (sorry!).</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/00000020-4.JPG" class="kg-image" alt="Linux: Becoming a Linux User for $175" loading="lazy" width="2000" height="1389"></figure><h2 id="for-work-im-still-on-a-mac">For work, I&apos;m still on a Mac</h2><p>I can&apos;t see this changing any time soon. Although my work would permit me to use a Linux setup, I&apos;d have to purchase a computer on my own dime since I have a working laptop already. My M2 Macbook Air works great for work, but if I had to reset it to factory settings then I think it would take me weeks to get back to where I am now. It&apos;s one of those <em>if it ain&apos;t broke don&apos;t fix it</em> situations.</p><h2 id="why-not-asahi-linux-then">Why not Asahi Linux then?</h2><p>The Asahi Linux project is creating compatibility with Linux and ARM based Macs. It&apos;s a really cool project and I&apos;ve read many posts of it working great. I just feel that I want to get away from Mac hardware entirely and have a clean break without the option to just switch back to MacOS. </p><figure class="kg-card kg-image-card"><img src="https://www.iclarified.com/images/news/80480/80480/80480-1280.jpg" class="kg-image" alt="Linux: Becoming a Linux User for $175" loading="lazy" width="1280" height="720"></figure><h1 id="and-so">And so..</h1><p>I hope to post updates of my Linux journey. </p><h1 id></h1>]]></content:encoded></item></channel></rss>