<?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>Sat, 04 Apr 2026 09:15:03 GMT</lastBuildDate><atom:link href="https://blog.nathanwillson.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><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><item><title><![CDATA[Guide: Ghost blog on Fly.io using Tigris S3 Object Storage (part 3)]]></title><description><![CDATA[<p>This is part 3 in a series about self hosting a Ghost blog on Fly.io. It assumes you have a running Ghost app on Fly (either using SQLite or MySQL). </p><p>In the first two posts we covered:</p><ol><li>Setting up a Ghost blog using SQLite</li><li>Setting up a Ghost blog</li></ol>]]></description><link>https://blog.nathanwillson.com/ghost-blog-with-tigris/</link><guid isPermaLink="false">6693e907ff5944013b3cf5cf</guid><category><![CDATA[ghost]]></category><category><![CDATA[fly.io]]></category><category><![CDATA[tigris]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sun, 14 Jul 2024 23:46:28 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/2zzsxTlVnTOiGWb1UBKblvTl-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/2zzsxTlVnTOiGWb1UBKblvTl-2.png" alt="Guide: Ghost blog on Fly.io using Tigris S3 Object Storage (part 3)"><p>This is part 3 in a series about self hosting a Ghost blog on Fly.io. It assumes you have a running Ghost app on Fly (either using SQLite or MySQL). </p><p>In the first two posts we covered:</p><ol><li>Setting up a Ghost blog using SQLite</li><li>Setting up a Ghost blog using MySQL</li></ol><p>Now we&apos;ll move onto saving assets (in particular photos that you upload to your blog posts) in a S3 Object Storage bucket.</p><p>This is what we&apos;re aiming for:</p><figure class="kg-card kg-image-card"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/ghost-tigris.png" class="kg-image" alt="Guide: Ghost blog on Fly.io using Tigris S3 Object Storage (part 3)" loading="lazy" width="1402" height="1644"></figure><h2 id="tigris-is-awesome">Tigris is awesome</h2><p>Tigris Global Object Store is a s3 compliant storage system built on Fly&apos;s infrastructure. We&apos;ll be connecting it to our Ghost Blog. For more details:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://fly.io/docs/reference/tigris/?ref=blog.nathanwillson.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Tigris Global Object Storage</div><div class="kg-bookmark-description">Documentation and guides from the team at Fly.io.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://static.ghost.org/v5.0.0/images/link-icon.svg" alt="Guide: Ghost blog on Fly.io using Tigris S3 Object Storage (part 3)"><span class="kg-bookmark-author">Fly</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://fly.io/static/images/fly-social-square.webp" alt="Guide: Ghost blog on Fly.io using Tigris S3 Object Storage (part 3)"></div></a></figure><h2 id="step-1-create-a-dockerfile">Step 1: Create a Dockerfile</h2><p>If you&apos;ve been following the guides up until this point then in your <code>fly.toml</code> file you might have the line:</p><pre><code class="language-toml"># fly.toml

# erase this!
[build]
  image = &apos;ghost:5.81.0&apos;</code></pre><p>But in addition to our Ghost image, we also need an Ghost s3 storage library. So, remove the image line from the <code>fly.toml</code> and create a <code>Dockerfile</code> that looks like</p><pre><code class="language-docker">FROM ghost:5.81.0-alpine
WORKDIR /var/lib/ghost
RUN npm install --prefix /tmp/ghos3 ghos3 &amp;&amp; \
  cp -r /tmp/ghos3/node_modules/ghos3 current/core/server/adapters/storage/s3 &amp;&amp; \
  rm -r /tmp/ghos3

RUN npm install ghos3 &amp;&amp; npm install aws-sdk</code></pre><p>This Dockerfile was written by underlost here: <a href="https://github.com/underlost/flyio-ghost-s3?ref=blog.nathanwillson.com">https://github.com/underlost/flyio-ghost-s3</a></p><p>I&apos;ve modified it to use <a href="https://github.com/laosb/ghos3?ref=blog.nathanwillson.com">ghos3</a> which is a recent rewrite of <a href="https://github.com/colinmeinke/ghost-storage-adapter-s3?ref=blog.nathanwillson.com">ghost-storage-adapter-s3</a>.</p><h2 id="step-2-create-a-tigris-object-storage-bucket">Step 2: Create a Tigris Object Storage Bucket</h2><p>You can basically follow <a href="https://fly.io/docs/reference/tigris/?ref=blog.nathanwillson.com">the guide for Tigris</a>, but the short version is:</p><pre><code>fly storage create &lt;my_bucket_name&gt; --public</code></pre><p>You&apos;ll get some credentials that you&apos;ll want to keep for later:</p><pre><code>BUCKET_NAME: my_bucket_name
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_ACCESS_KEY_ID: tid_xxxxxx
AWS_SECRET_ACCESS_KEY: tsec_xxxxxx</code></pre><h2 id="step-3-modify-flytoml-and-set-secrets">Step 3: Modify fly.toml and set secrets</h2><p>Add the following to you <code>fly.toml</code> under <code>[env]</code></p><pre><code class="language-toml"># fly.toml
# don&apos;t forget to replace &quot;my_bucket_name&quot; with your own name

