<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title>The Blog of Tom Webster - Blog Post</title>
  <id>https://www.samurailink3.com</id>
  <updated></updated>
  <subtitle>Hey! I’m Tom! I’m a tech guy who has spent far too much time building far too many things. I enjoy tinkering with a lot of different technologies: Programming, Networking, 3D Printing, 3D Design, GameDev, and more. Most of what I make is open source, if you’re feeling curious, check out my projects below.</subtitle>
  <link href="https://www.samurailink3.com"></link>
  <author>
    <name>Tom Webster</name>
  </author>
  <entry>
    <title>Debian: Waterfox Flatpak and KDE Plasma Browser Integration</title>
    <updated>2025-12-24T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Debian-Waterfox-Flatpak-and-KDE-Plasma-Browser-Integration-2d0b6786424180948225c976c7247033</id>
    <content type="html">&lt;html&gt;&lt;p&gt;If you’re moving to &lt;a href=&#39;https://www.waterfox.com/&#39;&gt;Waterfox&lt;/a&gt; because of the &lt;a href=&#39;https://blog.mozilla.org/en/mozilla/leadership/mozillas-next-chapter-anthony-enzor-demeo-new-ceo/&#39;&gt;Mozilla CEO’s recent threat that “[Firefox] will evolve into a modern AI browser…”&lt;/a&gt; and you use &lt;a href=&#39;https://kde.org/plasma-desktop/&#39;&gt;KDE Plasma&lt;/a&gt;, you may be surprised to see that the native browser integration doesn’t work. Don’t fret though, the problem is actually because Waterfox is looking for files that haven’t been copied to the right place. Let’s fix that.&lt;/p&gt;&#xA;&lt;h1&gt;The Workaround&lt;/h1&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/c548771c-0948-43ec-900b-abee3fa9ca98/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=5ca60c3ad21fd5ce57c76132eae94f7f2807d6e7d5a4aec0f96508d0b7ac56ad&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/c548771c-0948-43ec-900b-abee3fa9ca98/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=5ca60c3ad21fd5ce57c76132eae94f7f2807d6e7d5a4aec0f96508d0b7ac56ad&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;The most convenient way to install Waterfox and keep it updated automatically is through &lt;a href=&#39;https://flathub.org/en/apps/net.waterfox.waterfox&#39;&gt;Flathub&lt;/a&gt;. The files we need will be created once &lt;i&gt;the Firefox Flatpak is installed&lt;/i&gt;. Go ahead and install the &lt;a href=&#39;https://flathub.org/en/apps/org.mozilla.firefox&#39;&gt;Firefox Flatpak&lt;/a&gt; as well. You’ll also need &lt;a href=&#39;https://flathub.org/en/apps/com.github.tchx84.Flatseal&#39;&gt;Flatseal&lt;/a&gt; to add a permission to Waterfox.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Open your application launcher and search for “Background Services”. In the search bar, type &#34;browser”. You should see two services:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/426af1fb-513e-4a13-a2bd-5d66fb79fd89/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=1050b7150bdb902d64062faabe27f728877dc1ab272c5d1d86effdddea9fdaef&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/426af1fb-513e-4a13-a2bd-5d66fb79fd89/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=1050b7150bdb902d64062faabe27f728877dc1ab272c5d1d86effdddea9fdaef&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;Pause and unpause the &lt;i&gt;Plasma Browser Integration Flatpak Integration&lt;/i&gt; service. This step may be unnecessary, but will speed things up if you’re between polling cycles (this is mostly an assumption).&lt;/p&gt;&#xA;&lt;p&gt;Now, let’s copy some files:&lt;/p&gt;&#xA;&lt;code&gt;# Copy the integration host to Waterfox&#39;s flatpak data directory&#xA;cp ~/.var/app/org.mozilla.firefox/plasma-browser-integration-host ~/.var/app/net.waterfox.waterfox/plasma-browser-integration-host&#xA;# Make a director for the next file&#xA;mkdir ~/.var/app/net.waterfox.waterfox/.waterfox/native-messaging-hosts&#xA;# Use sed to replace the Firefox flatpak app id with Waterfox&#39;s flatpak app id,&#xA;# then output that file to where Waterfox will look for native-messaging-hosts.&#xA;# Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging&#xA;sed &#39;s/org\.mozilla\.firefox/net\.waterfox\.waterfox/&#39; ~/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/org.kde.plasma.browser_integration.json &gt; ~/.var/app/net.waterfox.waterfox/.waterfox/native-messaging-hosts/org.kde.plasma.browser_integration.json&lt;/code&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, open Flatseal and select Waterfox to edit its settings. We need to go to &lt;i&gt;Session Bus → Talks &lt;/i&gt;and add a new item: &lt;code&gt;org.kde.plasma.browser.integration&lt;/code&gt; .&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/5bf8d5ae-71bf-4cc3-b428-4e4e02ee0ab2/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=9986907c8c34116de8e80934b4b9d043ca116922c8b7eb56f8a8755a5435cb66&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/5bf8d5ae-71bf-4cc3-b428-4e4e02ee0ab2/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=9986907c8c34116de8e80934b4b9d043ca116922c8b7eb56f8a8755a5435cb66&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, when you manage the &lt;i&gt;Plasma Integration&lt;/i&gt; add-on and go to &lt;i&gt;Preferences&lt;/i&gt;, you’ll see options! Furthermore, you can control your browser’s media with your media keys and system tray icons.&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/54cc939c-8f88-4c76-872a-af462bce09d5/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=bc156d903080afb05bafc36784a702ddbc9ef8c69ee6b7138f409e63ae19d4e9&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/54cc939c-8f88-4c76-872a-af462bce09d5/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=bc156d903080afb05bafc36784a702ddbc9ef8c69ee6b7138f409e63ae19d4e9&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;h1&gt;The Real Fix&lt;/h1&gt;&#xA;&lt;p&gt;This is a cool workaround, but the real issue lies in &lt;a href=&#39;https://invent.kde.org/plasma/plasma-browser-integration&#39;&gt;https://invent.kde.org/plasma/plasma-browser-integration&lt;/a&gt;. The application watches for many different browsers, but not Waterfox… for now… hopefully… I have a merge request out to KDE with the fix for Waterfox, both Flatpak and native versions:&lt;/p&gt;&#xA;&lt;a href=&#39;https://invent.kde.org/plasma/plasma-browser-integration/-/merge_requests/180&#39;&gt;https://invent.kde.org/plasma/plasma-browser-integration/-/merge_requests/180&lt;/a&gt;&#xA;&lt;p&gt;If this is accepted, THIS WAS MERGED - it will eventually trickle down to various Linux distributions, fixing this problem without needing the tinkering shown here.&lt;/p&gt;&#xA;&lt;h1&gt;&lt;strong&gt;Personal Rant&lt;/strong&gt;&lt;/h1&gt;&#xA;&lt;p&gt;This move by Anthony Enzor-DeMeo is extremely disappointing and disheartening. Honestly, this &lt;strong&gt;&lt;i&gt;dank meme&lt;/i&gt;&lt;/strong&gt; is the best way to sum up how I feel about the situation:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/5a0f61ca-e235-4913-a3d7-ddba849404cf/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=e7e541c9c9e31178eb9a32b1d0d9cf58e342fb92ecf211b5e4a6ec11fe3dfc65&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/5a0f61ca-e235-4913-a3d7-ddba849404cf/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46635STGWWD%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110130Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIAZAoGndhF9Llbn7r1hjFf2%2F0bGMNF2K8DhzT0zoHBlkAiEA6RxH549YgrZsQPpSDHGh9%2BXweR00p9gZW%2BZFYjY5dYMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDIs3ODRjvDCFkFW3kSrcA3VZC7%2B4sZZUlcUj7q%2BI08iJJlkkvbwQQ1WIIO2w3zyarUFiHuvWANcS3pUNh0jC0jRDyeCE7fz%2FEpdUWUupkv%2B2pKifjeh%2BjEt6HXuDx0vjHwFhVYzWYazbUkuwX8RVHwlZIMrhlHj4npvCVFPYXacZYeCFXYbmz0u6SM0URzBXiPAX0xHmgm4LXUm%2BTK0I6GBIipRXtsp5mtIncIvI%2FRqoZojGC9rsiRdSPrf2xV847vyk8FZm5n8wlJQbPD3p80tR9Bcf4OTprqDTeRDLt0Fij%2BAyluh3nxqHMxL9tbjXAjLxkOr17igpSeDG8H75l0fYRC4fBAyQ3Cpkcg%2FmbGW9W5Tq8IkRW4cGK58%2Bmm42Q3lYcSlLpmC37Crg2Wv02chPwWw5zHnpSI%2FgQNCa3MiVu22DuT%2F2Kqvf%2FJ7F3gmiLtQAnX8yJC9J0QC4aOI4iEYIG%2BeilRGhhiwBF6SQPN8Znwi1dZgrx7mzJ6m556v7TTAPlKIRHVdq7uD7Ucmv71T9BKzk6URuhkzmyYciARFfOJ30k9sBb4QOQ%2BEcJY0bYlLtwBJHeW7aIz%2BZz4kY9YjQAEMsfSaqWF9zwlglDeCFO1cCuSwQvbg8J0%2Fc1caLLMWIDusaViLSK72OMMb%2F%2B88GOqUBPtAD5YUeTKjg0SnlRLGCMDJNYSOs%2FEa8IBljIqqvcdIaEmTyyAwzXHpSzZFBwWBI7ukeXSVo2wMGWQHbkf4er3Nmeg%2BycDz41fpqHF8I0kFazlR%2FqXJ9Sro5Hp1%2Ft07VL7YpYMyH%2BxPITy%2FJuIg6dOs95yy1qMnbasoZy%2FuhLuEv9tQdIZT8PQhHgOvRzvHJsJ8lwNymNeLqcRB%2F7kCBe22QZyFI&amp;X-Amz-Signature=e7e541c9c9e31178eb9a32b1d0d9cf58e342fb92ecf211b5e4a6ec11fe3dfc65&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;In return Alex Kontos of Waterfox replied with this:&lt;/p&gt;&#xA;&lt;a href=&#39;https://www.waterfox.com/blog/no-ai-here-response-to-mozilla/&#39;&gt;https://www.waterfox.com/blog/no-ai-here-response-to-mozilla/&lt;/a&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;So I installed Waterfox, imported my data, and tried it out. &lt;i&gt;Its just Firefox with all the bullshit removed.&lt;/i&gt; Exactly how it was advertised. Exactly what I wanted. No Pocket, no sponsored stories on the new-tab page, no VPN ads in my menus, no telemetry. Sensible defaults that just work. It even has Tree-Style Tabs right out of the box. Its been a few days, but so far, I’m happy with this being my daily driver.&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3marpca6yck2v&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3marpca6yck2v&lt;/a&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Debian-Waterfox-Flatpak-and-KDE-Plasma-Browser-Integration-2d0b6786424180948225c976c7247033" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>How to ruin your domain’s email reputation with ONE EASY TRICK!</title>
    <updated>2025-11-24T00:00:00Z</updated>
    <id>https://www.samurailink3.com/How-to-ruin-your-domain-s-email-reputation-with-ONE-EASY-TRICK-2b5b6786424180758e54e147d31ef139</id>
    <content type="html">&lt;html&gt;&lt;p&gt;A shop I regularly go to restructured their loyalty program and made you opt-in to marketing emails. Sure, fine, whatever, juice the numbers. The person at the counter told me I could instantly unsubscribe, so no real harm.&lt;/p&gt;&#xA;&lt;p&gt;Later, when I tried to unsubscribe, the site gave me this message:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;I shop here regularly. I gain and use my points quite often. Its a nice bonus for being a consistent customer. This is extremely fucking scammy. Its also a &lt;i&gt;fucking great way&lt;/i&gt; to destroy your domain’s email reputation.&lt;/p&gt;&#xA;&lt;p&gt;Actually, that’s a great idea. I made an email rule on Fastmail that automatically sends emails from this address directly to spam. I keep my points, I don’t get spammed, and in the process, I hurt their domain’s email reputation.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/1ca73ffd-eb50-4bad-8f28-09b865b9c69b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46653DFLY4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110131Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIGyEvxWBErkEzromxvQZ%2FznyIhAFCiLl548eQjSLad9wAiAkaveFsXA73IyClPLdsRATT240bNYxA4EOdLTyU1VYACqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMy75wZKyQLe3pVxieKtwDarbFceksADxM%2FhdQfeAuErP7YZZ92Xzm60LY8K%2BCRSlX4oGKp0csy71lMHoQUwNohimpIh2LbbpcAYoqSxS0LfvAvAgV%2FtbkyI4ml1bV1n6G1YFjY6QigoksqiwIZVfgDi6uDDQyGn9PDYSs8Lfc14gbPz6pbEIApXNG0yvU%2FFJOkZ8PQ3vMDGfoTjS4OkYP4WoleY2YwBJGlPda0WjEsi8fFRhlYZIk22MH04ER%2FsmSgLQvh91dVlGbjbjovbCIIO0hJ3gxEOqeEcyHnUJTk%2B2UP8JeLZSXlx%2FZIuFiYGYvDUigGkkHejPGf26w6Re9%2FUTa8UVd%2BwdlUtQENho2cz4rUoh9ZhL9UgrXnJ%2FEJyxhzrwVq35qAc5r5dmaYO9TWFixvq80qRqyecrQ42TWMNuf0dG22zLPbRj8xkjjcIbKdyyaLvHYCXkfrEQXxJNRD0QQvHojFhJejcVsr8YNRsl1McvwPvzPkUXHtq1QyogGuG6KMH6suRsP7StzCdZKMD3UCk%2BXPk0gr1fCSXeL8IRIioWVupBbSkk5aq8mxFs%2FANaR1%2FF3HW2osXFadHhaP3VLAwkQUSHflA5vOQQcS1FgwuQSzmeu%2B2gDCfC0x%2FyI8YXyo7dBfrEe3PMwzYT8zwY6pgFj5tFKlUbWKnEpbN%2Fmvi%2BD6N5cDlVQqq6iPiUoHZp0hWJV3toRW5ej6Qdo2U1kgDndj3y8xgabCSWtYoeM3sOzhfUp3xsvJomO10OIWtwxcAuGZzAFGf47BrDRSkMlUwA4yRbwK83yUeXsc8h1dBpudMt1QtdL2IMFTvml9Vbw6cAZNEUdxMwhrHjTjb2DnzhpQAOOUBzmzVp8vKbA%2BvUfjRSM7qPD&amp;X-Amz-Signature=168cc77d13ed4d31aec2e0df16dbd9d3b5a6e628864c4ced9a4d6b1776747a4d&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/1ca73ffd-eb50-4bad-8f28-09b865b9c69b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46653DFLY4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110131Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIGyEvxWBErkEzromxvQZ%2FznyIhAFCiLl548eQjSLad9wAiAkaveFsXA73IyClPLdsRATT240bNYxA4EOdLTyU1VYACqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMy75wZKyQLe3pVxieKtwDarbFceksADxM%2FhdQfeAuErP7YZZ92Xzm60LY8K%2BCRSlX4oGKp0csy71lMHoQUwNohimpIh2LbbpcAYoqSxS0LfvAvAgV%2FtbkyI4ml1bV1n6G1YFjY6QigoksqiwIZVfgDi6uDDQyGn9PDYSs8Lfc14gbPz6pbEIApXNG0yvU%2FFJOkZ8PQ3vMDGfoTjS4OkYP4WoleY2YwBJGlPda0WjEsi8fFRhlYZIk22MH04ER%2FsmSgLQvh91dVlGbjbjovbCIIO0hJ3gxEOqeEcyHnUJTk%2B2UP8JeLZSXlx%2FZIuFiYGYvDUigGkkHejPGf26w6Re9%2FUTa8UVd%2BwdlUtQENho2cz4rUoh9ZhL9UgrXnJ%2FEJyxhzrwVq35qAc5r5dmaYO9TWFixvq80qRqyecrQ42TWMNuf0dG22zLPbRj8xkjjcIbKdyyaLvHYCXkfrEQXxJNRD0QQvHojFhJejcVsr8YNRsl1McvwPvzPkUXHtq1QyogGuG6KMH6suRsP7StzCdZKMD3UCk%2BXPk0gr1fCSXeL8IRIioWVupBbSkk5aq8mxFs%2FANaR1%2FF3HW2osXFadHhaP3VLAwkQUSHflA5vOQQcS1FgwuQSzmeu%2B2gDCfC0x%2FyI8YXyo7dBfrEe3PMwzYT8zwY6pgFj5tFKlUbWKnEpbN%2Fmvi%2BD6N5cDlVQqq6iPiUoHZp0hWJV3toRW5ej6Qdo2U1kgDndj3y8xgabCSWtYoeM3sOzhfUp3xsvJomO10OIWtwxcAuGZzAFGf47BrDRSkMlUwA4yRbwK83yUeXsc8h1dBpudMt1QtdL2IMFTvml9Vbw6cAZNEUdxMwhrHjTjb2DnzhpQAOOUBzmzVp8vKbA%2BvUfjRSM7qPD&amp;X-Amz-Signature=168cc77d13ed4d31aec2e0df16dbd9d3b5a6e628864c4ced9a4d6b1776747a4d&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;I also sent off an email to their support channel. I don’t expect it will do anything to change their minds, but it won’t impact me.&lt;/p&gt;&#xA;&lt;p&gt;A lot of tech gives you control over your experience. Don’t let idiotic product decisions hurt you without a fight. Dig into your options menu. Use email rules. Block viciously.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3m6fuo3vkjc2k&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3m6fuo3vkjc2k&lt;/a&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/How-to-ruin-your-domain-s-email-reputation-with-ONE-EASY-TRICK-2b5b6786424180758e54e147d31ef139" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Halloween 2025 Data Analysis</title>
    <updated>2025-11-01T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Halloween-2025-Data-Analysis-29eb67864241806d8f1afc53f9894af7</id>
    <content type="html">&lt;html&gt;&lt;h1&gt;“THEY GOT BIG CANDY!!”&lt;/h1&gt;&#xA;&lt;p&gt;It’s another year and another chance to keep the legend alive! For the &lt;i&gt;fourth&lt;/i&gt; year in a row, we’ve been the “Full-Sized Candy Bar House” in our neighborhood. Just like &lt;a href=&#39;https://www.samurailink3.com/131b6786424180bc906ee3a5984d251c&#39;&gt;last year&lt;/a&gt;, we’re tracking candy selections using a pair of Notion databases. While this has continued to work well, the actual experience of doing the tracking in real-time is a bit too slow when group sizes reach above 3 people. I have some ideas on how to speed this up with buttons and automation in Notion, but that’s a problem for next year.&lt;/p&gt;&#xA;&lt;p&gt;As always, if you’d like to dig into the data yourself or follow along with this post in a side-browser, have at it:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt; &lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt; &lt;/p&gt;&#xA;&lt;h1&gt;New Experiments&lt;/h1&gt;&#xA;&lt;p&gt;This year, we cut back on the sheer variety we had. Last year we had &lt;strong&gt;40&lt;/strong&gt; different items. This year, we cut that down to &lt;strong&gt;25&lt;/strong&gt; varieties of candy and one&lt;i&gt; bonus toy&lt;/i&gt;. This seems to have cut down on option-paralysis and made our inventory &lt;i&gt;much&lt;/i&gt; easier to manage. Keeping the selection curtailed with a few specific experiments seems to be the best way going forward.&lt;/p&gt;&#xA;&lt;h2&gt;Toys&lt;/h2&gt;&#xA;&lt;p&gt;From an unrelated project, I had a lot of 3D printed articulated sheep laying around.&lt;/p&gt;&#xA;&lt;p&gt;The model is &lt;a href=&#39;https://www.printables.com/model/542561-articulated-sheep&#39;&gt;“Articulated Sheep” by Teigan Blackshaw&lt;/a&gt;. It’s a wonderful model, easy to print, and super cute.&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/7a88c0a4-d653-495b-b2df-08d1fa703040/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466WOPVUQPC%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110132Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBsaCXVzLXdlc3QtMiJHMEUCIG%2BWouNbXuR58XUZS3bh4rMBWjlFeiGZIW6iACxkMbMEAiEA1sXDeO4HA%2F7cLhMwSiGtGnr6ue%2FkPNTmx%2BQdqppNTJQqiAQI5P%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDJWB4gFcQ8HEwK0f4ircA2GPdeqhBc0Xc9QcIQhClczJVntZDs5ylGQE2Jdqv%2FJ2qSGOZRoMjB%2B%2FTWws9%2FDYrFeFJFKCEq3N92fh%2BH2V0VFXMdtOqJ5C%2BUhCKVt%2FZ0Lzk2RxbB5pFPqwzGc%2BO23cpf2Rev%2Bk8nWyQGmWOEDtKTNS7fP1wVesUbHt1kS%2F2t5fsA3ZsYQmglfzvya14jKQ9wSO%2FIn2nF26Gumup4m2kl7zxtO9X%2FL9%2Fnrmo0OEbNECBGhoVphlIjzOMfBKPpijlhEKIpl%2FK%2FT4f1xPwTFnIDN67bc7Bf%2FP3l8ybCtd0UqBkqmXrNtTkbrmPefckVvgpkeJT%2BqjGwLAmNpkxWH1QRR0D1UsBJGDu%2F3Lfc476PFTPbU7Ugjkk0F2qM%2F7R%2FyvrId2Ys936g6rXShkgeMYJ4agAj7l64Y7x8jBMg9c0MFcTR0fT3gMFT38GA2siuNr9eQ93pt0Fqj%2BiTUyTcbgM4xUv0kupp9vP032%2FaBLdgT6WLa972fJ0Isxnr1KRJtjJr08BEcmTKFLGMZsn8HugsI7CqW7JQVVuKFt%2F8EWEoRf4rhZNnc2HSSC7ERP3rIGHuXo3nofssnwXLpb%2Fv5Dfp97HRB7CNhj%2BN%2B9W518jzjn6TcjFt80i5mfalyDMO6g%2FM8GOqUBiSzN8tgUSc4kgbNBV8JaZTNPaU8RyO6VtgMf0t6MBttWWxe%2Fe8KIc6R306tkRuLSEETzxIwHfKgs6ofc7krnduM7giTpNibPTlMgdGSnzJOYTZC2%2FDMC5Hv4s75P%2BR%2Fho2XuyL%2BV441F6Z9z2KSNiaI7CfdOS%2Fz0IsO%2Fn9btFG1yWEgB1yuo%2FOhWLYqAZyw%2BQd%2F3cVT%2BUt4iUqNU%2BO%2FLszeIhXOq&amp;X-Amz-Signature=7da0bcdb963d5309c36bec25af079059439a92be9234a1bf647713c7102a888e&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/7a88c0a4-d653-495b-b2df-08d1fa703040/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466WOPVUQPC%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110132Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBsaCXVzLXdlc3QtMiJHMEUCIG%2BWouNbXuR58XUZS3bh4rMBWjlFeiGZIW6iACxkMbMEAiEA1sXDeO4HA%2F7cLhMwSiGtGnr6ue%2FkPNTmx%2BQdqppNTJQqiAQI5P%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDJWB4gFcQ8HEwK0f4ircA2GPdeqhBc0Xc9QcIQhClczJVntZDs5ylGQE2Jdqv%2FJ2qSGOZRoMjB%2B%2FTWws9%2FDYrFeFJFKCEq3N92fh%2BH2V0VFXMdtOqJ5C%2BUhCKVt%2FZ0Lzk2RxbB5pFPqwzGc%2BO23cpf2Rev%2Bk8nWyQGmWOEDtKTNS7fP1wVesUbHt1kS%2F2t5fsA3ZsYQmglfzvya14jKQ9wSO%2FIn2nF26Gumup4m2kl7zxtO9X%2FL9%2Fnrmo0OEbNECBGhoVphlIjzOMfBKPpijlhEKIpl%2FK%2FT4f1xPwTFnIDN67bc7Bf%2FP3l8ybCtd0UqBkqmXrNtTkbrmPefckVvgpkeJT%2BqjGwLAmNpkxWH1QRR0D1UsBJGDu%2F3Lfc476PFTPbU7Ugjkk0F2qM%2F7R%2FyvrId2Ys936g6rXShkgeMYJ4agAj7l64Y7x8jBMg9c0MFcTR0fT3gMFT38GA2siuNr9eQ93pt0Fqj%2BiTUyTcbgM4xUv0kupp9vP032%2FaBLdgT6WLa972fJ0Isxnr1KRJtjJr08BEcmTKFLGMZsn8HugsI7CqW7JQVVuKFt%2F8EWEoRf4rhZNnc2HSSC7ERP3rIGHuXo3nofssnwXLpb%2Fv5Dfp97HRB7CNhj%2BN%2B9W518jzjn6TcjFt80i5mfalyDMO6g%2FM8GOqUBiSzN8tgUSc4kgbNBV8JaZTNPaU8RyO6VtgMf0t6MBttWWxe%2Fe8KIc6R306tkRuLSEETzxIwHfKgs6ofc7krnduM7giTpNibPTlMgdGSnzJOYTZC2%2FDMC5Hv4s75P%2BR%2Fho2XuyL%2BV441F6Z9z2KSNiaI7CfdOS%2Fz0IsO%2Fn9btFG1yWEgB1yuo%2FOhWLYqAZyw%2BQd%2F3cVT%2BUt4iUqNU%2BO%2FLszeIhXOq&amp;X-Amz-Signature=7da0bcdb963d5309c36bec25af079059439a92be9234a1bf647713c7102a888e&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;So I figured I could pawn them off on the neighborhood trick-or-treaters 🙂. &lt;strong&gt;Before anyone thinks I cheated someone out of candy - The sheep was a “Bonus Item” you could take if you wanted to.&lt;/strong&gt; Most trick-or-treaters opted to take both a piece of candy and a sheep. One trick-or-treater opted out of the sheep, taking only candy. No trick-or-treaters took only a sheep.&lt;/p&gt;&#xA;&lt;p&gt;I started with 15 sheep and the inventory was exhausted in &lt;i&gt;40 minutes&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;3D printing articulated toys and models isn’t really hard, and it isn’t really expensive. I’m still pondering, but I think I may try to put out a few different toy varieties in various colors next year. There are no shortage of great models to choose from.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Lesson Learned:&lt;/strong&gt; We should have different databases because manually excluding the sheep in calculations annoys me. I should optimize for laziness wherever possible.&lt;/p&gt;&#xA;&lt;h2&gt;Twizzlers&lt;/h2&gt;&#xA;&lt;p&gt;We didn’t think these would be popular, but wanted to confirm our suspicions. You’ll see in the data later, we were sadly correct. Well… maybe not “sadly”, because we get to dispose of them ourselves.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Lesson Learned:&lt;/strong&gt; We like Twizzlers, nobody else does.&lt;/p&gt;&#xA;&lt;h2&gt;Nerds Rope&lt;/h2&gt;&#xA;&lt;p&gt;I think Nerds Rope and Nerds Gummy Clusters are some of my favorite “newer” candies. While they weren’t “unpopular”, they never hit the highs I was expecting from that candy. That said, we have a theory as to why they didn’t take off at the start of the evening and picked up towards the end:&lt;/p&gt;&#xA;&lt;p&gt;We staged the Nerds Ropes in the candy bowl in a way that they wouldn’t fall all over the place. Only about half of the candy was visible above the bowl. Compared to the candies around it (and especially compared to the &lt;strong&gt;towering&lt;/strong&gt; Airheads Xtremes in the next bowl over), they just weren’t “big enough”, they didn’t stick out among the crowd. Halfway through the night, we re-arranged the Nerd Ropes and stuck them up a bit higher. At that point, we saw the popularity pick up considerably, elevating Nerd Rope from “an unpopular disaster” to “solid pick”.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Lesson Learned: &lt;/strong&gt;Staging is likely more important than you think.&lt;/p&gt;&#xA;&lt;h1&gt;Notable Data Points&lt;/h1&gt;&#xA;&lt;p&gt;Here’s the juicy deets you’re here for:&lt;/p&gt;&#xA;&lt;h2&gt;Headline Stats&lt;/h2&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Total Candy Selections:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; 138 (which &lt;/li&gt;&lt;li&gt;&lt;i&gt;just &lt;/i&gt;&lt;/li&gt;&lt;li&gt;beats last year’s 137)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Trick-or-Treater-ers per Hour:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; ~42 (a bit slower than last year’s ~46)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Most Popular Candy Type: &lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Fruit (which almost doubled the amount of chocolate-candy selections)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Most Popular Item:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Airheads Xtremes - Rainbow Berry (for the &lt;/li&gt;&lt;li&gt;&lt;i&gt;second&lt;/i&gt;&lt;/li&gt;&lt;li&gt; year in a row)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;h2&gt;Popular (and unpopular) Picks&lt;/h2&gt;&#xA;&lt;p&gt;Again, &lt;i&gt;Airheads Xtremes&lt;/i&gt; led the pack, with the &lt;i&gt;Airheads&lt;/i&gt; brand taking &lt;strong&gt;24.6%&lt;/strong&gt; of all selections this year. The next most-popular pick was the ever-classic &lt;i&gt;Crunch Bar&lt;/i&gt; with &lt;strong&gt;14.4%&lt;/strong&gt;. Reese’s were slightly more popular this year, with 6 selections over last year’s 4. I was worried the classic peanut butter + chocolate combo was growing tired. This gives me a little hope.&lt;/p&gt;&#xA;&#xA;&lt;p&gt;Here’s what we ran out of:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;And here is a list of treats that got picked &lt;strong&gt;just once&lt;/strong&gt;. I hope you found your favorite thing:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;And just one treat didn’t get picked at all: Classic Lays. Oh well, more for me.&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Chips were &lt;i&gt;wildly&lt;/i&gt; more popular this year, making up &lt;strong&gt;7.9%&lt;/strong&gt; (11 total) of all selections. This eclipses the 3 chip selections made last year. People seemed extremely excited by the chip selection if they didn’t go right for the candy. If someone wanted a break from the sweet treats, they appreciated the differing option. Oddly, &lt;i&gt;Sun Chips&lt;/i&gt; were quite popular with very vocal fans, tying &lt;i&gt;Doritos - Nacho Cheese&lt;/i&gt; for first place in the chip selection category.&lt;/p&gt;&#xA;&lt;p&gt;We’re absolutely keeping chips for next year.&lt;/p&gt;&#xA;&#xA;&lt;p&gt;On brand selections, through the sheer popularity of Airheads Xtremes, the Airheads brand leads with &lt;strong&gt;24.6%&lt;/strong&gt; of all selections. Even though Skittles doesn’t crack the individual candy selection leaderboard until position 7, the sheer variety of Skittles means the brand overall is quite competitive, claiming the bronze with &lt;strong&gt;13.7%&lt;/strong&gt; of all selections. M&amp;Ms remain somewhat unpopular, but nearly match last year selections with 6 selections in 2025 and 7 selections in 2024.&lt;/p&gt;&#xA;&lt;p&gt;Sour Patch Kids experienced a &lt;i&gt;massive&lt;/i&gt; drop in popularity. In 2024, the brand claimed silver with 19 total selections. In 2025, this has dropped to just 6 selections. In looking at the data, I can only conclude that the lack of &lt;i&gt;Sour Patch Kids - Watermelon&lt;/i&gt; this year has had a truly devastating effect on the numbers.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Lesson Learned:&lt;/strong&gt; The Sour Patch Kids are &lt;i&gt;nothing&lt;/i&gt; without Watermelon.&lt;/p&gt;&#xA;&#xA;&lt;p&gt;Fruit candy dominated this year again with trends &lt;i&gt;mostly&lt;/i&gt; following 2024 (the exception being the large bump in chip popularity).&lt;/p&gt;&#xA;&#xA;&lt;h1&gt;What’s Next?&lt;/h1&gt;&#xA;&lt;p&gt;I’d still like to automate this data collection somewhat, but I’ll probably just start with better ergonomics. Buttons, dashboards, those sorts of things. Make it faster to insert data (and correct data when a trick-or-treater changes their mind). 3D printed toys will be interesting to experiment with. I want to see what we can do with this data going forward. Is there a candy company that would love to trial new packaging or flavors on an ultra-small scale? That’d be cool.&lt;/p&gt;&#xA;&lt;p&gt;Anyway, thanks for getting to the end, and I’ll be here again next year to dive into the super-niche world of Halloween Data Collection!&lt;/p&gt;&#xA;&lt;p&gt;👋&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3m4ltdxpqrc2f&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3m4ltdxpqrc2f&lt;/a&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Halloween-2025-Data-Analysis-29eb67864241806d8f1afc53f9894af7" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Windows 11 “upgrade” pushed me to reinstall Linux-on-the-Desktop and I’ve never loved my computer more</title>
    <updated>2025-10-27T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Windows-11-upgrade-pushed-me-to-reinstall-Linux-on-the-Desktop-and-I-ve-never-loved-my-computer-mo-29ab6786424180f19c01d2ac737dedcf</id>
    <content type="html">&lt;html&gt;&lt;h1&gt;OMG TLDR:&lt;/h1&gt;&#xA;&lt;ul&gt;&lt;li&gt;If you’re interested in escaping Windows 11 purgatory: Try Linux&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Your games likely “just work” thanks to Valve’s incredible work: &lt;/li&gt;&lt;li&gt;&lt;a href=&#39;https://store.steampowered.com/linux&#39;&gt;https://store.steampowered.com/linux&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;KDE fucking rules&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;My experience isn’t perfect, particularly with screen-sharing/game-recording. I am still troubleshooting issues there.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;h1&gt;The Backstory&lt;/h1&gt;&#xA;&lt;p&gt;Like a lot of people, I stubbornly stayed on Windows 10 until &lt;a href=&#39;https://support.microsoft.com/en-us/windows/windows-10-support-has-ended-on-october-14-2025-2ca8b313-1946-43d3-b55c-2b95b107f281&#39;&gt;support for it ended on October 14th, 2025&lt;/a&gt;. OS upgrades on Windows can be problematic, thus fresh re-installs are recommended. I’ve been using Windows operating systems since 3.1, this is a well-known fact to me. Regardless, I figured I’d try the direct upgrade anyway. Spoiler alert: Major instability issues. BSODs, driver incompatibility, massive performance degradation for GPU-intensive games (~25% FPS drop, eventually leading to full game crashes and system BSODs). Eugh, but fine, a complete reformat and reinstall from scratch was always part of my expectations. So I took a local backup, formatted my boot drive, and installed Windows 11 from a USB stick.&lt;/p&gt;&#xA;&lt;p&gt;I reinstalled drivers, ran updates, reinstalled a few critical software packages (like &lt;a href=&#39;https://www.firefox.com/&#39;&gt;Firefox&lt;/a&gt;), and thought my troubles were over. The only hurdles I expected were basic annoyances like turning off anti-features like OneDrive and CoPilot, and getting used to needing to hold &lt;code&gt;shift&lt;/code&gt; when I right-click in Explorer. The first day went fine enough. Played some games, even recorded some gameplay. Then I rebooted and everything went to hell.&lt;/p&gt;&#xA;&lt;p&gt;All graphically-intensive games started to run &lt;i&gt;extremely&lt;/i&gt; poorly. Gameplay would last for roughly 10 minutes before resulting in a crash-to-desktop. At one point, even Discord restarted itself. Every few minutes, my computer would lock up, then play my actions in fast-forward to catch up. I eventually diagnosed this issue to Windows Update “helpfully” installing a 15-month-old NVIDIA driver. Annoying, but solvable:&lt;/p&gt;&#xA;&lt;p&gt;I used &lt;a href=&#39;https://github.com/Wagnard/display-drivers-uninstaller&#39;&gt;DDU&lt;/a&gt; to completely shred the old driver in safe mode, then rebooted without any network to install the latest driver package available from NVIDIA’s website. This worked… for a day. Then symptoms started to crop up again, not as severe as before, but still plenty impactful.&lt;/p&gt;&#xA;&lt;p&gt;The main issue was the massive GPU performance dip. I have an RTX 4080, not the newest card, but really nice, plenty of headroom to play most games at their highest graphical fidelity. I play &lt;strong&gt;a lot&lt;/strong&gt; of &lt;i&gt;Hunt: Showdown&lt;/i&gt;, and I try to record gameplay so I can make clips for my friends and &lt;a href=&#39;https://www.samurailink3.com/118b678642418094b247ffef90b902aa?v=129b6786424180a9b978000cb2f20866&#39;&gt;the internet&lt;/a&gt;. The Windows 11 performance hit made recording gameplay impossible. It would freeze OBS, then Hunt, then my computer. Even lowering settings didn’t help.&lt;/p&gt;&#xA;&lt;p&gt;To troubleshoot, I did everything I could think of, including (but not limited to):&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Installing several older NVIDIA drivers (complete with DDU-backed safe-mode removal)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Deleting shader caches for impacted games&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Lowering in-game graphics settings&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Lowering OBS recording resolution and quality settings&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Checking temps with &lt;/li&gt;&lt;li&gt;&lt;a href=&#39;https://openhardwaremonitor.org/&#39;&gt;OpenHardwareMonitor&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;And so much more&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;I spent &lt;i&gt;days&lt;/i&gt; trying to make Windows 11 as performant as my Windows 10 install, but I just couldn’t make it work. I’m sure there’s some magic incantation somewhere (or something dumb I’ve forgotten to do), but I just couldn’t make it work. My options boiled down to:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Deal with it, this isn’t my computer anymore, its Microsoft’s.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Move back to Windows 10 and pay Microsoft their “protection money” through Windows 10 Extended Security Updates (ESU) program.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Move to a non-Windows platform.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;As long as the “supported/recommended” OS for my system was unusable, how bad could Linux-on-the-Desktop be? Its not like I’m any worse off if things don’t work. I dug a spare M.2 drive out of my parts bin, installed it, and downloaded &lt;a href=&#39;https://www.debian.org/&#39;&gt;Debian&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h1&gt;My History with Linux-on-the-Desktop&lt;/h1&gt;&#xA;&lt;p&gt;I’ve been an on-again-off-again Linux-on-the-Desktop user since 2006, first experimenting with &lt;a href=&#39;https://knopper.net/knoppix/index-en.html&#39;&gt;Knoppix live CDs&lt;/a&gt;, then moving onto a dual-boot &lt;a href=&#39;https://en.wikipedia.org/wiki/Ubuntu_version_history#0606&#39;&gt;Ubuntu 6.06&lt;/a&gt; install. During the Windows Vista era, Ubuntu was my daily-driver. During the Windows 8 era, I split my time between Arch Linux and Debian. I don’t want to waste a bunch of time detailing &lt;i&gt;every&lt;/i&gt; Linux experience I’ve had, just know that I’ve been here before and I’m not new to the world of Linux.&lt;/p&gt;&#xA;&lt;p&gt;The last time I left Linux was for two reasons:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Windows 10 was good.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;I was tired of streaming my games from a Windows PC in the other room.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;h1&gt;The First Hurdle: Bitlocker&lt;/h1&gt;&#xA;&lt;p&gt;My Windows install uses &lt;a href=&#39;https://support.microsoft.com/en-us/windows/bitlocker-drive-encryption-76b92ac9-1040-48d6-9f5f-d14b3c5fa178&#39;&gt;Bitlocker&lt;/a&gt;, Microsoft’s drive-encryption technology. This encryption tech is &lt;i&gt;tempermental&lt;/i&gt;. If anything changes with the system, it stops booting and requires you to put in a very long drive-recovery-code (essentially a boot password). Apparently this happened because I was chain-loading Grub→Windows and Bitlocker didn’t like that. After suspending and un-suspending bitlocker protection and never chain-loading Grub into the Windows boot loader, this problem went away.&lt;/p&gt;&#xA;&lt;p&gt;I actually thought this would be a bigger problem, but I’m just &lt;i&gt;not&lt;/i&gt; booting into Windows for much of anything anymore.&lt;/p&gt;&#xA;&lt;h1&gt;What Works&lt;/h1&gt;&#xA;&lt;h2&gt;The Basics&lt;/h2&gt;&#xA;&lt;p&gt;The days of Linux installs not recognizing hardware seems to be over (at least for me). Everything on my system just worked out of the box. Adding NVIDIA drivers wasn’t as easy as Ubuntu or Linux Mint makes it, but a brief glance at the &lt;a href=&#39;https://wiki.debian.org/NvidiaGraphicsDrivers&#39;&gt;Debian Wiki&lt;/a&gt; let me get some updated knowledge and walked me through the install process.&lt;/p&gt;&#xA;&lt;p&gt;Keep in mind, &lt;i&gt;this isn’t a complaint, this is praise&lt;/i&gt;. I chose Debian because I wanted more control than Ubuntu and less fiddling than Arch. This is the perfect amount of fiddling for me.&lt;/p&gt;&#xA;&lt;h2&gt;Gaming&lt;/h2&gt;&#xA;&lt;p&gt;Thanks to &lt;a href=&#39;https://store.steampowered.com/about/&#39;&gt;Steam&lt;/a&gt; and &lt;a href=&#39;https://github.com/ValveSoftware/Proton&#39;&gt;Proton&lt;/a&gt;, most games just work (honesty, every game I’ve tried worked perfectly, I just find it hard to claim &lt;i&gt;all games work&lt;/i&gt;). Even non-Steam games can be added to your library and use Proton (I run the GOG version of Cyberpunk 2077 this way). Very little fiddling needed. As a bonus, most games come with a 10-20 FPS increase running under Proton.&lt;/p&gt;&#xA;&lt;p&gt;Back in the day, if you wanted to see if a game worked under Wine, it was a lot of searching the internet in deep Linux forums, compiling wine hacks yourself, and trying shit to see if it worked. Rinse and repeat. Today, we’re a bit more civilized, we crowdsource that shit with &lt;a href=&#39;https://www.protondb.com/&#39;&gt;ProtonDB&lt;/a&gt;. Wanna know if your game works on Linux? Go there, search, find out. If there are launch flags or other tweaks to make, they’ll be listed.&lt;/p&gt;&#xA;&lt;p&gt;Getting mods to work with my GOG Cyberpunk install needed one of these launch options. After that, works just as good as Windows (but with a bonus slight FPS increase).&lt;/p&gt;&#xA;&lt;h2&gt;Accessing Bitlocker-encrypted Drives&lt;/h2&gt;&#xA;&lt;p&gt;I was &lt;i&gt;very&lt;/i&gt; surprised buy this. Apparently you can just click to mount Bitlocker drives in modern Linux systems. It just works:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/19a97afd-7ca9-429a-ac37-2b27317c7d20/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466W4FZKI43%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110134Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCxgUpn2kuvnjAm4a1kJY6KairzKTWdteLmBXzZEmGqpAIhAIFWk6HHAsswm42w%2FCeonlxAN8gYrlWKM%2BDoBK4HPhXbKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igy60gHmTzPbsuGcV%2BMq3ANKEMSjC1Y5XnQCXGUP0CBQRV9QbSI6JrAmjYO99NLeh0%2Fn%2FNh3TGoojjJnOR%2B6lNEBjySaupaKwVzODw%2B6LRk6b8OT6fWPa863AfdfDUr%2B12UyjBl9fvldUhjRTTp4i3r%2BvnlQEuUZSlsw9GgrQIi0EVyWdvPAwaL21GqlbPVsQFezYOHG0hsVmbfeNN5zZAp965fs71ps6W%2B4Il8I%2FAcOmwOV%2FSzfzmxsSVAxMq5CxAPnhI4qG6d80BdNGIDOcKTMzwdtswMIJWciRVZ%2FKc7EEXa7w2cTWZHKMEkel5zeUdnpny%2BFjCTMqOHYvh9ue46pw%2BOUwFBg4biCqwESH8L5Xevqf3xvTWAO7N4K3FEY9sFfP4YpW3Usl283znsHQC6oSOKHyxGiAzGid%2FOCjL4tdcPYjCgqNwfWF%2F7h2ZpoQHTFTh%2BIuSWsCkJL%2FagDKeVYsNtg7B3htmIpmHpRzNsyko6UElmQLB%2F3FsZLWXpfD3m6ilPz8ihWbVIuIwbwRDTKAaDAmjE1baT%2F6wyUwHjot0j74ajDz%2BvJwSHiCcZrEj%2BDf%2Bz45B9O17mSVwmdP6YNTlTuQl3Cy%2Bq1fCXmGLzD%2F6eNwxlPYQ6It%2FdYAi%2FoLPuFPyMl1yQq1XS1NTC3%2F%2FvPBjqkAQe0ecKfF2ni0BA1xhCgnEe5LzKNNUyBLayCURZSsAG2XgkT2TZh%2BCgq0KenMMfkKGVnzN5pRSn4P%2B8YhDm5uV9jyJ4zjtxpYK9daiv9QO9qv%2BEdSXNv1DYzaxwRjsGH3Mkg6SgzqjMMMAjWdThx8BBczZbJlOwLes5OCisJH0gQfgSglpfGguy%2Fene1pgTQUOAHu0Qq46ELl7lywa%2FNXKlEvKB5&amp;X-Amz-Signature=262b1fa4203a6876d7cfb6ba71826cc7bba7221693c1e135500b875bb2572d24&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/19a97afd-7ca9-429a-ac37-2b27317c7d20/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466W4FZKI43%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110134Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCxgUpn2kuvnjAm4a1kJY6KairzKTWdteLmBXzZEmGqpAIhAIFWk6HHAsswm42w%2FCeonlxAN8gYrlWKM%2BDoBK4HPhXbKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igy60gHmTzPbsuGcV%2BMq3ANKEMSjC1Y5XnQCXGUP0CBQRV9QbSI6JrAmjYO99NLeh0%2Fn%2FNh3TGoojjJnOR%2B6lNEBjySaupaKwVzODw%2B6LRk6b8OT6fWPa863AfdfDUr%2B12UyjBl9fvldUhjRTTp4i3r%2BvnlQEuUZSlsw9GgrQIi0EVyWdvPAwaL21GqlbPVsQFezYOHG0hsVmbfeNN5zZAp965fs71ps6W%2B4Il8I%2FAcOmwOV%2FSzfzmxsSVAxMq5CxAPnhI4qG6d80BdNGIDOcKTMzwdtswMIJWciRVZ%2FKc7EEXa7w2cTWZHKMEkel5zeUdnpny%2BFjCTMqOHYvh9ue46pw%2BOUwFBg4biCqwESH8L5Xevqf3xvTWAO7N4K3FEY9sFfP4YpW3Usl283znsHQC6oSOKHyxGiAzGid%2FOCjL4tdcPYjCgqNwfWF%2F7h2ZpoQHTFTh%2BIuSWsCkJL%2FagDKeVYsNtg7B3htmIpmHpRzNsyko6UElmQLB%2F3FsZLWXpfD3m6ilPz8ihWbVIuIwbwRDTKAaDAmjE1baT%2F6wyUwHjot0j74ajDz%2BvJwSHiCcZrEj%2BDf%2Bz45B9O17mSVwmdP6YNTlTuQl3Cy%2Bq1fCXmGLzD%2F6eNwxlPYQ6It%2FdYAi%2FoLPuFPyMl1yQq1XS1NTC3%2F%2FvPBjqkAQe0ecKfF2ni0BA1xhCgnEe5LzKNNUyBLayCURZSsAG2XgkT2TZh%2BCgq0KenMMfkKGVnzN5pRSn4P%2B8YhDm5uV9jyJ4zjtxpYK9daiv9QO9qv%2BEdSXNv1DYzaxwRjsGH3Mkg6SgzqjMMMAjWdThx8BBczZbJlOwLes5OCisJH0gQfgSglpfGguy%2Fene1pgTQUOAHu0Qq46ELl7lywa%2FNXKlEvKB5&amp;X-Amz-Signature=262b1fa4203a6876d7cfb6ba71826cc7bba7221693c1e135500b875bb2572d24&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;Just use your BitLocker Recovery Key as the password (dashes-included) and the drive unlocks.&lt;/p&gt;&#xA;&lt;h1&gt;What Kinda Works&lt;/h1&gt;&#xA;&lt;h2&gt;Sunshine Streaming&lt;/h2&gt;&#xA;&lt;p&gt;I remote into my desktop probably 400 times per day (slight exaggeration). I have an iPad that I use as a thin-client to access my desktop. It effectively means I can walk around the house and still use my PC. &lt;i&gt;In the middle of a 6-hour gaming retrospective on YouTube and don’t want to give up the indomitable &lt;/i&gt;&lt;i&gt;&lt;a href=&#39;https://ublockorigin.com/&#39;&gt;uBlock Origin&lt;/a&gt;&lt;/i&gt;&lt;i&gt;?&lt;/i&gt; Just remote in and walk away from your desk without missing a beat.&lt;/p&gt;&#xA;&lt;p&gt;Sunshine makes this super easy. On Windows.&lt;/p&gt;&#xA;&lt;p&gt;On Linux, there are some troublesome issues:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;The default Debian package does not install the systemd-user service file, so auto-starting Sunshine on graphical-session login simply does not work.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;If you’ve configured your monitors to sleep after a time, Sunshine does not wake them up (it does on Windows, though). Instead, you get a connection error.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;To solve the first issue, I wrote a cheeky bash script:&lt;/p&gt;&#xA;&lt;code&gt;#!/bin/bash&#xA;&#xA;/usr/bin/sleep 10&#xA;/usr/bin/sunshine&#xA;&lt;/code&gt;&#xA;&lt;p&gt;This waits until my KDE session is good and ready, then launches Sunshine. The “proper” way to solve this problem is just to make the systemd service file is installed, but this was fast and it worked. I added this to my KDE autostart list and its been working perfectly.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;To solve the second problem, I wrote a tiny service called Sunrise that watches for “monitor is missing” errors, then wakes the monitor and restarts Sunshine.&lt;/p&gt;&#xA;&lt;a href=&#39;https://github.com/samurailink3/sunrise&#39;&gt;https://github.com/samurailink3/sunrise&lt;/a&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;After that brief journey into software development, Sunshine is working well enough to function as a daily driver.&lt;/p&gt;&#xA;&lt;h1&gt;What Doesn’t Work&lt;/h1&gt;&#xA;&lt;h2&gt;Screen Sharing/Recording&lt;/h2&gt;&#xA;&lt;p&gt;It ain’t all perfect though. Sharing full-screen apps on Discord via Wayland results in the stream becoming frame-limited and low-resolution after a few minutes. Recording full screen apps on OBS results in the game starting to screen-tear and the frame rate getting very choppy. Its pretty disorienting to look at. There are some troubleshooting steps I have yet to try, particularly looking into compiling OBS myself and using &lt;a href=&#39;https://github.com/nowrep/obs-vkcapture&#39;&gt;obs-vkcapture&lt;/a&gt;. Something to dig into for the future.&lt;/p&gt;&#xA;&lt;p&gt;That said, Windows 11 can’t game+record at the same time, so I’m not any worse off.&lt;/p&gt;&#xA;&lt;h1&gt;Thoughts on KDE Plasma&lt;/h1&gt;&#xA;&lt;p&gt;I was always a Gnome user. I really appreciated the “simple by default, but &lt;i&gt;very&lt;/i&gt; customizable” Gnome 2. Gnome 3 was a disappointment, but with enough tweaks, config files, fragile extensions, and tears, I was able to make it work. This time I decided to give KDE Plasma a full try.&lt;/p&gt;&#xA;&lt;p&gt;I always shied away from KDE because it was “too big”. Too much by default. I wanted to start simple and add complication, not start with complication and pare it down until it got simpler. Turns out, my past fears are just unfounded with modern KDE. KDE is perfectly usable and simple out of the box. The experience feels &lt;i&gt;a lot&lt;/i&gt; like Gnome 2. If you just want a default system, KDE works perfectly, if you want to customize the hell out of it: KDE still works perfectly.&lt;/p&gt;&#xA;&lt;p&gt;If you hate how Windows 11 fucked with your right-click menu, I’ve got great news: KDE lets you customize it, easily. No shell scripts required, just go into the “configure” menu and its there:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/87c8db59-3ce5-42a6-a599-e66f6207b5a5/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466W4FZKI43%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110134Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCxgUpn2kuvnjAm4a1kJY6KairzKTWdteLmBXzZEmGqpAIhAIFWk6HHAsswm42w%2FCeonlxAN8gYrlWKM%2BDoBK4HPhXbKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igy60gHmTzPbsuGcV%2BMq3ANKEMSjC1Y5XnQCXGUP0CBQRV9QbSI6JrAmjYO99NLeh0%2Fn%2FNh3TGoojjJnOR%2B6lNEBjySaupaKwVzODw%2B6LRk6b8OT6fWPa863AfdfDUr%2B12UyjBl9fvldUhjRTTp4i3r%2BvnlQEuUZSlsw9GgrQIi0EVyWdvPAwaL21GqlbPVsQFezYOHG0hsVmbfeNN5zZAp965fs71ps6W%2B4Il8I%2FAcOmwOV%2FSzfzmxsSVAxMq5CxAPnhI4qG6d80BdNGIDOcKTMzwdtswMIJWciRVZ%2FKc7EEXa7w2cTWZHKMEkel5zeUdnpny%2BFjCTMqOHYvh9ue46pw%2BOUwFBg4biCqwESH8L5Xevqf3xvTWAO7N4K3FEY9sFfP4YpW3Usl283znsHQC6oSOKHyxGiAzGid%2FOCjL4tdcPYjCgqNwfWF%2F7h2ZpoQHTFTh%2BIuSWsCkJL%2FagDKeVYsNtg7B3htmIpmHpRzNsyko6UElmQLB%2F3FsZLWXpfD3m6ilPz8ihWbVIuIwbwRDTKAaDAmjE1baT%2F6wyUwHjot0j74ajDz%2BvJwSHiCcZrEj%2BDf%2Bz45B9O17mSVwmdP6YNTlTuQl3Cy%2Bq1fCXmGLzD%2F6eNwxlPYQ6It%2FdYAi%2FoLPuFPyMl1yQq1XS1NTC3%2F%2FvPBjqkAQe0ecKfF2ni0BA1xhCgnEe5LzKNNUyBLayCURZSsAG2XgkT2TZh%2BCgq0KenMMfkKGVnzN5pRSn4P%2B8YhDm5uV9jyJ4zjtxpYK9daiv9QO9qv%2BEdSXNv1DYzaxwRjsGH3Mkg6SgzqjMMMAjWdThx8BBczZbJlOwLes5OCisJH0gQfgSglpfGguy%2Fene1pgTQUOAHu0Qq46ELl7lywa%2FNXKlEvKB5&amp;X-Amz-Signature=ffa4e3a8c34c4c2e5da625e3691936fae962839e0fad243fd2a722bcb17b0e90&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/87c8db59-3ce5-42a6-a599-e66f6207b5a5/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466W4FZKI43%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110134Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCxgUpn2kuvnjAm4a1kJY6KairzKTWdteLmBXzZEmGqpAIhAIFWk6HHAsswm42w%2FCeonlxAN8gYrlWKM%2BDoBK4HPhXbKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igy60gHmTzPbsuGcV%2BMq3ANKEMSjC1Y5XnQCXGUP0CBQRV9QbSI6JrAmjYO99NLeh0%2Fn%2FNh3TGoojjJnOR%2B6lNEBjySaupaKwVzODw%2B6LRk6b8OT6fWPa863AfdfDUr%2B12UyjBl9fvldUhjRTTp4i3r%2BvnlQEuUZSlsw9GgrQIi0EVyWdvPAwaL21GqlbPVsQFezYOHG0hsVmbfeNN5zZAp965fs71ps6W%2B4Il8I%2FAcOmwOV%2FSzfzmxsSVAxMq5CxAPnhI4qG6d80BdNGIDOcKTMzwdtswMIJWciRVZ%2FKc7EEXa7w2cTWZHKMEkel5zeUdnpny%2BFjCTMqOHYvh9ue46pw%2BOUwFBg4biCqwESH8L5Xevqf3xvTWAO7N4K3FEY9sFfP4YpW3Usl283znsHQC6oSOKHyxGiAzGid%2FOCjL4tdcPYjCgqNwfWF%2F7h2ZpoQHTFTh%2BIuSWsCkJL%2FagDKeVYsNtg7B3htmIpmHpRzNsyko6UElmQLB%2F3FsZLWXpfD3m6ilPz8ihWbVIuIwbwRDTKAaDAmjE1baT%2F6wyUwHjot0j74ajDz%2BvJwSHiCcZrEj%2BDf%2Bz45B9O17mSVwmdP6YNTlTuQl3Cy%2Bq1fCXmGLzD%2F6eNwxlPYQ6It%2FdYAi%2FoLPuFPyMl1yQq1XS1NTC3%2F%2FvPBjqkAQe0ecKfF2ni0BA1xhCgnEe5LzKNNUyBLayCURZSsAG2XgkT2TZh%2BCgq0KenMMfkKGVnzN5pRSn4P%2B8YhDm5uV9jyJ4zjtxpYK9daiv9QO9qv%2BEdSXNv1DYzaxwRjsGH3Mkg6SgzqjMMMAjWdThx8BBczZbJlOwLes5OCisJH0gQfgSglpfGguy%2Fene1pgTQUOAHu0Qq46ELl7lywa%2FNXKlEvKB5&amp;X-Amz-Signature=ffa4e3a8c34c4c2e5da625e3691936fae962839e0fad243fd2a722bcb17b0e90&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;h1&gt;Issues Endemic to Linux&lt;/h1&gt;&#xA;&lt;p&gt;&lt;i&gt;I can’t stop tinkering&lt;/i&gt;. Its a problem. It takes a concentrated effort for me to not tweak and optimize and customize my setup instead of just &lt;i&gt;using&lt;/i&gt; my setup. When I was on Windows, this was easy: The computer wasn’t really mine, it was Microsoft’s.&lt;/p&gt;&#xA;&lt;p&gt;There are so many “&lt;a href=&#39;https://en.wikipedia.org/wiki/Black_box&#39;&gt;black boxes&lt;/a&gt;” on Windows systems. You can search around the internet for solutions, maybe do some debugging, maybe dive into &lt;code&gt;eventvwr&lt;/code&gt;, but with some problems, eventually you just hit a wall and hope the problem goes away.&lt;/p&gt;&#xA;&lt;p&gt;Linux doesn’t have this problem (for the most part, proprietary software still exists over here). Just about any problem has logs, debugging information, and a small army of turbo-nerd-tinkerers who ran into the exact same problem you did. The power and possibilities are &lt;i&gt;intoxicating&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The main reason I left Arch for Debian in the first place was because I was tired of getting sucked into tinkering with my computer when I should have been &lt;i&gt;using my computer&lt;/i&gt;. This is still a problem with me and Debian, just a bit less since so much works out of the box.&lt;/p&gt;&#xA;&lt;p&gt;Just know, if you’re the same kind of tech person I am, you may need to be extra-vigilant about &lt;i&gt;intentional usage&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;h1&gt;To Summarize&lt;/h1&gt;&#xA;&lt;p&gt;While my most recent experience hasn’t been perfect (and it never is), what’s here is more than good-enough, and certainly better than my Windows 11 experience. I left Linux for gaming, and now that gaming is on Linux, I don’t see a lot of reasons to go back to Windows.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3m4bxqgqkpc2t&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3m4bxqgqkpc2t&lt;/a&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Windows-11-upgrade-pushed-me-to-reinstall-Linux-on-the-Desktop-and-I-ve-never-loved-my-computer-mo-29ab6786424180f19c01d2ac737dedcf" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>The Best Game Clipping Setup</title>
    <updated>2025-09-02T00:00:00Z</updated>
    <id>https://www.samurailink3.com/The-Best-Game-Clipping-Setup-263b678642418046b6f9de83d8e5a551</id>
    <content type="html">&lt;html&gt;&lt;p&gt;I’m gonna try some YouTube stuff for a while. A lot of my projects are kinda cool and I want to talk about them and maybe inspire people to build stuff for themselves too.&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Download this Video (Right-Click, Save-Link As): &lt;a href=&#39;https://s3.samurailink3.com/tom-webster-videos/TomWebster-TheBestClippingSetup.mp4&#39;&gt;https://s3.samurailink3.com/tom-webster-videos/TomWebster-TheBestClippingSetup.mp4&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/The-Best-Game-Clipping-Setup-263b678642418046b6f9de83d8e5a551" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Things that make me uncomfortable: Missionaries vs Mercenaries</title>
    <updated>2025-03-11T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Things-that-make-me-uncomfortable-Missionaries-vs-Mercenaries-160b67864241809dbc45f75dc91dc250</id>
    <content type="html">&lt;html&gt;&lt;p&gt;I want to talk about something that makes me professionally uncomfortable: &lt;i&gt;Missionaries vs Mercenaries&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I’ve worked for a few larger tech firms over my career and have experienced both sides of this coin rather viscerally. Before I get into how these two concepts play into the larger organization, let’s take a look at what they are:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Missionaries - People who take the job because they enjoy the work. Missionaries aren’t in it for the money or prestige, they’re in it for the work itself, the challenge, because they believe in the vision. Missionaries largely thrive on intrinsic rewards.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Mercenaries - People who take the job &lt;/li&gt;&lt;li&gt;&lt;i&gt;because of&lt;/i&gt;&lt;/li&gt;&lt;li&gt; the extrinsic rewards, aka: They’re in it for the money/prestige. The work and mission don’t really matter, they’re gonna do a good job because it pays well and they want to keep that gravy-train rolling.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;I’m gonna try to keep this post generic, if you want to compare this article to my &lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt; and draw conclusions about which company is which, feel free. Just know that my intent isn’t to disparage or promote one company over another. If given the opportunity, I wouldn’t work for &lt;i&gt;either&lt;/i&gt; company today. One other note, this post talks about &lt;i&gt;money&lt;/i&gt;, a lot. I want you to know I understand that having this “debate” at all comes from a place of extreme privilege. A lot of people would be lucky to have a “problem” like this.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;The &lt;i&gt;missionary&lt;/i&gt; &lt;i&gt;company&lt;/i&gt; was &lt;i&gt;a weird place&lt;/i&gt;. Pretty cult-like, but that &lt;i&gt;mostly&lt;/i&gt; resulted in better experiences for the users. The job paid very well compared to &lt;i&gt;all jobs&lt;/i&gt;, but was actually pretty poor compared to other tech jobs of the same caliber. People successful at this company are hired as Staff/Principal roles at other firms. Ultimately, the people that were there were underpaid for the job they were doing.&lt;/p&gt;&#xA;&lt;p&gt;This was &lt;i&gt;by design&lt;/i&gt;. According to longtime employees and leadership, the “Missionary vs Mercenary” problem was “solved” by under-paying. It ensured the people at the company weren’t in it for the money. If they wanted money, they could find it easier at other companies. The people here were in it for the mission, for the customers, for the work. They &lt;i&gt;couldn’t&lt;/i&gt; be in it for the money, the money wasn’t there.&lt;/p&gt;&#xA;&lt;p&gt;This was bullshit, I knew it, everyone around me knew it. The whole “Missionaries vs Mercenaries” argument was an excuse to pay people less than they were worth. Here’s the part that makes me uncomfortable though: It worked.&lt;/p&gt;&#xA;&lt;p&gt;It’s gross to look back and admit this, but, yeah, it worked. The people I worked with at &lt;i&gt;missionary company&lt;/i&gt; largely didn’t care about the paycheck. It was hard work full of challenging problems, burnout, sociopathic leadership, and anti-employee rhetoric and systems. But people stayed because the work was so engrossing. Years later, I’m still referencing old projects and lessons learned there. I’m still friends with many people I met, and they’re &lt;i&gt;still&lt;/i&gt; among the very best people I’ve ever worked with.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;I ended up leaving &lt;i&gt;missionary company&lt;/i&gt; to work at &lt;i&gt;mercenary company&lt;/i&gt;. Mostly due to burn-out, but it helped that &lt;i&gt;mercenary company&lt;/i&gt; offered to double my salary, give me unlimited time off, and allow me to work from wherever I wanted. At this time, I never looked kindly on the “Missionary vs Mercenary” debate. It was a line given by company leadership to justify underpaying their workforce. It was bullshit, right?&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Working for &lt;i&gt;mercenary company&lt;/i&gt; was extremely enlightening for all the wrong reasons. Within the first month, I understood what happens to companies that fall far into that &lt;i&gt;mercenary&lt;/i&gt; spectrum. People were paid &lt;i&gt;extremely well &lt;/i&gt;and&lt;i&gt; &lt;/i&gt;the benefits were &lt;i&gt;outlandish.&lt;/i&gt; It was pretty insane, comparatively. I was working less and getting paid more than I ever had before. I wondered how this could be sustainable. Then the honeymoon period wore off and I realized: It wasn’t.&lt;/p&gt;&#xA;&lt;p&gt;The company was &lt;i&gt;suffering&lt;/i&gt;. Everything around me was lazily constructed. Hardly anyone took pride in their work, many people openly despised what they were building, but the work continued. No one fought for lower tech-debt, no one strove to change the company, leadership was completely absent. The excuse was “Mercenary company is a grass-roots type of place, we rely on teams to set roadmaps and set direction”. This meant that the leaf-node teams ended up running things without any sense of collaboration, wider vision, or cohesive direction. Teams largely built what was easy without any sense of what the company or customers needed. They built purely to justify their own existence and that’s it.&lt;/p&gt;&#xA;&lt;p&gt;This begs the question: Why have leadership at all if they’re just going to bury their heads in the sand? The company would have been better off without any senior leadership at all. The people running the company were also caught up in the mercenary-centric “rest and vest” cycle.&lt;/p&gt;&#xA;&lt;p&gt;This type of thinking was a constant problem for me. I was interested in &lt;i&gt;good engineering&lt;/i&gt;, building sustainable solutions to real problems. For the most part, the people around me were interested in playing politics to justify their paychecks. No one gave a shit about the customer. No one gave a shit about the product. Everyone gave a shit about the stock price. The mercenaries were running the show and no one in leadership was inconvenienced &lt;i&gt;enough&lt;/i&gt; to stop them.&lt;/p&gt;&#xA;&lt;p&gt;&lt;i&gt;Mercenary company &lt;/i&gt;was directionless and failing. Instead of taking a hard look at what their customers wanted or needed, they ended up choosing to build something easier. A shortcut to more money. They chose poorly because they didn’t care about the work and they didn’t care about their customers. They cared about the paycheck. So they built the easiest thing to justify that paycheck.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Here’s the part that makes me uncomfortable: &lt;i&gt;Mercenary company&lt;/i&gt; did the “right thing” by paying people well and offering stellar benefits, but it attracted exactly the wrong type of person. It attracted vultures and burn-out refugees (👋). The vultures ended up running the show and destroying the company from the inside-out. &lt;i&gt;Missionary company &lt;/i&gt;did the “wrong thing” and made excuses to pay people poorly, but it attracted the right type of person. It attracted people who wanted to build better experiences for customers. Largely, those people are chewed up and spat out after the company gets what they can out of them before significant burnout sets in.&lt;/p&gt;&#xA;&lt;p&gt;I’ve tried to come to a conclusion here a few times. This draft has been in my folder for &lt;i&gt;months&lt;/i&gt;. Ultimately, I’m just frustrated that doing the “right thing” (albeit, poorly) resulted in such dire conclusions. And I’m frustrated that doing the “wrong thing” keeps working.&lt;/p&gt;&#xA;&lt;p&gt;Personally, I believe you can pay people well &lt;i&gt;and&lt;/i&gt; curate a &lt;i&gt;missionary&lt;/i&gt; culture without devolving as long as you have good leadership that cares about the customer and the company (in that order).&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3lk4eatnfz222&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3lk4eatnfz222&lt;/a&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Things-that-make-me-uncomfortable-Missionaries-vs-Mercenaries-160b67864241809dbc45f75dc91dc250" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Stupid Slicer Tricks: Model Mashup</title>
    <updated>2025-02-11T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Stupid-Slicer-Tricks-Model-Mashup-196b6786424180f8b358d13b66e8de5d</id>
    <content type="html">&lt;html&gt;&lt;p&gt;Sometimes you don’t need a full CAD workflow to make modifications to a model, your slicer is capable of some truly amazing feats! Here’s a story about a time where I had a model that I loved that didn’t quite meet my requirements and a remix that &lt;i&gt;also&lt;/i&gt; didn’t meet my requirements. The “only” solution was to mash up the two models to make something a bit more customized.&lt;/p&gt;&#xA;&lt;p&gt;I’ve been trying to upgrade my &lt;i&gt;domains of nerddom&lt;/i&gt; and one area I’ve always been mostly-blind to is the physical craft of electronics. I know just enough to be dangerous and spot obvious problems (after they’ve combusted), but I’ve never delved into the practice of soldering or building/repairing my own electronics. Luckily, I have friends well-versed in this domain. I bought a &lt;a href=&#39;https://pine64.com/product/pinecil-smart-mini-portable-soldering-iron/&#39;&gt;PINECIL&lt;/a&gt; and had a friend walk me through the very basics of soldering.&lt;/p&gt;&#xA;&lt;p&gt;He recommended &lt;a href=&#39;https://www.printables.com/model/345083-rugged-multipart-pinecilts100ts80-case-v2&#39;&gt;this PINECIL case on Printables&lt;/a&gt;, which is an &lt;i&gt;extremely nice model&lt;/i&gt;. Functional and beautiful. The only issue with it is: I’m not gonna use those extra iron tips. While I have another tip, I have neither the experience nor knowledge to use them effectively yet. I’m still an absolute beginner at this stage, I just don’t know what I don’t know.&lt;/p&gt;&#xA;&lt;p&gt;My friend recommended a flux pen for my beginner soldering kit, but the default case didn’t come with a spot for it. Luckily, &lt;a href=&#39;https://www.printables.com/model/440145-rugged-multipart-pinecilts100ts80-case-flux-pensol&#39;&gt;this remix of the PINECIL case&lt;/a&gt; had a socket that fit perfectly, but &lt;i&gt;it was mashed up with the soldering iron tip holder that I didn’t need&lt;/i&gt;. I wanted to blend the models together so I could replace the spare-tip holder with the flux pen socket. Normally, I’d just take this out to &lt;a href=&#39;https://www.tinkercad.com/&#39;&gt;TinkerCAD&lt;/a&gt;, but I wanted to see if I could get away with a more “low-tech” solution.&lt;/p&gt;&#xA;&lt;p&gt;For better or worse, that low-tech solution was some &lt;i&gt;goblin engineering shit&lt;/i&gt; with PrusaSlicer:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/7f15bef2-e268-4966-a0cf-4ba59b66e40a/signal-2025-02-10-082524_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466X2466NQQ%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIHhXfhN0QOE8igJh%2F%2Br1wIO%2F6a3dfKpbhufAheEj3bYiAiB1kLBa1DVVF%2BCPWIjcBaCo9AVO2r0sqP8vQi%2BO4ZdA8yqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMGgf%2BsRStBsO5MYpjKtwDcKcv8JaAC74Rb5Az4Cl9lRWAVKlblONBsACBEmC17kyRAeR7Xr37bpHK7n%2FU0rFybmR7e%2FTQTuz1DTBPWCMtN8mip%2BxB6U5sBktZYzfD4P9VcpizlbNf5rW4%2BQb0NpOEZZRfwGzPP2rnjZ1lRQxttA%2BIAg5wnkxX%2FEFh7K4Jycnx5MrK5%2FntiukNKkkdy%2FGcIIJg6CBQusiHouRS0cSqcC9URnuqpXJTMeUkuH4Kww2jR7G3gM4fwR9Rz2DhsMJttyq99P3ZbwXPHl50gkfhNuAVO%2BP2AjoyHrKLnH8AvEum%2BQ4H0n2nLsVIk8MRKm5VOLPMC%2BtInZ05Oo53MttJKdvJY0aaWLqzf%2BTng6q7uGSURDHRMOSwrZ2GgjhdjiTqRDjLMq06VoMYpUUHbAvOo0kALrZyP4yIDCj%2BjcTpg1LbM%2FhIuUACoUlpiovane%2BKTjQhjpKtnqZqiTWA%2B5eoqNtcEWzJIOTfOBU4QGo8AKkPC4EBvDQuOJHyFJYYcb0DqwiY3Ec3fiTnmGvhrQbduzcqcSy4E7SN%2FHVhThlDe5He6ktGo%2FzsCVMGBuWlaKrgc91wGY3LcVBjT0NcDjFauRhKJ8cubcqDsGhCURKboySA1uHfWZhcBN6TVcMw1P%2F7zwY6pgE%2F8iSsiHtjxXOpu8ErjQqRs%2Bk0alFpe8jfFnJhADjBarj5md3ZcaCXidVUew2Xj3vimXsG1SF%2FD80GFDX4K7%2FAvr3BLd8AbSlmHtiHOyR8K5sZa6D5I8up5KYe0fhjgJc5u0I3S7%2FKXrGAMY76WWAV7D%2FrdOkxBivIHO8iSpIXHwPp9b8Xn3JWLsxzH08CNDFhN1Sc7jza1SFd4SBezqxIKW61Ym35&amp;X-Amz-Signature=c6eee7036483df73753aac1a8997cac1125fe27381f568ce6c09ccd682729647&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/7f15bef2-e268-4966-a0cf-4ba59b66e40a/signal-2025-02-10-082524_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466X2466NQQ%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIHhXfhN0QOE8igJh%2F%2Br1wIO%2F6a3dfKpbhufAheEj3bYiAiB1kLBa1DVVF%2BCPWIjcBaCo9AVO2r0sqP8vQi%2BO4ZdA8yqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMGgf%2BsRStBsO5MYpjKtwDcKcv8JaAC74Rb5Az4Cl9lRWAVKlblONBsACBEmC17kyRAeR7Xr37bpHK7n%2FU0rFybmR7e%2FTQTuz1DTBPWCMtN8mip%2BxB6U5sBktZYzfD4P9VcpizlbNf5rW4%2BQb0NpOEZZRfwGzPP2rnjZ1lRQxttA%2BIAg5wnkxX%2FEFh7K4Jycnx5MrK5%2FntiukNKkkdy%2FGcIIJg6CBQusiHouRS0cSqcC9URnuqpXJTMeUkuH4Kww2jR7G3gM4fwR9Rz2DhsMJttyq99P3ZbwXPHl50gkfhNuAVO%2BP2AjoyHrKLnH8AvEum%2BQ4H0n2nLsVIk8MRKm5VOLPMC%2BtInZ05Oo53MttJKdvJY0aaWLqzf%2BTng6q7uGSURDHRMOSwrZ2GgjhdjiTqRDjLMq06VoMYpUUHbAvOo0kALrZyP4yIDCj%2BjcTpg1LbM%2FhIuUACoUlpiovane%2BKTjQhjpKtnqZqiTWA%2B5eoqNtcEWzJIOTfOBU4QGo8AKkPC4EBvDQuOJHyFJYYcb0DqwiY3Ec3fiTnmGvhrQbduzcqcSy4E7SN%2FHVhThlDe5He6ktGo%2FzsCVMGBuWlaKrgc91wGY3LcVBjT0NcDjFauRhKJ8cubcqDsGhCURKboySA1uHfWZhcBN6TVcMw1P%2F7zwY6pgE%2F8iSsiHtjxXOpu8ErjQqRs%2Bk0alFpe8jfFnJhADjBarj5md3ZcaCXidVUew2Xj3vimXsG1SF%2FD80GFDX4K7%2FAvr3BLd8AbSlmHtiHOyR8K5sZa6D5I8up5KYe0fhjgJc5u0I3S7%2FKXrGAMY76WWAV7D%2FrdOkxBivIHO8iSpIXHwPp9b8Xn3JWLsxzH08CNDFhN1Sc7jza1SFd4SBezqxIKW61Ym35&amp;X-Amz-Signature=c6eee7036483df73753aac1a8997cac1125fe27381f568ce6c09ccd682729647&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/874fcefd-7a74-412b-a2fa-4e4d9b9f42c6/signal-2025-02-10-082556_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4662YEHDXNV%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIFGmuvy4sZoTB2UIhfp1K%2BXyiFdG2%2BH0CXDJLuBh1rF7AiEA8lUR4WbvysU0siSc0O9XFvfo%2FWv%2FIVSxlOukSTAHMiQqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDDA1W8ghyRuhY%2F%2F7AyrcA4Yz4iuzeBpkWWwsgwQKm%2FR7aaapGzlFW1TyMoQZVLxlc%2B9wvobhvZGpuNrp4jfDRXnevkJ6VAhdgylnoShTW%2BN5jmfKLc7nDJNMODatNtyTYrvzhakrWdhb5GAVxfROY33uxiWM%2BgNxbzJFFZy%2F9stXA5OCXnPz%2FNWcdAvNrpotcTAypcoUkPO5nsx%2BhuDqgWAp61Yyv7XbC8z1RDyDrv006KREiFOIinojimSAglIRlk4Ym2O68EZOmj7QSJxkvyTdrsSQDD1eB3XTsWteuCyZqEYrVCOZAp7qNjKYOpjT%2F9iKTN2OdJDic5PKzStSrRASTK0bFFjOEO4TS58HclqoxqGGdwol6GDwZ6nvDlphNW9GVLeiFCcZaR%2B1uYl8VG3bbaJXrj4akNgXU7XD9ZL7Gv8NWAvFLDUGl8Mw7Jysc58ghhWmolD87Gsj71LK9vjFgS3RBcTjuilx25eDQZkSphkiZEXobtNZyu0b3yZmIl0Cd7qCxe%2Fu6Ug5rFht4LShCXIyMYclc7PhTz5sPowMW1OBF9b%2Fzbt%2B3brUNfU3S42NCJcLcZECOUjiMj%2BMmGtI6O7z8HFdU%2BgXRWVaSN16H%2BHoH5g2rUeL%2FIsqHKsXHnz4i3MWDIr7l9iTMOn%2F%2B88GOqUBUkE86Dsp90C7LnxK1hiXLp8yHCI7m%2Bmn3CKeHbkaG39u5kEG5cevXQ2DNoNOM6tj%2BkyhJtzt5MtPr%2B9soKqAPliqjwOowKOwmC1XIWDJES1kOSh%2Fep%2Bylq7vtOBmM9jaL1z2vJts4d%2Fc1JErfPTWRAqXqV610xOiVtqgP7jjpU8BiKiaIwXljDqWcLwAfEITZhdx07fG70WCPPIkLpjrkfr8IMD8&amp;X-Amz-Signature=9206d17bc579fd4b1391ae6d4c090f69ad110764a40dbcdf07a07880076dcc7b&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/874fcefd-7a74-412b-a2fa-4e4d9b9f42c6/signal-2025-02-10-082556_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4662YEHDXNV%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIFGmuvy4sZoTB2UIhfp1K%2BXyiFdG2%2BH0CXDJLuBh1rF7AiEA8lUR4WbvysU0siSc0O9XFvfo%2FWv%2FIVSxlOukSTAHMiQqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDDA1W8ghyRuhY%2F%2F7AyrcA4Yz4iuzeBpkWWwsgwQKm%2FR7aaapGzlFW1TyMoQZVLxlc%2B9wvobhvZGpuNrp4jfDRXnevkJ6VAhdgylnoShTW%2BN5jmfKLc7nDJNMODatNtyTYrvzhakrWdhb5GAVxfROY33uxiWM%2BgNxbzJFFZy%2F9stXA5OCXnPz%2FNWcdAvNrpotcTAypcoUkPO5nsx%2BhuDqgWAp61Yyv7XbC8z1RDyDrv006KREiFOIinojimSAglIRlk4Ym2O68EZOmj7QSJxkvyTdrsSQDD1eB3XTsWteuCyZqEYrVCOZAp7qNjKYOpjT%2F9iKTN2OdJDic5PKzStSrRASTK0bFFjOEO4TS58HclqoxqGGdwol6GDwZ6nvDlphNW9GVLeiFCcZaR%2B1uYl8VG3bbaJXrj4akNgXU7XD9ZL7Gv8NWAvFLDUGl8Mw7Jysc58ghhWmolD87Gsj71LK9vjFgS3RBcTjuilx25eDQZkSphkiZEXobtNZyu0b3yZmIl0Cd7qCxe%2Fu6Ug5rFht4LShCXIyMYclc7PhTz5sPowMW1OBF9b%2Fzbt%2B3brUNfU3S42NCJcLcZECOUjiMj%2BMmGtI6O7z8HFdU%2BgXRWVaSN16H%2BHoH5g2rUeL%2FIsqHKsXHnz4i3MWDIr7l9iTMOn%2F%2B88GOqUBUkE86Dsp90C7LnxK1hiXLp8yHCI7m%2Bmn3CKeHbkaG39u5kEG5cevXQ2DNoNOM6tj%2BkyhJtzt5MtPr%2B9soKqAPliqjwOowKOwmC1XIWDJES1kOSh%2Fep%2Bylq7vtOBmM9jaL1z2vJts4d%2Fc1JErfPTWRAqXqV610xOiVtqgP7jjpU8BiKiaIwXljDqWcLwAfEITZhdx07fG70WCPPIkLpjrkfr8IMD8&amp;X-Amz-Signature=9206d17bc579fd4b1391ae6d4c090f69ad110764a40dbcdf07a07880076dcc7b&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;i&gt;What have I done??&lt;/i&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Instead of solving this problem “correctly”, I decided to solve it “good enough”. PrusaSlicer has a great feature called “Negative Volume Modifiers”. You can do a lot of different functions with these simple shapes, even adding text to models easily. This particular modifier just removed areas of a model, kinda like a big boxy 3D eraser.&lt;/p&gt;&#xA;&lt;p&gt;Let’s dive into what this mess actually does:&lt;/p&gt;&#xA;&lt;p&gt;The first model uses negative volumes to remove the spare tip holders from the model. It isn’t perfect and leaves some gaps in the original filet of the case, but it’s good enough:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/411be319-66a8-40ef-9195-2e2bbff71c68/signal-2025-02-10-082623_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46644BRQ6QL%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQDDNnzr6RaK06Pz9NZjRgbGYIB7jHwuOIZ79DGs9NqBCgIhAK6KH2JP7uSH40jCirf0IKlGi0H4Yh%2FyNYQy7SZVad%2F0KogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyRzG2H%2F56MaitZ%2Fzwq3AOoLX2P6p3wo84jSoJ%2BPju7pL%2BlmtwkbWRO4kOOw%2Fr5rMdcBztRt%2FJZZAZvaiSkYxImY4iiS7tumAs1DbdMtl%2BZxRsmla7mcCTSmDSoxyMHguOsuWbIXEAN%2F3uFikedbgLts6bB%2F0bL%2FfsxxFu0l5wfd2mya%2BAIqz0AerIJFLaJfg%2BSWg4Tt3JkNVnZPYRrUM7Qrw8%2BNQVNHd8h2Jqe4fOirRM4b8SH6RFnrXq4pz7YXJWb8x%2Fzsuc%2Fn8%2BlOVh0Wu1g9%2BxmW0foir4GRId8KxNdg0i0hDl55FWCoebONdbqKKZRh%2B9FNChEfoVeUJ0hSDsAYKnUKVTPqkGtGZaOzIKa9l3LYiSY6Z904vbPnRqWyM5AOyxmKNBpZDgdxEcSIq9rfcUd6aSm3X765EPgq%2FXwYV4t1kmZyCjf17BjVOqKvGg3nPvKrHLepokSDRXhZufXifjANvp2dp%2F9PjVH21kunjk45GoK2mSYFi3svBy%2BpeF%2FnirKOz2BnNRumip2vdRwl%2BaJTZdOZbii5Ce9O4eHHw58eQKucL2s8f9vtsEp5ND1JiXYWb013Z1ap2M4KTfWGk8q8TiLe%2FDp9OJLFmZZ4NTO6L%2FBowm1O%2FXl6aSgreJ%2FfTT7R3DJSbcKQDCfgPzPBjqkAVB6G6dfBWbwOye9QNrcTL4cGMKIpj%2FRTWlwc%2BDQX%2FzEtNP1KxeyZXIge8s3QnUrH12EtE2xFDiDMFmc2Y0%2FpMWKtv2X1Yd6qAACI1Yv29mWIGF9%2FosF810qimBc4rcWjw6jiZkNs%2FWKrgJ4ZQEy1tts4y0sKE0eNd2Brk3gqvuk3%2Bw9JZ5qPHenu6yop3xGh%2BXFRgQs50qoKSh9CcIYkTCqDHwl&amp;X-Amz-Signature=7991d591c0bf5646000ee090515f6c1932b4faac8bc773a8583678cdcd6a1e3c&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/411be319-66a8-40ef-9195-2e2bbff71c68/signal-2025-02-10-082623_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB46644BRQ6QL%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQDDNnzr6RaK06Pz9NZjRgbGYIB7jHwuOIZ79DGs9NqBCgIhAK6KH2JP7uSH40jCirf0IKlGi0H4Yh%2FyNYQy7SZVad%2F0KogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyRzG2H%2F56MaitZ%2Fzwq3AOoLX2P6p3wo84jSoJ%2BPju7pL%2BlmtwkbWRO4kOOw%2Fr5rMdcBztRt%2FJZZAZvaiSkYxImY4iiS7tumAs1DbdMtl%2BZxRsmla7mcCTSmDSoxyMHguOsuWbIXEAN%2F3uFikedbgLts6bB%2F0bL%2FfsxxFu0l5wfd2mya%2BAIqz0AerIJFLaJfg%2BSWg4Tt3JkNVnZPYRrUM7Qrw8%2BNQVNHd8h2Jqe4fOirRM4b8SH6RFnrXq4pz7YXJWb8x%2Fzsuc%2Fn8%2BlOVh0Wu1g9%2BxmW0foir4GRId8KxNdg0i0hDl55FWCoebONdbqKKZRh%2B9FNChEfoVeUJ0hSDsAYKnUKVTPqkGtGZaOzIKa9l3LYiSY6Z904vbPnRqWyM5AOyxmKNBpZDgdxEcSIq9rfcUd6aSm3X765EPgq%2FXwYV4t1kmZyCjf17BjVOqKvGg3nPvKrHLepokSDRXhZufXifjANvp2dp%2F9PjVH21kunjk45GoK2mSYFi3svBy%2BpeF%2FnirKOz2BnNRumip2vdRwl%2BaJTZdOZbii5Ce9O4eHHw58eQKucL2s8f9vtsEp5ND1JiXYWb013Z1ap2M4KTfWGk8q8TiLe%2FDp9OJLFmZZ4NTO6L%2FBowm1O%2FXl6aSgreJ%2FfTT7R3DJSbcKQDCfgPzPBjqkAVB6G6dfBWbwOye9QNrcTL4cGMKIpj%2FRTWlwc%2BDQX%2FzEtNP1KxeyZXIge8s3QnUrH12EtE2xFDiDMFmc2Y0%2FpMWKtv2X1Yd6qAACI1Yv29mWIGF9%2FosF810qimBc4rcWjw6jiZkNs%2FWKrgJ4ZQEy1tts4y0sKE0eNd2Brk3gqvuk3%2Bw9JZ5qPHenu6yop3xGh%2BXFRgQs50qoKSh9CcIYkTCqDHwl&amp;X-Amz-Signature=7991d591c0bf5646000ee090515f6c1932b4faac8bc773a8583678cdcd6a1e3c&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/af395e1b-fedc-491b-ab3d-275ccfcdec9c/signal-2025-02-10-082706_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4665C7ZUNQU%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIEYXGJpvRjEuQVeDs3AdT1gJ5IfAzEs%2F7rfQK7mxPj2DAiEAkBjpj9wAPmOVYhI09aduTGpVnLj5%2F5HQar2GFzMyxKQqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDD7r%2Bor5lXrzK7QrvircA7cS3v%2B6QBnbzZm2ewNr6E6VBCv30PqUaeSQaMSlLf%2BIH7yaWWz4QhsLqwnoGglEK3Z1nlnv9Vxyrw3zFsMSHZ0GKpQLj36BCThJcoU5iL6QmojtSf2yeeYVofiHqt0ZQhK%2Fm5eNfxa05PmJmBRRkrvwqaJzyJWS35Ye%2B%2B8HtEB2%2FuocpraIGTybyz3QFacx1sy%2FEtEMnGJEDJSEBwpZro497utQT%2FsaimQNvIJl6IL8d9zxdvmOAv3ZFgN9ac8Cw9r%2Fqnpv9zVFLKIVsm60sHPI3%2FlZRMYwwpn6EKM8rumaoWq3HnEJQiPQbjP5Iy5Zp%2F28HkeQK1gpxeFUIpbEzG61fGqTyd%2FrseZJYlx4JGWn8h0uoZNV10N1Ci3RUPJiZSU30AOkvRXO%2BqyUZHw8vpSLN80goMyYhPM4qfbnmSCnbqRap1kIz%2FG6jTwceV3FahL%2Bq2fLJqLu85iK4kTFoEw2C%2FquA5mykmyws1%2BQ9l8s0RVJIlrM%2F%2BIl3OSi%2BDwxPBn0ahW3MhdggtIlIIdhd9LOrIukzHfn3Vz0bAbd2Em5CbqdF8onQe7NZfIEKoMu5mrQVhMD50gjbgQsIiId7XUaIKUsMp4J4vWVyYzc4OQVKQLQD264pBER8iehMJeC%2FM8GOqUB7kuEMf0Bn4LU%2F3X1XDwjGN98ZPSHJjcrZEsofvvw4hFcvR9KpgtlZdHgR7n3Io1eCETARD9uQsdpWeZwqWEKmRP93t1aGXiOWsDDrwQ0En4fTmUikJHYmQPqyU4VDwHFyDlibA2UILBNIMENZl3hrQR7M%2BNuJVRmy1%2BH51IIDFxN6LtqrjObfgFYFqOlW76O%2FyIDBgpq9gHlWSmfyrsF0JRB8NGm&amp;X-Amz-Signature=adcc010d7e7da24289e0eec068722bc14ebdfbeb2ece4604ee3b9976ff4459e4&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/af395e1b-fedc-491b-ab3d-275ccfcdec9c/signal-2025-02-10-082706_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4665C7ZUNQU%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIEYXGJpvRjEuQVeDs3AdT1gJ5IfAzEs%2F7rfQK7mxPj2DAiEAkBjpj9wAPmOVYhI09aduTGpVnLj5%2F5HQar2GFzMyxKQqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDD7r%2Bor5lXrzK7QrvircA7cS3v%2B6QBnbzZm2ewNr6E6VBCv30PqUaeSQaMSlLf%2BIH7yaWWz4QhsLqwnoGglEK3Z1nlnv9Vxyrw3zFsMSHZ0GKpQLj36BCThJcoU5iL6QmojtSf2yeeYVofiHqt0ZQhK%2Fm5eNfxa05PmJmBRRkrvwqaJzyJWS35Ye%2B%2B8HtEB2%2FuocpraIGTybyz3QFacx1sy%2FEtEMnGJEDJSEBwpZro497utQT%2FsaimQNvIJl6IL8d9zxdvmOAv3ZFgN9ac8Cw9r%2Fqnpv9zVFLKIVsm60sHPI3%2FlZRMYwwpn6EKM8rumaoWq3HnEJQiPQbjP5Iy5Zp%2F28HkeQK1gpxeFUIpbEzG61fGqTyd%2FrseZJYlx4JGWn8h0uoZNV10N1Ci3RUPJiZSU30AOkvRXO%2BqyUZHw8vpSLN80goMyYhPM4qfbnmSCnbqRap1kIz%2FG6jTwceV3FahL%2Bq2fLJqLu85iK4kTFoEw2C%2FquA5mykmyws1%2BQ9l8s0RVJIlrM%2F%2BIl3OSi%2BDwxPBn0ahW3MhdggtIlIIdhd9LOrIukzHfn3Vz0bAbd2Em5CbqdF8onQe7NZfIEKoMu5mrQVhMD50gjbgQsIiId7XUaIKUsMp4J4vWVyYzc4OQVKQLQD264pBER8iehMJeC%2FM8GOqUB7kuEMf0Bn4LU%2F3X1XDwjGN98ZPSHJjcrZEsofvvw4hFcvR9KpgtlZdHgR7n3Io1eCETARD9uQsdpWeZwqWEKmRP93t1aGXiOWsDDrwQ0En4fTmUikJHYmQPqyU4VDwHFyDlibA2UILBNIMENZl3hrQR7M%2BNuJVRmy1%2BH51IIDFxN6LtqrjObfgFYFqOlW76O%2FyIDBgpq9gHlWSmfyrsF0JRB8NGm&amp;X-Amz-Signature=adcc010d7e7da24289e0eec068722bc14ebdfbeb2ece4604ee3b9976ff4459e4&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&#xA;&#xA;&lt;p&gt;This leaves us a space to add the flux pen socket!&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;The second model just removes the entire case &lt;i&gt;around&lt;/i&gt; the flux pen socket, leaving just the socket behind:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/bb2f7862-69e5-4c70-9029-2a000a8df7fb/signal-2025-02-10-082744_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=f4b6b8e91816cfa0c175d20cd8a704ec763003f34368f119c443e4d7ab2072b4&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/bb2f7862-69e5-4c70-9029-2a000a8df7fb/signal-2025-02-10-082744_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=f4b6b8e91816cfa0c175d20cd8a704ec763003f34368f119c443e4d7ab2072b4&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, we just gotta overlay those models &lt;i&gt;just right&lt;/i&gt; and….&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/b9e22796-3c02-47a0-aba0-ea8b22601f0a/signal-2025-02-10-082809_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=4e885c552efdc72d6a335f9cc8240d4b4174b57eecaca438cd7bbce7400b7597&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/b9e22796-3c02-47a0-aba0-ea8b22601f0a/signal-2025-02-10-082809_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=4e885c552efdc72d6a335f9cc8240d4b4174b57eecaca438cd7bbce7400b7597&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;Voila! We’ve now mashed up both of these models to come up with a jankier third option. Slicing this model &lt;i&gt;does result in some problematic g-code&lt;/i&gt; and PrusaSlicer isn’t too happy about it:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/12432808-b905-49fc-99f7-c7f372b8be0d/signal-2025-02-10-083002_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=2f3d00fadab59357a0913856c62ee9496ed3684d3faa99047574ee4b10650049&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/12432808-b905-49fc-99f7-c7f372b8be0d/signal-2025-02-10-083002_002.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB4666CJ66B5Y%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110136Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJIMEYCIQCliWmbdRtFiO1ZxQ8L7Lmiv1QsIBOYvKhLot3yPP4EZAIhAJLpNcEf5utGHcldmx2WJ6RyyCAJMZHhKf4NB4OoGcxNKogECOP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzF%2F%2BPaazxVx1aiZSgq3AP9RG%2FzvCSajhqgJBgTBwFWTYdjRaTKXVq5qtvLEhFKqsndvrWEJuJDs1Z22GrF5EOuSQhrAeJ%2B8L8kMLwJ7nJNKICb6mtVjzcPBg5YSGuL6WMiLpkIPpRgG1aNo%2F5r%2ByOJgcYiRa1qiGUHfDK6XTTVoPP9RZSDkf6MZJfXT9%2BY5SxXOFYSWs%2BjH7R4tlhKxfPHHLt70wXgeHViCB8Aj6SgG76C0ruWAE31nyG1KPGtuTvIcjtDc5wPjH6UQjKcS92Dhyi%2BjQXVKPE%2BvsczuxTD1wNVLDGwGr33lgLopyvtjLd3S6NivyzcpOntV8%2FdvrOSvNmxLfDTyYsSrek1H7MPgYltgrldhD77SgI9Qb3ryEEN7SkKQ7jqKiKwi3kGlfo3jpbQ9MO1aLWob63z6PU5LXcv1oEXLyEdNNyro1Jv9yvL1fRk6hdS%2BTODSYI%2Fx%2FyUTEALoJfW22vLZfxALHp9BRYdJvjNM13CmbyxPerEziqInXPc902udxfH8PrvGN7FdCOmR%2FGybbroldwsbkMRGaU12EBC9pS72o9VyI2ZFg0EGRaYRISXp36gH%2F8WN%2BBCz0EbL50yZskDyhIXmAxP%2Bw79dG3KIlyRkbuGyybi1QrU0Eo7Qv3%2FkDthHDDegPzPBjqkAYcMcO7OQtVsGy3vTz6bm05P8YTPeSHkFnRevuACF91VekbZfUnYdYN9NH4ADh%2BatO1TeEmhmKISIk217B8T4qzNsg8p1VYpPNeipcvSJGuChad6j9xWemDWdLXCx5%2BIp2Dnqmy6XLIlZ0Tukq6hkM4Gt0UV%2B0SS24lNy%2FWwZbdpCzPgCPlGN%2BFGboPZ%2FCdF7iW%2BKUhjjUhHOAbgGiNbUdmtKW1V&amp;X-Amz-Signature=2f3d00fadab59357a0913856c62ee9496ed3684d3faa99047574ee4b10650049&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;Sadly, this is actually &lt;i&gt;exactly what we want&lt;/i&gt;. We have two different models that &lt;i&gt;should be&lt;/i&gt; printed on top of each other. The only question was: Would this print correctly? Let’s take a closer look at the g-code preview and try to discern what’s going on.&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/95ce3145-f40f-4983-8cad-9ebcd28b32df/signal-2025-02-10-082850_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466UYRX4KOX%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIDlZpHwXjc8VpemCWWrbcjnbTnSbWs0YEGuWwtQWjquvAiBoglHO3uUq5zltsoNde6mtt6%2BdoR1rvzqdBjCv5D1oBCqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMJgo%2FHeO%2BTsA6xrz3KtwDrXbK00g5hyyidNQA4VNkZDlMnrLNtbbyl49DCcmDrJ4egd27AJNPixWJp%2B198mH5hvCNMzUufSzk%2F1sSWaY%2BRGdcLokipsp2J7v9Vac7DGBfpPqVRE38Zq5WmOoSYVBQO%2Bee3NEnhhNEDgzP6LGC05uoUZIa0t2raL4HKIxBTLmeEUKWo77tiE4U2ahQ02M3HIbFxFgwXuRwC%2BPeZWNdzlHcvYpSO6yXaxKEpxwkqWse9d3gFh8tY2WsiZuJgI1tlXP9Of9i%2BWLZV8SmV6ZSXzeV6F%2BkJCGxmaM2oltdE8dZ0WzixwkAM3QmsyWHcdZK0AxZre%2FJBCA%2FpdvozpfClk8hrPIgKxNZO1PMjxnpyFxT4An974stmTPwH4wud891jDmYeRyjmIJbbCVPCVO55eGpXnaA%2BcZuc8o%2FWLuc3jb44sp9GY%2BW1eaStSPYsLiufjC1CHsiSx0T4c%2FESdXUgOCx3MAwgT5xlcW1ZQMXFhQjJBW3r817Uk7CDI3lxDTeLEHUzGA0vdX5r2n8xpSxBkOYYoBKsIPhApU6neA9ZYKFiyvAQjFP4CENzRw%2BsavBlJf68eozyGojqp7%2BRmhSCNNTHhdtAzfg5aY6APMMCJbna4RV36ec6rhhiaEwj4D8zwY6pgGEiyNEwjYKKUHCSaIyAaYt7EZLJnfxpMq8gKCsX6%2FsCys2s0D7wLaWc50qDxwkaQ4aAybpyq6HesXnkNNJDr3%2BGiGetIzQdJv%2Fe81%2BloK60b0ibD4c5tRU%2FgV6oFeKbLRbMLeVfmCG2w2VrYGrRTSB%2BGviQghsfO6kKXMJVhWhy%2BpQpwWelilZeGmBULc5ImWtW2DwZGN5Hnj%2FYajeEyAPm35D8ukD&amp;X-Amz-Signature=b4b9e1c04be211ddb61e618ee0b4c67baf252722bf472397532177b7b7ad4141&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/95ce3145-f40f-4983-8cad-9ebcd28b32df/signal-2025-02-10-082850_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466UYRX4KOX%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110137Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJGMEQCIDlZpHwXjc8VpemCWWrbcjnbTnSbWs0YEGuWwtQWjquvAiBoglHO3uUq5zltsoNde6mtt6%2BdoR1rvzqdBjCv5D1oBCqIBAjj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIMJgo%2FHeO%2BTsA6xrz3KtwDrXbK00g5hyyidNQA4VNkZDlMnrLNtbbyl49DCcmDrJ4egd27AJNPixWJp%2B198mH5hvCNMzUufSzk%2F1sSWaY%2BRGdcLokipsp2J7v9Vac7DGBfpPqVRE38Zq5WmOoSYVBQO%2Bee3NEnhhNEDgzP6LGC05uoUZIa0t2raL4HKIxBTLmeEUKWo77tiE4U2ahQ02M3HIbFxFgwXuRwC%2BPeZWNdzlHcvYpSO6yXaxKEpxwkqWse9d3gFh8tY2WsiZuJgI1tlXP9Of9i%2BWLZV8SmV6ZSXzeV6F%2BkJCGxmaM2oltdE8dZ0WzixwkAM3QmsyWHcdZK0AxZre%2FJBCA%2FpdvozpfClk8hrPIgKxNZO1PMjxnpyFxT4An974stmTPwH4wud891jDmYeRyjmIJbbCVPCVO55eGpXnaA%2BcZuc8o%2FWLuc3jb44sp9GY%2BW1eaStSPYsLiufjC1CHsiSx0T4c%2FESdXUgOCx3MAwgT5xlcW1ZQMXFhQjJBW3r817Uk7CDI3lxDTeLEHUzGA0vdX5r2n8xpSxBkOYYoBKsIPhApU6neA9ZYKFiyvAQjFP4CENzRw%2BsavBlJf68eozyGojqp7%2BRmhSCNNTHhdtAzfg5aY6APMMCJbna4RV36ec6rhhiaEwj4D8zwY6pgGEiyNEwjYKKUHCSaIyAaYt7EZLJnfxpMq8gKCsX6%2FsCys2s0D7wLaWc50qDxwkaQ4aAybpyq6HesXnkNNJDr3%2BGiGetIzQdJv%2Fe81%2BloK60b0ibD4c5tRU%2FgV6oFeKbLRbMLeVfmCG2w2VrYGrRTSB%2BGviQghsfO6kKXMJVhWhy%2BpQpwWelilZeGmBULc5ImWtW2DwZGN5Hnj%2FYajeEyAPm35D8ukD&amp;X-Amz-Signature=b4b9e1c04be211ddb61e618ee0b4c67baf252722bf472397532177b7b7ad4141&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/51d5975b-fcfd-4e0d-9b39-086f5de5c1ad/signal-2025-02-10-082947_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466VRO2ECD7%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110139Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBsaCXVzLXdlc3QtMiJIMEYCIQCNTGPPeVjChjk0ukZmWyou92IEmKhMWDNQZRe%2FdHVggwIhAPdCO%2FfyfkSO9FZCResQM8thICLh3VcEaSfmnoCNB%2BqJKogECOT%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyhPkbH7oB0aC35r2Aq3AN57PU55ux9HKlmMXdIydzhpGf1Ebh9Teo%2B4uOmAZg8NY91SkXiHkmqZRDU3tusaJvdT8yZ8QIcU7mjX9DyxokLb0e7YfopyhQRzNIAOQERDeGrakCoNF3nAdsrt4e%2FuN7G5UrAGF7PJMRkVt60mXFp%2BzSqFmDKXvClzMJ1Cb%2BQgh8hE9g1eo719JHeMmlUV%2BCEZE2No78HJC%2FpHGmPK0BIK2KkU6Q9X16AMz2%2BMTGwGjJkGhX4pvGu0%2B5pclkW%2FufLg%2Bnj0QMhl4wQdiVFei7Mf2N0170Ot%2B6nbBHe45B1XWPqlUUb3fvQpTC7bHoRwjVEXKDJa%2B6iRC9kvxxbP2RaDTc3G15UNLv6Lrn4NK00FBHXkNAfggHkWOQDazKkfnWvuBiMYbFHYGnDjzSLPuLSOdCySAe%2FWTYCi1dPk6afHXhH%2F9vfqohu%2B6GCXk69aTa%2FDoBq00tfVZiCWNR6XQvcti0aJI%2F6RPLydGvJCGVpVSG8HvbdbCi2VRoreoLiGQmKwPYewty1tAakmcDW2WwF9Lcy5Eh1k09mcNgDCDSAgenEaGv2JCLpsdAgsWp74Y8MUE0Jv7MusqImc3%2Fwrj925Cov%2FB%2BelRgUrT%2FUcOJMNyBT3TyUlQAf%2BHPEWTDEm%2FzPBjqkASG5h4gGOV45eBMOYYi4j0tEzHIv%2BQ7IpHseUEwa1m3EdI7w47yIa%2BwBNXLQpHWVgjWoDRmirfBcUJucbdp%2F%2FHGmt3O%2BBbvzfp87a1oBu%2FXpSrEr79u6cHVxXMhKCJnM8X6rzksmXqhPLGZBYd4QxotmLURhLON72rGsTwmWNahngKtXxKweuDR86awf9JsMACG49F1NzqoMkNxgjDn3Ptwlly46&amp;X-Amz-Signature=d25d7456876a445c8f6dc1aef5456f0d96d9f68f6460816420bcfb9e5887d0f8&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/51d5975b-fcfd-4e0d-9b39-086f5de5c1ad/signal-2025-02-10-082947_002.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466VRO2ECD7%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110139Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBsaCXVzLXdlc3QtMiJIMEYCIQCNTGPPeVjChjk0ukZmWyou92IEmKhMWDNQZRe%2FdHVggwIhAPdCO%2FfyfkSO9FZCResQM8thICLh3VcEaSfmnoCNB%2BqJKogECOT%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyhPkbH7oB0aC35r2Aq3AN57PU55ux9HKlmMXdIydzhpGf1Ebh9Teo%2B4uOmAZg8NY91SkXiHkmqZRDU3tusaJvdT8yZ8QIcU7mjX9DyxokLb0e7YfopyhQRzNIAOQERDeGrakCoNF3nAdsrt4e%2FuN7G5UrAGF7PJMRkVt60mXFp%2BzSqFmDKXvClzMJ1Cb%2BQgh8hE9g1eo719JHeMmlUV%2BCEZE2No78HJC%2FpHGmPK0BIK2KkU6Q9X16AMz2%2BMTGwGjJkGhX4pvGu0%2B5pclkW%2FufLg%2Bnj0QMhl4wQdiVFei7Mf2N0170Ot%2B6nbBHe45B1XWPqlUUb3fvQpTC7bHoRwjVEXKDJa%2B6iRC9kvxxbP2RaDTc3G15UNLv6Lrn4NK00FBHXkNAfggHkWOQDazKkfnWvuBiMYbFHYGnDjzSLPuLSOdCySAe%2FWTYCi1dPk6afHXhH%2F9vfqohu%2B6GCXk69aTa%2FDoBq00tfVZiCWNR6XQvcti0aJI%2F6RPLydGvJCGVpVSG8HvbdbCi2VRoreoLiGQmKwPYewty1tAakmcDW2WwF9Lcy5Eh1k09mcNgDCDSAgenEaGv2JCLpsdAgsWp74Y8MUE0Jv7MusqImc3%2Fwrj925Cov%2FB%2BelRgUrT%2FUcOJMNyBT3TyUlQAf%2BHPEWTDEm%2FzPBjqkASG5h4gGOV45eBMOYYi4j0tEzHIv%2BQ7IpHseUEwa1m3EdI7w47yIa%2BwBNXLQpHWVgjWoDRmirfBcUJucbdp%2F%2FHGmt3O%2BBbvzfp87a1oBu%2FXpSrEr79u6cHVxXMhKCJnM8X6rzksmXqhPLGZBYd4QxotmLURhLON72rGsTwmWNahngKtXxKweuDR86awf9JsMACG49F1NzqoMkNxgjDn3Ptwlly46&amp;X-Amz-Signature=d25d7456876a445c8f6dc1aef5456f0d96d9f68f6460816420bcfb9e5887d0f8&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&#xA;&#xA;&lt;p&gt;This is just a guess, but it appears that one layer of the model is printed &lt;i&gt;twice&lt;/i&gt; in the same location. Obviously, this isn’t optimal, but as long as it doesn’t fail the print completely, it should be fine. This is actually the first time I’m seeing this type of error in PrusaSlicer. Lucky for me, PrusaSlicer complains, but doesn’t actually stop me from doing some absolutely hacky bullshit.&lt;/p&gt;&#xA;&lt;p&gt;&lt;i&gt;I absolutely love this.&lt;/i&gt; While it may be conventional to just hard-stop the user from doing something dumb, the fact that it complains, &lt;i&gt;then allows the behavior anyway&lt;/i&gt; is fucking fantastic. This is how open-source software should work: Tell me what I’m doing is dumb as hell, but then let me do it anyway, consequences be damned.&lt;/p&gt;&#xA;&lt;p&gt;I sent the job off to the printer and watched as it double-printed the flux pen socket. The nozzle tried to put down molten plastic into already-solidified plastic, and that didn’t work too well. It squiggled out and made a small mess, but didn’t actually fail or nozzle-crash. The next layer went down perfectly well. No complaints, no issues. It just did the job.&lt;/p&gt;&#xA;&lt;p&gt;Check out the final product to see the results:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;Don’t get me wrong, this is &lt;i&gt;absolutely&lt;/i&gt; a hack/goblin engineering/mess, but the bottom line is that &lt;strong&gt;IT WORKS&lt;/strong&gt;. Your projects may not need so many messy hacks or slicer trickery, but its always good to have these skills in your back pocket. Even if its not the “right way” to do something, its often the faster way.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;For more slicer trickery, check out these great videos from some of my favorite 3D printing YouTubers:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.youtube.com/watch?v=o-NYbUFzxLw&#39;&gt;https://www.youtube.com/watch?v=o-NYbUFzxLw&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.youtube.com/watch?v=DaU8-2XadIE&#39;&gt;https://www.youtube.com/watch?v=DaU8-2XadIE&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.youtube.com/watch?v=iziTcOLKfFc&#39;&gt;https://www.youtube.com/watch?v=iziTcOLKfFc&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you would like to print this model (or the remix), check out the following links:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.printables.com/model/345083-rugged-multipart-pinecilts100ts80-case-v2&#39;&gt;https://www.printables.com/model/345083-rugged-multipart-pinecilts100ts80-case-v2&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.printables.com/model/440145-rugged-multipart-pinecilts100ts80-case-flux-pensol&#39;&gt;https://www.printables.com/model/440145-rugged-multipart-pinecilts100ts80-case-flux-pensol&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3lhwqkr43z22f&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3lhwqkr43z22f&lt;/a&gt;&#xA;&#xA;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Stupid-Slicer-Tricks-Model-Mashup-196b6786424180f8b358d13b66e8de5d" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Why are all the publish dates “wrong”?</title>
    <updated>2024-11-22T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Why-are-all-the-publish-dates-wrong-146b6786424180a18d3fd4ca80844210</id>
    <content type="html">&lt;html&gt;&lt;p&gt;If you’ve been following this site and/or &lt;a href=&#39;https://bsky.app/profile/samurailink3.com&#39;&gt;my posts on BlueSky&lt;/a&gt;, you’ve probably seen projects pop up with &lt;i&gt;weird dates&lt;/i&gt;. The best example is my &lt;a href=&#39;https://www.samurailink3.comDoor Handle Slammer Stopper - My Stupidest Useful 3D Print&#39;&gt;Door Handle Slammer Stopper - My Stupidest Useful 3D Print&lt;/a&gt; posted on 2024-11-20, but with a publish date of 2024-03-05, what’s up with that?&lt;/p&gt;&#xA;&lt;p&gt;I’d like to treat this site as part-blog/part-portfolio. For the Slammer Stopper, I finalized the design in March, but didn’t write or post about it publicly until a couple days ago. Because I view my projects as portfolio artifacts, I’d like to keep the dates accurate to when they were actually completed. It gives me (and readers) a more-accurate timeline of what I’ve worked on and when.&lt;/p&gt;&#xA;&lt;p&gt;I’ve played around with adding separating “publish date” and “project date”, but this adds a lot of confusion around what means what to readers and around how I should the posts. In the end, I’ve decided that its fine to “lose” the data as to when the article was published, because it has pretty limited value. The real value is the timeline and content itself.&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Why-are-all-the-publish-dates-wrong-146b6786424180a18d3fd4ca80844210" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Do Dollars Make Sense for Incident Management?</title>
    <updated>2024-11-21T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Do-Dollars-Make-Sense-for-Incident-Management-145b6786424180b0856bd41565c028cf</id>
    <content type="html">&lt;html&gt;&lt;p&gt;I did some writing for the blog on &lt;a href=&#39;https://rootly.com/&#39;&gt;https://rootly.com/&lt;/a&gt; and I’m very happy with how it turned out. Go check out the full article here:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://rootly.com/blog/do-dollars-make-sense-for-incident-management&#39;&gt;https://rootly.com/blog/do-dollars-make-sense-for-incident-management&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Do-Dollars-Make-Sense-for-Incident-Management-145b6786424180b0856bd41565c028cf" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Software Engineering Essentials: Flags</title>
    <updated>2024-11-14T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Software-Engineering-Essentials-Flags-13eb678642418046bc42f61ee05ba703</id>
    <content type="html">&lt;html&gt;&lt;p&gt;&lt;i&gt;Software Engineering Essentials&lt;/i&gt; is a series of blog posts designed to help you get started with a wide variety of software engineering topics. This post was originally part of my &lt;a href=&#39;https://gitlab.com/samurailink3/go-twitter/&#39;&gt;Go-Twitter project&lt;/a&gt;, a project-based curriculum designed to take someone from “zero” to “competent” in the world of software engineering.&lt;/p&gt;&#xA;&lt;p&gt;All writing for this post is licensed under &lt;code&gt;CC-BY-4.0&lt;/code&gt;, while all code is licensed under the &lt;code&gt;MIT&lt;/code&gt; license.&lt;/p&gt;&#xA;&lt;h1&gt;Flags&lt;/h1&gt;&#xA;&lt;h2&gt;Flag Usage&lt;/h2&gt;&#xA;&lt;p&gt;Flags are effectively &lt;i&gt;named arguments&lt;/i&gt;, with some extra features. Instead of&#xA;needing to remember the order of your program&#39;s arguments, you can just specify&#xA;the name of the flag and what that variable should be set to. Let&#39;s take a look&#xA;at a simple example:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;01-simple-flags/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;flag&#34;&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;greeting := flag.String(&#34;greeting&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;name := flag.String(&#34;name&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;flag.Parse()&#xA;&#x9;fmt.Printf(&#34;%s, %s!\\n&#34;, *greeting, *name)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now, if we run this program:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go&#xA;  !&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Huh, that looks weird. This is because our &lt;code&gt;greeting&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; variables are&#xA;empty. We never specified the flags. How do we know what the flags are? Flags&#xA;have built-in help functionality that will display all of the options available.&#xA;Most programs use &lt;code&gt;-h&lt;/code&gt; or &lt;code&gt;--help&lt;/code&gt; to display program usage text, including&#xA;flags and arguments.&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -h&#xA;Usage of /tmp/go-build1913891522/b001/exe/main:&#xA;  -greeting string&#xA;&#xA;  -name string&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So now we can see that this program accepts the &lt;code&gt;-greeting&lt;/code&gt; and &lt;code&gt;-name&lt;/code&gt; flags.&#xA;Let&#39;s set some values:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting=&#34;Sup&#34; -name Tom&#xA;Sup, Tom!&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Flag arguments can be set with some optional grammar to help avoid problems with&#xA;ambiguity or spaces:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;code&gt;flag value&lt;/code&gt;&lt;/li&gt;&lt;li&gt;: For one-word values&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;code&gt;flag &#34;value with spaces or special characters&#34;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;: For multi-word values or&#xA;values that contain some special characters.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;code&gt;flag=&#34;value&#34;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;: This is the &lt;/li&gt;&lt;li&gt;&lt;i&gt;belt-and-suspenders&lt;/i&gt;&lt;/li&gt;&lt;li&gt; of flag declaration.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;We can also specify the flags in any order:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -name=&#34;General&#34; -greeting=&#34;Hello There&#34;&#xA;Hello There, General!&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Or not specify a value for &lt;code&gt;-name&lt;/code&gt; at all!&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting=&#34;Hello There&#34;&#xA;Hello There, !&#xA;&lt;/code&gt;&#xA;&lt;p&gt;We can even abuse the flags to make the program finish the meme:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting=&#34;General Kenobi\\!&#34; -name=&#34;You are a bold one&#34;&#xA;General Kenobi!, You are a bold one!&#xA;&lt;/code&gt;&#xA;&lt;h2&gt;Defaults&lt;/h2&gt;&#xA;&lt;p&gt;Let&#39;s re-create the program that inserted profiles into a database from the end&#xA;of the &lt;i&gt;Command Line Arguments&lt;/i&gt; chapter, but with flags instead:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;02-profile-saver/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;flag&#34;&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;&#34;, &#34;&#34;)&#xA;&#x9;flag.Parse()&#xA;&#xA;&#x9;fmt.Printf(`Profile Name: %s %s&#xA;&#x9;Rice or Beans: %s&#xA;&#x9;Favorites:&#xA;&#x9;&#x9;Fruit: %s&#xA;&#x9;&#x9;Vegetable: %s&#xA;&#x9;&#x9;Pasta: %s&#xA;&#x9;&#x9;Desert: %s&#xA;&#x9;&#x9;Dessert: %s&#xA;&#x9;&#x9;Meal: %s&#xA;`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Right now if we run this program with no flags set, we get this results:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go&#xA;Profile Name:&#xA;        Rice or Beans:&#xA;        Favorites:&#xA;                Fruit:&#xA;                Vegetable:&#xA;                Pasta:&#xA;                Desert:&#xA;                Dessert:&#xA;                Meal:&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Lots of blank spaces here. We don&#39;t want to commit empty values to the database,&#xA;it can introduce &lt;i&gt;ambiguity&lt;/i&gt;. Did this person not have a favorite pasta or did&#xA;we just forget to add the data? Being &lt;i&gt;explicit&lt;/i&gt; in programming has several&#xA;benefits, including clarity-of-intent, so let&#39;s add some &lt;i&gt;defaults&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Default values can provide the &lt;i&gt;usual&lt;/i&gt; options or placeholder data. Flags make&#xA;this easy. For now, let&#39;s just use &lt;code&gt;UNKNOWN&lt;/code&gt; as the profile placeholder. With&#xA;the stanadrd &lt;code&gt;flag&lt;/code&gt; package, setting defaults is easy. Let&#39;s take a look at the&#xA;GoDocs for &lt;code&gt;flag.String&lt;/code&gt;, available &lt;a href=&#39;https://pkg.go.dev/flag#String&#39;&gt;here&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;So the &lt;i&gt;second&lt;/i&gt; function argument allows us to specify a default value for that particular flag. Let&#39;s do that now:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;02-profile-saver/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;flag&#34;&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;flag.Parse()&#xA;&#xA;&#x9;fmt.Printf(`Profile Name: %s %s&#xA;&#x9;Rice or Beans: %s&#xA;&#x9;Favorites:&#xA;&#x9;&#x9;Fruit: %s&#xA;&#x9;&#x9;Vegetable: %s&#xA;&#x9;&#x9;Pasta: %s&#xA;&#x9;&#x9;Desert: %s&#xA;&#x9;&#x9;Dessert: %s&#xA;&#x9;&#x9;Meal: %s&#xA;`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now when we run this program without any flags set:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go&#xA;Profile Name: UNKNOWN UNKNOWN&#xA;        Rice or Beans: UNKNOWN&#xA;        Favorites:&#xA;                Fruit: UNKNOWN&#xA;                Vegetable: UNKNOWN&#xA;                Pasta: UNKNOWN&#xA;                Desert: UNKNOWN&#xA;                Dessert: UNKNOWN&#xA;                Meal: UNKNOWN&#xA;&lt;/code&gt;&#xA;&lt;p&gt;There we go, that at least looks more complete.&lt;/p&gt;&#xA;&lt;p&gt;Now let&#39;s actually use those flags to set some values... but wait, what were the&#xA;flags again? Let&#39;s use &lt;code&gt;-h&lt;/code&gt; to see what options we have:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h&#xA;Usage of /tmp/go-build261536590/b001/exe/main:&#xA;  -desert string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -dessert string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -first-name string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -fruit string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -last-name string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -meal string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -pasta string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -rice-or-beans string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -veg string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Ah, got it. Now let&#39;s actually use these:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -first-name Bob -last-name Sacamano -fruit Apple -veg &#34;Green Beans&#34; -pasta Manicotti -meal Steak -desert Sahara -dessert Sundae -ri&#xA;ce-or-beans Rice&#xA;Profile Name: Bob Sacamano&#xA;        Rice or Beans: Rice&#xA;        Favorites:&#xA;                Fruit: Apple&#xA;                Vegetable: Green Beans&#xA;                Pasta: Manicotti&#xA;                Desert: Sahara&#xA;                Dessert: Sundae&#xA;                Meal: Steak&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So now we&#39;re able to print out all of the profile information using flags. Its&#xA;still a lot of flags to remember, but at least we have the default help text and&#xA;the order of options doesn&#39;t matter.&lt;/p&gt;&#xA;&lt;h2&gt;Help Text&lt;/h2&gt;&#xA;&lt;p&gt;As we saw above, &lt;code&gt;-h&lt;/code&gt; is pretty helpful to understand which flags are available,&#xA;what type is expected, and what the defaults are, but we can add some more&#xA;context through &lt;i&gt;help text&lt;/i&gt;. As always, &lt;a href=&#39;https://pkg.go.dev/flag#String&#39;&gt;reading the documentation&lt;/a&gt; can help us find the right place to add help text to our flags. For&#xA;now, let&#39;s just add one piece of help text and see how it compares to the rest&#xA;of the flags:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;02-profile-saver/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;flag&#34;&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;UNKNOWN&#34;, &#34;the first name of the user profile&#34;)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;UNKNOWN&#34;, &#34;&#34;)&#xA;&#x9;flag.Parse()&#xA;&#xA;&#x9;fmt.Printf(`Profile Name: %s %s&#xA;&#x9;Rice or Beans: %s&#xA;&#x9;Favorites:&#xA;&#x9;&#x9;Fruit: %s&#xA;&#x9;&#x9;Vegetable: %s&#xA;&#x9;&#x9;Pasta: %s&#xA;&#x9;&#x9;Desert: %s&#xA;&#x9;&#x9;Dessert: %s&#xA;&#x9;&#x9;Meal: %s&#xA;`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now let&#39;s check out the help text:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h&#xA;Usage of /tmp/go-build1166983742/b001/exe/main:&#xA;  -desert string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -dessert string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -first-name string&#xA;        the first name of the user profile (default &#34;UNKNOWN&#34;)&#xA;  -fruit string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -last-name string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -meal string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -pasta string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -rice-or-beans string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;  -veg string&#xA;         (default &#34;UNKNOWN&#34;)&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Neat! Now let&#39;s add the rest:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;02-profile-saver/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;flag&#34;&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;UNKNOWN&#34;, &#34;the first name of the user profile&#34;)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;UNKNOWN&#34;, &#34;the last name of the user profile&#34;)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite fruit&#34;)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite vegetable&#34;)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite type of pasta&#34;)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s all-time favorite meal&#34;)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;UNKNOWN&#34;, &#34;whether the user prefers a side of rice or beans&#34;)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite desert (usually hot and sandy)&#34;)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite dessert (usually a sweet after-meal treat)&#34;)&#xA;&#x9;flag.Parse()&#xA;&#xA;&#x9;fmt.Printf(`Profile Name: %s %s&#xA;&#x9;Rice or Beans: %s&#xA;&#x9;Favorites:&#xA;&#x9;&#x9;Fruit: %s&#xA;&#x9;&#x9;Vegetable: %s&#xA;&#x9;&#x9;Pasta: %s&#xA;&#x9;&#x9;Desert: %s&#xA;&#x9;&#x9;Dessert: %s&#xA;&#x9;&#x9;Meal: %s&#xA;`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;And if we check out help text:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h&#xA;Usage of /tmp/go-build2677080627/b001/exe/main:&#xA;  -desert string&#xA;        the user&#39;s favorite desert (usually hot and sandy) (default &#34;UNKNOWN&#34;)&#xA;  -dessert string&#xA;        the user&#39;s favorite dessert (usually a sweet after-meal treat) (default &#34;UNKNOWN&#34;)&#xA;  -first-name string&#xA;        the first name of the user profile (default &#34;UNKNOWN&#34;)&#xA;  -fruit string&#xA;        the user&#39;s favorite fruit (default &#34;UNKNOWN&#34;)&#xA;  -last-name string&#xA;        the last name of the user profile (default &#34;UNKNOWN&#34;)&#xA;  -meal string&#xA;        the user&#39;s all-time favorite meal (default &#34;UNKNOWN&#34;)&#xA;  -pasta string&#xA;        the user&#39;s favorite type of pasta (default &#34;UNKNOWN&#34;)&#xA;  -rice-or-beans string&#xA;        whether the user prefers a side of rice or beans (default &#34;UNKNOWN&#34;)&#xA;  -veg string&#xA;        the user&#39;s favorite vegetable (default &#34;UNKNOWN&#34;)&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So much better. Now our program has clear flagged arguments, defaults set, and&#xA;some help text to explain what each flag is intended for. However, this could be&#xA;cleaned up a bit more. To clean up the help text, let&#39;s dig into &lt;i&gt;constants&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;h2&gt;Constants&lt;/h2&gt;&#xA;&lt;p&gt;&lt;i&gt;Constants&lt;/i&gt; work very similarly to variables, but with one critical difference:&#xA;They cannot be changed at runtime. Constants are set in your source code and&#xA;compiled-in, after that, the value cannot be changed. Constants are helpful to&#xA;document and organize variables that control large pieces of your application or&#xA;that are used in a variety of places, but the values won&#39;t change.&lt;/p&gt;&#xA;&lt;p&gt;Let&#39;s dig in with an example. Let&#39;s say you were making a program that talks to&#xA;a website to get information. The functions you write to talk to the website are&#xA;all going to use the same address over and over and over again. Instead of&#xA;typing &lt;code&gt;https://www.example.com&lt;/code&gt; over and over again, you can put this address&#xA;in a &lt;i&gt;constant declaration&lt;/i&gt; near the top of your file, then just reference it&#xA;using the constant name:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;const (&#xA;       website = &#34;&lt;https://www.example.com&gt;&#34;&#xA;)&#xA;&#xA;func main() {&#xA;       fmt.Println(website)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So constants work kinda like variables, but let&#39;s see what happens when we try&#xA;to change the value in our &lt;code&gt;main&lt;/code&gt; function:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;&#x9;&#34;fmt&#34;&#xA;)&#xA;&#xA;const (&#xA;&#x9;website = &#34;&lt;https://www.example.com&gt;&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;fmt.Println(website)&#xA;&#x9;website = &#34;A new value&#34;&#xA;&#x9;fmt.Println(website)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;When we run it, we get this:&lt;/p&gt;&#xA;&lt;code&gt;main.go:13:2: cannot assign to website (untyped string constant &#34;&lt;https://www.example.com&gt;&#34;)&#xA;&lt;/code&gt;&#xA;&lt;p&gt;The constants must remain constant. Trying to change them results in the program&#xA;refusing to build.&lt;/p&gt;&#xA;&lt;p&gt;So, constants can be useful to help avoid re-typing, but they can also help&#xA;generally clean up your code. Right now in our profile-saver program, we have a&#xA;lot of loose help text that we can clean up by moving those help strings from&#xA;the &lt;code&gt;flag.String&lt;/code&gt; functions into a constant declaration. So we can turn this:&lt;/p&gt;&#xA;&lt;code&gt;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;UNKNOWN&#34;, &#34;the first name of the user profile&#34;)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;UNKNOWN&#34;, &#34;the last name of the user profile&#34;)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite fruit&#34;)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite vegetable&#34;)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite type of pasta&#34;)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s all-time favorite meal&#34;)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;UNKNOWN&#34;, &#34;whether the user prefers a side of rice or beans&#34;)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite desert (usually hot and sandy)&#34;)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;UNKNOWN&#34;, &#34;the user&#39;s favorite dessert (usually a sweet after-meal treat)&#34;)&#xA;...&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Into this:&lt;/p&gt;&#xA;&lt;code&gt;const (&#xA;&#x9;firstNameHelp   = &#34;the first name of the user profile&#34;&#xA;&#x9;lastNameHelp    = &#34;the last name of the user profile&#34;&#xA;&#x9;fruitHelp       = &#34;the user&#39;s favorite fruit&#34;&#xA;&#x9;vegHelp         = &#34;the user&#39;s favorite vegetable&#34;&#xA;&#x9;pastaHelp       = &#34;the user&#39;s favorite type of pasta&#34;&#xA;&#x9;mealHelp        = &#34;the user&#39;s all-time favorite meal&#34;&#xA;&#x9;riceOrBeansHelp = &#34;whether the user prefers a side of rice or beans&#34;&#xA;&#x9;desertHelp      = &#34;the user&#39;s favorite desert (usually hot and sandy)&#34;&#xA;&#x9;dessertHelp     = &#34;the user&#39;s favorite dessert (usually a sweet after-meal treat)&#34;&#xA;)&#xA;&#xA;func main() {&#xA;&#x9;firstName := flag.String(&#34;first-name&#34;, &#34;UNKNOWN&#34;, firstNameHelp)&#xA;&#x9;lastName := flag.String(&#34;last-name&#34;, &#34;UNKNOWN&#34;, lastNameHelp)&#xA;&#x9;favFruit := flag.String(&#34;fruit&#34;, &#34;UNKNOWN&#34;, fruitHelp)&#xA;&#x9;favVeg := flag.String(&#34;veg&#34;, &#34;UNKNOWN&#34;, vegHelp)&#xA;&#x9;favPasta := flag.String(&#34;pasta&#34;, &#34;UNKNOWN&#34;, pastaHelp)&#xA;&#x9;favMeal := flag.String(&#34;meal&#34;, &#34;UNKNOWN&#34;, mealHelp)&#xA;&#x9;riceOrBeans := flag.String(&#34;rice-or-beans&#34;, &#34;UNKNOWN&#34;, riceOrBeansHelp)&#xA;&#x9;favDesert := flag.String(&#34;desert&#34;, &#34;UNKNOWN&#34;, desertHelp)&#xA;&#x9;favDessert := flag.String(&#34;dessert&#34;, &#34;UNKNOWN&#34;, dessertHelp)&#xA;...&#xA;&lt;/code&gt;&#xA;&lt;p&gt;There&#39;s still a lot of text, but its a bit more organized and easier to work&#xA;with. This is one of the nicest bonus features of constants: They make great&#xA;organizational tools. If you have certain aspects of your program that can be&#xA;tuned by yourself or another developer, it may be a good idea to put them in a&#xA;&lt;code&gt;const&lt;/code&gt; declaration at the top of the file so they&#39;re easy to access and&#xA;conveniently grouped.&lt;/p&gt;&#xA;&lt;p&gt;Its worth keeping in mind that not everything can be a constant. Due to the way&#xA;the Go compiler works, complex expressions cannot be pre-evaluated. Said another&#xA;way: Constants are &lt;i&gt;baked into&lt;/i&gt; your application when you run &lt;code&gt;go build&lt;/code&gt;.&#xA;According to the &lt;a href=&#39;https://go.dev/ref/spec#Constants&#39;&gt;Go language spec&lt;/a&gt;, the only&#xA;valid constant types are:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Runes (characters)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Strings&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Booleans (&lt;/li&gt;&lt;li&gt;&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;&lt;li&gt; or &lt;/li&gt;&lt;li&gt;&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;&lt;li&gt;)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Numeric types (&lt;/li&gt;&lt;li&gt;&lt;code&gt;int&lt;/code&gt;&lt;/li&gt;&lt;li&gt;s, &lt;/li&gt;&lt;li&gt;&lt;code&gt;float&lt;/code&gt;&lt;/li&gt;&lt;li&gt;s, etc...)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;If you want the same style of organization for types that don&#39;t work as&#xA;constants, you can use a &lt;code&gt;var&lt;/code&gt; declaration instead:&lt;/p&gt;&#xA;&lt;code&gt;var (&#xA;&#x9;aMap = map[int]string{&#xA;&#x9;&#x9;1: &#34;one&#34;,&#xA;&#x9;&#x9;2: &#34;two&#34;,&#xA;&#x9;&#x9;3: &#34;three&#34;,&#xA;&#x9;}&#xA;)&#xA;&lt;/code&gt;&#xA;&lt;h2&gt;Appendix and References&lt;/h2&gt;&#xA;&lt;h3&gt;References&lt;/h3&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://pkg.go.dev/flag#String&#39;&gt;GoDocs: flag.String&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://go.dev/ref/spec#Constants&#39;&gt;Go Language Reference: Constants&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;h3&gt;Appendix&lt;/h3&gt;&#xA;&lt;h3&gt;How Spaces Can Affect Flags&lt;/h3&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting Hello There -name General&#xA;Hello, !&#xA;&#xA;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting &#34;Hello There&#34; -name General&#xA;Hello There, General!&#xA;&#xA;go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting=&#34;Hello There&#34; -name General&#xA;Hello There, General!&#xA;&lt;/code&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Software-Engineering-Essentials-Flags-13eb678642418046bc42f61ee05ba703" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Software Engineering Essentials: Command Line Arguments</title>
    <updated>2024-11-13T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Software-Engineering-Essentials-Command-Line-Arguments-13eb6786424180fe83e5d259a20d28ca</id>
    <content type="html">&lt;html&gt;&lt;p&gt;&lt;i&gt;Software Engineering Essentials&lt;/i&gt; is a series of blog posts designed to help you get started with a wide variety of software engineering topics. This post was originally part of my &lt;a href=&#39;https://gitlab.com/samurailink3/go-twitter/&#39;&gt;Go-Twitter project&lt;/a&gt;, a project-based curriculum designed to take someone from “zero” to “competent” in the world of software engineering.&lt;/p&gt;&#xA;&lt;p&gt;All writing for this post is licensed under &lt;code&gt;CC-BY-4.0&lt;/code&gt;, while all code is licensed under the &lt;code&gt;MIT&lt;/code&gt; license.&lt;/p&gt;&#xA;&lt;h1&gt;Command Line Arguments&lt;/h1&gt;&#xA;&lt;h2&gt;What are command line arguments?&lt;/h2&gt;&#xA;&lt;p&gt;Command line arguments are one way to pass user input into your application.&#xA;Let&#39;s take a look at the common Unix utility &lt;code&gt;ls&lt;/code&gt;. &lt;code&gt;ls&lt;/code&gt; is a small application&#xA;that lists files and directories, just like double-clicking on a folder in&#xA;Windows Explorer, &lt;code&gt;ls&lt;/code&gt; allows you to peer into directories and get additional&#xA;information about files, like ownership information, size, last modification&#xA;timestamps, and more. &lt;code&gt;ls&lt;/code&gt;, like many command line applications, can take in&#xA;&lt;i&gt;arguments&lt;/i&gt;:&lt;/p&gt;&#xA;&lt;code&gt;ls some-directory&#xA;&lt;/code&gt;&#xA;&lt;p&gt;In this example &lt;code&gt;ls&lt;/code&gt; is the application you are running and &lt;code&gt;some-directory&lt;/code&gt; is&#xA;the &lt;i&gt;argument&lt;/i&gt;. What you&#39;re asking &lt;code&gt;ls&lt;/code&gt; to do is give you a listing of all files&#xA;and folders inside of &lt;code&gt;some-directory&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2&gt;How can my program use arguments?&lt;/h2&gt;&#xA;&lt;p&gt;Because programming is generally open-ended, you can utilize command line&#xA;arguments to control any aspect of your program. You could use an argument to&#xA;control which IP address and port your application listens for requests on, or&#xA;what to print, or if an operation should target your test or production&#xA;environments. Let&#39;s take a look at a simple example:&lt;/p&gt;&#xA;&lt;p&gt;Here&#39;s a simple Go program that will print a person&#39;s name and age on the&#xA;command line:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;01-simple-arguments/main.go&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import (&#xA;    &#34;fmt&#34;&#xA;    &#34;os&#34;&#xA;)&#xA;&#xA;func main() {&#xA;    fmt.Printf(&#34;My name is %s and my age is %s.&#34;, os.Args[1], os.Args[2])&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So if we run &lt;code&gt;go run example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go Bob 99&lt;/code&gt;, we&#39;ll see: &lt;code&gt;My name is Bob and my age is 99.&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;In this example, we use the &lt;code&gt;os&lt;/code&gt; package to get an array of arguments provided&#xA;on the command line. Arrays in Go start at &lt;code&gt;0&lt;/code&gt;, so we&#39;re actually printing the&#xA;second and third argument. The first argument is the location of the program&#xA;we&#39;re running. Since we&#39;re using &lt;code&gt;go run&lt;/code&gt;, it will be in a temporary,&#xA;auto-generated location. Let&#39;s add this block of code to our program and look at&#xA;each argument individually:&lt;/p&gt;&#xA;&lt;code&gt;fmt.Println(&#34;Our program&#39;s arguments:&#34;)&#xA;for i, arg := range os.Args {&#xA;    fmt.Printf(&#34;os.Args[%d]: &#39;%s&#39;\\n&#34;, i, arg)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;This code will print out each argument and its number:&lt;/p&gt;&#xA;&lt;code&gt;Our program&#39;s arguments:&#xA;os.Args[0]: &#39;/tmp/go-build201851007/b001/exe/main&#39;&#xA;os.Args[1]: &#39;Bob&#39;&#xA;os.Args[2]: &#39;99&#39;&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So &lt;code&gt;/tmp/go-build201851007/b001/exe/main&lt;/code&gt; is the temporary compiled version of&#xA;our program, made by the &lt;code&gt;go run&lt;/code&gt; command. The other arguments, &lt;code&gt;Bob&lt;/code&gt; and &lt;code&gt;99&lt;/code&gt;&#xA;are the arguments we provided on the command line.&lt;/p&gt;&#xA;&lt;p&gt;So, what happens if we provide less than 2 arguments to our program?&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go Bob&#xA;panic: runtime error: index out of range [2] with length 2&#xA;&#xA;goroutine 1 [running]:&#xA;main.main()&#xA;        /home/samurailink3/git/go-twitter/example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go:9 +0x21c&#xA;exit status 2&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Uh oh... &lt;code&gt;panic: runtime error: index out of range [2] with length 2&lt;/code&gt;... That&#xA;doesn&#39;t look good. What happened is we tried to access a variable that was&#xA;outside of the array. Here&#39;s a simple diagram of the &lt;code&gt;os.Args&lt;/code&gt; array and what we&#39;re trying to access:&lt;/p&gt;&#xA;&lt;code&gt;          Arg 0: Our program         Arg 1: Our provided name     Arg 2: What we&#39;re trying to access&#xA;                 |                           |                                     |&#xA;                 v                           v                                     v&#xA;[ &#34;/tmp/go-build201851007/b001/exe/main&#34;,  &#34;Bob&#34; ]&#xA;&lt;/code&gt;&#xA;&lt;p&gt;The &lt;code&gt;os.Args&lt;/code&gt; array only contains 2 items (&lt;code&gt;os.Args[0]&lt;/code&gt; and &lt;code&gt;os.Args[1]&lt;/code&gt;), but&#xA;we&#39;re trying to access something in the &lt;i&gt;third&lt;/i&gt; place (&lt;code&gt;os.Args[2]&lt;/code&gt;), beyond the&#xA;end of the array. This is an issue, so the code &lt;code&gt;panic&lt;/code&gt;s. How can we defend&#xA;against that? We want to avoid &lt;code&gt;panic&lt;/code&gt;ing and let the user know they&#39;re missing&#xA;a required field.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;len&lt;/code&gt; check:&lt;/p&gt;&#xA;&lt;code&gt;if len(os.Args) &lt; 3 {&#xA;    // Do something here&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;We can get the number of items present in the &lt;code&gt;os.Args&lt;/code&gt; array and do something&#xA;if we don&#39;t have enough. First, let&#39;s print out a helpful message for the user:&lt;/p&gt;&#xA;&lt;code&gt;if len(os.Args) &lt; 3 {&#xA;    fmt.Println(&#34;This program requires two arguments: Name and Age, you have&#34;, len(os.Args)-1)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;When we run this with only the first argument, we get:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go Bob&#xA;This program requires two arguments: Name and Age, you have 1&#xA;panic: runtime error: index out of range [2] with length 2&#xA;&#xA;goroutine 1 [running]:&#xA;main.main()&#xA;        /home/samurailink3/git/go-twitter/example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go:13 +0x29b&#xA;exit status 2&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Generally, we don&#39;t want to show panics to the user, so let&#39;s exit with a&#xA;non-successful error code (to learn more about error codes, check out &lt;a href=&#39;https://tldp.org/LDP/abs/html/exitcodes.html&#39;&gt;the&#xA;Advanced Bash-Scripting Guide&lt;/a&gt;):&lt;/p&gt;&#xA;&lt;code&gt;&#x9;if len(os.Args) &lt; 3 {&#xA;&#x9;&#x9;fmt.Println(&#34;This program requires two arguments: Name and Age, you have&#34;, len(os.Args)-1)&#xA;&#x9;&#x9;// See &#34;Advanced Bash-Scripting Guide: Exit Codes With Special Meanings&#34;&#xA;&#x9;&#x9;// in the References section for why we use &#34;2&#34; here.&#xA;&#x9;&#x9;os.Exit(2)&#xA;&#x9;}&#xA;&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now when we run our program with the missing argument, we get:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go Bob&#xA;This program requires two arguments: Name and Age, you have 1&#xA;exit status 2&#xA;&lt;/code&gt;&#xA;&lt;p&gt;That&#39;s &lt;i&gt;way&lt;/i&gt; cleaner. Now if we run it with the right number of arguments, we see:&lt;/p&gt;&#xA;&lt;code&gt;go run example-code/02-command-line-arguments-and-flags/01-command-line-arguments/01-simple-arguments/main.go Bob 99&#xA;My name is Bob and my age is 99.&#xA;Our program&#39;s arguments:&#xA;os.Args[0]: &#39;/tmp/go-build1771918657/b001/exe/main&#39;&#xA;os.Args[1]: &#39;Bob&#39;&#xA;os.Args[2]: &#39;99&#39;&#xA;&lt;/code&gt;&#xA;&lt;h2&gt;When should I use arguments?&lt;/h2&gt;&#xA;&lt;p&gt;Arguments are best used when an application has a limited number of distinct&#xA;options or your program operates on an unlimited number of the same argument.&#xA;Let&#39;s expand that previous statement:&lt;/p&gt;&#xA;&lt;p&gt;So &lt;code&gt;git&lt;/code&gt; is a great example of having a limited (and segmented) number of&#xA;distinct options. It can do &lt;strong&gt;a lot&lt;/strong&gt;, but the options are &lt;i&gt;segmented&lt;/i&gt; by&#xA;sub-command. So, something &lt;code&gt;git branch&lt;/code&gt; would have pretty simple arguments, like&#xA;&lt;code&gt;git branch -D branch-name&lt;/code&gt; and &lt;code&gt;git branch --track local-branch remote-branch&lt;/code&gt;.&#xA;There are a couple of arguments to keep in your head, not too bad.&lt;/p&gt;&#xA;&lt;p&gt;Another program we talked about earlier, &lt;code&gt;ls&lt;/code&gt;, is a great example of having an&#xA;unlimited number of the same argument. So you can run &lt;code&gt;ls&lt;/code&gt; with zero arguments,&#xA;which just shows the files and folders present in your current directory, but we&#xA;can also run &lt;code&gt;ls some-directory and-another-one but-also-a-third&lt;/code&gt; to see what&#39;s&#xA;inside those three distinct directories. &lt;code&gt;ls&lt;/code&gt; supports an unlimited number of&#xA;arguments, but each one does the same thing, so its not hard to remember or&#xA;think about.&lt;/p&gt;&#xA;&lt;p&gt;Let&#39;s say you had a program to insert profile information into a database. You&#xA;track the following information about people:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;First Name&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Last Name&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Fruit&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Vegetable&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Pasta&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Meal&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Rice or Beans&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Desert&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Favorite Dessert&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;Your application in use may look like this:&lt;/p&gt;&#xA;&lt;code&gt;new-profile Bob Sacamano Apple GreenBeans Manicotti Steak Sahara Sundae&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Your users will need to keep this whole list in their heads &lt;i&gt;in order&lt;/i&gt;. That&#39;s a&#xA;pretty bad user experience and its likely that someone will forget a field or&#xA;worse, get the order of the arguments wrong. This would add data into the wrong&#xA;fields in your database. There are too many options to keep track of here,&#xA;arguments aren&#39;t a good choice for this program. Case-in-point: The above&#xA;example forgets the &#34;Rice or Beans&#34; field. Did you catch it?&lt;/p&gt;&#xA;&lt;p&gt;This application would be perfect for &lt;i&gt;flags&lt;/i&gt;, so we&#39;ll cover that in the next&#xA;section.&lt;/p&gt;&#xA;&lt;h2&gt;Appendix and References&lt;/h2&gt;&#xA;&lt;h3&gt;References&lt;/h3&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://tldp.org/LDP/abs/html/exitcodes.html&#39;&gt;Advanced Bash-Scripting Guide: Exit Codes With Special Meanings&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Software-Engineering-Essentials-Command-Line-Arguments-13eb6786424180fe83e5d259a20d28ca" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Software Engineering Essentials: Working with GitLab (and GitHub)</title>
    <updated>2024-11-12T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Software-Engineering-Essentials-Working-with-GitLab-and-GitHub-13eb67864241803aa952e2900ea5517a</id>
    <content type="html">&lt;html&gt;&lt;p&gt;&lt;i&gt;Software Engineering Essentials&lt;/i&gt; is a series of blog posts designed to help you get started with a wide variety of software engineering topics. This post was originally part of my &lt;a href=&#39;https://gitlab.com/samurailink3/go-twitter/&#39;&gt;Go-Twitter project&lt;/a&gt;, a project-based curriculum designed to take someone from “zero” to “competent” in the world of software engineering.&lt;/p&gt;&#xA;&lt;p&gt;All writing for this post is licensed under &lt;code&gt;CC-BY-4.0&lt;/code&gt;, while all code is licensed under the &lt;code&gt;MIT&lt;/code&gt; license.&lt;/p&gt;&#xA;&lt;h1&gt;GitLab&lt;/h1&gt;&#xA;&lt;h2&gt;Branch Etiquette&lt;/h2&gt;&#xA;&lt;p&gt;Git makes working in groups much easier than manually copying and pasting code&#xA;together from multiple sources. A centralized Git hosting service can make it&#xA;even easier. Most of the time when multiple people work on a project, changes&#xA;are committed to feature-specific branches. These can be bug-fixes, new&#xA;features, refactors, or anything else. The important thing to keep in mind is to&#xA;keep your changes organized in the proper branches.&lt;/p&gt;&#xA;&lt;p&gt;If you&#39;re working on a bug-fix on a branch named &lt;code&gt;front-page-css-bugfix&lt;/code&gt;, don&#39;t&#xA;pollute that branch with unnecessary changes like fixing test cases in an&#xA;unrelated function. The changes in a branch should correspond to that branch. If&#xA;you &lt;i&gt;really&lt;/i&gt; want to do that work and save it somewhere, feel free to create a&#xA;new branch. Branches are free and easy to use, feel free to make new ones as you&#xA;need.&lt;/p&gt;&#xA;&lt;h2&gt;Merge Requests&lt;/h2&gt;&#xA;&lt;p&gt;Merge Requests (or Pull Requests in GitHub lingo) are a mechanism in most Git&#xA;hosting services designed to make it easier to handle merging in new changes.&#xA;While Git has a lot of tools for managing changes and merges, the web interface&#xA;is a bit easier to get started with for newcomers.&lt;/p&gt;&#xA;&lt;p&gt;More important than the change itself is the Code Review process that merge&#xA;requests assist with. Chances are, your merge request won&#39;t be accepted&#xA;immediately and without question. Most of the time, we have something to fix or&#xA;something to change. The merge request view allows other team members to comment&#xA;on your diff and even highlight individual lines or blocks of changes to leave&#xA;comments. If you&#39;ve ever used the &#34;Track Changes&#34; feature of Microsoft Office&#xA;product, you&#39;ll be well-acquainted with this interface.&lt;/p&gt;&#xA;&lt;p&gt;When a merge request is accepted, your commits in the merge request will be&#xA;applied to the branch your request targets (usually the main development&#xA;branch). Then, whenever people fetch and pull changes, your commits will be&#xA;pulled down to their local repositories. This keeps everyone in lock-step with&#xA;each other while you all work on the same codebase.&lt;/p&gt;&#xA;&lt;p&gt;Your old branches should be deleted once merged. This isn&#39;t a technical&#xA;requirement, it just keeps things clean.&lt;/p&gt;&#xA;&lt;h2&gt;Protecting Branches&lt;/h2&gt;&#xA;&lt;p&gt;Because the main development branch is used for collaboration and keeping&#xA;everyone on the project on the same page, you usually don&#39;t want to commit&#xA;changes directly to it. Instead, you commit changes to the main development&#xA;branch through &lt;i&gt;merge requests&lt;/i&gt;. This ensures that the commits people are adding&#xA;are reviewed and communicated before they are added to the main branch.&lt;/p&gt;&#xA;&lt;p&gt;Most Git hosting services have a feature that will allow you to &lt;i&gt;protect&lt;/i&gt;&#xA;certain branches. Protecting a branch can prevent things like deletion and/or&#xA;directly pushing changes onto that branch. Instead changes are added in through&#xA;merge requests.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Software-Engineering-Essentials-Working-with-GitLab-and-GitHub-13eb67864241803aa952e2900ea5517a" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Software Engineering Essentials: Version Control with Git</title>
    <updated>2024-11-11T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Software-Engineering-Essentials-Version-Control-with-Git-13eb678642418056af79fd6fb24d3107</id>
    <content type="html">&lt;html&gt;&lt;p&gt;&lt;i&gt;Software Engineering Essentials&lt;/i&gt; is a series of blog posts designed to help you get started with a wide variety of software engineering topics. This post was originally part of my &lt;a href=&#39;https://gitlab.com/samurailink3/go-twitter/&#39;&gt;Go-Twitter project&lt;/a&gt;, a project-based curriculum designed to take someone from “zero” to “competent” in the world of software engineering.&lt;/p&gt;&#xA;&lt;p&gt;All writing for this post is licensed under &lt;code&gt;CC-BY-4.0&lt;/code&gt;, while all code is licensed under the &lt;code&gt;MIT&lt;/code&gt; license.&lt;/p&gt;&#xA;&lt;h1&gt;Git&lt;/h1&gt;&#xA;&lt;h2&gt;What is Git?&lt;/h2&gt;&#xA;&lt;p&gt;&#34;Git is a free and open source distributed version control system designed to&#xA;handle everything from small to very large projects with speed and efficiency.&#34;&lt;/p&gt;&#xA;&lt;p&gt;Git is an application designed to manage your source code and make it easy to&#xA;keep version history, but why would you want this? Programming gets messy,&#xA;especially in large projects. Let&#39;s say you&#39;re testing an experimental bugfix,&#xA;you may be tempted to copy/paste your code into a different folder and name it,&#xA;&lt;code&gt;my-project_bugfix&lt;/code&gt;, but what happens when you have &lt;code&gt;my-project_bugfix2&lt;/code&gt; or&#xA;&lt;code&gt;my-project_bugfix_real&lt;/code&gt;. Copying around folders of source code might work&#xA;initially, but will quickly get messy, especially if you need to work with other&#xA;people. Git provides a system to help keep your code organized, easy to work&#xA;with, and easy to collaborate on.&lt;/p&gt;&#xA;&lt;p&gt;A Git repository is essentially a place where you can store files to track and&#xA;share changes to those files. Git repositories (or &lt;i&gt;repos&lt;/i&gt;, for short) are&#xA;usually used to store and track changes to source code files.&lt;/p&gt;&#xA;&lt;h2&gt;Installing Git&lt;/h2&gt;&#xA;&lt;p&gt;Git is available on a wide variety of platforms and you can find an installer&#xA;&lt;a href=&#39;https://git-scm.com/download/&#39;&gt;here&lt;/a&gt;. There are some options to pick during setup:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Select Components:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Default&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Choosing the default editor used by Git:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Use Visual Studio Code as Git&#39;s&#xA;default editor&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Adjusting the name of the initial branch in new repositories:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Override the&#xA;default branch name for new repositories: &lt;/li&gt;&lt;li&gt;&lt;code&gt;main&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Adjusting your PATH environment:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Git from the command line and also from&#xA;3rd-party software&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Choosing the SSH executable:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Use external OpenSSH&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Choosing HTTPS transport backend:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; UUse the OpenSSL library&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Configuring the line ending conversions:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Checkout as-is, commit as-is&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Configuring the terminal emulator to use with Git Bash:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Use MinTTY (the&#xA;default terminal of MSYS2)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Choose the default behavior of &lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;git pull&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Only ever fast-forward&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Choose a credential helper:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Git Credential Manager&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Configuring extra options:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Check both &lt;/li&gt;&lt;li&gt;&lt;i&gt;Enable file system caching&lt;/i&gt;&lt;/li&gt;&lt;li&gt; and&#xA;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Enable symbolic links&lt;/i&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Configuring experimental options:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Leave all options unchecked&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;h2&gt;Configuring SSH and Setting Up Your GitLab Account&lt;/h2&gt;&#xA;&lt;p&gt;First &lt;a href=&#39;https://gitlab.com/users/sign_up&#39;&gt;register for GitLab&lt;/a&gt;. Next, we need to create an SSH&#xA;key so we can authenticate to GitLab.&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Press &lt;/li&gt;&lt;li&gt;&lt;code&gt;Windows+R&lt;/code&gt;&lt;/li&gt;&lt;li&gt; to bring up the &#34;Run&#34; dialog box.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Type &lt;/li&gt;&lt;li&gt;&lt;code&gt;powershell&lt;/code&gt;&lt;/li&gt;&lt;li&gt; and press &lt;/li&gt;&lt;li&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/li&gt;&lt;li&gt; to open up the PowerShell prompt.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Type &lt;/li&gt;&lt;li&gt;&lt;code&gt;ssh-keygen&lt;/code&gt;&lt;/li&gt;&lt;li&gt; and press &lt;/li&gt;&lt;li&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/li&gt;&lt;li&gt; to start the key creation process&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://gitlab.com/-/profile/keys&#39;&gt;Click here&lt;/a&gt; to go to your GitLab User Settings to&#xA;manage your SSH keys. In the &lt;i&gt;Add an SSH key&lt;/i&gt; section, copy and paste the entire&#xA;&lt;i&gt;public key file&lt;/i&gt; from before into the &lt;i&gt;Key&lt;/i&gt; text box, then click &lt;i&gt;Add key&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Now bring up the &lt;code&gt;powershell&lt;/code&gt; window you had open before and use the &lt;code&gt;ssh-add&lt;/code&gt;&#xA;command. Type your key passphrase and press &lt;code&gt;Enter&lt;/code&gt;. Remember: Your passphrase&#xA;will not be shown here as you type it.&lt;/p&gt;&#xA;&lt;p&gt;This command will add your key to an &lt;code&gt;ssh-agent&lt;/code&gt;. This program just makes it&#xA;easier to use your key. Instead of needing to type your passphrase every time&#xA;you use the key, you just add it to the agent and use the passphrase only once.&#xA;You&#39;ll need to do this each time the computer reboots.&lt;/p&gt;&#xA;&lt;p&gt;You can see what keys the agent has with the command: &lt;code&gt;ssh-add -l&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;You can remove keys from the agent, thereby &#34;re-locking&#34; them, with the command:&#xA;&lt;code&gt;ssh-add -D&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h2&gt;How Git views the World&lt;/h2&gt;&#xA;&lt;h3&gt;A Brief Introduction to Hashes&lt;/h3&gt;&#xA;&lt;p&gt;Git&#39;s view of the world is through &lt;i&gt;hashes&lt;/i&gt;. A hash function is a way to &#34;boil&#xA;down&#34; data to a fixed-length value. This sounds complex, but its simple in&#xA;practice. Let&#39;s look at the current hash of this file I&#39;m writing now:&lt;/p&gt;&#xA;&lt;code&gt;~ sha1sum Handouts/01-source-code-version-control-and-hosting/01-git.md&#xA;0b4597c1d9294a633ea584d715ce6681e57cb82a  Handouts/01-source-code-version-control-and-hosting/01-git.md&#xA;&lt;/code&gt;&#xA;&lt;p&gt;But as I write more, change the content, and save, the hash will change:&lt;/p&gt;&#xA;&lt;code&gt;~ sha1sum Handouts/01-source-code-version-control-and-hosting/01-git.md&#xA;8393d5018367212dcfe4fa6e477fdf5deff28576  Handouts/01-source-code-version-control-and-hosting/01-git.md&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So hashing is a way to take data of any amount and get a fixed-length value that&#xA;is unique to that data&#39;s content. Some practical uses involve things like &lt;i&gt;error&#xA;checking, data deduplication, and protection against corruption or tampering&lt;/i&gt;.&#xA;Git uses hashes to identify individual objects like files, directories,&#xA;references, commits, and more.&lt;/p&gt;&#xA;&lt;h3&gt;Diffs on Diffs on Diffs&lt;/h3&gt;&#xA;&lt;p&gt;Git heavily utilizes the concept of &lt;code&gt;diff&lt;/code&gt;s as a way to understand the&#xA;differences between files (and multiple historical versions of the same file).&#xA;This makes merges easier to manage and makes for more efficient file storage.&lt;/p&gt;&#xA;&lt;p&gt;Let&#39;s check out &lt;code&gt;diff&lt;/code&gt; in action:&lt;/p&gt;&#xA;&lt;p&gt;I have two files, &lt;code&gt;a.txt&lt;/code&gt; and &lt;code&gt;b.txt&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;p&gt;a.txt:&lt;/p&gt;&#xA;&lt;code&gt;Test&#xA;This is file a&#xA;&lt;/code&gt;&#xA;&lt;p&gt;b.txt:&lt;/p&gt;&#xA;&lt;code&gt;Test&#xA;This is file b&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Here&#39;s what I get when I run &lt;code&gt;diff a.txt b.txt&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;2c2&#xA;&lt; This is file a&#xA;---&#xA;&gt; This is file b&#xA;&lt;/code&gt;&#xA;&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; only shows us the differences between the files. Git can use this to&#xA;figure out what changed between different objects. A &lt;i&gt;commit&lt;/i&gt; in Git is like&#xA;taking a snapshot of the diff of the entire repository. As you make more&#xA;commits, you are creating historical records of what changed in that repository&#xA;at that moment in time. By playing the diffs in reverse, you can effectively&#xA;wind back the clock and travel to past, undoing changes.&lt;/p&gt;&#xA;&lt;p&gt;So how does git &#34;play back&#34; those diffs? This is an operation called &lt;i&gt;patching&lt;/i&gt;.&#xA;A patch is a series of changes intended to be applied to an existing system.&lt;/p&gt;&#xA;&lt;p&gt;So if I had a program and someone found a typo: &lt;code&gt;hello.go&lt;/code&gt;&lt;/p&gt;&#xA;&lt;code&gt;package main&#xA;&#xA;import &#34;fmt&#34;&#xA;&#xA;func main() {&#xA;&#x9;fmt.Println(&#34;henlo world&#34;)&#xA;}&#xA;&lt;/code&gt;&#xA;&lt;p&gt;They could send me a small patch to fix it:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;typo-fix.patch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;code&gt;--- hello.go&#x9;2023-01-01 21:25:05.694853862 +0000&#xA;+++ hello.go&#x9;2023-01-01 21:26:15.863385402 +0000&#xA;@@ -3,5 +3,5 @@&#xA; import &#34;fmt&#34;&#xA;&#xA; func main() {&#xA;-&#x9;fmt.Println(&#34;henlo world&#34;)&#xA;+&#x9;fmt.Println(&#34;hello world&#34;)&#xA; }&#xA;&lt;/code&gt;&#xA;&lt;p&gt;And I could use &lt;code&gt;patch hello.go typo-fix.patch&lt;/code&gt; or &lt;code&gt;git apply typo-fix.patch&lt;/code&gt; to&#xA;apply the diff. Now my application will print &lt;code&gt;hello world&lt;/code&gt;!&lt;/p&gt;&#xA;&lt;p&gt;A Git repository is just hashed objects and diffs stacked on top of diffs.&lt;/p&gt;&#xA;&lt;h3&gt;Extra Reading&lt;/h3&gt;&#xA;&lt;p&gt;If you enjoyed this section, feel free to check out &lt;a href=&#39;https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain&#39;&gt;Git Internals&lt;/a&gt; chapter in the official book for an ever deeper dive.&lt;/p&gt;&#xA;&lt;h2&gt;Using Git on the Command Line&lt;/h2&gt;&#xA;&lt;p&gt;Git is an open source application and there are a ton of products and graphical&#xA;interfaces designed to make working with it easier. While these can work well in&#xA;some straight-forward circumstances, they all tend to fall apart pretty badly&#xA;when things go off the &#34;happy path&#34;, and &lt;a href=&#39;https://xkcd.com/1597/&#39;&gt;it can be difficult to get back to a&#xA;good state&lt;/a&gt;. For this reason, we&#39;ll focus on using the command-line&#xA;version of Git. Once you&#39;re comfortable with this interface, you can absolutely&#xA;use any Git-compatible application you want, including &lt;a href=&#39;https://code.visualstudio.com/docs/sourcecontrol/overview&#39;&gt;VS Code&#39;s excellent Git&#xA;integration&lt;/a&gt;, just don&#39;t skip learning the CLI basics first.&lt;/p&gt;&#xA;&lt;h3&gt;Project Creation&lt;/h3&gt;&#xA;&lt;p&gt;A Git repository is just a folder with another &lt;code&gt;.git&lt;/code&gt; folder inside of it. In&#xA;the &lt;code&gt;.git&lt;/code&gt; folder are a bunch of directories and files that Git uses to make&#xA;sense of your codebase. These include hashed objects, diffs, and metadata used&#xA;to keep things organized and human-compatible. You shouldn&#39;t need to dive into&#xA;this directory or change anything.&lt;/p&gt;&#xA;&lt;p&gt;The command to instantiate a new Git repository on your local system is: &lt;code&gt;git init my-project-name&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Once you run this command, you&#39;ll need a folder named &lt;code&gt;my-project-name&lt;/code&gt;. All of&#xA;your code, as well as anything you want to keep under version control (like&#xA;documentation files), should go here.&lt;/p&gt;&#xA;&lt;h3&gt;Cloning an Existing Project&lt;/h3&gt;&#xA;&lt;p&gt;Git is a distributed system, which means projects don&#39;t have to live purely on&#xA;your local system. Many programmers utilize a Git-hosting service to store and&#xA;share their code with other contributors. These include services like&#xA;&lt;a href=&#39;https://github.com/&#39;&gt;GitHub&lt;/a&gt;, &lt;a href=&#39;https://about.gitlab.com/&#39;&gt;GitLab&lt;/a&gt;, and &lt;a href=&#39;https://bitbucket.org/product&#39;&gt;Bitbucket&lt;/a&gt;. Don&#39;t worry&#xA;about signing up for any of these services just yet, the next section will walk&#xA;you through the basics of &lt;a href=&#39;https://about.gitlab.com/&#39;&gt;GitLab&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Git hosting services give you an easy way to interact with other contributors,&#xA;report and accept issues, manage merge requests, and most importantly: Share&#xA;your code with the world. Its important to understand that Git is a distributed&#xA;system &lt;i&gt;by default&lt;/i&gt;, you don&#39;t &lt;i&gt;need&lt;/i&gt; these hosting sites to share or&#xA;collaborate on code, but they do make it easier.&lt;/p&gt;&#xA;&lt;p&gt;&lt;i&gt;You can think about Git hosting services like YouTube or Vimeo. You can&#xA;absolutely share video files with people or even make your own video hosting&#xA;service, but using someone else&#39;s service is so much easier. It is important to&#xA;keep in mind that Git is not GitHub. GitHub can be used to host a Git&#xA;repository, just like YouTube can be used to host a video. The hosting service&#xA;and the core technology are two different things.&lt;/i&gt;&lt;/p&gt;&#xA;&lt;p&gt;Most Git hosting sites will give you one or two &lt;i&gt;Clone&lt;/i&gt; addresses you can use to&#xA;copy down the project locally. Most often these addresses are &lt;i&gt;SSH&lt;/i&gt; and/or&#xA;&lt;i&gt;HTTPS&lt;/i&gt; links. You can use these addresses in this command:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git clone &lt;https://github.com/torvalds/linux.git&lt;/code&gt;&gt;&lt;/p&gt;&#xA;&lt;p&gt;or like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git clone git@github.com:torvalds/linux.git&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;i&gt;Typically&lt;/i&gt;, an &lt;code&gt;https&lt;/code&gt; repository link is going to be &lt;i&gt;read-only&lt;/i&gt;. You can pull&#xA;down changes, but won&#39;t be able to push your own changes (unless you set up&#xA;HTTPS authentication). An &lt;code&gt;ssh&lt;/code&gt; repository link (the one that looks like&#xA;&lt;code&gt;git@gitlab&lt;/code&gt;) is &lt;i&gt;typically&lt;/i&gt; going to be &lt;i&gt;read+write&lt;/i&gt;. You will be able to push&#xA;your changes to the remote repository.&lt;/p&gt;&#xA;&lt;p&gt;This will clone the repository hosted on &lt;i&gt;GitHub&lt;/i&gt;, by the user &lt;i&gt;torvalds&lt;/i&gt;, with&#xA;the project name &lt;i&gt;linux&lt;/i&gt;, into a directory on your local system called &lt;i&gt;linux&lt;/i&gt;.&#xA;After you run that command, you can browse the Linux source code on your own&#xA;system. Because Git is decentralized, you obtain a &lt;i&gt;full copy&lt;/i&gt; of the source&#xA;code repository, including history, commits, branches, tags, and other Git&#xA;metadata. If you want, you can see what the code looked like last year, or even&#xA;before then. You can see when certain features were merged in, or when a certain&#xA;version-number was tagged.&lt;/p&gt;&#xA;&lt;p&gt;One thing to keep in mind is some data found on the Git host site &lt;i&gt;isn&#39;t&#xA;included with the repository&lt;/i&gt;. These would be things like merge request&#xA;discussions, issues and bug reports, and project milestones and planning&#xA;features. By default, Git does not track these items in the repository, so they&#xA;are instead hosted directly by the Git hosting service.&lt;/p&gt;&#xA;&lt;h3&gt;Viewing and Committing Changes&lt;/h3&gt;&#xA;&lt;p&gt;Git&#39;s version control isn&#39;t automated, users need to manually make &#34;checkpoints&#34;&#xA;of their changes to let Git know &#34;these changes are important to track&#34;. In Git,&#xA;these &#34;checkpoints&#34; are called &lt;i&gt;commits&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A &lt;i&gt;commit&lt;/i&gt; is a way to tell Git &#34;here are the changes I want to track&#34;. Not&#xA;everything in a repository folder is automatically tracked. We need to tell Git&#xA;what files we want to track. For now though, let&#39;s see how Git see&#39;s the world:&lt;/p&gt;&#xA;&lt;p&gt;In this example, I have a new repository and I&#39;ve created my first file with&#xA;some &#34;Hello, World!&#34; text in it. I can use &lt;code&gt;git status&lt;/code&gt; to see what Git thinks&#xA;of the files in the repository:&lt;/p&gt;&#xA;&lt;code&gt;~/git/my-project-name git status&#xA;On branch main&#xA;&#xA;No commits yet&#xA;&#xA;Untracked files:&#xA;  (use &#34;git add &lt;file&gt;...&#34; to include in what will be committed)&#xA;        my-first-file.txt&#xA;&#xA;nothing added to commit but untracked files present (use &#34;git add&#34; to track)&gt;&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Git sees a new file that it has never tracked before: &lt;code&gt;my-first-file.txt&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, let&#39;s follow Git&#39;s helpful advice, add the file, and re-run the status&#xA;command to see what changes:&lt;/p&gt;&#xA;&lt;code&gt;~/git/my-project-name git add my-first-file.txt&#xA;~/git/my-project-name git status&#xA;On branch main&#xA;&#xA;No commits yet&#xA;&#xA;Changes to be committed:&#xA;  (use &#34;git rm --cached &lt;file&gt;...&#34; to unstage)&#xA;        new file:   my-first-file.txt&#xA;&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now it looks like we have added the file to our &lt;i&gt;staging area&lt;/i&gt;, but we haven&#39;t&#xA;committed. We&#39;re only preparing the files for commit, we haven&#39;t made the&#xA;&#34;checkpoint&#34; yet. To do that, let&#39;s use &lt;code&gt;git commit&lt;/code&gt;. By default, a text editor&#xA;will pop up or display in your terminal. This is used for writing a &lt;i&gt;commit&#xA;message&lt;/i&gt;. A &lt;i&gt;commit message&lt;/i&gt; is what will be logged with your code changes into&#xA;Git history. So if you&#39;re working on a shared project, what you write here will&#xA;be viewable by anyone with access to the repository.&lt;/p&gt;&#xA;&lt;p&gt;Writing good commit messages is an art form and practice makes perfect. Try to&#xA;include enough context with your messages, so a &#34;code detective&#34; down the line&#xA;can figure out what your change did without needing to read the code itself.&#xA;Keep in mind that &lt;strong&gt;you&lt;/strong&gt; will be that &#34;code detective&#34; one day, and &lt;strong&gt;the code&#xA;you&#39;ll be digging into may very well be your own&lt;/strong&gt;. Trying to track down a bug&#xA;and running into a commit that just says &#34;fix&#34; is infuriating. Make it easy on&#xA;yourself, write good commit messages.&lt;/p&gt;&#xA;&lt;p&gt;A good Git commit message explains &lt;i&gt;what changed&lt;/i&gt; and &lt;i&gt;why&lt;/i&gt;. The first line is&#xA;reserved for a title, the second line skipped, and the third line and beyond is&#xA;for your longer write-up. This is so &lt;code&gt;git log --oneline&lt;/code&gt; and other Git tools can&#xA;display text nicely. This isn&#39;t a hard rule though, people can (and will)&#xA;disregard good commit message etiquette, the software will work with it all the&#xA;same. &lt;a href=&#39;https://zachholman.com/posts/git-commit-history/&#39;&gt;Zach Holman has a great post about commit messages and why they just&#xA;don&#39;t matter&lt;/a&gt;, so there are&#xA;multiple schools of thought on the subject.&lt;/p&gt;&#xA;&lt;p&gt;Back to the example. So I&#39;ve written this as my commit message:&lt;/p&gt;&#xA;&lt;code&gt;Added my-first-file.txt to use in a Git example&#xA;&#xA;I&#39;ve added this file with default text in order to show Git examples&#xA;using commit messages.&#xA;&lt;/code&gt;&#xA;&lt;p&gt;I saved the file and exited the text editor. Git then uses this to construct a&#xA;commit. It returned this text:&lt;/p&gt;&#xA;&lt;code&gt;[main (root-commit) 3fa8cef] Added my-first-file.txt to use in a Git example&#xA; 1 file changed, 1 insertion(+)&#xA; create mode 100644 my-first-file.txt&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now I can use &lt;code&gt;git log&lt;/code&gt; to see my history:&lt;/p&gt;&#xA;&lt;code&gt;commit 3fa8cefb1c854a550158a58d6443ec2b27e957fe (HEAD -&gt; main)&#xA;Author: Tom Webster &lt;tom@samurailink3.com&gt;&#xA;Date:   Wed Jan 4 00:17:48 2023 +0000&#xA;&#xA;    Added my-first-file.txt to use in a Git example&#xA;&#xA;    I&#39;ve added this file with default text in order to show Git examples&#xA;    using commit messages.&#xA;&lt;/code&gt;&#xA;&lt;p&gt;To get a more compact view, you can use &lt;code&gt;git log --oneline&lt;/code&gt; and that will&#xA;display your commit titles in a list:&lt;/p&gt;&#xA;&lt;code&gt;3fa8cef (HEAD -&gt; main) Added my-first-file.txt to use in a Git example&#xA;&lt;/code&gt;&#xA;&lt;p&gt;You may have noticed that long string of jumbled text above:&#xA;&lt;code&gt;3fa8cefb1c854a550158a58d6443ec2b27e957fe&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;This is a &lt;i&gt;commit ID&lt;/i&gt;. A &lt;code&gt;commit ID&lt;/code&gt; is a hash used to identify the change&#xA;you&#39;ve added to history. Since hashes are designed to be unique, we only need to&#xA;display enough characters to identify a single commit, we don&#39;t need to display&#xA;the full ID from above, we can just display &lt;code&gt;3fa8cef&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A simple example of this is like an ID system with too many digits. Let&#39;s say&#xA;you have a list of the best fruits, but each ID has 10 digits, so you end up&#xA;with a table like this:&lt;/p&gt;&#xA;&lt;code&gt;Fruit       - ID Number&#xA;&#xA;Apple       - 0000000001&#xA;Orange      - 0000000002&#xA;Banana      - 0000000003&#xA;Strawberry  - 0000000004&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So much wasted space in that ID number. If we think we&#39;ll have less than 100&#xA;fruits in this list, we can just use 2 digits to display the ID instead of the&#xA;full 10:&lt;/p&gt;&#xA;&lt;code&gt;Fruit       - ID Number&#xA;&#xA;Apple       - 01&#xA;Orange      - 02&#xA;Banana      - 03&#xA;Strawberry  - 04&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Hashes in Git operate in mostly the same way: It will give you the shortest&#xA;unambiguous hash, but the minimum number of digits is &lt;strong&gt;4&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;h3&gt;Git Remotes&lt;/h3&gt;&#xA;&lt;p&gt;Git Remotes are &lt;i&gt;remote repositories&lt;/i&gt;. Because Git is a distributed system, you&#xA;can have multiple remote repositories you can read from or write to. If you&#xA;cloned this course repository from GitLab, you&#39;ll probably see this when you run&#xA;&lt;code&gt;git remote -v&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;code&gt;origin  &lt;https://gitlab.com/samurailink3/go-twitter&gt; (fetch)&#xA;origin  &lt;https://gitlab.com/samurailink3/go-twitter&gt; (push)&#xA;&lt;/code&gt;&#xA;&lt;p&gt;By convention, the first remote added to a Git repository is named &lt;i&gt;origin&lt;/i&gt;, but&#xA;this is only a general convention, not a hard rule.&lt;/p&gt;&#xA;&lt;p&gt;You can have multiple remotes and custom rules for pushing and pulling changes&#xA;for each of them if you want. Most often you&#39;ll just have one remote, hosted by&#xA;one of the popular Git hosting services. This setup will allow you to push&#xA;changes to an online repository and/or pull changes that have been pushed, but&#xA;you haven&#39;t pulled to your local repository.&lt;/p&gt;&#xA;&lt;p&gt;If your repository was created on your local system only, without cloning from a&#xA;Git hosting service, you won&#39;t have any remotes configured initially. You can&#xA;always add a remote with:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git remote add remote-name remote-address&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git remote add origin git@gitlab.com:samurailink3/go-twitter.git&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h3&gt;Pulling Changes&lt;/h3&gt;&#xA;&lt;p&gt;Git has two different methods to pull changes into your local repository:&#xA;&lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;pull&lt;/code&gt;. Fetching changes pulls down new objects, diffs, and&#xA;references from a Git remote down to your local repository. &lt;i&gt;Its important to&#xA;keep keep in mind that fetching won&#39;t change files in your local workspace.&lt;/i&gt; The&#xA;changes are instead cached as part of that Git remote.&lt;/p&gt;&#xA;&lt;p&gt;To fetch changes from the &lt;code&gt;origin&lt;/code&gt; remote, you can run:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git fetch origin&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you&#39;d like to fetch changes from &lt;i&gt;all&lt;/i&gt; remotes, you can use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git fetch --all&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;To actually update files in your local workspace, you would use &lt;code&gt;git pull&lt;/code&gt;. This&#xA;will import changes from the configured remote branch into your local branch.&#xA;This means things like viewing history or checking diffs is near-instantaneous&#xA;because all of the changes are cached locally, Git doesn&#39;t need to go back to&#xA;the internet to get more data. Once you fetch, those changes are stored locally.&lt;/p&gt;&#xA;&lt;h3&gt;Pushing Changes&lt;/h3&gt;&#xA;&lt;p&gt;Once you have commits you would like to push to your configured remote, you can&#xA;use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git push remote-name branch-name&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git push origin main&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;This tells Git, &#34;Take my local commits and push them to the &lt;code&gt;origin&lt;/code&gt; remote on&#xA;the &lt;code&gt;main&lt;/code&gt; branch. Git will automatically figure out which commits are missing&#xA;on the remote and push only what&#39;s needed to make them match.&lt;/p&gt;&#xA;&lt;h3&gt;Branches and Checkouts&lt;/h3&gt;&#xA;&lt;p&gt;Branches in Git are a way to create other &#34;timelines&#34; of history in your&#xA;repository. So, if your main development branch is &lt;code&gt;main&lt;/code&gt;, but you want to work&#xA;on a complex feature without introducing bugs or breakages on the &lt;code&gt;main&lt;/code&gt; branch,&#xA;you can create a new branch named &lt;code&gt;my-new-feature&lt;/code&gt; and start committing changes&#xA;to that branch. By default, the first branch of a new Git repository is called&#xA;&lt;code&gt;main&lt;/code&gt;, or in older versions of Git, &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;To see a list of branches in your local repository, you can use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git branch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;To see &lt;i&gt;all&lt;/i&gt; branches, even those present on remotes, you can use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git branch --all&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;To create a new branch, you can just use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git branch my-new-branch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now, if you look at your list of branches with &lt;code&gt;git branch&lt;/code&gt;, you&#39;ll see it in&#xA;your list of local branches:&lt;/p&gt;&#xA;&lt;code&gt;* main&#xA;  my-new-branch&#xA;&lt;/code&gt;&#xA;&lt;p&gt;But if you look, you can see the &lt;code&gt;*&lt;/code&gt; next to &lt;code&gt;main&lt;/code&gt;, and if you run &lt;code&gt;git status&lt;/code&gt;, you&#39;ll see that you&#39;re still on the &lt;code&gt;main&lt;/code&gt; branch:&lt;/p&gt;&#xA;&lt;code&gt;On branch main&#xA;&lt;/code&gt;&#xA;&lt;p&gt;So how do we switch branches?&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git checkout my-new-branch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now when you run &lt;code&gt;git status&lt;/code&gt;, you&#39;ll see:&lt;/p&gt;&#xA;&lt;code&gt;On branch my-new-branch&#xA;nothing to commit, working tree clean&#xA;&lt;/code&gt;&#xA;&lt;p&gt;Now, when you commit changes, you&#39;ll be committing to &lt;code&gt;my-new-branch&lt;/code&gt; instead of&#xA;&lt;code&gt;main&lt;/code&gt;. To get back to the &lt;code&gt;main&lt;/code&gt; branch, run &lt;code&gt;git checkout main&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;To save time, you can create and check out a new branch with one command:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git checkout -b my-new-branch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you&#39;d like to base your branch on a specific branch, you can use:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git checkout -b my-new-branch main&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;or&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git checkout -b my-new-branch origin/some-other-branch&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h3&gt;Additional Reading and Git Workflows&lt;/h3&gt;&#xA;&lt;p&gt;There are many strategies and theories on when you should make new branches and&#xA;how you should organize them. When starting out: simpler is better. I&#39;d&#xA;recommend reading about &lt;a href=&#39;https://docs.github.com/en/get-started/quickstart/github-flow&#39;&gt;GitHub Flow&lt;/a&gt;. The quick reference for this&#xA;workflow is:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Create a new branch for your specific change&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Make your commits&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Push your branch to your remote&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Create a merge request (called a Pull Request in GitHub&#39;s terminology)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;If you&#39;re working in a group: Get sign off from your team and address any&#xA;comments or requests&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Merge your changes&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Delete your branch&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;If you are working on software that needs multiple versions and maintenance&#xA;releases (like operating systems or desktop software), something a bit more&#xA;complex, like &lt;a href=&#39;https://nvie.com/posts/a-successful-git-branching-model/&#39;&gt;git-flow&lt;/a&gt;, could help.&lt;/p&gt;&#xA;&lt;h2&gt;Appendix and References&lt;/h2&gt;&#xA;&lt;h3&gt;References&lt;/h3&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://git-scm.com/&#39;&gt;The Git Website&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://git-scm.com/download/&#39;&gt;Download Git&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository&#39;&gt;Git Basics&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://www.codecademy.com/learn/learn-git-introduction&#39;&gt;Codecademy Interactive Git Course&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://gitlab.com/users/sign_up&#39;&gt;Register for GitLab&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://gitlab.com/-/profile/keys&#39;&gt;GitLab Profile SSH Keys&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain&#39;&gt;Git Internals&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://xkcd.com/1597/&#39;&gt;XKCD: Git&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://code.visualstudio.com/docs/sourcecontrol/overview&#39;&gt;VS Code: Git&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://github.com/&#39;&gt;GitHub&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://about.gitlab.com/&#39;&gt;GitLab&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://bitbucket.org/product&#39;&gt;Bitbucket&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://zachholman.com/posts/git-commit-history/&#39;&gt;Zach Holman: Utter Disregard for Git Commit History&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://docs.github.com/en/get-started/quickstart/github-flow&#39;&gt;GitHub Flow&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;a href=&#39;https://nvie.com/posts/a-successful-git-branching-model/&#39;&gt;git-flow&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Software-Engineering-Essentials-Version-Control-with-Git-13eb678642418056af79fd6fb24d3107" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Halloween 2024 Data Analysis</title>
    <updated>2024-11-04T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Halloween-2024-Data-Analysis-131b6786424180bc906ee3a5984d251c</id>
    <content type="html">&lt;html&gt;&lt;h1&gt;“OH ITS THAT HOUSE!!”&lt;/h1&gt;&#xA;&lt;p&gt;For the third year in a row, we’ve been the “Full-Sized Candy Bar House” in our neighborhood. We have always done rough inventory and after-the-fact tracking on Google Sheets, but that was mostly to guide our spending for next year. This year I wanted to try something a bit over-engineered, thus &lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Using Notion and a pair of linked databases I was able to build a Trick or Treat inventory. I then built a “Selections” database. Each time a kid took a piece of candy, I cut a new DB record with the type. This automatically kicked off inventory deductions on the Inventory database and regenerated the graphs. After adding in some metadata, we are able to slice and dice the data to get some interesting insights into which candy was taken and &lt;i&gt;when&lt;/i&gt; (each record has a “created at” timestamp, so we can do &lt;i&gt;temporal analysis&lt;/i&gt; as well).&lt;/p&gt;&#xA;&lt;p&gt;If you’d like to dig into this data yourself, feel free:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt; &lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#39;https://www.samurailink3.comUntitled&#39;&gt;Untitled&lt;/a&gt; &lt;/p&gt;&#xA;&lt;h1&gt;Notable Data Points&lt;/h1&gt;&#xA;&lt;p&gt;Let’s dig into some of the more interesting data points.&lt;/p&gt;&#xA;&lt;h2&gt;Headline Stats&lt;/h2&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Total Selections:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; 137 (roughly 46 trick-or-treat-ers per hour)&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Most Popular Type:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Fruit-type Candy&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Most Popular Brand:&lt;/strong&gt;&lt;/li&gt;&lt;li&gt; Skittles&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Most Popular Item (number of selections): &lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Airheads Xtremes - Rainbow Berry&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;h2&gt;Popular (and unpopular) Picks&lt;/h2&gt;&#xA;&lt;p&gt;Sour candy seems to be a big driver (~34% of all picks) with simpler chocolate-types following closely behind:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;Its worth noting that both Airheads varieties were the first to be exhausted (aka: “sold out”). A treat containing peanuts or peanut butter seemed to lower its popularity. This follows along with other anecdotal data from previous years.&lt;/p&gt;&#xA;&lt;p&gt;Simple chocolate-type candy tended to win out over “more complicated” varieties. Things like plain Hershey’s bars and Crunch bars were more popular than Snickers or Twix. This also follows trends from the previous two years.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Speaking of “exhausted”, here’s what we ran out of:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&lt;i&gt;(which kinda bummed me out because I wanted at least one Crunch bar to be left over)&lt;/i&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Some treats got picked &lt;i&gt;just once&lt;/i&gt;, glad someone found their favorite thing:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;Some treats just didn’t get picked &lt;i&gt;at all&lt;/i&gt;:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;We wanted to trial out cookies and chips to give chaperones a chance to grab something, but in the very rare case an adult did pick something, it was usually a classic candy bar (eg: Snickers). Our Cookie/Chip experiment was a &lt;i&gt;resounding failure&lt;/i&gt;:&lt;/p&gt;&#xA;&#xA;&lt;h1&gt;What’s Next?&lt;/h1&gt;&#xA;&lt;p&gt;This data-collection exercise was extremely entertaining (and will absolutely help inform purchasing decisions for next year), but there were &lt;i&gt;small problems&lt;/i&gt;. Manual data collection doesn’t seem that hard, but when you have a group of 10 kids crowd around a small table and grab items simultaneously, it can get hard to track. I’d like to automate this.&lt;/p&gt;&#xA;&lt;p&gt;I’m not sure if that will involve something like weighed candy bowls or a vending machine or what, but I’m kicking around some ideas with friends. In any case, we’ll likely end up doing similar data collection next year, just to compare.&lt;/p&gt;&#xA;&lt;hr/&gt;&#xA;&lt;h3&gt;🦋 Interact with this post on Bluesky:&lt;/h3&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#39;https://bsky.app/profile/samurailink3.com/post/3lb3vky7op222&#39;&gt;https://bsky.app/profile/samurailink3.com/post/3lb3vky7op222&lt;/a&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&#xA;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Halloween-2024-Data-Analysis-131b6786424180bc906ee3a5984d251c" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Rom Browser</title>
    <updated>2024-10-24T00:00:00Z</updated>
    <id>https://www.samurailink3.com/Rom-Browser-127b678642418023b287de17e9d1fe12</id>
    <content type="html">&lt;html&gt;&lt;p&gt;aka: TAHMS RAHMS&lt;/p&gt;&#xA;&lt;p&gt;I finished a project over the weekend: Rom Browser - A simple web app designed to let my friends easily browse rom collections.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/3d3f45d3-73eb-4532-8d87-81450dd5f2c3/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466ZYFPRD4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110145Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIQDTSCtM5Vut95%2FqFy2d4ftawTLJxuY7I5bzeY3RATqPzwIgOcVfBupMuawbD%2Ffp8RN6g2FM8C2qBILxpZgAMw22PGMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDOb1IzK7mCOukktrsSrcA3p8FOO6o%2Fidj2V5r%2Ft%2FswcL1xm%2Belllh1bRhCm%2FWuFnlJJUxbZW7G1Icqq4wUW4moDUE%2BAkHBM9etn1LhfxnpvZdMg6oo%2F7y4ZovtSNS5xK2AqKkilbAGj6vKkISvrHO4eQOSAYwbimVlUGE9GPHRjqlMpS9bHtrGniEoLM2%2FzAM6OdK8utoKGCeDIJOHdlhBf52UGecdI6ig9SW61rjd91jbLmTY4Jx%2FykiAAQeJ8G5VtHyMuFaNS8kdAJ3AWRHTaCal6HGEETuOtS%2BCayOqOdPiiDZ0OkmOGrOWeF2AePI5rSxF4oJG6S8d7lgDQH156oujRb9iyJciNc%2FvO7UAsDamEoAeIzu67PQ1XWYj9lXBLeYBbq%2FPbsc7shfpOYiKGriISCDnDjL5a%2BDxcxyNltVQWfPAvcw8EBqDWyxFxOJSG7CVQIE%2FceA1292LThEr4iPZkfrkfsevZfuBXzit3Xxm662szgX7EFA0S%2FWiHzcXyfG5JjS7RF23voasbXOT7a8G263wfV7ls4Zw0dDYoltFoGl2XqhML03oMIsiPsVWLaG5QoTdTaIEBa0RqrqZ91M6YN3sr7boer0JM9s1JcQlfTCGlK0DtqAR%2FpURbmftRLZToacWWYdYBdMPaA%2FM8GOqUB5l4an44F5WzA3J97txTgbCMG9R11chOg3Bgybj9qTTTpHRJsN%2F2lKV%2FShxjIR02rVx18eRuHg%2FQ7ae8TAf1WjM4n96nzJTyVTvR27udMekapxmrCs4Rb%2FufO6miXlg0Nd%2BPB3VMdFvnaEYek7b%2FdClb9geQuL0hap%2F7a6hPe38ILOWZGHiyjtz%2BvXeRBSPpbxQyiwOTHN0JwN98Yjhjn0TQU%2FTco&amp;X-Amz-Signature=8eb4d707d60b2d103545cd85483d56b2bce2b27a30866bbd81709a97d5f5ca9e&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/3d3f45d3-73eb-4532-8d87-81450dd5f2c3/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466ZYFPRD4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110145Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIQDTSCtM5Vut95%2FqFy2d4ftawTLJxuY7I5bzeY3RATqPzwIgOcVfBupMuawbD%2Ffp8RN6g2FM8C2qBILxpZgAMw22PGMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDOb1IzK7mCOukktrsSrcA3p8FOO6o%2Fidj2V5r%2Ft%2FswcL1xm%2Belllh1bRhCm%2FWuFnlJJUxbZW7G1Icqq4wUW4moDUE%2BAkHBM9etn1LhfxnpvZdMg6oo%2F7y4ZovtSNS5xK2AqKkilbAGj6vKkISvrHO4eQOSAYwbimVlUGE9GPHRjqlMpS9bHtrGniEoLM2%2FzAM6OdK8utoKGCeDIJOHdlhBf52UGecdI6ig9SW61rjd91jbLmTY4Jx%2FykiAAQeJ8G5VtHyMuFaNS8kdAJ3AWRHTaCal6HGEETuOtS%2BCayOqOdPiiDZ0OkmOGrOWeF2AePI5rSxF4oJG6S8d7lgDQH156oujRb9iyJciNc%2FvO7UAsDamEoAeIzu67PQ1XWYj9lXBLeYBbq%2FPbsc7shfpOYiKGriISCDnDjL5a%2BDxcxyNltVQWfPAvcw8EBqDWyxFxOJSG7CVQIE%2FceA1292LThEr4iPZkfrkfsevZfuBXzit3Xxm662szgX7EFA0S%2FWiHzcXyfG5JjS7RF23voasbXOT7a8G263wfV7ls4Zw0dDYoltFoGl2XqhML03oMIsiPsVWLaG5QoTdTaIEBa0RqrqZ91M6YN3sr7boer0JM9s1JcQlfTCGlK0DtqAR%2FpURbmftRLZToacWWYdYBdMPaA%2FM8GOqUB5l4an44F5WzA3J97txTgbCMG9R11chOg3Bgybj9qTTTpHRJsN%2F2lKV%2FShxjIR02rVx18eRuHg%2FQ7ae8TAf1WjM4n96nzJTyVTvR27udMekapxmrCs4Rb%2FufO6miXlg0Nd%2BPB3VMdFvnaEYek7b%2FdClb9geQuL0hap%2F7a6hPe38ILOWZGHiyjtz%2BvXeRBSPpbxQyiwOTHN0JwN98Yjhjn0TQU%2FTco&amp;X-Amz-Signature=8eb4d707d60b2d103545cd85483d56b2bce2b27a30866bbd81709a97d5f5ca9e&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;h1&gt;Why? What? Where can I get it?&lt;/h1&gt;&#xA;&lt;p&gt;I &lt;i&gt;fucking love&lt;/i&gt; classic games. Whatever “classic gaming” means to you, love it, hell yeah, 10/10, can’t get enough. I love the idea of shared and open culture, freedom of information, and a more limited copyright system. I love roms, emulation, and game preservation. Now that you know where I’m coming from, let’s talk about the &lt;i&gt;why&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A lot of people I know spent a very long time collecting and organizing rom packs for a wide variety of systems. Conveniently, I recently purchased a storage server for my homelab:&lt;/p&gt;&#xA;&lt;a href=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/6bd5d215-4eb2-4961-993f-e84269fe69d5/Server.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466ZYFPRD4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110145Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIQDTSCtM5Vut95%2FqFy2d4ftawTLJxuY7I5bzeY3RATqPzwIgOcVfBupMuawbD%2Ffp8RN6g2FM8C2qBILxpZgAMw22PGMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDOb1IzK7mCOukktrsSrcA3p8FOO6o%2Fidj2V5r%2Ft%2FswcL1xm%2Belllh1bRhCm%2FWuFnlJJUxbZW7G1Icqq4wUW4moDUE%2BAkHBM9etn1LhfxnpvZdMg6oo%2F7y4ZovtSNS5xK2AqKkilbAGj6vKkISvrHO4eQOSAYwbimVlUGE9GPHRjqlMpS9bHtrGniEoLM2%2FzAM6OdK8utoKGCeDIJOHdlhBf52UGecdI6ig9SW61rjd91jbLmTY4Jx%2FykiAAQeJ8G5VtHyMuFaNS8kdAJ3AWRHTaCal6HGEETuOtS%2BCayOqOdPiiDZ0OkmOGrOWeF2AePI5rSxF4oJG6S8d7lgDQH156oujRb9iyJciNc%2FvO7UAsDamEoAeIzu67PQ1XWYj9lXBLeYBbq%2FPbsc7shfpOYiKGriISCDnDjL5a%2BDxcxyNltVQWfPAvcw8EBqDWyxFxOJSG7CVQIE%2FceA1292LThEr4iPZkfrkfsevZfuBXzit3Xxm662szgX7EFA0S%2FWiHzcXyfG5JjS7RF23voasbXOT7a8G263wfV7ls4Zw0dDYoltFoGl2XqhML03oMIsiPsVWLaG5QoTdTaIEBa0RqrqZ91M6YN3sr7boer0JM9s1JcQlfTCGlK0DtqAR%2FpURbmftRLZToacWWYdYBdMPaA%2FM8GOqUB5l4an44F5WzA3J97txTgbCMG9R11chOg3Bgybj9qTTTpHRJsN%2F2lKV%2FShxjIR02rVx18eRuHg%2FQ7ae8TAf1WjM4n96nzJTyVTvR27udMekapxmrCs4Rb%2FufO6miXlg0Nd%2BPB3VMdFvnaEYek7b%2FdClb9geQuL0hap%2F7a6hPe38ILOWZGHiyjtz%2BvXeRBSPpbxQyiwOTHN0JwN98Yjhjn0TQU%2FTco&amp;X-Amz-Signature=cd71355ddce465d043c8ae325419e47a9ee002752dbb6fa7497d3690d8b2261d&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39;&gt;&lt;img style =&#39;width:50vw;&#39; src=&#39;https://prod-files-secure.s3.us-west-2.amazonaws.com/6882aefd-0704-4f0d-86a6-e9f8000b81d4/6bd5d215-4eb2-4961-993f-e84269fe69d5/Server.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;X-Amz-Credential=ASIAZI2LB466ZYFPRD4C%2F20260509%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20260509T110145Z&amp;X-Amz-Expires=3600&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBoaCXVzLXdlc3QtMiJHMEUCIQDTSCtM5Vut95%2FqFy2d4ftawTLJxuY7I5bzeY3RATqPzwIgOcVfBupMuawbD%2Ffp8RN6g2FM8C2qBILxpZgAMw22PGMqiAQI4%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDOb1IzK7mCOukktrsSrcA3p8FOO6o%2Fidj2V5r%2Ft%2FswcL1xm%2Belllh1bRhCm%2FWuFnlJJUxbZW7G1Icqq4wUW4moDUE%2BAkHBM9etn1LhfxnpvZdMg6oo%2F7y4ZovtSNS5xK2AqKkilbAGj6vKkISvrHO4eQOSAYwbimVlUGE9GPHRjqlMpS9bHtrGniEoLM2%2FzAM6OdK8utoKGCeDIJOHdlhBf52UGecdI6ig9SW61rjd91jbLmTY4Jx%2FykiAAQeJ8G5VtHyMuFaNS8kdAJ3AWRHTaCal6HGEETuOtS%2BCayOqOdPiiDZ0OkmOGrOWeF2AePI5rSxF4oJG6S8d7lgDQH156oujRb9iyJciNc%2FvO7UAsDamEoAeIzu67PQ1XWYj9lXBLeYBbq%2FPbsc7shfpOYiKGriISCDnDjL5a%2BDxcxyNltVQWfPAvcw8EBqDWyxFxOJSG7CVQIE%2FceA1292LThEr4iPZkfrkfsevZfuBXzit3Xxm662szgX7EFA0S%2FWiHzcXyfG5JjS7RF23voasbXOT7a8G263wfV7ls4Zw0dDYoltFoGl2XqhML03oMIsiPsVWLaG5QoTdTaIEBa0RqrqZ91M6YN3sr7boer0JM9s1JcQlfTCGlK0DtqAR%2FpURbmftRLZToacWWYdYBdMPaA%2FM8GOqUB5l4an44F5WzA3J97txTgbCMG9R11chOg3Bgybj9qTTTpHRJsN%2F2lKV%2FShxjIR02rVx18eRuHg%2FQ7ae8TAf1WjM4n96nzJTyVTvR27udMekapxmrCs4Rb%2FufO6miXlg0Nd%2BPB3VMdFvnaEYek7b%2FdClb9geQuL0hap%2F7a6hPe38ILOWZGHiyjtz%2BvXeRBSPpbxQyiwOTHN0JwN98Yjhjn0TQU%2FTco&amp;X-Amz-Signature=cd71355ddce465d043c8ae325419e47a9ee002752dbb6fa7497d3690d8b2261d&amp;X-Amz-SignedHeaders=host&amp;x-amz-checksum-mode=ENABLED&amp;x-id=GetObject&#39; alt=&#39;Unfortunately, the Notion API does not expose Alt-Text, so please open the full link if you need that.&#39;&gt;&lt;/img&gt;&lt;/a&gt;&lt;br/&gt;&#xA;&lt;p&gt;So now my friends had a place to back up their collections. But we wanted more than just backups, we wanted an easy way to share these collections. At first, I tried issuing everyone S3 &lt;a href=&#39;https://min.io/&#39;&gt;MinIO&lt;/a&gt; keys, but, believe it or not, most people don’t use the AWS CLI on a daily basis. They wanted something easier to browse.&lt;/p&gt;&#xA;&lt;p&gt;Thus, &lt;i&gt;Rom Browser&lt;/i&gt;. I’ve needed to get deeper into frontend development for a while, and this project was just complicated enough to get really hands-on with something.&lt;/p&gt;&#xA;&lt;p&gt;So… where can you get it? You &lt;i&gt;can’t. &lt;/i&gt;This project is &lt;i&gt;extremely specific&lt;/i&gt; to my particular hosting setup. From networking to the folder structures, its just not generic enough to be easily hosted. There are a few useful things to share that I’ll post below, but right now there isn’t any linked GitLab project. So this isn’t a &lt;i&gt;project announcement&lt;/i&gt;, its just a &lt;i&gt;blog post&lt;/i&gt;.&lt;/p&gt;&#xA;&lt;h1&gt;The Requirements&lt;/h1&gt;&#xA;&lt;h2&gt;Private Access&lt;/h2&gt;&#xA;&lt;p&gt;I’m not &lt;i&gt;trying&lt;/i&gt; to get in trouble, and hosting a roms site on the open internet is an easy way to do that. This needs to be Wireguard-only, completely private network.&lt;/p&gt;&#xA;&lt;h2&gt;Simple Backend, Learn Frontend&lt;/h2&gt;&#xA;&lt;p&gt;This project shouldn’t be overly-complex on the backend. I have plenty of experience there, what I need to concentrate on is building the site itself. The backend can be dumb and shortcut-filled in an effort to prioritize frontend-learning.&lt;/p&gt;&#xA;&lt;h2&gt;Provide a Good User Experience&lt;/h2&gt;&#xA;&lt;p&gt;The entire point of this project is to be more usable than the AWS CLI. The site should be intuitive and make users happy. Use domain names, not IPs. Use HTTPS to avoid scary browser warnings.&lt;/p&gt;&#xA;&lt;h2&gt;Make MinIO Transparent&lt;/h2&gt;&#xA;&lt;p&gt;The user should never be aware that they’re interacting with MinIO. I won’t use pre-signed URLs or bucket rules to provide direct access. This will run through a pass-through downloader.&lt;/p&gt;&#xA;&lt;h1&gt;The Tech&lt;/h1&gt;&#xA;&lt;p&gt;Rom Browser is broken up into three components:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;Data Exporter - This just lists every object in the bucket, marshals the folder structure into struct variables, then exports this big-ass list of games as a JSON file.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;File Server - This handles distributing that JSON cache file and serving rom downloads to users.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Frontend - This takes in that games cache file, then provides several useful views into the games data complete with download links.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Some of the benefits of this design are:&lt;/p&gt;&#xA;&lt;ul&gt;&lt;li&gt;No MinIO keys in the frontend, no chance of leaks.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Users that have MinIO access can independently add new roms to the collection and this will be reflected on the site automatically when Data Exporter runs.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;No need for a complicated backend database scheme or anything. Its just a 5MB JSON blob. Not ideal, but good enough for casual use.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;ul&gt;&lt;li&gt;Keeps me focused on the frontend and user experience, not on backend optimization.&lt;/li&gt;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;For the frontend, I decided to try out &lt;a href=&#39;https://vuejs.org/&#39;&gt;https://vuejs.org/&lt;/a&gt;. I didn’t need the complexity of React, but wanted something a bit opinionated because I just don’t know enough about frontend development to have strong opinions yet.&lt;/p&gt;&#xA;&lt;h1&gt;Networking, Certificates, and Architecture&lt;/h1&gt;&#xA;&lt;p&gt;This site would be hosted on Wireguard at a private &lt;code&gt;10.x.x.x&lt;/code&gt; address. To support my “Good UX” goal, I bound this &lt;code&gt;10.&lt;/code&gt; address to a DNS name. Then, there was a new problem: HTTP-only sites give scary warnings in modern browsers. If I really want to provide the best user experience, I need HTTPS. I love &lt;a href=&#39;https://letsencrypt.org/&#39;&gt;Let’s Encrypt&lt;/a&gt;, but had only ever used it for internet-accessible sites. Never for anything internal.&lt;/p&gt;&#xA;&lt;p&gt;After some poking around, I found &lt;a href=&#39;https://go-acme.github.io/lego/&#39;&gt;LEGO&lt;/a&gt;, a Let’s Encrypt client that supports ACME DNS challenges. I plugged in my DNS-provider’s API key and it was able to issue a cert for my internal site without a problem. I then wrote a simple bash script to automate the renewal process and move the certs into the correct location.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;The Data Exporter would be cron’d out to run once per hour. Doing the full list command, even on the local network, takes a bit over a minute. That’s &lt;i&gt;far too long&lt;/i&gt; to make a user wait. So we need to run this ahead of time and cache the result. To avoid needing to write any sync-scripts to keep the cache file consistent across container volumes, I just &lt;a href=&#39;https://www.redhat.com/en/blog/hard-links-linux&#39;&gt;hard-linked&lt;/a&gt; it.&lt;/p&gt;&#xA;&lt;p&gt;The File Server is pretty simple. It listens for any requests to &lt;code&gt;/download&lt;/code&gt;, then calls the appropriate handler. If the client asks for &lt;code&gt;/download/games.json&lt;/code&gt;, the File Server reads the cache file into memory and provides it to the user. For anything else, it checks the path against a filename map to ensure it is in the dataset, downloads the object into memory, then hands it to the user. This helps obfuscate-away MinIO.&lt;/p&gt;&#xA;&lt;p&gt;The frontend is just a simple &lt;a href=&#39;https://vuejs.org/&#39;&gt;https://vuejs.org/&lt;/a&gt; site and uses &lt;a href=&#39;https://get.foundation/index.html&#39;&gt;Foundation&lt;/a&gt; for the styling. All of this is served from a single domain and backed by a Let’s Encrypt cert.&lt;/p&gt;&#xA;&lt;h1&gt;Cool Stuff&lt;/h1&gt;&#xA;&lt;h2&gt;Use Caddy to bind multiple containers under same domain&lt;/h2&gt;&#xA;&lt;p&gt;I discovered that you can use Caddy to bind multiple servers together under one domain, then just switch who gets the request based on the path. In the example below, I’m serving the site through the frontend container, but any calls to &lt;code&gt;emulation.example.com/download/&lt;/code&gt; is passed through to the File Server container.&lt;/p&gt;&#xA;&lt;code&gt;https://emulation.example.com {&#xA;        tls /etc/caddy/certificates/emulation.example.com.crt /etc/caddy/certificates/emulation.example.com.key&#xA;        reverse_proxy 10.1.2.3:3000&#xA;        reverse_proxy /download/* 10.1.2.3:8080&#xA;}&lt;/code&gt;&#xA;&lt;h2&gt;Use Dockerfile to compile and containerize your software&lt;/h2&gt;&#xA;&lt;p&gt;I wanted to just &lt;code&gt;git push&lt;/code&gt; from my local machine and actually build the software on my server, but I didn’t want to clutter it up with a bunch of NodeJS stuff. Luckily, Docker makes this process pretty clean and easy. You can actually use a multi-step Dockerfile to build &lt;i&gt;and then&lt;/i&gt; containerize your software. Here’s an example for a simple Go program:&lt;/p&gt;&#xA;&lt;code&gt;FROM golang:1 AS builder&#xA;WORKDIR /opt/builder&#xA;COPY . ./&#xA;RUN CGO_ENABLED=0 GOOS=linux go build&#xA;&#xA;FROM alpine:latest&#xA;EXPOSE 8080&#xA;COPY --from=builder /opt/builder/fileserver /bin/fileserver&#xA;CMD [&#34;/bin/fileserver&#34;, &#34;-config&#34;, &#34;/data/config.toml&#34;]&lt;/code&gt;&#xA;&lt;p&gt;And here’s what I used for the frontend:&lt;/p&gt;&#xA;&lt;code&gt;FROM node:23 AS builder&#xA;WORKDIR /opt/builder&#xA;COPY . ./&#xA;RUN npm install&#xA;RUN npm run build&#xA;&#xA;FROM nginx:latest&#xA;EXPOSE 80&#xA;COPY --from=builder /opt/builder/dist/ /usr/share/nginx/html/&lt;/code&gt;&#xA;&lt;p&gt;I then wrote a bash file to build all components in a single command.&lt;/p&gt;&#xA;&lt;h1&gt;So… did it work? Was this better than the AWS CLI?&lt;/h1&gt;&#xA;&lt;p&gt;My friends absolutely loved it. The site is easy to use, fast, and provided me with a perfect opportunity to jump into frontend development. I’m extremely happy with VueJS, Foundation, &lt;a href=&#39;https://www.truenas.com/&#39;&gt;TrueNAS&lt;/a&gt;, MinIO, and the work I put into gluing all these together.&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;p&gt;Until next time, thanks for reading!&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Rom-Browser-127b678642418023b287de17e9d1fe12" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>Notion Calendar Exporter Update: Assignees in Title</title>
    <updated>2024-10-19T16:08:00-05:00</updated>
    <id>https://www.samurailink3.com/Notion-Calendar-Exporter-Update-Assignees-in-Title-124b6786424180768397fc676ba832fa</id>
    <content type="html">&lt;html&gt;&lt;p&gt;I’ve made updates to my &lt;a href=&#39;https://www.samurailink3.comNotion Calendar Exporter&#39;&gt;Notion Calendar Exporter&lt;/a&gt; project: You can now elect to show the names of assignees in the event title.&lt;/p&gt;&#xA;&lt;p&gt;Before, your event would look like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;Build PC&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;But now it can be:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;[Tom Webster] Build PC&lt;/code&gt; or even &lt;code&gt;[Tom] Build PC&lt;/code&gt; if you set the option.&lt;/p&gt;&#xA;&lt;p&gt;Check out the read me on the GitLab page for the details:&lt;/p&gt;&#xA;&lt;a href=&#39;https://gitlab.com/samurailink3/notioncalendarexporter&#39;&gt;https://gitlab.com/samurailink3/notioncalendarexporter&lt;/a&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/Notion-Calendar-Exporter-Update-Assignees-in-Title-124b6786424180768397fc676ba832fa" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
  <entry>
    <title>New Site!</title>
    <updated>2024-10-07T00:00:00Z</updated>
    <id>https://www.samurailink3.com/New-Site-118b6786424180e4b1e9d2bf07a49347</id>
    <content type="html">&lt;html&gt;&lt;p&gt;I’ve moved my personal website to &lt;a href=&#39;https://www.notion.so/&#39;&gt;Notion&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For a while, I’ve been struggling to figure out what to do with my site. I always wanted it to be part blog, part project portfolio, part technical resource, but I kept running into distractions to make any kind of content-flow sustainable. I was always finding something to fix, something to optimize, something to build. For a person like me, this is simply too alluring to ignore:&lt;/p&gt;&#xA;&#xA;&lt;p&gt;And it would just go on.. and on… and on!&lt;/p&gt;&#xA;&lt;p&gt;I never ended up writing much because I was too “busy” fiddling around. While there is benefit in just playing around and exploring various aspects of tech, it wasn’t solving my core need: Be a place where I can write and publish. Working on the writing platform was too tempting, especially compared to the “less fun” aspects, like, you know, actually writing things down.&lt;/p&gt;&#xA;&lt;p&gt;So, I’m trying out Notion.&lt;/p&gt;&#xA;&lt;p&gt;At some point I need to write up a more in-depth review of Notion, but for now, here’s the pitch/justification: Hosting my site on Notion removes (most of) the temptations around fiddling with the site-tech itself. Notion should be &lt;i&gt;just-extendable-enough&lt;/i&gt; to power my site, get it organized roughly the way I want, and get out of the way. It &lt;i&gt;should&lt;/i&gt; allow me to focus more on actually putting stuff out there and less on the systems around that content delivery.&lt;/p&gt;&#xA;&lt;p&gt;Will it actually work out that way? Who knows?! But its interesting enough to be worth the try. So that’s why the site looks different (although showing anything other than an SSL error from a server that was ignored for far too long is better).&lt;/p&gt;&#xA;&lt;p&gt;&lt;/p&gt;&#xA;&lt;/html&gt;</content>
    <link href="https://www.samurailink3.com/New-Site-118b6786424180e4b1e9d2bf07a49347" rel="alternate"></link>
    <author>
      <name>Tom Webster</name>
    </author>
  </entry>
</feed>