[env]
  ##### Tigris S3
  storage__active = &quot;s3&quot;
  # storage__s3__accessKeyId = &quot;&quot; # set as secret
  # storage__s3__secretAccessKey = &quot;&quot; # set as secret
  storage__s3__region = &quot;auto&quot;
  storage__s3__bucket = &quot;my_bucket_name&quot;
  storage__s3__assetHost = &quot;https://fly.storage.tigris.dev/my_bucket_name&quot;
  storage__s3__endpoint = &quot;https://fly.storage.tigris.dev&quot;</code></pre><ul><li>region is auto</li><li>the asset host (storage__s3__assetHost) needs the bucket name in the path</li><li>for uploading (storage__s3__endpoint) should omit the bucket name</li></ul><p>Next, set secrets for your app using the credentials from when you made the bucket:</p><pre><code>fly secrets set --stage storage__s3__accessKeyId=tid_xxxx storage__s3__secretAccessKey=tsec_xxx</code></pre><h2 id="step-4-deploy-and-check-that-it-works">Step 4: Deploy and check that it works:</h2><p>Run <code>fly deploy</code> to deploy your app. </p><p>Then create a draft blog post and try uploading an image. </p><p>You can see the image in your bucket by running the following:</p><pre><code>flyctl storage dashboard &lt;bucket_name&gt;</code></pre><p>This should open up Tigris&apos; dashboard and you can browse the files in your bucket. It&apos;s neato burrito. </p><hr><h2 id="final-example">Final Example:</h2><p></p><p>Putting it all together with MySQL and Tigris Object Storage your <code>fly.toml</code> might look like:</p><pre><code class="language-toml"># fly.toml app configuration file generated for nathan-ghost-blog on 2024-07-13T22:34:38+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = &apos;my-ghost-blog&apos;
primary_region = &apos;sea&apos;

[env]
  ##### MYSQL Database
  database__client = &apos;mysql&apos;
  # database__connection__user = &apos;&apos; # set as secret
  # database__connection__password = &apos;&apos; # set as secret
  database__connection__host = &apos;my-blog-db.internal&apos;
  database__connection__port = &apos;3306&apos;
  database__connection__database = &apos;blog_db&apos;
  database__connection__useSSL = &apos;false&apos;
  database__useSSL = &apos;false&apos;
  ##### Tigris S3
  storage__active = &quot;s3&quot;
  # storage__s3__accessKeyId = &quot;&quot; # set as secret
  # storage__s3__secretAccessKey = &quot;&quot; # set as secret
  storage__s3__region = &quot;auto&quot;
  storage__s3__bucket = &quot;my-blog-storage&quot;
  storage__s3__assetHost = &quot;https://fly.storage.tigris.dev/my-blog-storage&quot;
  storage__s3__endpoint = &quot;https://fly.storage.tigris.dev&quot;
  url = &apos;https://my-ghost-blog.fly.dev&apos;

[http_service]
  internal_port = 2368
  force_https = true
  auto_start_machines = true
  min_machines_running = 0
  processes = [&apos;app&apos;]

[[vm]]
  memory = &apos;1gb&apos;
  cpu_kind = &apos;shared&apos;
  cpus = 1
</code></pre>]]></content:encoded></item><item><title><![CDATA[Guide: Ghost blog on Fly.io using MySQL (part 2)]]></title><description><![CDATA[<p>This part 2 in a series about using Ghost on Fly.io. In part 1, we setup a Ghost blog with SQLite as the database. In this post we&apos;ll be setting up a MySQL database and configuring our Ghost app to use it.</p><p>By the end we&apos;</p>]]></description><link>https://blog.nathanwillson.com/ghost-blog-using-mysql/</link><guid isPermaLink="false">6693e30aff5944013b3cf57b</guid><category><![CDATA[ghost]]></category><category><![CDATA[fly.io]]></category><category><![CDATA[mysql]]></category><dc:creator><![CDATA[Nathan Willson]]></dc:creator><pubDate>Sat, 13 Jul 2024 23:40:00 GMT</pubDate><media:content url="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/2zzsxTlVnTOiGWb1UBKblvTl-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/2zzsxTlVnTOiGWb1UBKblvTl-1.png" alt="Guide: Ghost blog on Fly.io using MySQL (part 2)"><p>This part 2 in a series about using Ghost on Fly.io. In part 1, we setup a Ghost blog with SQLite as the database. In this post we&apos;ll be setting up a MySQL database and configuring our Ghost app to use it.</p><p>By the end we&apos;ll have a system that looks like:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://fly.storage.tigris.dev/nathan-blog-storage/2024/07/ghost_mysql.png" class="kg-image" alt="Guide: Ghost blog on Fly.io using MySQL (part 2)" loading="lazy" width="1349" height="1644"><figcaption><span style="white-space: pre-wrap;">Fly Machine running Ghost connected to a Machine running MySQL</span></figcaption></figure><h2 id="step-1-create-a-mysql-database">Step 1: Create a MySQL database</h2><p>The details on how to create a MySQL database are here: <a href="https://fly.io/docs/app-guides/mysql-on-fly/?ref=blog.nathanwillson.com">https://fly.io/docs/app-guides/mysql-on-fly/</a></p><p>There are some things you should keep in mind:</p><ul><li>The MySQL database will run on a single VM. If that VM crashes then you&apos;ll have to start it again.</li><li>This is NOT managed MySQL. This is a self-hosted MySQL image on a VM. If you need something resilient, then there are plenty of services out there.</li></ul><h3 id="before-configuring-ghost-verify-that-your-mysql-database-works">Before configuring Ghost, verify that your MySQL database works</h3><p>The way I do that is to ssh into the machine running the MySQL image, logging into the database and confirming it works:</p><pre><code># SSH into the app
fly ssh console -a &lt;mysql app name&gt;

# From inside the vm run:
mysql &lt;my_database_name&gt; -u root -p
Enter password:

# use the password you set for MYSQL_ROOT_PASSWORD

# run a command like &quot;SHOW DATABASES&quot; just to see that things work
mysql&gt; SHOW DATABASES;</code></pre><h2 id="step-2-configure-ghost-to-use-mysql">Step 2: Configure Ghost to use MySQL</h2><p>The next steps are to update you apps <code>fly.toml</code> and set some secrets.</p><h3 id="update-the-flytoml-with-some-ghost-config">Update the fly.toml with some Ghost config</h3><p>In the last post we configured the <code>fly.toml</code> to use a SQLite database. We&apos;ll modify the env database bit to look like the following:</p><pre><code class="language-toml"># fly.toml

app = &lt;ghost app name&gt;
primary_region = &apos;sea&apos;

[env]
  ##### MYSQL Database
  database__client = &apos;mysql&apos;
  # database__connection__user = &apos;&apos; # set as secret
  # database__connection__password = &apos;&apos; # set as secret
  database__connection__host = &apos;&lt;mysql app name&gt;.internal&apos;
  database__connection__port = &apos;3306&apos;
  database__connection__database = &apos;&lt;database name&gt;&apos;
  database__connection__useSSL = &apos;false&apos;
  database__useSSL = &apos;false&apos;

...</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">Note: <code spellcheck="false" style="white-space: pre-wrap;">database__connection__host = &apos;&lt;mysql app name&gt;.internal&apos;</code> is using Fly&apos;s internal private network. Each app in your organization is visible to each other over a private network (unless you set it otherwise). You can read more about it here: <a href="https://fly.io/docs/networking/private-networking/?ref=blog.nathanwillson.com">https://fly.io/docs/networking/private-networking/</a></div></div><h3 id="set-some-secrets">Set some secrets:</h3><p>Then we need to set a few secrets on our Ghost app. </p><pre><code>fly secrets set --stage database__connection__user=root database__connection__password=[[MYSQL_ROOT_PASSWORD]] -a &lt;ghost app name&gt;</code></pre><p>This will set secrets on your app for the next deploy. I&apos;m using the <strong>root</strong> user and the <strong>root password</strong> (that&apos;s what worked for me). </p><hr><h2 id="final-example">Final Example: </h2><p>So for example, let&apos;s say I created an app called <code>my-ghost-blog-db</code> and I created a database in it called <code>my_db</code> . Then my <code>fly.toml</code> might look like:</p><pre><code class="language-toml"># fly.toml

app = &apos;my-ghost-blog&apos;
primary_region = &apos;sea&apos;

[build]
  image = &apos;ghost:5.81.0&apos;

[env]
  ##### MYSQL Database
  database__client = &apos;mysql&apos;
  # database__connection__user = &apos;&apos; # set as secret
  # database__connection__password = &apos;&apos; # set as secret
  database__connection__host = &apos;my-ghost-blog-db.internal&apos;
  database__connection__port = &apos;3306&apos;
  database__connection__database = &apos;my_db&apos;
  database__connection__useSSL = &apos;false&apos;
  database__useSSL = &apos;false&apos;
  url = &apos;https://my-ghost-blog.fly.dev&apos;

[http_service]
  internal_port = 2368
  force_https = true
  auto_start_machines = true
  min_machines_running = 0
  processes = [&apos;app&apos;]

[[vm]]
  memory = &apos;1gb&apos;
  cpu_kind = &apos;shared&apos;
  cpus = 1
</code></pre><p>And I&apos;d run something like:</p><pre><code>fly secrets set --stage database__connection__user=root database__connection__password=mysecurepassword1234 -a my-ghost-blog</code></pre><h2 id="deploy-and-check-that-it-works">Deploy and check that it works</h2><p>Run <code>fly deploy</code> for your Ghost app and run <code>fly logs</code> in a separate app to check that it&apos;s connecting to the database properly. </p>]]></content:encoded></item></channel></rss>