<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>TIL on foosel.net</title><link>https://foosel.net/til/</link><description>Recent content in TIL on foosel.net</description><generator>Hugo -- 0.145.0</generator><language>en-us</language><copyright>Gina Häußge (foosel)</copyright><lastBuildDate>Fri, 23 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://foosel.net/til/index.xml" rel="self" type="application/rss+xml"/><item><title>How to work around tailscale breaking IPv6 on the host</title><link>https://foosel.net/til/how-to-work-around-tailscale-breaking-ipv6-on-the-host/</link><pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-work-around-tailscale-breaking-ipv6-on-the-host/</guid><description>&lt;p>Yesterday I ran into an incredibly weird issue. I installed the &lt;a href="https://tailscale.com/">Tailscale&lt;/a> client on two of my virtual servers hosted in the Hetzner Cloud (running Ubuntu) and suddenly the websites they offered stopped working. I suspected Tailscale and indeed, &lt;code>tailscale down&lt;/code> immediately restored functionality.&lt;/p>
&lt;p>The websites in question are actually on GitHub Pages and my servers are just acting as reverse proxy to resolve domain and TLS, and a look into the web server&amp;rsquo;s &lt;code>error.log&lt;/code> showed that the issue in serving was that the server could no longer reach its upstream at &lt;code>github.io&lt;/code> when Tailscale was active. It wasn&amp;rsquo;t a general loss of external connectivity though - IPv4 addresses still worked great, the webserver however was trying to connect to the upstream via IPv6 and this is where things failed.&lt;/p></description><content:encoded><![CDATA[<p>Yesterday I ran into an incredibly weird issue. I installed the <a href="https://tailscale.com/">Tailscale</a> client on two of my virtual servers hosted in the Hetzner Cloud (running Ubuntu) and suddenly the websites they offered stopped working. I suspected Tailscale and indeed, <code>tailscale down</code> immediately restored functionality.</p>
<p>The websites in question are actually on GitHub Pages and my servers are just acting as reverse proxy to resolve domain and TLS, and a look into the web server&rsquo;s <code>error.log</code> showed that the issue in serving was that the server could no longer reach its upstream at <code>github.io</code> when Tailscale was active. It wasn&rsquo;t a general loss of external connectivity though - IPv4 addresses still worked great, the webserver however was trying to connect to the upstream via IPv6 and this is where things failed.</p>
<p>I did some quick tests, pinging Google&rsquo;s DNS on both IPv4 (<code>8.8.8.8</code>) and IPv6 (<code>2001:4860:4860::8888</code>) with Tailscale running, and that showed that while Tailscale was running, IPv6 connectivity just broke down completely while IPv4 continued to work:</p>
<pre tabindex="0"><code>$ sudo tailscale up &amp;&amp; ping -c 3 8.8.8.8 &amp;&amp; sudo tailscale down
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.98 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=3.69 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=117 time=3.63 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 3.631/3.771/3.986/0.162 ms

$ sudo tailscale up &amp;&amp; ping -c 3 2001:4860:4860::8888 &amp;&amp; sudo tailscale down
PING 2001:4860:4860::8888(2001:4860:4860::8888) 56 data bytes

--- 2001:4860:4860::8888 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2051ms
</code></pre><p>As a next step, I checked the route that would be taken for resolving <code>2001:4860:4860::8888</code> with and without Tailscale:</p>
<pre tabindex="0"><code>$ ip route get 2001:4860:4860::8888
2001:4860:4860::8888 from :: via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1 metric 1024 pref medium

$ sudo tailscale up &amp;&amp; ip route get 2001:4860:4860::8888 &amp;&amp; sudo tailscale down
2001:4860:4860::8888 from :: via fe80::1 dev eth0 src fd7a:115c:a1e0::aaaa:bbbb metric 1024 pref medium
</code></pre><p>So, the issue was that for some reason, while Tailscale was running the system decided to use Tailscale&rsquo;s internal IPv6 <code>fd7a:115c:a1e0::aaaa:bbbb</code> set on <code>tailscale0</code> as the source IP but send the packet through the default route and <code>eth0</code>, and that didn&rsquo;t work. It basically hijacked the IPv6 traffic, even when the Tailnet wasn&rsquo;t even involved.</p>
<p>The routes looked fine to me and some buddies I asked also didn&rsquo;t spot anything amiss:</p>
<pre tabindex="0"><code>$ ip -6 rule show
0:      from all lookup local
5210:   from all fwmark 0x80000/0xff0000 lookup main
5230:   from all fwmark 0x80000/0xff0000 lookup default
5250:   from all fwmark 0x80000/0xff0000 unreachable
5270:   from all lookup 52
32766:  from all lookup main

$ ip -6 route show table local
local ::1 dev lo proto kernel metric 0 pref medium
local aaaa:bbbb:cccc:dddd::1 dev eth0 proto kernel metric 0 pref medium
local fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 0 pref medium
local fe80::5fdc:58a7:1a93:da5a dev tailscale0 proto kernel metric 0 pref medium
local fe80::9400:ff:fe0d:61a1 dev eth0 proto kernel metric 0 pref medium
multicast ff00::/8 dev eth0 proto kernel metric 256 pref medium
multicast ff00::/8 dev tailscale0 proto kernel metric 256 pref medium

$ ip -6 route show table 52
fd7a:115c:a1e0::53 dev tailscale0 metric 1024 pref medium
fd7a:115c:a1e0::/48 dev tailscale0 metric 1024 pref medium

$ ip -6 route show table main
aaaa:bbbb:cccc:dddd::/64 dev eth0 proto kernel metric 256 pref medium
fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::1 dev eth0 metric 1024 pref medium
</code></pre><p>From what I could see, firing up Tailscale would add the following new rules to the routing table:</p>
<pre tabindex="0"><code>$ ip route show table all &gt; ts-off.txt
$ sudo tailscale up &amp;&amp; ip route show table all &gt; ts-on.txt &amp;&amp; sudo tailscale down
$ diff ts-off.txt ts-on.txt
0a1,2
&gt; 100.a.b.c dev tailscale0 table 52
&gt; 100.100.100.100 dev tailscale0 table 52
3a6
&gt; local 100.x.y.z dev tailscale0 table local proto kernel scope host src 100.x.y.z
12a16,17
&gt; fd7a:115c:a1e0::53 dev tailscale0 table 52 metric 1024 pref medium
&gt; fd7a:115c:a1e0::/48 dev tailscale0 table 52 metric 1024 pref medium
13a19
&gt; fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 256 pref medium
17a24
&gt; local fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 table local proto kernel metric 0 pref medium
</code></pre><p>I still have not figured out what is actually going on there, and a reproduction on a fresh server so far also wasn&rsquo;t successful. The problem is that packets are being sent with the wrong source IPv6, but that&rsquo;s just a symptom of the underlying cause.</p>
<p>Thankfully, my buddy Jub came up with the workaround to change the default route to use a fixed IPv6 source address - the correct one - and that solved the issue (by fixing the symptom):</p>
<pre tabindex="0"><code>ip -6 route replace default via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1
</code></pre><p>I put that on a new <code>post-up</code> line into the network setup in <code>/etc/network/interface.d/50-cloud-init.cfg</code></p>
<pre tabindex="0"><code>auto eth0:0
iface eth0:0 inet6 static
    address aaaa:bbbb:cccc:dddd::/64
    gateway fe80::1
    post-up route add -net :: netmask 0 gw fe80::1%eth0 || true
    post-up ip -6 route replace default via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1 || true
    pre-down route del -net :: netmask 0 gw fe80::1%eth0 || true
</code></pre><p>A reboot confirmed that this works as a <strong>workaround</strong>.</p>
<p>But as I mentioned, I still can&rsquo;t make any sense of the underlying issue. I found <a href="https://github.com/tailscale/tailscale/issues/17936">one open bug report in Tailscale&rsquo;s bug tracker</a> that sounded familiar, but it didn&rsquo;t fully match my situation. I also have to admit that my administration skills kinda get a bit fuzzy when it comes to full blown route analysis &amp; debugging - so should you have any ideas at all what is actually causing this behaviour here, please get in touch <a href="https://chaos.social/@foosel">on Mastodon</a> - I&rsquo;d love to see this mystery solved, but am out of my depth here 😅</p>
]]></content:encoded></item><item><title>How to set the internal schedule on a Roomba 960 using rest980</title><link>https://foosel.net/til/how-to-set-the-internal-schedule-on-a-roomba-960-using-rest980/</link><pubDate>Wed, 09 Jul 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-set-the-internal-schedule-on-a-roomba-960-using-rest980/</guid><description>&lt;p>For some reason my Roomba 960 decided to fall off the cloud, or at least the official app refuses to see it.&lt;/p>
&lt;p>I thankfully already have an instance of &lt;a href="https://github.com/koalazak/rest980">rest980&lt;/a> running in my homelab anyhow, and it is still happily chatting with
the bot. And tbh, I might just block cloud access again as having everything local is better anyhow.&lt;/p>
&lt;p>In any case, I wanted to disable the schedule currently set on it internally to switch to scheduling stuff from my home automation,
but without the app working I wasn&amp;rsquo;t sure on how. So I went hunting through rest980&amp;rsquo;s source - as the README didn&amp;rsquo;t tell me what
I was looking for - and found that I could program the weekly schedule with some easy &lt;code>curl&lt;/code> magic via the &lt;code>/api/local/config/week&lt;/code>
endpoint.&lt;/p></description><content:encoded><![CDATA[<p>For some reason my Roomba 960 decided to fall off the cloud, or at least the official app refuses to see it.</p>
<p>I thankfully already have an instance of <a href="https://github.com/koalazak/rest980">rest980</a> running in my homelab anyhow, and it is still happily chatting with
the bot. And tbh, I might just block cloud access again as having everything local is better anyhow.</p>
<p>In any case, I wanted to disable the schedule currently set on it internally to switch to scheduling stuff from my home automation,
but without the app working I wasn&rsquo;t sure on how. So I went hunting through rest980&rsquo;s source - as the README didn&rsquo;t tell me what
I was looking for - and found that I could program the weekly schedule with some easy <code>curl</code> magic via the <code>/api/local/config/week</code>
endpoint.</p>
<p>Firing off a <code>GET</code> against that, this is the data structure I received:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;cycle&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;none&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;none&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;h&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">9</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">9</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;m&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>cycle</code> seems to be the on/off button from Sunday at index 0 to Saturday on index 6. <code>start</code> schedules a cleaning run, <code>none</code> disables it.
<code>h</code> is the hours on which to start each day, and <code>m</code> the minute.</p>
<p>What I wanted to do was to set all of the days to off, and this I achieved with this combined <code>GET</code>/<code>POST</code> call with some <code>jq</code> manipulation in the middle:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl $REST980_URL/api/local/config/week | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    jq <span style="color:#e6db74">&#39;.cycle[] = &#34;none&#34;&#39;</span> | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl --json @- $REST980_URL/api/local/config/week
</span></span></code></pre></div><p>Another problem - hopefully - solved! I&rsquo;ll see tomorrow if this <em>really</em> disabled the schedule 😅 but I&rsquo;m optimistic!</p>
]]></content:encoded></item><item><title>How to automatically sync screenshots from the Steamdeck to Immich</title><link>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/</link><pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/</guid><description>&lt;p>As part of &lt;a href="https://chaos.social/@foosel/114105591362840338">my ongoing effort to reduce my dependency on US services&lt;/a>, I just moved my photos
from Google Photos to a self-hosted &lt;a href="https://immich.app/">immich&lt;/a> instance (which I btw can only recommend so far).&lt;/p>
&lt;p>You might remember from &lt;a href="https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">a previous TIL&lt;/a>
that I had my Steamdeck configured to push my screenshots into a custom album on Google Photos. Obviously I had to change that now as well,
but sadly couldn&amp;rsquo;t use the existing &lt;a href="https://rclone.org/">rclone&lt;/a>-based setup for it.&lt;/p></description><content:encoded><![CDATA[<p>As part of <a href="https://chaos.social/@foosel/114105591362840338">my ongoing effort to reduce my dependency on US services</a>, I just moved my photos
from Google Photos to a self-hosted <a href="https://immich.app/">immich</a> instance (which I btw can only recommend so far).</p>
<p>You might remember from <a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">a previous TIL</a>
that I had my Steamdeck configured to push my screenshots into a custom album on Google Photos. Obviously I had to change that now as well,
but sadly couldn&rsquo;t use the existing <a href="https://rclone.org/">rclone</a>-based setup for it.</p>
<p>My first idea was to utilize <a href="https://github.com/simulot/immich-go">immich-go</a>, as I have just successfully used that for the
three day long import of over 50000 pictures from my Google Photos takeout into immich. But that turned out to not be the right tool here: in order to not even try to
upload already existing files it will fetch an asset list from immich first, and while that really improves performance for large batch imports,
it takes way too long for uploading a single new screenshot.</p>
<p>So instead I went with something self-built which utilizes <a href="https://immich.app/docs/api/">immich&rsquo;s API</a>.</p>
<h2 id="a-custom-upload-script">A custom upload script</h2>
<p>The first part is this little bash script that will take a file as input, upload it to a pre-configured immich instance and also add it to a
pre-defined album (which already has to exist). This lives in <code>~/.local/bin/immich-upload.sh</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>IMMICH_SERVER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://immich.example.com&#34;</span>
</span></span><span style="display:flex;"><span>IMMICH_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;your api key goes here&#34;</span>
</span></span><span style="display:flex;"><span>IMMICH_ALBUM<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;your album id goes here&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>INPUT<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;immich-upload.sh &lt;file&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>file_modified<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>stat -c %Y <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span> | date --iso-8601<span style="color:#f92672">=</span>seconds<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>name<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>basename <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ---</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Uploading </span>$INPUT<span style="color:#e6db74"> to immich...&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upload<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -sL --request POST <span style="color:#e6db74">&#34;</span>$IMMICH_SERVER<span style="color:#e6db74">/api/assets&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;Content-Type: multipart/form-data&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;Accept: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;X-API-Key: </span>$IMMICH_KEY<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -F <span style="color:#e6db74">&#34;deviceId=\&#34;curl/steamdeck\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -F <span style="color:#e6db74">&#34;deviceAssetId=\&#34;</span>$name<span style="color:#e6db74">-</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -F <span style="color:#e6db74">&#34;fileCreatedAt=\&#34;</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -F <span style="color:#e6db74">&#34;fileModifiedAt=\&#34;</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -F <span style="color:#e6db74">&#34;assetData=@\&#34;</span>$INPUT<span style="color:#e6db74">\&#34;&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>id<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$upload<span style="color:#e6db74">&#34;</span> | jq -r .id<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Uploaded file, asset id is </span>$id<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ---</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Adding file to album </span>$IMMICH_ALBUM<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>payload<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>jq -n --arg id $id <span style="color:#e6db74">&#39;{ids:[$ARGS.named.id]}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>album<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -sL --request PUT <span style="color:#e6db74">&#34;</span>$IMMICH_SERVER<span style="color:#e6db74">/api/albums/</span>$IMMICH_ALBUM<span style="color:#e6db74">/assets&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;Accept: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -H <span style="color:#e6db74">&#34;X-API-Key: </span>$IMMICH_KEY<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -d <span style="color:#e6db74">&#34;</span>$payload<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;... done&#34;</span>
</span></span></code></pre></div><h2 id="reacting-to-new-screenshots">Reacting to new screenshots</h2>
<p>I use <a href="https://github.com/watchexec/watchexec">watchexec</a> to listen for changes in my custom screenshot folder
(<a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">see this TIL post on how to set that up</a>)
and calling the upload script with the correct file name. I downloaded a release build of <code>watchexec</code> and threw it into <code>~/.local/bin</code>, then created another
script <code>~/.local/bin/sync-screenshots</code> that takes care of setting all of the correct parameters<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>WATCHEXEC<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.local/bin/watchexec&#34;</span>
</span></span><span style="display:flex;"><span>FOLDER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.steam_screenshots&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">${</span>WATCHEXEC<span style="color:#e6db74">}</span> --exts jpg,png,mp4 --fs-events create --emit-events-to environment -w $FOLDER -o queue -p -v -- <span style="color:#e6db74">&#39;/home/deck/.local/bin/immich-upload.sh &#34;$WATCHEXEC_COMMON_PATH/$WATCHEXEC_CREATED_PATH&#34;&#39;</span>
</span></span></code></pre></div><h2 id="putting-it-all-together">Putting it all together</h2>
<p>Finally, a new systemd unit in <code>~/.config/systemd/user/sync-screenshots.service</code> takes care of starting this bash script and keeping it running:</p>
<pre tabindex="0"><code>[Unit]
Description=Sync Steam Screenshots

[Service]
ExecStart=%h/.local/bin/sync-screenshots
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
</code></pre><p>I enabled and started that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl --user enable sync-screenshots
</span></span><span style="display:flex;"><span>systemctl --user start sync-screenshots
</span></span></code></pre></div><p>Then I took a screenshot and confirmed that the script had run:</p>
<pre tabindex="0"><code>Mar 25 15:17:03 steamdeck sync_screenshots[77135]: [Running: /home/deck/.local/bin/immich-upload.sh &#34;$WATCHEXEC_COMMON_PATH/$WATCHEXEC_CREATED_PATH&#34;]
Mar 25 15:17:03 steamdeck sync_screenshots[77188]: Uploading /home/deck/.steam_screenshots/7_20250325151703_1.png to immich...
Mar 25 15:17:04 steamdeck sync_screenshots[77188]: Uploaded file, asset id is 141dc605-edef-48f1-83b5-00bd9d72b13e
Mar 25 15:17:04 steamdeck sync_screenshots[77188]: Adding file to album 0ce35e68-a564-4e26-921e-c486cd9e4725...
Mar 25 15:17:05 steamdeck sync_screenshots[77188]: ... done
Mar 25 15:17:05 steamdeck sync_screenshots[77135]: [Command was successful]
</code></pre><p>And indeed, upon checking my immich instance, I was also looking at the freshly uploaded screenshot. Mission accomplished!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>It&rsquo;s currently reacting to newly added <code>jpg</code>, <code>png</code> or <code>mp4</code> files. The latter is in preparation of hopefully another toolchain to automatically convert clips from
<a href="https://store.steampowered.com/gamerecording">Steam&rsquo;s game recorder</a> that will automatically push its results into the screenshot folder as well, but that&rsquo;s only an idea for now.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to fix VirtualBox on Fedora 40 with Kernel 6.12+</title><link>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-40-with-kernel-612/</link><pubDate>Thu, 27 Feb 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-40-with-kernel-612/</guid><description>&lt;p>I (accidentally&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>) did a software update on my laptop last night, and this morning when I needed my Win10 VM for something, VirtualBox threw an error like this at me:&lt;/p>
&lt;blockquote>
&lt;p>VirtualBox can&amp;rsquo;t operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot (VERR_VMX_IN_VMX_ROOT_MODE).&lt;/p>&lt;/blockquote>
&lt;p>A quick web search for &amp;ldquo;fedora update virtualbox vboxisomaker&amp;rdquo; gave me &lt;a href="https://discussion.fedoraproject.org/t/139896">this forum post&lt;/a> and consequently
&lt;a href="https://www.virtualbox.org/ticket/22248">this bug report&lt;/a>, in which I found the solution: I just had to add the kernel parameter &lt;code>kvm.enable_virt_at_load=0&lt;/code> to disable KVM -
which comes enabled by default since Kernel 6.12. I accomplished that with grubby:&lt;/p></description><content:encoded><![CDATA[<p>I (accidentally<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>) did a software update on my laptop last night, and this morning when I needed my Win10 VM for something, VirtualBox threw an error like this at me:</p>
<blockquote>
<p>VirtualBox can&rsquo;t operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot (VERR_VMX_IN_VMX_ROOT_MODE).</p></blockquote>
<p>A quick web search for &ldquo;fedora update virtualbox vboxisomaker&rdquo; gave me <a href="https://discussion.fedoraproject.org/t/139896">this forum post</a> and consequently
<a href="https://www.virtualbox.org/ticket/22248">this bug report</a>, in which I found the solution: I just had to add the kernel parameter <code>kvm.enable_virt_at_load=0</code> to disable KVM -
which comes enabled by default since Kernel 6.12. I accomplished that with grubby:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo grubby --update-kernel<span style="color:#f92672">=</span>ALL --args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kvm.enable_virt_at_load=0&#34;</span>
</span></span></code></pre></div><p>After a reboot, VirtualBox started again.</p>
<p>Given how often VirtualBox breaks for me on updates, long term I think I really need to find a different solution&hellip; And yes, I also really need to upgrade to Fedora 41, I know 😉</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I needed to a quick reboot before an online call and promptly forgot to uncheck &ldquo;install updates&rdquo; on the reboot dialog. Which made me be late on the call. Meh.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to fetch additional data for a flux query from a json file</title><link>https://foosel.net/til/how-to-fetch-additional-data-for-a-flux-query-from-a-json-file/</link><pubDate>Thu, 26 Dec 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-fetch-additional-data-for-a-flux-query-from-a-json-file/</guid><description>&lt;p>My buddy Romses is currently taking care of the &lt;a href="https://datagnome.de">Datenzwerg deployment&lt;/a> at 38c3, and like at every event where we deploy them I&amp;rsquo;m updating our page and &lt;a href="https://grafana.datagnome.de">Grafana dashboard&lt;/a> with the locations of the gnomes.&lt;/p>
&lt;p>So far the latter was always quite annoying: We have only the names of the gnomes in our influx data, and adding the location/deployment status to the graph thus meant having something like this for every single graph:&lt;/p></description><content:encoded><![CDATA[<p>My buddy Romses is currently taking care of the <a href="https://datagnome.de">Datenzwerg deployment</a> at 38c3, and like at every event where we deploy them I&rsquo;m updating our page and <a href="https://grafana.datagnome.de">Grafana dashboard</a> with the locations of the gnomes.</p>
<p>So far the latter was always quite annoying: We have only the names of the gnomes in our influx data, and adding the location/deployment status to the graph thus meant having something like this for every single graph:</p>
<pre tabindex="0"><code class="language-flux" data-lang="flux">import &#34;strings&#34;
import &#34;dict&#34;

locations = [
    &#34;Bashful&#34;: &#34;Uptime Bar&#34;,
    &#34;Dopey&#34;: &#34;c3cat&#34;,
    &#34;Grumpy&#34;: &#34;Späti&#34;,
    &#34;Happy&#34;: &#34;Kidspace&#34;,
    &#34;Hefty&#34;: &#34;HASS Assembly&#34;,
    &#34;Moopsy&#34;: &#34;Chaospost&#34;,
    &#34;Kinky&#34;: &#34;Eventphone&#34;,
    &#34;Nerdy&#34;: &#34;House of Tea&#34;,
    &#34;Sleepy&#34;: &#34;DDOS Bar&#34;,
    &#34;Sneezy&#34;: &#34;Wohnzimmer&#34;
]

from(bucket: &#34;datagnome&#34;)
  [...]
  |&gt; map(fn: (r) =&gt; ({r with device: r.device + &#34; (&#34; + dict.get(dict: locations, key: r.device, default: &#34;?&#34;) + &#34;)&#34;}))
</code></pre><p>Which of course means that I had to update this <code>locations</code> dict for every single panel, on every single deployment, at least twice (start and end of the event).</p>
<p>I finally decided I had to solve this differently and just now figured out how to keep the deployment info in <a href="https://github.com/romses/Datenzwerg/blob/main/docs/deployment.json">a JSON file on our git repo</a> and then querying <em>that</em> from the graphs, instead of manually keeping the lookup data updated in more than one place:</p>
<pre tabindex="0"><code class="language-flux" data-lang="flux">import &#34;strings&#34;
import &#34;dict&#34;
import &#34;http/requests&#34;
import &#34;experimental/json&#34;

response = requests.get(url: &#34;https://raw.githubusercontent.com/romses/Datenzwerg/refs/heads/main/docs/deployment.json&#34;)
data = json.parse(data: response.body)
locations = dict.fromList(pairs: data)

from(bucket: &#34;datagnome&#34;)
  [...]
  |&gt; map(fn: (r) =&gt; ({r with device: r.device + &#34; (&#34; + dict.get(dict: locations, key: r.device, default: &#34;?&#34;) + &#34;)&#34;}))
</code></pre><p>So far seems to work well, and I&rsquo;m very happy to be able to do this faster now (and also more easily from my phone, should I need to).</p>
<p>Next step: Figuring out how to use that JSON file to also keep the event box on the home page updated. But that&rsquo;s for another day :)</p>
]]></content:encoded></item><item><title>How to make transparent GIFs more easily sharable by adding a checkerboard background</title><link>https://foosel.net/til/how-to-make-transparent-gifs-easier-shareable-by-adding-a-checkerboard-background/</link><pubDate>Tue, 15 Oct 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-make-transparent-gifs-easier-shareable-by-adding-a-checkerboard-background/</guid><description>&lt;p>I&amp;rsquo;m currently taking part in the &lt;a href="https://social.horrorhub.club/@stina_marie/113220760493893634">&amp;quot;#hARToween&amp;quot; daily art challenge&lt;/a>, as I want to
work on my pixelart skills and drawing a 128x128px pixel drawing each day&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> for a month seemed
like a good idea. You can follow my posts &lt;a href="https://chaos.social/@foosel/113233763230193057">in this thread&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;m using &lt;a href="https://aseprite.org/">Aseprite&lt;/a>, and recently came across &lt;a href="https://sprngr.itch.io/aseprite-record">the &amp;ldquo;record for aseprite&amp;rdquo; script for it&lt;/a>
that allows taking regular snapshots of what I&amp;rsquo;m currently drawing so a timelapse can be created from
that. And that works nicely, but I had to realize that the timelapse would come with transparency until
I came to the background during my drawing, which looked really weird when sharing the resulting GIF.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently taking part in the <a href="https://social.horrorhub.club/@stina_marie/113220760493893634">&quot;#hARToween&quot; daily art challenge</a>, as I want to
work on my pixelart skills and drawing a 128x128px pixel drawing each day<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> for a month seemed
like a good idea. You can follow my posts <a href="https://chaos.social/@foosel/113233763230193057">in this thread</a>.</p>
<p>I&rsquo;m using <a href="https://aseprite.org/">Aseprite</a>, and recently came across <a href="https://sprngr.itch.io/aseprite-record">the &ldquo;record for aseprite&rdquo; script for it</a>
that allows taking regular snapshots of what I&rsquo;m currently drawing so a timelapse can be created from
that. And that works nicely, but I had to realize that the timelapse would come with transparency until
I came to the background during my drawing, which looked really weird when sharing the resulting GIF.</p>
<p>So I looked into adding the usual transparency checkerboard background to the GIF with a quick script,
and of course, <a href="https://imagemagick.org/">ImageMagick</a> once more to the rescue. Alas, the resulting GIF was quite large and ImageMagick&rsquo;s
optimization options caused glitches in the GIF. So I looked for another option to optimize the GIF and
came across <a href="https://www.lcdf.org/gifsicle/">gifsicle</a>.</p>
<p>The result is this bash script which will take a GIF and an optional background image to set,
add the background (or a freshly generated checkerboard pattern of the right size) to the GIF, then compress
the GIF:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>GIF<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>BG<span style="color:#f92672">=</span>$2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BASE<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>basename <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GIF%.*<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>OUTPUT<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$BASE<span style="color:#e6db74">.bg.gif&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f <span style="color:#e6db74">&#34;</span>$BG<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Adding background image </span>$BG<span style="color:#e6db74"> to all frames of </span>$GIF<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>    magick <span style="color:#e6db74">&#34;</span>$GIF<span style="color:#e6db74">&#34;</span> -coalesce null: <span style="color:#e6db74">&#34;</span>$BG<span style="color:#e6db74">&#34;</span> -compose dstOver -layers composite <span style="color:#e6db74">&#34;</span>$OUTPUT<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    size<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>gifsicle --sinfo <span style="color:#e6db74">&#34;</span>$GIF<span style="color:#e6db74">&#34;</span> | grep <span style="color:#e6db74">&#34;logical screen&#34;</span> | xargs echo -n | cut -d<span style="color:#e6db74">&#34; &#34;</span> -f 3<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Generating a </span>$size<span style="color:#e6db74"> checkerboard pattern and adding it as background to all frames of </span>$GIF<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>    magick <span style="color:#e6db74">&#34;</span>$GIF<span style="color:#e6db74">&#34;</span> -coalesce null: <span style="color:#ae81ff">\(</span> -size $size tile:pattern:checkerboard <span style="color:#ae81ff">\)</span> -compose dstOver -layers composite <span style="color:#e6db74">&#34;</span>$OUTPUT<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Optimizing </span>$OUTPUT<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>gifsicle --batch -O3 --lossy<span style="color:#f92672">=</span><span style="color:#ae81ff">35</span> <span style="color:#e6db74">&#34;</span>$OUTPUT<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;...done!&#34;</span>
</span></span></code></pre></div><p>Example call:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>$ gif_bg 14.gif 
</span></span><span style="display:flex;"><span>Generating a 256x256 checkerboard pattern and adding it as background to all frames of 14.gif...
</span></span><span style="display:flex;"><span>Optimizing 14.bg.gif...
</span></span><span style="display:flex;"><span>...done!
</span></span></code></pre></div><p>I&rsquo;m quite happy with the result:</p>















        
        

        
            
            
        

        
            <blockquote class="toot-blockquote" cite="https://chaos.social@foosel/status/113306303596486838">
                <div class="toot-header">
                    <a class="toot-profile" href="https://chaos.social/@foosel" rel="noopener">
                        <img
                            src="https://assets.chaos.social/accounts/avatars/000/235/099/original/a2e381e9aab4a693.png"
                            alt="Mastodon avatar for @foosel@chaos.social"
                            loading="lazy"
                        />
                    </a>
                    <div class="toot-author">
                        <a class="toot-author-name" href="https://chaos.social/@foosel" rel="noopener">Gina Häußge</a>
                        <a class="toot-author-handle" href="https://chaos.social/@foosel" rel="noopener">@foosel@chaos.social</a>
                    </div>
                </div>
                <p>Now also with a creation timelapse (after I finally managed to get a proper workflow going for that)</p>
                
                    
                        
                    
                    <div class="toot-img-grid-0">
                    
                        
                    
                    </div>
                    
                    
                        
                        
                            
                            <style>
                                .img-f64237a660efa590d99cd4bd48022b22 {
                                    aspect-ratio: 256 / 256;
                                }
                            </style>
                            <div class="ctr toot-video-wrapper">
                                <video loop autoplay muted playsinline controls controlslist="nofullscreen" class="ctr toot-media-img img-f64237a660efa590d99cd4bd48022b22">
                                    <source src="https://assets.chaos.social/media_attachments/files/113/306/301/353/935/058/original/891b82c9cf56ffd5.mp4">
                                    <p class="legal ctr">(Your browser doesn&rsquo;t support the <code>video</code> tag.)</p>
                                </video></div>
                        
                    
                
                
                
                <div class="toot-footer">
                    <a href="https://chaos.social/@foosel/113306303596486838" class="toot-date" rel="noopener">October 14, 2024, 14:43</a>&nbsp;<span class="pokey">(UTC)</span>
                </div>
            </blockquote>
        
    
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Well, almost, I was at MRMCD and then a bit under the weather after and thus missed some days that I&rsquo;m now trying to catch up on again.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How I (hopefully) fixed the flickering of my UHKv2's left half</title><link>https://foosel.net/til/how-i-hopefully-fixed-the-flickering-of-my-uhkv2s-left-half/</link><pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-i-hopefully-fixed-the-flickering-of-my-uhkv2s-left-half/</guid><description>&lt;p>I&amp;rsquo;ve been a long time user of the &lt;a href="https://ultimatehackingkeyboard.com/">Ultimate Hacking Keyboard (UHK)&lt;/a>. It started with acquiring version 1 in 2020 and upgrading to version 2 in 2022. The UHK is a split keyboard with two halves connected by a bridge cable. While the first version was rock solid for me, with the second one I sadly started to experience intermittent flickering of the left half, during which the left side became unresponsive. Sometimes the half would then even outright disconnect completely. Unplugging and replugging it would usually fix the issue, but was annoying.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;ve been a long time user of the <a href="https://ultimatehackingkeyboard.com/">Ultimate Hacking Keyboard (UHK)</a>. It started with acquiring version 1 in 2020 and upgrading to version 2 in 2022. The UHK is a split keyboard with two halves connected by a bridge cable. While the first version was rock solid for me, with the second one I sadly started to experience intermittent flickering of the left half, during which the left side became unresponsive. Sometimes the half would then even outright disconnect completely. Unplugging and replugging it would usually fix the issue, but was annoying.</p>
<p>I reached out to UHK support, they confirmed they had gotten reports about this from other customers as well and were still investigating. They even created <a href="https://ultimatehackingkeyboard.com/repair/fixing-misbehaving-keys">an FAQ page</a>. In the end, after much troubleshooting I got a replacement, and for a while it looked like things were fixed. But then the flickering returned, just less frequent. For the longest time I&rsquo;ve now just lived with having to occasionally replug my keyboard. But on Friday I found myself on speaker phone with my mom, helping her debug an issue with her PC through screen sharing, with my mobile located between the two halves of the keyboard, and the flickering started with a vengeance! I had to replug the keyboard three or four times during the call, which was super annoying.</p>
<p>So I decided investigate once more, to see if maybe some new information had come up. And indeed it had, I found <a href="https://forum.ultimatehackingkeyboard.com/t/left-module-flickering-with-mobile-phone/152">this thread on the UHK forums</a>:</p>
<blockquote>
<p>Thought I’d start a topic about an issue which I’m aware at least a few people have experienced. Which is that the left module’s connection can flicker on/off if your mobile phone is placed to the left of it.</p>
<p>I realised this initially when I tried to capture a video of it happening and couldn’t do it, realising that the act of picking up my phone resolved it. These days I keep the phone away from that side of desk. It’s not a particular problem for me, but I’m sure it’ll be a confusing issue for people when they first encounter it.</p>
<p>My suspicion is that the I2C connection between the two halves has less protection against interference than USB. But it might be interesting to hear from actual engineers about this. And also if people have found a way to mitigate it.</p></blockquote>
<p>And that got immediately confirmed by UHK:</p>
<blockquote>
<p>Admittedly, the UHK is rather sensitive to electromagnetic interference, and we send <a href="https://ultimatehackingkeyboard.com/repair/fixing-misbehaving-keys">this troubleshooting guide</a> to our customers when they encounter such issues. We couldn’t catch this issue in the design phase, but fortunately, people can almost always work around it.</p>
<p>I2C is a likely reason; the bus is too long and has too many ICs on it. Future UHK versions will use UART between the halves. Another possible cause is insufficient ground pour on the PCBs, which will be much increased in future versions as well.</p></blockquote>
<p>and matches my experiences perfectly! When it got really bad on Friday, the phone was actively being used and right next to the left half. And I usually keep my phone in my right pocket, and only occassionally put it on the desk, which could explain why the flickering is so intermittent for me. I checked the (adjusted) FAQ entry and found this advice at the end of it:</p>
<blockquote>
<p>If [the issue persists], no matter what, then you can reduce the communication speed of the internal I2C bus of the UHK, making it more stable. The default value is 100000. You can half the communication speed by running the following smart macro commands, preferably in the <code>$onInit</code> macro:</p>
<pre tabindex="0"><code>set macroEngine.extendedCommands 1
set i2cBaudRate 500000
</code></pre><p>which should make communication more stable, but the smaller the value, the less responsive your UHK will get, which you will notice below a certain value</p></blockquote>
<p>I&rsquo;ve now applied that through the UHK Agent software, and so far (after a test call to my phone located in the same position as on Friday), things seem stable. Time will tell if this is a permanent fix, but I&rsquo;m hopeful. And I&rsquo;m glad I found this information, as it explains the issue and gives me a way to mitigate it (I might add some aluminum foil to the bottom of the left half too, as suggested).</p>
<p>I&rsquo;m also glad that UHK is aware of the issue and working on a fix for future versions. I&rsquo;m looking forward to that, as I really like the UHK otherwise.</p>
]]></content:encoded></item><item><title>How to check for cloud IPs in nginx</title><link>https://foosel.net/til/how-to-check-for-cloud-ips-in-nginx/</link><pubDate>Mon, 01 Jul 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-check-for-cloud-ips-in-nginx/</guid><description>&lt;p>I&amp;rsquo;m currently busy mitigating a &lt;a href="https://octoprint.org/blog/2024/06/28/stats-manipulation/">stats manipulation on OctoPrint&lt;/a>, and one of the steps I&amp;rsquo;m taking is blocking off several cloud options from accessing the tracking endpoint - and &lt;em>only&lt;/em> that.&lt;/p>
&lt;p>Since we are talking about several thousand of IPs here in at least 1.5k of CIDR ranges, I was looking for the best way to do that that wouldn&amp;rsquo;t cause a lot of performance impact - the tracking server needs to be fast.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently busy mitigating a <a href="https://octoprint.org/blog/2024/06/28/stats-manipulation/">stats manipulation on OctoPrint</a>, and one of the steps I&rsquo;m taking is blocking off several cloud options from accessing the tracking endpoint - and <em>only</em> that.</p>
<p>Since we are talking about several thousand of IPs here in at least 1.5k of CIDR ranges, I was looking for the best way to do that that wouldn&rsquo;t cause a lot of performance impact - the tracking server needs to be fast.</p>
<p>A list of all CIDR ranges with <code>deny</code> turned out to not work thanks to my endpoint definition in nginx using <code>return</code> statements, and those are apparently evaluated before <code>allow</code> and <code>deny</code> statements. But then I got the hint to look at the <a href="https://nginx.org/en/docs/http/ngx_http_geo_module.html"><code>geo</code> module</a> and with that it was easy to build a map of IP ranges that should be matched and just combining that with an <code>if</code>.</p>
<p>For a first test I created a converter for <a href="https://github.com/PodderApps/ipcat">this list of IP ranges</a>, filtering for the really big players:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>URL<span style="color:#f92672">=</span>https://raw.githubusercontent.com/PodderApps/ipcat/main/datacenters.csv
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $1 !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>	DATA<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s $URL | grep -E <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>	DATA<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s $URL<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;geo $is_cloud {&#39;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;    default 0;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">while</span> IFS<span style="color:#f92672">=</span> read -r line; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	start_ip<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo $line | cut -d, -f1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>	end_ip<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo $line | cut -d, -f2<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>	comment<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo $line | cut -d, -f3<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	script<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;import ipaddress; start_ip=ipaddress.IPv4Address(\&#34;</span>$start_ip<span style="color:#e6db74">\&#34;); end_ip=ipaddress.IPv4Address(\&#34;</span>$end_ip<span style="color:#e6db74">\&#34;); print(next(ipaddress.summarize_address_range(start_ip, end_ip)))&#34;</span>
</span></span><span style="display:flex;"><span>	cidr<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>python3 -c <span style="color:#e6db74">&#34;</span>$script<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	echo <span style="color:#e6db74">&#34;    </span>$cidr<span style="color:#e6db74"> 1; # </span>$comment<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span> <span style="color:#f92672">&lt;&lt;&lt;</span> <span style="color:#e6db74">&#34;</span>$DATA<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;}&#39;</span>
</span></span></code></pre></div><p>Calling this like this then created a <code>conf</code> file:</p>
<pre tabindex="0"><code class="language-prompt" data-lang="prompt">$ sudo ./generate_is_cloud_map &#34;AWS|DigitalOcean|Google&#34; &gt; /etc/nginx/snippets/is-cloud.conf
$ cat /etc/nginx/snippets/ip-cloud.conf
geo $is_cloud {
    default 0;
    3.0.0.0/15; # Amazon AWS
    # ...
}
</code></pre><p>which I then could use in my nginx <code>location</code> config through an include and an <code>if</code> statement:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">include</span> snippets/is-cloud.conf;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/mylocation</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">if</span> <span style="color:#e6db74">(</span>$is_cloud = <span style="color:#ae81ff">1</span><span style="color:#e6db74">)</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">return</span> <span style="color:#ae81ff">403</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For now this seems to work. I&rsquo;m going to give this a day or so and then look into further IP sources and also blocking off the IPv6 ranges.</p>
]]></content:encoded></item><item><title>How to quickly create a header modifying reverse proxy with mitmproxy</title><link>https://foosel.net/til/how-to-quickly-create-a-header-modifying-reverse-proxy-with-mitmproxy/</link><pubDate>Tue, 12 Mar 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-quickly-create-a-header-modifying-reverse-proxy-with-mitmproxy/</guid><description>&lt;p>I&amp;rsquo;m currently in the process of testing some changes on OctoPrint involving its automatic user login via request headers, and
for that needed to quickly set up a reverse proxy that would modify the headers of the requests going to the development server
for some quick testing.&lt;/p>
&lt;p>Specifically, I wanted a quick CLI tool that would allow me to set up a reverse proxy listening on port 5555, forwarding to
&lt;code>http://localhost:5000&lt;/code> while also setting the headers &lt;code>X-Remote-User&lt;/code> to &lt;code>remote&lt;/code> and &lt;code>X-Remote-Host&lt;/code> to &lt;code>localhost:5555&lt;/code>.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently in the process of testing some changes on OctoPrint involving its automatic user login via request headers, and
for that needed to quickly set up a reverse proxy that would modify the headers of the requests going to the development server
for some quick testing.</p>
<p>Specifically, I wanted a quick CLI tool that would allow me to set up a reverse proxy listening on port 5555, forwarding to
<code>http://localhost:5000</code> while also setting the headers <code>X-Remote-User</code> to <code>remote</code> and <code>X-Remote-Host</code> to <code>localhost:5555</code>.</p>
<p>Enter <a href="https://mitmproxy.org/"><code>mitmproxy</code></a>, or more specifically its <code>mitmdump</code> tool, which turned out to be a great tool for this job.</p>
<p>All I needed was to run the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mitmdump --mode reverse:http://localhost:5000@5555 --modify-headers <span style="color:#e6db74">&#34;/X-Remote-User/remote&#34;</span> --modify-headers <span style="color:#e6db74">&#34;/X-Forwarded-Host/localhost:5555&#34;</span>
</span></span></code></pre></div><p>This does the following:</p>
<ul>
<li><code>--mode reverse:http://localhost:5000@5555</code> sets up a reverse proxy listening on port 5555, forwarding to <code>http://localhost:5000</code></li>
<li><code>--modify-headers &quot;/X-Remote-User/remote&quot;</code> sets the <code>X-Remote-User</code> header to <code>remote</code></li>
<li><code>--modify-headers &quot;/X-Forwarded-Host/localhost:5555&quot;</code> sets the <code>X-Forwarded-Host</code> header to <code>localhost:5555</code></li>
</ul>
<p>With that the <a href="https://community.octoprint.org/t/reverse-proxy-configuration-examples/1107">reverse proxy test page in OctoPrint</a>
turned all green and I could test my changes without having to set up an actual reverse proxy in front of the development server.</p>
]]></content:encoded></item><item><title>How to print Deutsche Post stamps via the command line on a Brother QL label printer</title><link>https://foosel.net/til/how-to-print-deutsche-post-stamps-via-the-command-line-on-a-brother-ql-label-printer/</link><pubDate>Thu, 11 Jan 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-print-deutsche-post-stamps-via-the-command-line-on-a-brother-ql-label-printer/</guid><description>&lt;p>&lt;em>Update from 2024-01-12: I&amp;rsquo;ve updated the scripts to support both 50mm and 62mm wide labels, and added some more whitespace trimming to the basic stamp. The post has been adjusted accordingly.&lt;/em>&lt;/p>
&lt;p>I recently acquired a &lt;a href="https://www.brother-usa.com/products/QL820NWB">Brother QL-820NWB label printer&lt;/a> to be able to quickly create labels for boxes and such, and ideally also print out Deutsche Post&amp;rsquo;s &amp;ldquo;print yourself&amp;rdquo; stamps with it. The Deutsche Post stamp shop allows me to download PDFs targeting the 62mm wide endless labels for that printer, for the two types of stamps I&amp;rsquo;m interested in (stamp, and address label with stamp). But my attempts in printing those directly to the printer through Gnome&amp;rsquo;s printer integration weren&amp;rsquo;t successful, things were too small, the cutter didn&amp;rsquo;t work etc.&lt;/p></description><content:encoded><![CDATA[<p><em>Update from 2024-01-12: I&rsquo;ve updated the scripts to support both 50mm and 62mm wide labels, and added some more whitespace trimming to the basic stamp. The post has been adjusted accordingly.</em></p>
<p>I recently acquired a <a href="https://www.brother-usa.com/products/QL820NWB">Brother QL-820NWB label printer</a> to be able to quickly create labels for boxes and such, and ideally also print out Deutsche Post&rsquo;s &ldquo;print yourself&rdquo; stamps with it. The Deutsche Post stamp shop allows me to download PDFs targeting the 62mm wide endless labels for that printer, for the two types of stamps I&rsquo;m interested in (stamp, and address label with stamp). But my attempts in printing those directly to the printer through Gnome&rsquo;s printer integration weren&rsquo;t successful, things were too small, the cutter didn&rsquo;t work etc.</p>
<p>I knew that printing to the printer via my local instance of <a href="https://github.com/pklaus/brother_ql_web">brother_ql_web</a> works flawlessly, and that the library this is based on, <a href="https://github.com/pklaus/brother_ql">brother_ql</a>, has a command line interface. So I thought, why not just convert the PDFs to individual PNGs, and then print those?</p>
<p>Through the magic of some shell scripting, I&rsquo;m now able to do just that, right from the command line.</p>
<p>I installed the <code>brother_ql</code> Python package via pip:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install --user brother_ql
</span></span></code></pre></div><p>I had to do a little manual patch to make it work with the latest versions of the required Pillow dependency, by editing <code>brother_ql/conversion.py</code> and changing <code>Image.ANTIALIAS</code> to <code>Image.LANCZOS</code>.</p>
<p>I also made sure my <code>.bash_profile</code> contains the address and model of my printer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export BROTHER_QL_PRINTER<span style="color:#f92672">=</span>tcp://192.168.x.x
</span></span><span style="display:flex;"><span>export BROTHER_QL_MODEL<span style="color:#f92672">=</span>QL-820NWB
</span></span></code></pre></div><p>Then I created two shell scripts, one for printing stamps and one for printing labels.</p>
<p>The first one, <code>porto_print</code>, takes care of printing the stamps. It resizes, trims, adds a new border and then extends to the native width for the selected label size (50mm by default, or 62mm if requested) while keeping a right alignment:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Usage:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   porto_print &lt;pdf&gt; [50|62]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tmpdir<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>mktemp -d<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cleanup<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  rm -rf <span style="color:#e6db74">&#34;</span>$tmpdir<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>trap cleanup EXIT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>PDF<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>LABEL<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>2<span style="color:#66d9ef">:-</span>50<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>PNG<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>basename <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PDF%.*<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> $LABEL in
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;50&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    WIDTH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;554&#34;</span>
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;62&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    WIDTH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;696&#34;</span>
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span>  *<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Unsupported label size: </span>$LABEL<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit -1
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Converting PDF to individual PNGs...&#34;</span>
</span></span><span style="display:flex;"><span>pdftoppm <span style="color:#e6db74">&#34;</span>$PDF<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span>$tmpdir<span style="color:#e6db74">/</span>$PNG<span style="color:#e6db74">&#34;</span> -png -r <span style="color:#ae81ff">600</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> file in <span style="color:#66d9ef">$(</span>ls $tmpdir/*.png<span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Printing </span>$file<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>  mogrify -background white -bordercolor white -resize 696x -trim -border 25x25 -gravity east -extent <span style="color:#e6db74">${</span>WIDTH<span style="color:#e6db74">}</span>x284 <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  brother_ql print -l $LABEL <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>The second one, <code>porto_address_print</code>, does basically the same, just with slightly different parameters and left alignment:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Usage:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   porto_address_print &lt;pdf&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tmpdir<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>mktemp -d<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cleanup<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  rm -rf <span style="color:#e6db74">&#34;</span>$tmpdir<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>trap cleanup EXIT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>PDF<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>LABEL<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>2<span style="color:#66d9ef">:-</span>50<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>PNG<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>basename <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PDF%.*<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> $LABEL in
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;50&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    WIDTH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;554&#34;</span>
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;62&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    WIDTH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;696&#34;</span>
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span>  *<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Unsupported label size: </span>$LABEL<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit -1
</span></span><span style="display:flex;"><span>    ;;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Converting PDF to individual PNGs...&#34;</span>
</span></span><span style="display:flex;"><span>pdftoppm <span style="color:#e6db74">&#34;</span>$PDF<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span>$tmpdir<span style="color:#e6db74">/</span>$PNG<span style="color:#e6db74">&#34;</span> -png -r <span style="color:#ae81ff">600</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> file in <span style="color:#66d9ef">$(</span>ls $tmpdir/*.png<span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Printing </span>$file<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>  mogrify -bordercolor white -background white -resize 696x -trim -border 25x25 -gravity West -extent <span style="color:#e6db74">${</span>WIDTH<span style="color:#e6db74">}</span>x839 <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 
</span></span><span style="display:flex;"><span>  brother_ql print -l $LABEL <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>Both of these were placed under <code>~/.local/bin</code> and made executable. I can now call them both from anywhere on the command line, just passing the path to the PDF to print.</p>
<p>The result is one or more nicely printed stamps or address labels, ready to be stuck to an envelope:</p>
<p><img alt="Printed stamp and printed stamp with address label, freshly printed from example files through the two scripts" loading="lazy" src="/til/how-to-print-deutsche-post-stamps-via-the-command-line-on-a-brother-ql-label-printer/result.jpg"></p>
<p>Now, this should hopefully make it easier for me to print all those address labels for OctoPrint sticker shipments in the future ;) Next step: Automated QR code labels for the various boxes on my shelves ^^</p>
]]></content:encoded></item><item><title>How to force paperless-ngx to consume signed PDFs</title><link>https://foosel.net/til/how-to-force-paperless-to-consume-signed-pdfs/</link><pubDate>Wed, 06 Dec 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-force-paperless-to-consume-signed-pdfs/</guid><description>&lt;p>I use &lt;a href="">paperless-ngx&lt;/a> to manage my documents, together with some rules that automaticaly
ingest PDFs from my mail boxes. However, I noticed that a recently received invoice from AWS
was not ingested as expected. Looking at the logs I found this error message for it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>invoice.pdf: Error occurred while consuming document invoice.pdf: DigitalSignatureError: Input PDF has a digital signature. OCR would alter the document,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>invalidating the signature.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I don&amp;rsquo;t know if a software update brought this refusal to run OCR on signed PDFs, or if AWS
simply so long didn&amp;rsquo;t send me signed PDFs, but I needed to find a way to force paperless to
ingest signed things as well as having all of that stuff stored in paperless is a vital part
of my accounting workflow.&lt;/p></description><content:encoded><![CDATA[<p>I use <a href="">paperless-ngx</a> to manage my documents, together with some rules that automaticaly
ingest PDFs from my mail boxes. However, I noticed that a recently received invoice from AWS
was not ingested as expected. Looking at the logs I found this error message for it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>invoice.pdf: Error occurred while consuming document invoice.pdf: DigitalSignatureError: Input PDF has a digital signature. OCR would alter the document,
</span></span><span style="display:flex;"><span>invalidating the signature.
</span></span></code></pre></div><p>I don&rsquo;t know if a software update brought this refusal to run OCR on signed PDFs, or if AWS
simply so long didn&rsquo;t send me signed PDFs, but I needed to find a way to force paperless to
ingest signed things as well as having all of that stuff stored in paperless is a vital part
of my accounting workflow.</p>
<p>A quick search for the error message brought me to
<a href="https://github.com/paperless-ngx/paperless-ngx/discussions/4047">this discussion on the paperless-ngx GitHub repository</a>
and therein I also found the <a href="https://github.com/paperless-ngx/paperless-ngx/discussions/4047#discussioncomment-7019544">solution</a>,
which is to set the <code>PAPERLESS_OCR_USER_ARGS</code> config option to
<code>{&quot;invalidate_digital_signatures&quot;: true}</code>.</p>
<p>As I run paperless via Docker I needed to add the following to the <code>environment</code> section in my paperless
<code>docker-compose.yml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">paperless</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">PAPERLESS_OCR_USER_ARGS</span>: <span style="color:#e6db74">&#39;{&#34;invalidate_digital_signatures&#34;: true}&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># ...</span>
</span></span></code></pre></div><p>And after adding this and a quick <code>docker compose up -d</code> things seem to now work as expected
again. Yay!</p>
]]></content:encoded></item><item><title>How to fix GRUB after a SteamOS update</title><link>https://foosel.net/til/how-to-fix-grub-after-steamos-update/</link><pubDate>Sat, 25 Nov 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-fix-grub-after-steamos-update/</guid><description>&lt;p>My partner just ran into an issue after updating his SteamDeck to the latest SteamOS version (3.4.x to 3.5.7).&lt;/p>
&lt;p>He has a dual boot setup running using &lt;a href="https://github.com/jlobue10/SteamDeck_rEFInd">rEFInd&lt;/a>, and while that survived the OS update just fine, when he wanted to return to SteamOS after a quick stint in Windows today, he was greeted by a GRUB boot menu.&lt;/p>
&lt;p>Detective foosel to the rescue.&lt;/p>
&lt;p>Attempting to boot the SteamOS entry in grub resulted in an error like this (with another device UUID):&lt;/p></description><content:encoded><![CDATA[<p>My partner just ran into an issue after updating his SteamDeck to the latest SteamOS version (3.4.x to 3.5.7).</p>
<p>He has a dual boot setup running using <a href="https://github.com/jlobue10/SteamDeck_rEFInd">rEFInd</a>, and while that survived the OS update just fine, when he wanted to return to SteamOS after a quick stint in Windows today, he was greeted by a GRUB boot menu.</p>
<p>Detective foosel to the rescue.</p>
<p>Attempting to boot the SteamOS entry in grub resulted in an error like this (with another device UUID):</p>
<pre tabindex="0"><code>error: no such device: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
error: file `/boot/vmlinuz-linux-neptune` not found.
error: you need to load the kernel first.

Press any key to continue...
</code></pre><p>So it couldn&rsquo;t find it&rsquo;s boot device and due to that also not the kernel stored thereon.</p>
<p>Entering the Deck&rsquo;s boot manager and manually booting <code>\efi\steamos\steamoscl.efi</code> also led to the same situation.</p>
<p>I was able to still boot into SteamOS via the fallback entry however (which also had a different boot device UUID).</p>
<p>And it took me way too long<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> to simply just get the idea to let Linux update its GRUB entries:</p>
<pre tabindex="0"><code>sudo update-grub
</code></pre><p>That fixed it.</p>
<p>No idea if the dual boot setup played a roll in this mess or if it was just some random hiccup, my Deck&rsquo;s update went without a hitch 🤷‍♀️ But if it happens again I now have this entry to check 😁</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I reinstalled rEFInd, rebuilt the EFI entries (<code>sudo efibootmgr -c -d /dev/nvme0n1 -p 1 -L &quot;SteamOS&quot; -l \\efi\\steamos\\steamcl.efi</code>) and the initramfs files (<code>mkinitcpio -P</code>) before getting the idea to maybe start at the bottom instead of the top.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to fix VirtualBox on Fedora 38 with Kernel 6.4.10+ by adding a missing include</title><link>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-38-with-kernel-6410/</link><pubDate>Thu, 07 Sep 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-38-with-kernel-6410/</guid><description>&lt;p>I recently did a software update on my laptop running Fedora 38, and that also brought in a kernel update. Starting my Win10 VirtualBox VM afterwards no longer worked as it needed the kernel module to be recompiled. However, that failed:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ sudo /sbin/vboxconfig
[sudo] password for gina:
vboxdrv.sh: Stopping VirtualBox services.
depmod: WARNING: could not open modules.order at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
depmod: WARNING: could not open modules.builtin at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
depmod: WARNING: could not open modules.builtin.modinfo at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
vboxdrv.sh: Starting VirtualBox services.
vboxdrv.sh: Building VirtualBox kernel modules.
egrep: warning: egrep is obsolescent; using grep -E
vboxdrv.sh: failed: Look at /var/log/vbox-setup.log to find out what went wrong.
There were problems setting up VirtualBox. To re-start the set-up process, run
/sbin/vboxconfig
as root. If your system is using EFI Secure Boot you may need to sign the
kernel modules (vboxdrv, vboxnetflt, vboxnetadp, vboxpci) before you can load
them. Please see your Linux system&amp;#39;s documentation for more information.
&lt;/code>&lt;/pre>&lt;p>A look into &lt;code>/var/log/vbox-setup.log&lt;/code> revealed an error along the lines of this one&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>:&lt;/p></description><content:encoded><![CDATA[<p>I recently did a software update on my laptop running Fedora 38, and that also brought in a kernel update. Starting my Win10 VirtualBox VM afterwards no longer worked as it needed the kernel module to be recompiled. However, that failed:</p>
<pre tabindex="0"><code>$ sudo /sbin/vboxconfig 
[sudo] password for gina: 
vboxdrv.sh: Stopping VirtualBox services.
depmod: WARNING: could not open modules.order at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
depmod: WARNING: could not open modules.builtin at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
depmod: WARNING: could not open modules.builtin.modinfo at /lib/modules/6.3.8-200.fc38.x86_64: No such file or directory
vboxdrv.sh: Starting VirtualBox services.
vboxdrv.sh: Building VirtualBox kernel modules.
egrep: warning: egrep is obsolescent; using grep -E
vboxdrv.sh: failed: Look at /var/log/vbox-setup.log to find out what went wrong.

There were problems setting up VirtualBox.  To re-start the set-up process, run
  /sbin/vboxconfig
as root.  If your system is using EFI Secure Boot you may need to sign the
kernel modules (vboxdrv, vboxnetflt, vboxnetadp, vboxpci) before you can load
them. Please see your Linux system&#39;s documentation for more information.
</code></pre><p>A look into <code>/var/log/vbox-setup.log</code> revealed an error along the lines of this one<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<pre tabindex="0"><code>/tmp/akmodsbuild.bPlgZsDR/BUILD/VirtualBox-kmod-7.0.10/_kmod_build_6.4.10-200.fc38.x86_64/vboxnetflt/linux/VBoxNetFlt-linux.c: In function &#39;vboxNetFltLinuxForwardToIntNetInner&#39;:
/tmp/akmodsbuild.bPlgZsDR/BUILD/VirtualBox-kmod-7.0.10/_kmod_build_6.4.10-200.fc38.x86_64/vboxnetflt/linux/VBoxNetFlt-linux.c:1570:40: error: implicit declaration of function &#39;skb_gso_segment&#39;; did you mean &#39;skb_gso_reset&#39;? [-Werror=implicit-function-declaration]
 1570 |             struct sk_buff *pSegment = skb_gso_segment(pBuf, 0 /*supported features*/);
      |                                        ^~~~~~~~~~~~~~~
      |                                        skb_gso_reset
/tmp/akmodsbuild.bPlgZsDR/BUILD/VirtualBox-kmod-7.0.10/_kmod_build_6.4.10-200.fc38.x86_64/vboxnetflt/linux/VBoxNetFlt-linux.c:1570:40: warning: initialization of &#39;struct sk_buff *&#39; from &#39;int&#39; makes pointer from integer without a cast [-Wint-conversion]
cc1: some warnings being treated as errors
</code></pre><p>I did some web searching and came across <a href="https://discussion.fedoraproject.org/t/87492">this post on the Fedora forums</a> with someone having the exact same issue, and found <a href="https://discussion.fedoraproject.org/t/6-4-10-200-fc38-x86-64-created-problems-with-virtual-box/87492/12">a solution in the comments courtesy of Peter Francis</a>:</p>
<blockquote>
<p>Adding</p>
<pre><code>#include &lt;net/gso.h&gt;
</code></pre>
<p>below the line</p>
<pre><code>#include &lt;linux/inetdevice.h&gt;
</code></pre>
<p>in <code>/usr/share/virtualbox/src/vboxhost/vboxnetflt/linux/VBoxNetFlt-linux.c</code> fixed it for me.</p>
<p>Hopefully when the file gets overwritten on next VirtualBox update the fix will be already added by VirtualBox’s programmers.</p></blockquote>
<p>And what can I say, it also fixed it for me! And should this not get fixed in the next update, now I&rsquo;ll know where to find the solution again - my own TIL post 😉</p>
<p>PS: Something tells me this won&rsquo;t be the last VirtualBox related TIL post I&rsquo;ll write&hellip;</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>This is copy-pasted from someone else, as my log file got overwritten by the successful compile later. However it looked very much like this error, apart from my Kernel already being at 6.4.13.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to monitor network traffic on my USG via Wireshark</title><link>https://foosel.net/til/how-to-monitor-network-traffic-on-my-usg-via-wireshark/</link><pubDate>Mon, 28 Aug 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-monitor-network-traffic-on-my-usg-via-wireshark/</guid><description>&lt;p>I&amp;rsquo;m currently trying to figure out some internal network issues&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and for that need to monitor the traffic of a specific device on my network. I&amp;rsquo;m using a Unifi USG as my router (behind the ISP&amp;rsquo;s Fritzbox that I consider hostile since it&amp;rsquo;s not mine). I found &lt;a href="https://www.reddit.com/r/Ubiquiti/comments/ar444z/what_is_the_best_way_to_monitor_traffic_of_a/egkv91p/">this post on reddit&lt;/a> that explains how to capture traffic on the USG via &lt;code>tcpdump&lt;/code> and send it through the SSH session to Wireshark on my laptop:&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently trying to figure out some internal network issues<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> and for that need to monitor the traffic of a specific device on my network. I&rsquo;m using a Unifi USG as my router (behind the ISP&rsquo;s Fritzbox that I consider hostile since it&rsquo;s not mine). I found <a href="https://www.reddit.com/r/Ubiquiti/comments/ar444z/what_is_the_best_way_to_monitor_traffic_of_a/egkv91p/">this post on reddit</a> that explains how to capture traffic on the USG via <code>tcpdump</code> and send it through the SSH session to Wireshark on my laptop:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh admin@192.168.1.1 <span style="color:#e6db74">&#39;sudo tcpdump -f -i eth1 -w - src 192.168.1.12&#39;</span> | wireshark -k -i - 
</span></span></code></pre></div><p>I could confirm that this works and created a small script to make it easier to use by throwing this into <code>~/.local/bin/gatedump</code><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>ARGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>TCPDUMP<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sudo tcpdump -f -w - </span>$ARGS<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ssh usg <span style="color:#e6db74">&#34;</span>$TCPDUMP<span style="color:#e6db74">&#34;</span> | wireshark -k -i -
</span></span></code></pre></div><p>This now allows me to easily run <code>tcpdump</code> remotely with custom arguments, e.g. <code>gatedump -i eth1 host 192.168.1.123</code>, and have it fire up Wireshark automatically. Wish me luck I&rsquo;ll now be able to figure out what&rsquo;s going on on my network, because it&rsquo;s driving me up the wall.</p>
<p><em>Update from 2023-12-06</em>: In case you are wondering how this story ended, the issue resolved itself with the next OS update of my partner&rsquo;s iPhone. So whatever caused it, it&rsquo;s gone now, and I hope for good.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The iPhone of my partner seems to do something that makes my ISP&rsquo;s router freak out and drop packets every couple of minutes. No issue when he&rsquo;s not here or doesn&rsquo;t have it connected to the WiFi, immediate packet loss when it&rsquo;s on the WiFi. It started at the start of this month and we are both currently out of explanations.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><code>ssh usg</code> does automatically use the correct host, port and user thanks to an entry in <code>~/.ssh/config</code>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to make MkDocs support site_url relative URLs</title><link>https://foosel.net/til/how-to-make-mkdocs-support-siteurl-relative-urls/</link><pubDate>Thu, 27 Jul 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-make-mkdocs-support-siteurl-relative-urls/</guid><description>&lt;p>I&amp;rsquo;m currently &lt;em>finally&lt;/em> back on converting the &lt;a href="https://octoprint.org">OctoPrint&lt;/a> docs to using Markdown and &lt;a href="https://mkdocs.org">MkDocs&lt;/a>.&lt;/p>
&lt;p>Since I have some images in the docs that I want to be able to reference without having to use relative URLs (&lt;code>../../../../images/&lt;/code>),
especially since that would tie things in OctoPrint&amp;rsquo;s source tree structure too close to things in its documentation tree structure that
might or might not end up being in a different repository in the future, I needed a way to use absolute URLs here (&lt;code>/images/&lt;/code>). But
since the docs will most likely also end up being hosted on a version specific subpath of &lt;code>docs.octoprint.org&lt;/code>, just using
(host) absolute URLs would not work either and break.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently <em>finally</em> back on converting the <a href="https://octoprint.org">OctoPrint</a> docs to using Markdown and <a href="https://mkdocs.org">MkDocs</a>.</p>
<p>Since I have some images in the docs that I want to be able to reference without having to use relative URLs (<code>../../../../images/</code>),
especially since that would tie things in OctoPrint&rsquo;s source tree structure too close to things in its documentation tree structure that
might or might not end up being in a different repository in the future, I needed a way to use absolute URLs here (<code>/images/</code>). But
since the docs will most likely also end up being hosted on a version specific subpath of <code>docs.octoprint.org</code>, just using
(host) absolute URLs would not work either and break.</p>
<p>There&rsquo;s <a href="https://github.com/mkdocs/mkdocs/issues/1592">several</a> <a href="https://github.com/mkdocs/mkdocs/issues/192">issues</a> on the MkDocs issue
tracker about workflow problems caused by this, but the suggested workarounds like using <a href="https://mkdocs-macros-plugin.readthedocs.io/">macros</a>
to prefix a variable&rsquo;s contents to related URLs didn&rsquo;t work for me due to me also heavily relying on <a href="mkdocstrings.github.io/">mkdocstrings</a>,
and anything contained in docstrings is not processed by macros<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>So I got the idea to implement a minimal MkDocs plugin that would just turn all URLs contained in <code>href</code> and <code>src</code> attributes
that are prefixed with a custom schema <code>site:</code> schema into <em>site relative</em> URLs, with this effect. Example:</p>
<table>
  <thead>
      <tr>
          <th>URL</th>
          <th>site_url</th>
          <th>resulting URL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>site:images/foo.png</code></td>
          <td><code>https://docs.octoprint.org/</code></td>
          <td><code>/images/foo.png</code></td>
      </tr>
      <tr>
          <td><code>site:images/foo.png</code></td>
          <td><code>https://docs.octoprint.org/1.9.x/</code></td>
          <td><code>/1.9.x/images/foo.png</code></td>
      </tr>
  </tbody>
</table>
<p>Using a <a href="https://www.mkdocs.org/user-guide/configuration/#hooks">hook</a> I could register a callback for the
<a href="https://www.mkdocs.org/dev-guide/plugins/#on_page_content"><code>on_page_content</code></a> event that would then replace all URLs as needed
in the generated page HTML.</p>
<p>And this is the resulting <code>site_urls.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> logging
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> urllib.parse
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> mkdocs.plugins
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>log <span style="color:#f92672">=</span> logging<span style="color:#f92672">.</span>getLogger(<span style="color:#e6db74">&#34;mkdocs&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>SITE_URLS_REGEX <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>compile(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;(href|src)=&#34;site:([^&#34;]+)&#34;&#39;</span>, re<span style="color:#f92672">.</span>IGNORECASE)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@mkdocs.plugins.event_priority</span>(<span style="color:#ae81ff">50</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">on_page_content</span>(html, page, config, files):
</span></span><span style="display:flex;"><span>    site_url <span style="color:#f92672">=</span> config[<span style="color:#e6db74">&#34;site_url&#34;</span>]
</span></span><span style="display:flex;"><span>    path <span style="color:#f92672">=</span> urllib<span style="color:#f92672">.</span>parse<span style="color:#f92672">.</span>urlparse(site_url)<span style="color:#f92672">.</span>path
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> path:
</span></span><span style="display:flex;"><span>        path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> path<span style="color:#f92672">.</span>endswith(<span style="color:#e6db74">&#34;/&#34;</span>):
</span></span><span style="display:flex;"><span>        path <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#34;/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_replace</span>(<span style="color:#66d9ef">match</span>):
</span></span><span style="display:flex;"><span>        param <span style="color:#f92672">=</span> <span style="color:#66d9ef">match</span><span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        url <span style="color:#f92672">=</span> <span style="color:#66d9ef">match</span><span style="color:#f92672">.</span>group(<span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> url<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#34;/&#34;</span>):
</span></span><span style="display:flex;"><span>            url <span style="color:#f92672">=</span> url[<span style="color:#ae81ff">1</span>:]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        log<span style="color:#f92672">.</span>info(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Replacing site:</span><span style="color:#e6db74">{</span>match<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">2</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74"> with </span><span style="color:#e6db74">{</span>path<span style="color:#e6db74">}{</span>url<span style="color:#e6db74">}</span><span style="color:#e6db74">...&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>param<span style="color:#e6db74">}</span><span style="color:#e6db74">=&#34;</span><span style="color:#e6db74">{</span>path<span style="color:#e6db74">}{</span>url<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> SITE_URLS_REGEX<span style="color:#f92672">.</span>sub(_replace, html)
</span></span></code></pre></div><p>that I&rsquo;ve registered as a hook in my <code>mkdocs.yaml</code> like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">hooks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">site_urls.py</span>
</span></span></code></pre></div><p>Seems to work just fine, both for images and links! 😄</p>
<p><strong>Update 2023-07-28</strong>: I&rsquo;ve now published this as a proper plugin on PyPI, see <a href="https://pypi.org/project/mkdocs-site-urls/">mkdocs-site-urls</a>.
With that, all you need to do - given you are already on MkDocs 1.5 or newer - is installing the plugin via <code>pip install mkdocs-site-urls</code> and
then adding this to your <code>mkdocs.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">plugins</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">site-urls</span>
</span></span></code></pre></div><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>As I found out when I wanted to add a <code>version_added</code> macro, which simply didn&rsquo;t render.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>Why ESPHome might fail compiling a custom component with 'fatal error: string: No such file or directory'</title><link>https://foosel.net/til/why-esphome-might-fail-compiling-a-custom-component/</link><pubDate>Fri, 14 Jul 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/why-esphome-might-fail-compiling-a-custom-component/</guid><description>&lt;p>I just spent several hours trying to figure out why ESPHome refused to compile a custom component I was working on. The error message I got was&lt;/p>
&lt;pre tabindex="0">&lt;code>Compiling .pioenvs/datenzwerg-sleepy/src/esphome/components/sound_pressure/sound_pressure_sensor.c.o
In file included from src/esphome/components/sound_pressure/sound_pressure_sensor.h:3,
from src/esphome/components/sound_pressure/sound_pressure_sensor.c:1:
src/esphome/core/component.h:3:10: fatal error: string: No such file or directory
3 | #include &amp;lt;string&amp;gt;
| ^~~~~~~~
compilation terminated.
Compiling .pioenvs/datenzwerg-sleepy/src/main.cpp.o
*** [.pioenvs/datenzwerg-sleepy/src/esphome/components/sound_pressure/sound_pressure_sensor.c.o] Error 1
&lt;/code>&lt;/pre>&lt;p>Other external and internal components compiled just fine, so that was quite a head scratcher, until I just &lt;em>finally&lt;/em> noticed something in my source tree: my custom component&amp;rsquo;s source file had the file ending &lt;code>.c&lt;/code> instead of &lt;code>.cpp&lt;/code>. And that caused all of this, a quick rename from &lt;code>sound_pressure_sensor.c&lt;/code> to &lt;code>sound_pressure_sensor.cpp&lt;/code> resolved the compilation error 🤦‍♀️&lt;/p></description><content:encoded><![CDATA[<p>I just spent several hours trying to figure out why ESPHome refused to compile a custom component I was working on. The error message I got was</p>
<pre tabindex="0"><code>Compiling .pioenvs/datenzwerg-sleepy/src/esphome/components/sound_pressure/sound_pressure_sensor.c.o
In file included from src/esphome/components/sound_pressure/sound_pressure_sensor.h:3,
                 from src/esphome/components/sound_pressure/sound_pressure_sensor.c:1:
src/esphome/core/component.h:3:10: fatal error: string: No such file or directory
    3 | #include &lt;string&gt;
      |          ^~~~~~~~
compilation terminated.
Compiling .pioenvs/datenzwerg-sleepy/src/main.cpp.o
*** [.pioenvs/datenzwerg-sleepy/src/esphome/components/sound_pressure/sound_pressure_sensor.c.o] Error 1
</code></pre><p>Other external and internal components compiled just fine, so that was quite a head scratcher, until I just <em>finally</em> noticed something in my source tree: my custom component&rsquo;s source file had the file ending <code>.c</code> instead of <code>.cpp</code>. And that caused all of this, a quick rename from <code>sound_pressure_sensor.c</code> to <code>sound_pressure_sensor.cpp</code> resolved the compilation error 🤦‍♀️</p>
<p>And since it was utterly impossible for me to find anything about this particular problem online, I decided to share my mishap with all of you here, so that in the future someone else doing an internet search for this will have more luck than me today and not waste hours on this 😅</p>
]]></content:encoded></item><item><title>How to fix VirtualBox on Fedora 38 with Kernel 6.3.5 by disabling IBT</title><link>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-38-with-kernel-635/</link><pubDate>Tue, 13 Jun 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-fix-virtualbox-on-fedora-38-with-kernel-635/</guid><description>&lt;p>For accounting and some windows only software (👋 Affinity Designer) I have a Windows 10 VM running in VirtualBox on my Framework running Fedora 38. Apparently I got a kernel update recently and as of this morning the VM refused to start. It just hung, and a look into &lt;code>journalctl&lt;/code> showed something like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>Jun 13 10:23:50 draper kernel: traps: Missing ENDBR: 0xffff9b688c308f30
&lt;/code>&lt;/pre>&lt;p>After some searching I came across &lt;a href="https://forums.virtualbox.org/viewtopic.php?p=536761#p536761">this thread on the VirtualBox forums&lt;/a> which explained the issue and also includes the solution. Apparently the VirtualBox kernel driver &lt;a href="https://www.virtualbox.org/ticket/21435">triggers Intel&amp;rsquo;s IBT (indirect branch tracking)&lt;/a>. The solution is to disable that&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> by passing &lt;code>ibt=off&lt;/code> as a kernel parameter:&lt;/p></description><content:encoded><![CDATA[<p>For accounting and some windows only software (👋 Affinity Designer) I have a Windows 10 VM running in VirtualBox on my Framework running Fedora 38. Apparently I got a kernel update recently and as of this morning the VM refused to start. It just hung, and a look into <code>journalctl</code> showed something like this:</p>
<pre tabindex="0"><code>Jun 13 10:23:50 draper kernel: traps: Missing ENDBR: 0xffff9b688c308f30
</code></pre><p>After some searching I came across <a href="https://forums.virtualbox.org/viewtopic.php?p=536761#p536761">this thread on the VirtualBox forums</a> which explained the issue and also includes the solution. Apparently the VirtualBox kernel driver <a href="https://www.virtualbox.org/ticket/21435">triggers Intel&rsquo;s IBT (indirect branch tracking)</a>. The solution is to disable that<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> by passing <code>ibt=off</code> as a kernel parameter:</p>
<pre tabindex="0"><code>sudo grubby --update-kernel=ALL --args=&#34;ibt=off&#34;
</code></pre><p>After a reboot I could rebuild the vbox kernel driver via <code>/sbin/vboxconfig</code>, which ran through without issues, and after that the VM started up just fine.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Honestly, I&rsquo;d prefer to keep <a href="https://lwn.net/Articles/889475/">IBT</a> enabled for security reasons, but I need the VM to work. Let&rsquo;s hope VirtualBox fixes this soon, though given how long this seems to have gone on I&rsquo;m a bit skeptical.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to export a Godot 4 game to run on the web on itch.io</title><link>https://foosel.net/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/</link><pubDate>Sun, 14 May 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/</guid><description>&lt;p>On the &lt;a href="https://itch.io/jam/go-godot-jam-4">Go Godot Jam 4&lt;/a> Discord I just saw some people having issues with how to get HTML5 exports from Godot 4 to work on itch.io, and since I just had to do this for &lt;a href="https://foosel.itch.io/pew-pew-pew-danger-zone">my own game submission to the jam&lt;/a> as well I decided to jot my steps down here (and on the Discord too) as it seems to be a bit of a pain for people.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>First of all export your game using the &amp;ldquo;Web&amp;rdquo; export template&lt;/p></description><content:encoded><![CDATA[<p>On the <a href="https://itch.io/jam/go-godot-jam-4">Go Godot Jam 4</a> Discord I just saw some people having issues with how to get HTML5 exports from Godot 4 to work on itch.io, and since I just had to do this for <a href="https://foosel.itch.io/pew-pew-pew-danger-zone">my own game submission to the jam</a> as well I decided to jot my steps down here (and on the Discord too) as it seems to be a bit of a pain for people.</p>
<ol>
<li>
<p>First of all export your game using the &ldquo;Web&rdquo; export template</p>
<p><img alt="The export dialog of the Godot Engine 4 editor with the Web export template selected" loading="lazy" src="/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/export.png"></p>
</li>
<li>
<p>Navigate to your export folder, make sure to rename your <code>.html</code> file to <code>index.html</code></p>
</li>
<li>
<p>Zip all of it up, with all the files right within the root of the zip file</p>
<p><img alt="The contents of the created zip file, the html file has been renamed to index.html, all files are in the zip&rsquo;s root" loading="lazy" src="/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/zip.png"></p>
</li>
<li>
<p>Upload to itch, make sure to check &ldquo;This file will be played in the browser&rdquo;</p>
<p><img alt="itch.io&rsquo;s upload dialog, the mentioned option is checked next to the zip upload" loading="lazy" src="/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/upload.png"></p>
</li>
<li>
<p>Scroll down to &ldquo;Embed Options&rdquo; and make sure &ldquo;<code>SharedArrayBuffer</code> support&rdquo; is checked</p>
<p><img alt="The mentioned embed options, &ldquo;Shared Array Buffer support&rdquo; is checked" loading="lazy" src="/til/how-to-export-a-godot-4-game-to-run-on-the-web-on-itchio/embed_options.png"></p>
</li>
</ol>
]]></content:encoded></item><item><title>How to make the Home Assistant app sync properly under iOS</title><link>https://foosel.net/til/how-to-make-the-home-assistant-app-sync-properly-under-ios/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-make-the-home-assistant-app-sync-properly-under-ios/</guid><description>&lt;p>While I&amp;rsquo;m strongly rooted in the Android camp, my partner has an iPhone, and on what seems to be every iOS update, the Home Assistant app installed on his phone stops syncing in the background.&lt;/p>
&lt;p>That wouldn&amp;rsquo;t be so bad if a lot of the home automations didn&amp;rsquo;t factor in presence status which gets synced through that, so this has been a source of minor annoyance whenever his status refused to mirror his presence or absence. It just happened again and because every single time now we&amp;rsquo;ve had to try to remember how to fix it, here&amp;rsquo;s a quick TIL to encourage my memory 😅&lt;/p></description><content:encoded><![CDATA[<p>While I&rsquo;m strongly rooted in the Android camp, my partner has an iPhone, and on what seems to be every iOS update, the Home Assistant app installed on his phone stops syncing in the background.</p>
<p>That wouldn&rsquo;t be so bad if a lot of the home automations didn&rsquo;t factor in presence status which gets synced through that, so this has been a source of minor annoyance whenever his status refused to mirror his presence or absence. It just happened again and because every single time now we&rsquo;ve had to try to remember how to fix it, here&rsquo;s a quick TIL to encourage my memory 😅</p>
<p>And it&rsquo;s simple really. Open the iOS settings, scroll down to the Home Assistant settings and make sure that location sharing is set to &ldquo;Always&rdquo;.</p>
]]></content:encoded></item><item><title>How to add Battle.net games to the Steamdeck</title><link>https://foosel.net/til/how-to-add-battle-net-games-to-the-steamdeck/</link><pubDate>Sat, 15 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-battle-net-games-to-the-steamdeck/</guid><description>&lt;p>&lt;em>Update 2023-06-07: It turns out that these days, the by far easiest way to get Battle.net on the SteamDeck is using &lt;a href="https://github.com/moraroy/NonSteamLaunchers-On-Steam-Deck">NonSteamLaunchers-On-Steam-Deck&lt;/a>, as I recently saw on &lt;a href="https://www.gamingonlinux.com/2023/05/get-battlenet-ea-epic-games-and-more-on-steam-deck-the-easy-way/">Gaming On Linux&lt;/a>. I haven&amp;rsquo;t gotten a chance to try this myself yet, but it certainly looks very much straight forward, albeit not featuring individual game entries in Steam. For your quick Diablo fix, it should hopefully be just fine though.&lt;/em>&lt;/p>
&lt;p>Battle.net is currently having a spring sale and I&amp;rsquo;ve been eyeing Diablo II: Resurrected for a while now, so I jumped on the chance (and while at it also got StarCraft Remastered). But given that these days I primarily game on the Steamdeck, I needed to find a way to install Battle.net on my deck and also install individual launchers for the games.&lt;/p></description><content:encoded><![CDATA[<p><em>Update 2023-06-07: It turns out that these days, the by far easiest way to get Battle.net on the SteamDeck is using <a href="https://github.com/moraroy/NonSteamLaunchers-On-Steam-Deck">NonSteamLaunchers-On-Steam-Deck</a>, as I recently saw on <a href="https://www.gamingonlinux.com/2023/05/get-battlenet-ea-epic-games-and-more-on-steam-deck-the-easy-way/">Gaming On Linux</a>. I haven&rsquo;t gotten a chance to try this myself yet, but it certainly looks very much straight forward, albeit not featuring individual game entries in Steam. For your quick Diablo fix, it should hopefully be just fine though.</em></p>
<p>Battle.net is currently having a spring sale and I&rsquo;ve been eyeing Diablo II: Resurrected for a while now, so I jumped on the chance (and while at it also got StarCraft Remastered). But given that these days I primarily game on the Steamdeck, I needed to find a way to install Battle.net on my deck and also install individual launchers for the games.</p>
<h2 id="battlenet">Battle.net</h2>
<p>I first made the mistake of trying my luck with installing the Battle.net launcher directly via Steam Proton. Trust me: Don&rsquo;t. Lots of work, doesn&rsquo;t end up well. Instead, I went with <a href="https://lutris.net/">Lutris</a>. Lutris has been installed for a while, but if that&rsquo;s not yet on your deck, install that first via the Discover app. Then:</p>
<ol>
<li>Enter desktop mode</li>
<li>Navigate to <a href="https://lutris.net/games/battlenet/">https://lutris.net/games/battlenet/</a> and click on &ldquo;Install&rdquo;. That will fire up an install script in your Lutris setup. Follow the steps shown to you by the app.</li>
<li>Once the installer has run its course, log into Battle.net. You should now be able to download stuff.</li>
<li>Right click on Battle.net in Lutris, select &ldquo;Add steam shortcut&rdquo; (if that&rsquo;s not available, but &ldquo;Delete steam shortcut&rdquo; is - nothing to do, the shortcut has already been added). This will become available after Steam has been restarted, don&rsquo;t worry about it now.</li>
</ol>
<h2 id="diablo-ii-resurrected">Diablo II: Resurrected</h2>
<p>This will create a Lutris app that uses the same Wine prefix as your Battle.net install. This is important! In desktop mode:</p>
<ol>
<li>Navigate to <a href="https://lutris.net/games/diablo-2-ressurected/">https://lutris.net/games/diablo-2-ressurected/</a> (not a typo, there <em>is</em> a typo in that slug indeed), click &ldquo;Install&rdquo;. Follow the steps.</li>
<li>If not yet done, launch Battle.net and install Diablo II: Resurrected.</li>
<li>Launch Diablo once via Battle.net, exit as soon as you can.</li>
<li>In Lutris, right click on the Diablo entry, select &ldquo;Configure&rdquo; and go to &ldquo;Game options&rdquo;</li>
<li>Click &ldquo;Browse&rdquo; next to &ldquo;Executable&rdquo;. Change it to to <code>/home/deck/Games/battlenet/drive_c/Program Files (x86)/Diablo II Resurrected/D2R.exe</code> (so, one folder up, into the Diablo folder and there select <code>D2R.exe</code>)</li>
<li>Under Arguments add <code>-launch</code></li>
<li>Save</li>
</ol>
<p>Make sure the Steam shortcut for the app is added.</p>
<h2 id="starcraft">StarCraft</h2>
<p>Just as with Diablo II, it is important that the StarCraft entry uses the same Wine prefix as Battle.net. Either duplicate the existing &ldquo;Diablo II: Resurrected&rdquo; entry or alternatively follow steps 1 and 2 above. Then do the following with your game entry:</p>
<ol>
<li>Launch Battle.net, install Starcraft Remastered.</li>
<li>Right click on the designated game entry (either your duplicate of Diablo II, or the Diablo II entry you don&rsquo;t intend to use), select &ldquo;Configure&rdquo;</li>
<li>Under &ldquo;Game info&rdquo;, change the name to &ldquo;Starcraft Remastered&rdquo; and the identifier to &ldquo;starcraft-remastered&rdquo;</li>
<li>Under &ldquo;Game options&rdquo;, change the executable to <code>/home/deck/Games/battlenet/drive_c/Program Files (x86)/StarCraft/x86_64/StarCraft.exe</code> and add <code>-launch</code> to the Arguments</li>
<li>Save</li>
</ol>
<p>The art in Lutris should update automatically. Make sure the Steam shortcut for the app is added.</p>
<p>Once all of that is done, head back into game mode. Feel free to change the art for the newly created entries, e.g. through something like the <a href="https://github.com/SteamGridDB/decky-steamgriddb">SteamGridDB</a> plugin for <a href="https://deckbrew.xyz/">Decky</a>. I also set up a dedicated &ldquo;Battle.net&rdquo; collection and added Battle.net itself plus both games to it.</p>
<p><img alt="Screenshot from the Steamdeck, showing a &ldquo;Battle.net&rdquo; collection containing Battle.net, Diablo II: Resurrected and StarCraft Remastered shortcuts" loading="lazy" src="/til/how-to-add-battle-net-games-to-the-steamdeck/steamdeck-battlenet-1.png"></p>
<p>You should now be able to launch your games through their individual Steam shortcuts. Note that the way we have set up things, Battle.net will not be fully available, so if you need that (for multiplayer or for unlocking DLCs) you&rsquo;ll need to launch through the Battle.net shortcut instead. In the case of the Cartoon skin for StarCraft Remastered, launching Starcraft once through Battle.net sufficed however ^^</p>
<p><img alt="Screenshot of the cartoon skin for StarCraft Remastered" loading="lazy" src="/til/how-to-add-battle-net-games-to-the-steamdeck/steamdeck-battlenet-2.png"></p>
]]></content:encoded></item><item><title>How to use Obsidian's Dataview plugin to visualize frontmatter</title><link>https://foosel.net/til/how-to-use-obsidians-dataview-plugin-to-visualize-frontmatter/</link><pubDate>Fri, 14 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-use-obsidians-dataview-plugin-to-visualize-frontmatter/</guid><description>&lt;p>For every &lt;a href="https://octoprint.org">OctoPrint&lt;/a> release I run through several update tests: I flash a specific OctoPi version, push it to a specific OctoPrint version, configure the release channel, then see if updating to the newest release (candidate) works fine. I use &lt;a href="https://octoprint.org/blog/2020/07/29/automating-octoprints-release-tests/">my testrig and its automation scripts&lt;/a> for that and usually go through something between 5 and 10 separate scenarios.&lt;/p>
&lt;p>So far all of these scenarios were noted down as a Markdown table in my release checklist that these days I prepare in my &lt;a href="https://obsidian.md">Obsidian&lt;/a> vault, including manually adjusting the testrig commands to match the scenario. Having to take care of the latter is something that has been annoying for a long time now, and during the preparation for &lt;a href="https://github.com/OctoPrint/OctoPrint/releases/tag/1.9.0rc5">yesterday&amp;rsquo;s release candidate&lt;/a> I decided enough is enough and looked into improving my tooling a bit. In the end, I used Obsidian&amp;rsquo;s quite amazing &lt;a href="https://blacksmithgu.github.io/obsidian-dataview/">Dataview plugin&lt;/a> to query the information about the planned test scenarios from the checklist&amp;rsquo;s frontmatter, build the testrig command from that, then render all of this as a table, complete with some checkboxes for state logging during the tests and a copy button for the command.&lt;/p></description><content:encoded><![CDATA[<p>For every <a href="https://octoprint.org">OctoPrint</a> release I run through several update tests: I flash a specific OctoPi version, push it to a specific OctoPrint version, configure the release channel, then see if updating to the newest release (candidate) works fine. I use <a href="https://octoprint.org/blog/2020/07/29/automating-octoprints-release-tests/">my testrig and its automation scripts</a> for that and usually go through something between 5 and 10 separate scenarios.</p>
<p>So far all of these scenarios were noted down as a Markdown table in my release checklist that these days I prepare in my <a href="https://obsidian.md">Obsidian</a> vault, including manually adjusting the testrig commands to match the scenario. Having to take care of the latter is something that has been annoying for a long time now, and during the preparation for <a href="https://github.com/OctoPrint/OctoPrint/releases/tag/1.9.0rc5">yesterday&rsquo;s release candidate</a> I decided enough is enough and looked into improving my tooling a bit. In the end, I used Obsidian&rsquo;s quite amazing <a href="https://blacksmithgu.github.io/obsidian-dataview/">Dataview plugin</a> to query the information about the planned test scenarios from the checklist&rsquo;s frontmatter, build the testrig command from that, then render all of this as a table, complete with some checkboxes for state logging during the tests and a copy button for the command.</p>
<p>Here&rsquo;s the format of the frontmatter for the latest release candidate 1.9.0rc5:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">update_test_type</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">update_tests</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">0.17.0</span>-<span style="color:#ae81ff">py3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">update_test_type</span>: <span style="color:#ae81ff">simplepip</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">additional</span>: <span style="color:#ae81ff">pip=21.3.1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">0.18.0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">start</span>: <span style="color:#ae81ff">1.8.7</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">0.18.0</span>-<span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">1.0.0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">start</span>: <span style="color:#ae81ff">1.8.7</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">1.0.0</span>-<span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">stable</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">1.0.0</span>-<span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">start</span>: <span style="color:#ae81ff">1.9</span><span style="color:#ae81ff">.0rc3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">image</span>: <span style="color:#ae81ff">1.0.0</span>-<span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">channel</span>: <span style="color:#ae81ff">maintenance</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">start</span>: <span style="color:#ae81ff">1.9</span><span style="color:#ae81ff">.0rc4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">wip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">done</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>And this is the <code>dataviewjs</code> query I used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">createCheckbox</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">checked</span>) =&gt; {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">checkbox</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">container</span>.<span style="color:#a6e22e">createEl</span>(<span style="color:#e6db74">&#34;input&#34;</span>, {<span style="color:#e6db74">&#34;type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;checkbox&#34;</span>});
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">checkbox</span>.<span style="color:#a6e22e">checked</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">checked</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">checkbox</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">createDropdown</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">options</span>, <span style="color:#a6e22e">selected</span>) =&gt; {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">dropdown</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">container</span>.<span style="color:#a6e22e">createEl</span>(<span style="color:#e6db74">&#34;select&#34;</span>);
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">options</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">value</span>, <span style="color:#a6e22e">idx</span>) =&gt; {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">option</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">dropdown</span>.<span style="color:#a6e22e">createEl</span>(<span style="color:#e6db74">&#34;option&#34;</span>, {<span style="color:#e6db74">&#34;text&#34;</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">value</span>, <span style="color:#e6db74">&#34;value&#34;</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">value</span>});
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">option</span>.<span style="color:#a6e22e">selected</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">value</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">selected</span>);
</span></span><span style="display:flex;"><span>	});
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">dropdown</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">createCopyButton</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">command</span>) =&gt; {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">button</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">container</span>.<span style="color:#a6e22e">createEl</span>(<span style="color:#e6db74">&#34;button&#34;</span>, {<span style="color:#e6db74">&#34;text&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Copy&#34;</span>})
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">button</span>.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#34;click&#34;</span>, (<span style="color:#a6e22e">evt</span>) =&gt; {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">preventDefault</span>();
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">navigator</span>.<span style="color:#a6e22e">clipboard</span>.<span style="color:#a6e22e">writeText</span>(<span style="color:#a6e22e">command</span>);
</span></span><span style="display:flex;"><span>	});
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">button</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">DUTS</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;pia&#34;</span>, <span style="color:#e6db74">&#34;pib&#34;</span>, <span style="color:#e6db74">&#34;pic&#34;</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">update_type</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">dv</span>.<span style="color:#a6e22e">current</span>().<span style="color:#a6e22e">update_test_type</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">&#34;maintenance&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">tests</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">dv</span>.<span style="color:#a6e22e">current</span>().<span style="color:#a6e22e">update_tests</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">dv</span>.<span style="color:#a6e22e">table</span>([<span style="color:#e6db74">&#34;ID&#34;</span>, <span style="color:#e6db74">&#34;WIP&#34;</span>, <span style="color:#e6db74">&#34;Done&#34;</span>, <span style="color:#e6db74">&#34;DUT&#34;</span>, <span style="color:#e6db74">&#34;OctoPi&#34;</span>, <span style="color:#e6db74">&#34;Channel&#34;</span>, <span style="color:#e6db74">&#34;Start&#34;</span>, <span style="color:#e6db74">&#34;Command&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>], <span style="color:#a6e22e">tests</span>
</span></span><span style="display:flex;"><span>	.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">idx</span>) =&gt; {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">dut</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">dut</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">DUTS</span>[<span style="color:#a6e22e">idx</span> <span style="color:#f92672">%</span> <span style="color:#a6e22e">DUTS</span>.<span style="color:#a6e22e">length</span>];
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">version</span> <span style="color:#f92672">=</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">start</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">start</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;stock&#34;</span>) <span style="color:#f92672">?</span> <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">`,version=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">start</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">command</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">switch</span> (<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">update_test_type</span>) {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;simplepip&#34;</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`fab flashhost_flash_and_provision:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">image</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> octopi_test_simplepip`</span>;
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">additional</span>) {
</span></span><span style="display:flex;"><span>					<span style="color:#a6e22e">command</span> <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#34;:&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">additional</span>;
</span></span><span style="display:flex;"><span>				}
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">default</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`fab flashhost_flash_and_provision:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">image</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> octopi_test_update_</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">update_type</span><span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">channel</span><span style="color:#e6db74">}${</span><span style="color:#a6e22e">version</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">additional</span>) {
</span></span><span style="display:flex;"><span>				  <span style="color:#a6e22e">command</span> <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#34;,&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">additional</span>
</span></span><span style="display:flex;"><span>				}
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wipCheckbox</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createCheckbox</span>(<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">wip</span>);
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">doneCheckbox</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createCheckbox</span>(<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">done</span>);
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">dutDropdown</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createDropdown</span>(<span style="color:#a6e22e">DUTS</span>, <span style="color:#a6e22e">dut</span>);
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">copyButton</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createCopyButton</span>(<span style="color:#a6e22e">command</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> [<span style="color:#a6e22e">idx</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">wipCheckbox</span>, <span style="color:#a6e22e">doneCheckbox</span>, <span style="color:#a6e22e">dutDropdown</span>, <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">image</span>, <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">channel</span>, <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">start</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">start</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;stock&#34;</span>, <span style="color:#e6db74">&#34;`&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">command</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;`&#34;</span>, <span style="color:#a6e22e">copyButton</span>]
</span></span><span style="display:flex;"><span>	}));
</span></span></code></pre></div><p>This iterates over the list of scenarios, and for each generates a command, assigns one of the DUTs (Device Under Test) of the testrig, creates checkboxes for WIP and Done seeded from the frontmatter data and also a copy button. This then gets rendered as a table.</p>
<p>Throwing that query in a <code>dataviewjs</code> typed markdown code fence yields a nice rendition of the data that allows me to check if I have forgotten anything, track my progress with the checkboxes and also allows me to easily copy the generated command with the click of a button, so I don&rsquo;t have to manually copy paste stuff anymore:</p>
<p><img alt="A screenshot of a table visualizing the test scenarios. The columns are ID, WIP, Done, DUT, OctoPi, Channel, Start and Command, the rows are the secnarios. Each scenario has a testrig command attached that can be copied with a dedicated button." loading="lazy" src="/til/how-to-use-obsidians-dataview-plugin-to-visualize-frontmatter/scenario-table.png"></p>
<p>What I have not yet figured out is how to manipulate the frontmatter directly via the checkboxes and DUT dropdowns (for logging purposes) - right now I still have to edit the state changes manually to persist them. The <a href="https://github.com/chhoumann/MetaEdit">MetaEdit</a> plugin for Obsidian looked promising, but that doesn&rsquo;t seem to be flexible enough to manipulate a list of items (yet).</p>
<p>In any case, this helped me a ton yesterday, and will save me a lot of time with future releases and release candidates!</p>
]]></content:encoded></item><item><title>How to override the EDID data of a monitor under Linux</title><link>https://foosel.net/til/how-to-override-the-edid-data-of-a-monitor-under-linux/</link><pubDate>Tue, 11 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-override-the-edid-data-of-a-monitor-under-linux/</guid><description>&lt;p>I&amp;rsquo;m slowly but surely fixing all the issues I had after switching back to Linux as my main OS, so here&amp;rsquo;s another TIL 😉&lt;/p>
&lt;p>My secondary monitor is a 24&amp;quot; DELL with a resolution of 1920x1200, so 16:10, instead of the more common 1080p and 16:9. In order to be able to connect all my three monitors to my laptop, I make use of both the laptop&amp;rsquo;s HDMI port as well as a USB-C dock that has 2 HDMI ports. The 4k main display is connected directly to the laptop&amp;rsquo;s HDMI port, secondary and tertiary display are connected to the dock.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m slowly but surely fixing all the issues I had after switching back to Linux as my main OS, so here&rsquo;s another TIL 😉</p>
<p>My secondary monitor is a 24&quot; DELL with a resolution of 1920x1200, so 16:10, instead of the more common 1080p and 16:9. In order to be able to connect all my three monitors to my laptop, I make use of both the laptop&rsquo;s HDMI port as well as a USB-C dock that has 2 HDMI ports. The 4k main display is connected directly to the laptop&rsquo;s HDMI port, secondary and tertiary display are connected to the dock.</p>
<p>The problem now was that as long as I had both secondary and tertiary connected, I could only select up to 1080p for the secondary. That would not have been that big of an issue if that wouldn&rsquo;t have caused the monitor to try to scale the 16:9 output to 16:10, stretching it. With only the secondary display attached however, it would detect the 16:10 resolution just fine.</p>
<p>On Windows, with the same hardware setup (albeit a different laptop), this was quickly solved with <a href="https://www.monitortests.com/forum/Thread-Custom-Resolution-Utility-CRU">a third party tool</a> and ran just fine for months. On Linux I was stumped for the past few weeks since switching. Adding <code>video=</code> lines to the kernel parameters - as suggested by various guides - didn&rsquo;t seem to do the trick, and that appears to be the only option these days with Wayland in the mix to do something like this.</p>
<p>Today however I fell over <a href="https://wiki.archlinux.org/title/kernel_mode_setting#Forcing_modes_and_EDID">this helpful section</a> on the ArchLinux wiki, and that gave me an idea. Apparently the EDID information of the secondary is correct when it is connected alone, or I wouldn&rsquo;t be able to select 1920x1200 then. So I should be able to just grab the EDID data from it when working and force that to be used all the time via a kernel parameter.</p>
<p>I disconnected my tertiary display and verified the secondary was now again being detected as supporting 1920x1200. Then I first figured out its port by checking which of the available devices showed as connected (and wasn&rsquo;t the internal laptop screen or the 4k primary):</p>
<pre tabindex="0"><code>$ for p in /sys/class/drm/*/status; do con=${p%/status}; echo -n &#34;${con#*/card?-}: &#34;; cat $p; done
DP-1: disconnected
DP-2: connected
DP-3: disconnected
DP-4: disconnected
DP-5: disconnected
DP-6: connected
DP-7: disconnected
eDP-1: connected
</code></pre><p>In my case that turned out to be <code>DP-6</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. Next I quickly dumped the EDID information to a new file <code>/usr/lib/firmware/edid/dell-24-1200p.bin</code>:</p>
<pre tabindex="0"><code>sudo mkdir -p /usr/lib/firmware/edid
sudo cp /sys/class/drm/card1-DP-6/edid /usr/lib/firmware/edid/dell-24-1200p.bin
</code></pre><p>Then I used <code>grubby</code> to add a <code>drm.edid_firmware</code> kernel mode setting to all kernel entries for <code>DP-6</code> that tells the kernel to use this EDID file:</p>
<pre tabindex="0"><code>sudo grubby --update-kernel=ALL --args=&#34;drm.edid_firmware=DP-6:edid/dell-24-1200p.bin&#34;
</code></pre><p>And one reboot later I could finally select 1920x1200 for my display! 🥳</p>
<p><em>Addendum from 2023-05-24</em>*: Recently the display&rsquo;s identifier&rsquo;s started switching between <code>DP-6</code> and <code>DP-8</code>, sometimes even when waking up from sleep. I&rsquo;m not sure why, and I so far did not have time to investigate, but just so I or anyone else stumbling over this knows how to do this in the future, it&rsquo;s easy to set custom edid data on multiple devices as well, in my case:</p>
<pre tabindex="0"><code>sudo grubby --update-kernel=ALL --args=&#34;drm.edid_firmware=DP-6:edid/dell-24-1200p.bin,DP-8:edid/dell-24-1200p.bin&#34;
</code></pre><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>DP-2</code> is the primary and <code>eDP-1</code> the laptop&rsquo;s internal display&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to reduce the titlebar size of Gnome 43</title><link>https://foosel.net/til/how-to-reduce-the-titlebar-size-of-gnome/</link><pubDate>Tue, 11 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-reduce-the-titlebar-size-of-gnome/</guid><description>&lt;p>A few weeks ago I switched back to Linux as my primary OS, on a newly acquired refurbished &lt;a href="https://frame.work">Framework Laptop 11&lt;/a>, and one thing that&amp;rsquo;s since been bothering me on my chosen desktop environment Gnome&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> has been the HUGE titlebars:&lt;/p>
&lt;p>&lt;img alt="Before: A quite tall title bar with a lot of padding, wasting space" loading="lazy" src="https://foosel.net/til/how-to-reduce-the-titlebar-size-of-gnome/before.png">&lt;/p>
&lt;p>So I finally dug into solving this quickly, and came across &lt;a href="https://www.reddit.com/r/gnome/comments/y61xhm/comment/ivay6db/">this post on Reddit&lt;/a> with a quite nice solution. I modified &lt;code>~/.config/gtk-3.0/gtk.css&lt;/code> and added the following contents:&lt;/p></description><content:encoded><![CDATA[<p>A few weeks ago I switched back to Linux as my primary OS, on a newly acquired refurbished <a href="https://frame.work">Framework Laptop 11</a>, and one thing that&rsquo;s since been bothering me on my chosen desktop environment Gnome<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> has been the HUGE titlebars:</p>
<p><img alt="Before: A quite tall title bar with a lot of padding, wasting space" loading="lazy" src="/til/how-to-reduce-the-titlebar-size-of-gnome/before.png"></p>
<p>So I finally dug into solving this quickly, and came across <a href="https://www.reddit.com/r/gnome/comments/y61xhm/comment/ivay6db/">this post on Reddit</a> with a quite nice solution. I modified <code>~/.config/gtk-3.0/gtk.css</code> and added the following contents:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#f92672">window</span>.<span style="color:#a6e22e">ssd</span> <span style="color:#f92672">headerbar</span>.<span style="color:#a6e22e">titlebar</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">padding-top</span>: <span style="color:#ae81ff">2</span><span style="color:#66d9ef">px</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">padding-bottom</span>: <span style="color:#ae81ff">2</span><span style="color:#66d9ef">px</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">min-height</span>: <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#f92672">window</span>.<span style="color:#a6e22e">ssd</span> <span style="color:#f92672">headerbar</span>.<span style="color:#a6e22e">titlebar</span> <span style="color:#f92672">button</span>.<span style="color:#a6e22e">titlebutton</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">padding</span>: <span style="color:#ae81ff">1</span><span style="color:#66d9ef">px</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">min-height</span>: <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">min-width</span>: <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That resulted in this:</p>
<p><img alt="After: The title bar reduced to the bare minimum in height, with only a minimal amount of padding, no more wasting space" loading="lazy" src="/til/how-to-reduce-the-titlebar-size-of-gnome/after.png"></p>
<p>And now I&rsquo;m happy, at least with non-Gnome apps, my chosen development environment VSCode included.</p>
<p><em>Update 2023-04-30: Alas, that no longer works under Gnome 44, so for now I&rsquo;m stuck with oversized titlebars again.</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Currently Gnome 43.3 running under Wayland on Fedora Workstation 37&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to create an animated tile in Godot 4's tilemaps</title><link>https://foosel.net/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/</link><pubDate>Sun, 09 Apr 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/</guid><description>&lt;p>Over the past four days I&amp;rsquo;ve been doing &lt;a href="https://chaos.social/@foosel/110145537112426679">a personal crash course on game development&lt;/a> together with my partner&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>, ending up in building &lt;a href="https://chaos.social/@foosel/110165066201016720">a small platformer in two days&lt;/a>. We decided to use &lt;a href="https://godotengine.org">Godot Engine&lt;/a>, as I had been circling it for a while now &lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, and it turned out this decision was good because it was been a quite amazing experience, even for a total gamedev beginner like me 👍&lt;/p>
&lt;p>What cost me a bunch of time however is trying to figure out how to create an animated tile in a &lt;a href="https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html">tilemap&lt;/a>, which I finally figured out yesterday, and so I&amp;rsquo;m writing it down here for future me and everyone else wondering about this.&lt;/p></description><content:encoded><![CDATA[<p>Over the past four days I&rsquo;ve been doing <a href="https://chaos.social/@foosel/110145537112426679">a personal crash course on game development</a> together with my partner<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, ending up in building <a href="https://chaos.social/@foosel/110165066201016720">a small platformer in two days</a>. We decided to use <a href="https://godotengine.org">Godot Engine</a>, as I had been circling it for a while now <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>, and it turned out this decision was good because it was been a quite amazing experience, even for a total gamedev beginner like me 👍</p>
<p>What cost me a bunch of time however is trying to figure out how to create an animated tile in a <a href="https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html">tilemap</a>, which I finally figured out yesterday, and so I&rsquo;m writing it down here for future me and everyone else wondering about this.</p>
<p>First of all, you need a graphic of your tile-to-be-animated. It should have all frames side by side, like this example:</p>
<p><img alt="Screenshot of a png, 64x16px in size, with four frames of a cartoonish floating carrot" loading="lazy" src="/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/step1.png"></p>
<p>This is going to become a floating 16x16px carot, its animation consisting of four frames.</p>
<p>Add it to a tileset by dragging the resource into the left column, and make sure that <em>only the first frame</em> becomes part of your tileset! If you have let automatic atlas tooling do its thing, <em>delete</em> the other frames again under &ldquo;Setup&rdquo;:</p>
<p><img alt="Screenshot of Godot 4&rsquo;s tileset editor, showing the freshly added carrot, deleting wrongly added tiles" loading="lazy" src="/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/step2.png"></p>
<p>Then switch to the tileset editor&rsquo;s &ldquo;Select&rdquo; panel and click on the first frame of your animation. On the left you&rsquo;ll see some options for that tile. Open &ldquo;Animation&rdquo;, then increase the number of animation frames to the number of your available frames (4 for our carrot here), and after that make sure to click on &ldquo;Add Frame&rdquo; once for each of your frames. You then can also define how long the frame will be shown, 1s by default. 0.3s will result in something like 10fps.</p>
<p><img alt="Screenshot of Godot 4&rsquo;s tileset editor, showing the final animation setup on the &ldquo;Select&rdquo; panel as described in the text" loading="lazy" src="/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/step3.png"></p>
<p>After that, placing the <em>first frame</em> in a tilemap should show it being animated now:</p>
<video controls preload="auto" width="100%"  playsinline class="html-video">
    <source src="/til/how-to-create-an-animated-tile-in-godot-4s-tilemaps/step4.webm" type="video/webm">
  <span></span>
</video>
<p>🥳</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Him focusing on pixel art, me on the coding bits.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Not only because it&rsquo;s also open source, though that also plays a big part!&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>About SSH escape sequences</title><link>https://foosel.net/til/about-ssh-escape-sequences/</link><pubDate>Wed, 22 Mar 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/about-ssh-escape-sequences/</guid><description>&lt;p>OpenSSH&amp;rsquo;s &lt;code>ssh&lt;/code> command supports a bunch of escape sequences while a session is running, by default triggered by the &lt;code>~&lt;/code> character. According to &lt;a href="https://linux.die.net/man/1/ssh">&lt;code>man ssh&lt;/code>&lt;/a> a list of available commands can be requested with &lt;code>~?&lt;/code>. And indeed, hitting &lt;code>~?&lt;/code> within an open SSH session prints some helpful information:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plain" data-lang="plain">&lt;span style="display:flex;">&lt;span>$ ~?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Supported escape sequences:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~. - terminate connection (and any multiplexed sessions)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~B - send a BREAK to the remote system
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~C - open a command line
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~R - request rekey
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~V/v - decrease/increase verbosity (LogLevel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~^Z - suspend ssh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~# - list forwarded connections
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~&amp;amp; - background ssh (when waiting for connections to terminate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~? - this message
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ~~ - send the escape character by typing it twice
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(Note that escapes are only recognized immediately after newline.)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I most commonly require &lt;code>~.&lt;/code> to disconnect from a broken SSH session (e.g. something I still had open on my laptop when I sent it to sleep).&lt;/p></description><content:encoded><![CDATA[<p>OpenSSH&rsquo;s <code>ssh</code> command supports a bunch of escape sequences while a session is running, by default triggered by the <code>~</code> character. According to <a href="https://linux.die.net/man/1/ssh"><code>man ssh</code></a> a list of available commands can be requested with <code>~?</code>. And indeed, hitting <code>~?</code> within an open SSH session prints some helpful information:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>$ ~?
</span></span><span style="display:flex;"><span>Supported escape sequences:
</span></span><span style="display:flex;"><span> ~.   - terminate connection (and any multiplexed sessions)
</span></span><span style="display:flex;"><span> ~B   - send a BREAK to the remote system
</span></span><span style="display:flex;"><span> ~C   - open a command line
</span></span><span style="display:flex;"><span> ~R   - request rekey
</span></span><span style="display:flex;"><span> ~V/v - decrease/increase verbosity (LogLevel)
</span></span><span style="display:flex;"><span> ~^Z  - suspend ssh
</span></span><span style="display:flex;"><span> ~#   - list forwarded connections
</span></span><span style="display:flex;"><span> ~&amp;   - background ssh (when waiting for connections to terminate)
</span></span><span style="display:flex;"><span> ~?   - this message
</span></span><span style="display:flex;"><span> ~~   - send the escape character by typing it twice
</span></span><span style="display:flex;"><span>(Note that escapes are only recognized immediately after newline.)
</span></span></code></pre></div><p>I most commonly require <code>~.</code> to disconnect from a broken SSH session (e.g. something I still had open on my laptop when I sent it to sleep).</p>
<p>The command line opened via <code>~C</code> is quite interesting as well, as it allows configuration of port forwards on the fly, while the session is already running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>ssh&gt; help
</span></span><span style="display:flex;"><span>Commands:
</span></span><span style="display:flex;"><span>      -L[bind_address:]port:host:hostport    Request local forward
</span></span><span style="display:flex;"><span>      -R[bind_address:]port:host:hostport    Request remote forward
</span></span><span style="display:flex;"><span>      -D[bind_address:]port                  Request dynamic forward
</span></span><span style="display:flex;"><span>      -KL[bind_address:]port                 Cancel local forward
</span></span><span style="display:flex;"><span>      -KR[bind_address:]port                 Cancel remote forward
</span></span><span style="display:flex;"><span>      -KD[bind_address:]port                 Cancel dynamic forward
</span></span></code></pre></div><p>This is once again a &ldquo;TIL&rdquo; that I didn&rsquo;t actually learn about only today, but I keep forgetting about it and then need to frantically google whenever I need it. I hope this way I&rsquo;ll finally remember this stuff 😅</p>
]]></content:encoded></item><item><title>How to add an audio delay for video conferencing on Linux/Pulseaudio</title><link>https://foosel.net/til/how-to-add-an-audio-delay-for-video-conferencing-on-linuxpulseaudio/</link><pubDate>Sat, 11 Mar 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-an-audio-delay-for-video-conferencing-on-linuxpulseaudio/</guid><description>&lt;p>After recently switching to work under Linux, I needed a way to replicate &lt;a href="../how-to-add-an-audio-delay-for-video-conferencing-on-windows">my existing solution for delaying audio under Windows&lt;/a> under Linux/Pulseaudio.&lt;/p>
&lt;p>To once again explain my situation, I use &lt;a href="https://obsproject.com/">OBS&lt;/a> also for video conferencing, through the virtual camera&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. OBS does not offer a built-in way to provide a virtual microphone with all the filters and such applied as well (in my case noise reduction and a limiter), so I need to solve this in a separate way. Additionally, my camera setup has a small delay of around 350ms that I also need to compensate by delaying my audio.&lt;/p></description><content:encoded><![CDATA[<p>After recently switching to work under Linux, I needed a way to replicate <a href="../how-to-add-an-audio-delay-for-video-conferencing-on-windows">my existing solution for delaying audio under Windows</a> under Linux/Pulseaudio.</p>
<p>To once again explain my situation, I use <a href="https://obsproject.com/">OBS</a> also for video conferencing, through the virtual camera<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. OBS does not offer a built-in way to provide a virtual microphone with all the filters and such applied as well (in my case noise reduction and a limiter), so I need to solve this in a separate way. Additionally, my camera setup has a small delay of around 350ms that I also need to compensate by delaying my audio.</p>
<p>OBS offers a monitoring port that you can push your audio devices on, and that will get the filters applied, but <a href="https://obsproject.com/forum/threads/connecting-obs-with-zoom-without-av-syncing-issues.123960/post-469274">none of the configured offsets</a>. On Windows, I solved this <a href="../how-to-add-an-audio-delay-for-video-conferencing-on-windows">by using a combination of two Virtual Cable devices and RadioDelay between the two</a>.</p>
<p>Today I rebuild basically the same setup on Linux via a bunch of virtual <a href="https://www.freedesktop.org/wiki/Software/PulseAudio/">Pulseaudio</a> devices. I have this in <code>~/.local/bin/obs-virtual-mic</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># create virtual speaker</span>
</span></span><span style="display:flex;"><span>pactl load-module module-null-sink sink_name<span style="color:#f92672">=</span>Virtual-Speaker sink_properties<span style="color:#f92672">=</span>device.description<span style="color:#f92672">=</span>Virtual-Speaker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># create delayed virtual speaker &amp; associated mic</span>
</span></span><span style="display:flex;"><span>pactl load-module module-null-sink sink_name<span style="color:#f92672">=</span>Virtual-Speaker-Delayed sink_properties<span style="color:#f92672">=</span>device.description<span style="color:#f92672">=</span>Virtual-Speaker-Delayed
</span></span><span style="display:flex;"><span>pactl load-module module-remap-source source_name<span style="color:#f92672">=</span>Remap-Source master<span style="color:#f92672">=</span>Virtual-Speaker-Delayed.monitor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># copy from virtual to delayed virtual with 350ms delay</span>
</span></span><span style="display:flex;"><span>pactl load-module module-loopback latency_msec<span style="color:#f92672">=</span><span style="color:#ae81ff">350</span> source<span style="color:#f92672">=</span>Virtual-Speaker.monitor sink<span style="color:#f92672">=</span>Virtual-Speaker-Delayed
</span></span></code></pre></div><p>Let&rsquo;s take a closer look at what this does:</p>
<ul>
<li>create two virtual sinks <code>Virtual-Speaker</code> and <code>Virtual-Speaker-Delayed</code> with <code>pactl load-module module-null-sink ...</code></li>
<li>create a virtual source for the delayed sink via <code>pactl load-module module-remap-source ...</code></li>
<li>finally mirror the sound from <code>Virtual-Speaker.monitor</code> to <code>Virtual-Speaker-Delayed</code> while adding a latency of 350ms</li>
</ul>
<p>I run this. Then OBS gets set to use <code>Virtual-Speaker</code> as monitor. In my video conferencing software I then use <code>Virtual-Speaker-Delayed</code> as my input to get my video and audio synced up.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I prefer the flexibility and control it gives me with regards to how I show up, how my screen shows up, etc, vs what you usually get from your run-of-the-mill video conferencing tool.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to remap keys under Linux and Wayland</title><link>https://foosel.net/til/how-to-remap-keys-under-linux-and-wayland/</link><pubDate>Fri, 03 Mar 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-remap-keys-under-linux-and-wayland/</guid><description>&lt;p>&lt;em>Edit 2024-09-09: Please be advised that this post refers to an older version of &lt;code>keyd&lt;/code> that still used a different configuration format. An older version also stated the config file was stored at &lt;code>~/.config/keyd&lt;/code>, that was an error on my part. Thanks to a reader for the related heads-up!&lt;/em>&lt;/p>
&lt;p>As a German living in Germany with umlauts in my last name and a US ANSI keyboard layout on all my devices&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> I need to remap some stuff to be able to easily type ä, ö, ü and ß. On Windows I solved this with &lt;a href="https://www.autohotkey.com/">Autohotkey&lt;/a>, mapping &lt;code>AltGr&lt;/code>+&lt;code>a&lt;/code> to &lt;code>ä&lt;/code>, &lt;code>AltGr&lt;/code>+&lt;code>o&lt;/code> to &lt;code>ö&lt;/code>, &lt;code>AltGr&lt;/code>+&lt;code>u&lt;/code> to &lt;code>ü&lt;/code> and &lt;code>AltGr&lt;/code>+&lt;code>s&lt;/code> to &lt;code>ß&lt;/code> (well, technically &lt;code>RAlt&lt;/code> - the right &lt;code>Alt&lt;/code> key). That has burned itself into my muscle memory now, and so while currently setting up my new Framework laptop under Linux, with Gnome running on Wayland, I was looking for a way to remap the keys to this layout as well.&lt;/p></description><content:encoded><![CDATA[<p><em>Edit 2024-09-09: Please be advised that this post refers to an older version of <code>keyd</code> that still used a different configuration format. An older version also stated the config file was stored at <code>~/.config/keyd</code>, that was an error on my part. Thanks to a reader for the related heads-up!</em></p>
<p>As a German living in Germany with umlauts in my last name and a US ANSI keyboard layout on all my devices<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> I need to remap some stuff to be able to easily type ä, ö, ü and ß. On Windows I solved this with <a href="https://www.autohotkey.com/">Autohotkey</a>, mapping <code>AltGr</code>+<code>a</code> to <code>ä</code>, <code>AltGr</code>+<code>o</code> to <code>ö</code>, <code>AltGr</code>+<code>u</code> to <code>ü</code> and <code>AltGr</code>+<code>s</code> to <code>ß</code> (well, technically <code>RAlt</code> - the right <code>Alt</code> key). That has burned itself into my muscle memory now, and so while currently setting up my new Framework laptop under Linux, with Gnome running on Wayland, I was looking for a way to remap the keys to this layout as well.</p>
<p>In the old days, I would have written an <code>.Xmodmap</code> file and called it a day, but that no longer works under Wayland. Thankfully however there&rsquo;s a whole new generation of mapping tools that instead of depending on the X server allow remapping right at the kernel input level, and one of them is <a href="https://github.com/rvaiya/keyd">keyd</a> which I used to solve my umlaut problem, and while at it also added a fancy mod layer and even a mouse layer.</p>
<p>First of all, in Gnome I set the keyboard layout to &ldquo;English (intl., with AltGr dead keys)&rdquo;, making it look like this:</p>
<p><img alt="A screenshot of the US international keyboard layout in the Gnome Settings. It&rsquo;s visible that on the third level ä is on q, ö on p, ü on y and ß on s." loading="lazy" src="/til/how-to-remap-keys-under-linux-and-wayland/us-intl-layout.png"></p>
<p>Then I downloaded, compiled and installed <code>keyd</code> and created a config file at <code>/etc/keyd/default.cfg</code> with the following contents:</p>
<pre tabindex="0"><code>[ids]
*

[main]
capslock = layer(mod)
rightalt = layer(dia)
rightcontrol = overload(control, sysrq)

[dia]
a = G-q
o = G-p
u = G-y
s = G-s
e = G-5
` = G-S-;

[mod]
alt = layer(mouse)
j = left
k = down
l = right
i = up
u = home
o = end
y = pageup
h = pagedown
p = delete
; = insert

[mouse]
j = kp4
k = kp2
l = kp6
i = kp8
f = leftmouse
s = rightmouse
d = middlemouse
</code></pre><p>What this does is first of all attach  two layers <code>mod</code> and <code>dia</code> to <code>CapsLock</code> and right <code>Alt</code> respectively, and then it also gives the right <code>Ctrl</code> key a second purpose in life. Holding it still makes it act like your regular <code>Ctrl</code> key, but merely tapping it now will make it act like <code>PrintScreen</code> aka <code>SysRq</code>, allowing me to take screenshots more quickly than my laptop&rsquo;s keyboard layout would regularly allow<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>But let&rsquo;s take a closer look at the layers.</p>
<h2 id="solving-my-umlaut-problem">Solving my umlaut problem</h2>
<p>Let&rsquo;s start with the <code>dia</code> layer, since that is what solves my umlaut problem. I couldn&rsquo;t get the compose key to work for me, so I went with mapping my desired shortcuts to the right shortcuts on the international layout for the key to pop up:</p>
<ul>
<li><code>a</code> -&gt; <code>AltGr</code>+<code>q</code> (<code>ä</code>)</li>
<li><code>o</code> -&gt; <code>AltGr</code>+<code>p</code> (<code>ö</code>)</li>
<li><code>u</code> -&gt; <code>AltGr</code>+<code>y</code> (<code>ü</code>)</li>
<li><code>s</code> -&gt; <code>AltGr</code>+<code>s</code> (<code>ß</code>)</li>
<li><code>e</code> -&gt; <code>AltGr</code>+<code>5</code> (<code>€</code>)</li>
<li><code>`</code> -&gt; <code>AltGr</code>+<code>Shift</code>+<code>;</code> (<code>°</code>)</li>
</ul>
<h2 id="adding-a-mod-layer">Adding a mod layer&hellip;</h2>
<p>While at it I decided to also add another feature I&rsquo;m used to from my UHK, and that is the Mod layer together with its arrow keys, home, end etc. So I replicated that as well, which is the <code>mod</code> layer here. And because I cannot remember a single time in my life where I ever needed <code>CapsLock</code>, that became my mod key. With Capslock held, we have the following mappings:</p>
<ul>
<li><code>j</code> -&gt; <code>left</code></li>
<li><code>k</code> -&gt; <code>down</code></li>
<li><code>l</code> -&gt; <code>right</code></li>
<li><code>i</code> -&gt; <code>up</code></li>
<li><code>u</code> -&gt; <code>home</code></li>
<li><code>o</code> -&gt; <code>end</code></li>
<li><code>y</code> -&gt; <code>pageup</code></li>
<li><code>h</code> -&gt; <code>pagedown</code></li>
<li><code>p</code> -&gt; <code>delete</code></li>
<li><code>;</code> -&gt; <code>insert</code></li>
</ul>
<p>I&rsquo;ve since also enabled <code>CapsLock</code> as <code>Mod</code> key on my UHK, in the hopes that this will accelerate my muscle memory learning process.</p>
<h2 id="-and-adding-a-mouse-layer-too">&hellip; and adding a mouse layer too!</h2>
<p>And then I thought, hm, can I maybe even add a mouse layer? And yes, I can. So I added a <code>mouse</code> layer, which is activated by holding <code>Alt</code> and <code>CapsLock</code> together. I enabled mouse keys in Gnome&rsquo;s accessibility settings, which allows me to move the mouse cursor with the numpad keys. My keyboard does not <em>have</em> numpad keys, but keyd doesn&rsquo;t care, and so we end up with this mapping:</p>
<ul>
<li><code>j</code> -&gt; move cursor left</li>
<li><code>k</code> -&gt; move cursor down</li>
<li><code>l</code> -&gt; move cursor right</li>
<li><code>i</code> -&gt; move cursor up</li>
<li><code>f</code> -&gt; left mouse button</li>
<li><code>s</code> -&gt; right mouse button</li>
<li><code>d</code> -&gt; middle mouse button</li>
</ul>
<p>I&rsquo;m not sure I&rsquo;ll actually use this a lot tbh, this was more a case of &ldquo;can it be done?&rdquo; and &ldquo;why not?&rdquo;. But it&rsquo;s nice to have the option. Something I still need to experiment on however are the acceleration settings, because out of the box this was way too slow for me, so after finding an answer <a href="https://askubuntu.com/a/1234995">here</a> I changed the mouse key parameters a bit via</p>
<pre tabindex="0"><code>gsettings set org.gnome.desktop.a11y.keyboard mousekeys-max-speed 2000;
gsettings set org.gnome.desktop.a11y.keyboard mousekeys-init-delay 20;
gsettings set org.gnome.desktop.a11y.keyboard mousekeys-accel-time 2000;
</code></pre><p>But I&rsquo;m not 100% happy with this yet and need to play around with things a bit more.</p>
<h2 id="fixing-the-disable-touchpad-while-typing-feature">Fixing the &ldquo;Disable Touchpad while Typing&rdquo; feature</h2>
<p>One problem arose from all of this reconfiguration, and that was that the &ldquo;Disable Touchpad while Typing&rdquo; (DWT) feature was no longer working, which turned out to be a rather big deal - I kept unintentionally moving the cursor or even clicking on things while typing. Thankfully a quick search made me stumble over <a href="https://linuxtouchpad.org/libinput/2022/05/07/disable-while-typing.htmls">this helpful post</a> that not only explained the issue but also showed me how to solve it.</p>
<p>To summarize, DWT works by pairing up touchpad and keyboard either by them having the same vendor and product id, or being marked as &ldquo;internal&rdquo;. In the case of keyd acting as my keyboard, neither was true anymore and thus DWT no longer worked. The solution was to modify the <code>libinput</code> properties of the virtual keyd keyboard such that it would be marked as &ldquo;internal&rdquo;. Putting this into <code>/etc/libinput/local-overrides.quirks</code> sufficed:</p>
<pre tabindex="0"><code>[Virtual Keyboard]
MatchUdevType=keyboard
MatchName=keyd virtual keyboard
AttrKeyboardIntegration=internal
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>keyd seems like a powerful tool, and even though I haven&rsquo;t actually yet tested it, should at any point I want to switch to X11 or a blank terminal on this laptop now, all these mappings should continue to function (as long as I set the keyboard layout to English international). That&rsquo;s definitely a way cleaner solution than the <code>xmodmap</code> approach I was using in the past. And I can now finally type my name again!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>US ANSI layout is <em>so</em> much nicer for programming than the German ISO DE layout. I switched around three years ago when I got my first <a href="https://ultimatehackingkeyboard.com/">UHK</a> and haven&rsquo;t looked back.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I tend take a <em>lot</em> of screenshots (mostly OctoPrint related), so this is a big deal for me.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to make dnf default to yes</title><link>https://foosel.net/til/how-to-make-dnf-default-to-yes/</link><pubDate>Thu, 02 Mar 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-make-dnf-default-to-yes/</guid><description>&lt;p>I&amp;rsquo;m currently in the process of setting up my new &lt;a href="https://frame.work">Frame.work laptop&lt;/a>, and since I&amp;rsquo;ve been using Debian-derivatives for the past two decades now, I decided to use the opportunity, try something new for once and installed &lt;a href="https://fedoraproject.org">Fedora&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Something that got annoying quickly is that the package manager command &lt;code>dnf&lt;/code> defaults to &amp;ldquo;no&amp;rdquo; when asking if you really want to install a package plus its dependencies. I&amp;rsquo;m very used to &lt;code>apt&lt;/code>&amp;rsquo;s behaviour here that allows me to type &lt;code>sudo apt install &amp;lt;package&amp;gt;&lt;/code> and then just hit &lt;code>Enter&lt;/code> on the sanity check. I wanted the same for &lt;code>dnf&lt;/code>, but without bypassing the sanity check altogether. I did some digging together with my buddy &lt;a href="https://ben.sycha.uk/">Ben&lt;/a> and we found the answer.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently in the process of setting up my new <a href="https://frame.work">Frame.work laptop</a>, and since I&rsquo;ve been using Debian-derivatives for the past two decades now, I decided to use the opportunity, try something new for once and installed <a href="https://fedoraproject.org">Fedora</a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Something that got annoying quickly is that the package manager command <code>dnf</code> defaults to &ldquo;no&rdquo; when asking if you really want to install a package plus its dependencies. I&rsquo;m very used to <code>apt</code>&rsquo;s behaviour here that allows me to type <code>sudo apt install &lt;package&gt;</code> and then just hit <code>Enter</code> on the sanity check. I wanted the same for <code>dnf</code>, but without bypassing the sanity check altogether. I did some digging together with my buddy <a href="https://ben.sycha.uk/">Ben</a> and we found the answer.</p>
<p>Edit the file <code>/etc/dnf/dnf.conf</code> and add the following line to the <code>[main]</code> section:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>defaultyes<span style="color:#f92672">=</span>True
</span></span></code></pre></div><p>And once that&rsquo;s done, the sanity check now is <code>Y/n</code> instead of <code>y/N</code> and you can just hit <code>Enter</code> to install the package.</p>
<p>(This is btw the first post written on the new laptop and I&rsquo;m really enjoying it so far!)</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>To all my Debian friends: It&rsquo;s really just curiousity and expanding my horizon, no need to try to convert me back or anything like that 😉&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to add itch.io games to the Steamdeck</title><link>https://foosel.net/til/how-to-add-itchio-games-to-the-steamdeck/</link><pubDate>Fri, 24 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-itchio-games-to-the-steamdeck/</guid><description>&lt;p>I&amp;rsquo;m currently setting up some alternative game stores on my Steamdeck, specifically &lt;a href="https://www.emudeck.com/">Emudeck&lt;/a> for my retro collection, &lt;a href="https://heroicgameslauncher.com/">Heroic Launcher&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> for GOG and Epic, and also &lt;a href="https://itch.io">itch.io&lt;/a>.&lt;/p>
&lt;p>I stumbled across &lt;a href="https://www.reddit.com/r/SteamDeck/comments/vwili3/better_way_to_itchio_on_steam_deck/">this Reddit post&lt;/a> that recommended to use the itch.io Windows launcher instead of the native Linux one:&lt;/p>
&lt;blockquote>
&lt;p>Itch.io has an app, that even has linux version. But it has issues - it can only use one wine version, if you have it installed globally, it can&amp;rsquo;t even handle linux games well. It pretends to install them, and when you launch them it opens a directory with the zip file&amp;hellip; Or it just doesn&amp;rsquo;t work after installation. Then you need to add all the games to steam, setup their images, and other stuff. There&amp;rsquo;s boilr for that, but it doesn&amp;rsquo;t find everything, and most of the indies are not in the database anyway.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently setting up some alternative game stores on my Steamdeck, specifically <a href="https://www.emudeck.com/">Emudeck</a> for my retro collection, <a href="https://heroicgameslauncher.com/">Heroic Launcher</a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> for GOG and Epic, and also <a href="https://itch.io">itch.io</a>.</p>
<p>I stumbled across <a href="https://www.reddit.com/r/SteamDeck/comments/vwili3/better_way_to_itchio_on_steam_deck/">this Reddit post</a> that recommended to use the itch.io Windows launcher instead of the native Linux one:</p>
<blockquote>
<p>Itch.io has an app, that even has linux version. But it has issues - it can only use one wine version, if you have it installed globally, it can&rsquo;t even handle linux games well. It pretends to install them, and when you launch them it opens a directory with the zip file&hellip; Or it just doesn&rsquo;t work after installation. Then you need to add all the games to steam, setup their images, and other stuff. There&rsquo;s boilr for that, but it doesn&rsquo;t find everything, and most of the indies are not in the database anyway.</p></blockquote>
<p>Sounds reasonable to go with the Windows version then, so I followed the post and got everything working. Quick summary in case the link goes stale:</p>
<ul>
<li>In desktop mode, download the Windows installer from <a href="https://itch.io/app">https://itch.io/app</a>, add it as a non steam game, configure stable Proton for it, launch it, complete the installer and log in.</li>
<li>Open Dolphin, navigate to <code>home/deck/.steam/steam/steamapps/compatdata</code></li>
<li>Click on the search icon, check &ldquo;From here&rdquo;, search for <code>itch</code> and enter the first found folder of that name. Look at the address bar, you&rsquo;ll be in a subfolder of something like <code>/home/deck/.steam/steam/steamapps/compatdata/&lt;number&gt;</code> for a random <code>&lt;number&gt;</code>, this parent folder is what to use for <code>&lt;basefolder&gt;</code> in any following steps.</li>
<li>In desktop Steam, open the preferences of your non-steam itch.io installer &ldquo;game&rdquo;. Replace &ldquo;Target&rdquo; with <code>&lt;basefolder&gt;/pfx/drive_c/users/steamuser/Desktop/itch.lnk</code> and &ldquo;Start in&rdquo; with <code>&lt;basefolder&gt;/pfx/drive_c/users/steamuser/AppData/Local/Itch</code>. Rename it to &ldquo;itch.io&rdquo; or whatever else you want it to be called<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</li>
</ul>
<p>I then followed the steps to also allow <a href="https://steamgriddb.github.io/steam-rom-manager/">Steam Rom Manager</a> to detect my itch.io games, and created a custom itch.io parser. Here I had to slightly deviate from the suggested steps. Again, summarised here for reference:</p>
<ul>
<li>Parser type: glob</li>
<li>Title: <code>itch.io</code></li>
<li>Steam category: <code>${itch.io}</code></li>
<li>Steam directory: <code>${steamdirglobal}</code></li>
<li>ROMs directory: <code>&lt;basefolder&gt;/pfx/drive_c/users/steamuser/AppData/Roaming/itch/apps</code></li>
<li>Executable modifier: <code>&quot;${exePath}&quot;</code> (with quotes!)</li>
<li>User&rsquo;s glob: <code>${title}/{*/,}!(Unity*).exe</code> <strong>(this one is different than on the Reddit post, see below for why!)</strong></li>
<li>Leave anything else as is.</li>
</ul>
<p>I changed the glob pattern as the original setting of <code>${title}/{*/*,*}.exe</code> was happily detecting the Unity crash handler executable contained in some games as additional entry, obviously not what I wanted.</p>
<p>After some trial and error I thankfully was able to solve this with the slightly different glob pattern of <code>${title}/{*/,}!(Unity*).exe</code>. It now matches any <code>exe</code> right in the game folder or one folder deep that <em>doesn&rsquo;t</em> start with the string <code>Unity</code>. And if push comes to shove I can add additional forbidden patterns as well.</p>
<p>Even Unity games now only generate one entry, and seem to work fine once I&rsquo;ve figured out the right Proton version 👍</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Basically a one click install from the &ldquo;Discover&rdquo; app in desktop mode.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>My SteamGridDB <a href="https://deckbrew.xyz/">Decky</a> plugin seemed happy with that name as I was able to quickly download matching artwork once back in Game mode.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to automatically sync screenshots from the Steamdeck to Google Photos</title><link>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/</link><pubDate>Sun, 19 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/</guid><description>&lt;p>As a follow-up to &lt;a href="https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-drive/">my earlier post about how to sync screenshots to Google Drive&lt;/a> here&amp;rsquo;s how to achieve the same but with a dedicated &amp;ldquo;Steamdeck&amp;rdquo; album on Google Photos instead.&lt;/p>
&lt;p>Once again we are using &lt;code>rclone&lt;/code> for syncing.&lt;/p>
&lt;p>First I created a new target &lt;code>gphoto&lt;/code> by running &lt;code>~/bin/rclone config&lt;/code> again and then following &lt;a href="https://rclone.org/googlephotos/">these steps&lt;/a>. Quick summary:&lt;/p>
&lt;ol>
&lt;li>&lt;code>New remote&lt;/code>&lt;/li>
&lt;li>&lt;code>gphoto&lt;/code>&lt;/li>
&lt;li>Empty application ID and secret&lt;/li>
&lt;li>Full access&lt;/li>
&lt;li>No advanced config&lt;/li>
&lt;li>Use web browser to authenticate&lt;/li>
&lt;/ol>
&lt;p>I then created a new album:&lt;/p></description><content:encoded><![CDATA[<p>As a follow-up to <a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-drive/">my earlier post about how to sync screenshots to Google Drive</a> here&rsquo;s how to achieve the same but with a dedicated &ldquo;Steamdeck&rdquo; album on Google Photos instead.</p>
<p>Once again we are using <code>rclone</code> for syncing.</p>
<p>First I created a new target <code>gphoto</code> by running <code>~/bin/rclone config</code> again and then following <a href="https://rclone.org/googlephotos/">these steps</a>. Quick summary:</p>
<ol>
<li><code>New remote</code></li>
<li><code>gphoto</code></li>
<li>Empty application ID and secret</li>
<li>Full access</li>
<li>No advanced config</li>
<li>Use web browser to authenticate</li>
</ol>
<p>I then created a new album:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rclone mkdir gphoto:album/Steamdeck
</span></span></code></pre></div><p>and adjusted <code>~/bin/sync_screenshots</code> to use the new remote and remote path:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>REMOTE_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;gphoto&#39;</span>
</span></span><span style="display:flex;"><span>REMOTE_DIR<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;album/Steamdeck&#39;</span>
</span></span></code></pre></div><p>That was all.</p>
<p>Obviously the same can be done with any of the other sync targets that <code>rclone</code> supports, of which <a href="https://rclone.org/overview/">there are many</a>. For ownCloud or NextCloud it looks like <a href="https://rclone.org/webdav/">WebDAV</a> is the right option to choose.</p>
<p><em>Update 2025-03-25</em>: There&rsquo;s now also a <a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/">TIL on how to do the same for Immich</a>.</p>
]]></content:encoded></item><item><title>How to add a switch for a port forward on Unifi to Home Assistant</title><link>https://foosel.net/til/how-to-add-a-switch-for-a-port-forward-on-unifi-to-home-assistant/</link><pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-a-switch-for-a-port-forward-on-unifi-to-home-assistant/</guid><description>&lt;p>This is admittedly something I did not learn today but rather learned and adapted a couple years ago &lt;a href="https://community.home-assistant.io/t/automating-unifi-port-forwarding-based-upon-presence-detection/168185">from this post on the Home Assistant forum&lt;/a>, but I just had to use it again today and so I figured I&amp;rsquo;d write it down with all the bells and whistles just in case I ever need this information again - or anyone else does.&lt;/p>
&lt;p>First of all, in your unifi controller you should create a new user that Home Assistant will act as to manage your port forward(s) for you. So, log into the controller, go into Settings &amp;gt; Administrators and add a new Administrator user&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p></description><content:encoded><![CDATA[<p>This is admittedly something I did not learn today but rather learned and adapted a couple years ago <a href="https://community.home-assistant.io/t/automating-unifi-port-forwarding-based-upon-presence-detection/168185">from this post on the Home Assistant forum</a>, but I just had to use it again today and so I figured I&rsquo;d write it down with all the bells and whistles just in case I ever need this information again - or anyone else does.</p>
<p>First of all, in your unifi controller you should create a new user that Home Assistant will act as to manage your port forward(s) for you. So, log into the controller, go into Settings &gt; Administrators and add a new Administrator user<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Then create your port forward in Settings &gt; Routing &amp; Firewall &gt; Port Forwarding. Take note of the id of the port forward you have created - you can find it by clicking edit on it again, it will be the number at the end of the URL of the edit page. E.g. if the URL looks like this: <code>https://my.unifi.controller/manage/site/default/settings/portforward/edit/1234567890</code> then this is the id of the port forward: <code>1234567890</code>.</p>
<p>Next, copy this shell script to <code>/config/scripts/unifi.sh</code> in your Home Assistant. Make sure to adjust <code>https://my.unifi.controller</code> (and, if necessary, the site <code>default</code>) to your own values.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># based on https://community.home-assistant.io/t/automating-unifi-port-forwarding-based-upon-presence-detection/168185</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cookie<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>mktemp<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>headers<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>mktemp<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>curl_cmd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;curl --silent --cookie </span><span style="color:#e6db74">${</span>cookie<span style="color:#e6db74">}</span><span style="color:#e6db74"> --cookie-jar </span><span style="color:#e6db74">${</span>cookie<span style="color:#e6db74">}</span><span style="color:#e6db74"> -D </span><span style="color:#e6db74">${</span>headers<span style="color:#e6db74">}</span><span style="color:#e6db74"> --insecure&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BASEURL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://my.unifi.controller&#34;</span>
</span></span><span style="display:flex;"><span>SITE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;default&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>auth<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  USERNAME<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>  PASSWORD<span style="color:#f92672">=</span>$2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># authenticate against unifi controller</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">${</span>curl_cmd<span style="color:#e6db74">}</span> --output /dev/null -d <span style="color:#e6db74">&#34;{\&#34;username\&#34;:\&#34;</span>$USERNAME<span style="color:#e6db74">\&#34;, \&#34;password\&#34;:\&#34;</span>$PASSWORD<span style="color:#e6db74">\&#34;}&#34;</span> $BASEURL/api/login
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># grab the `x-csrf-token` and strip the newline (added when upgraded to controller 6.1.26)</span>
</span></span><span style="display:flex;"><span>  csrf<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>awk -v FS<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;: &#39;</span> <span style="color:#e6db74">&#39;/^x-csrf-token/{print $2}&#39;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>headers<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | tr -d <span style="color:#e6db74">&#39;\r&#39;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  echo $csrf
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>portfwd<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  USERNAME<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>  PASSWORD<span style="color:#f92672">=</span>$2
</span></span><span style="display:flex;"><span>  FORWARD_ID<span style="color:#f92672">=</span>$3
</span></span><span style="display:flex;"><span>  FORWARD_ENABLED<span style="color:#f92672">=</span>$4
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># authenticate against unifi controller</span>
</span></span><span style="display:flex;"><span>  csrf<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>auth $USERNAME $PASSWORD<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># enable/disable firewall rule</span>
</span></span><span style="display:flex;"><span>  body<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span><span style="color:#e6db74">${</span>curl_cmd<span style="color:#e6db74">}</span> -X GET $BASEURL/api/s/default/rest/portforward/$FORWARD_ID | jq <span style="color:#e6db74">&#39;.data[0] | .enabled=&#39;</span>$FORWARD_ENABLED<span style="color:#e6db74">&#39;&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">${</span>curl_cmd<span style="color:#e6db74">}</span> -X PUT $BASEURL/api/s/default/rest/portforward/$FORWARD_ID -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> -H <span style="color:#e6db74">&#34;x-csrf-token: </span><span style="color:#e6db74">${</span>csrf<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -d @&lt;<span style="color:#f92672">(</span>echo <span style="color:#e6db74">&#34;</span>$body<span style="color:#e6db74">&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>isportfwd<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  USERNAME<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>  PASSWORD<span style="color:#f92672">=</span>$2
</span></span><span style="display:flex;"><span>  FORWARD_ID<span style="color:#f92672">=</span>$3
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># authenticate against unifi controller</span>
</span></span><span style="display:flex;"><span>  csrf<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>auth $USERNAME $PASSWORD<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">${</span>curl_cmd<span style="color:#e6db74">}</span> -X GET $BASEURL/api/s/default/rest/portforward/$FORWARD_ID | jq <span style="color:#e6db74">&#39;.data[0].enabled&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>Now, let&rsquo;s imagine you want to add a switch for an SFTP port forward that you&rsquo;ve just created. Then, in your <code>secrets.yaml</code> file, add the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">unifi_forward_sftp_check</span>: <span style="color:#e6db74">&#39;/bin/bash /config/scripts/unifi.sh isportfwd &lt;user&gt; &lt;password&gt; &lt;forward_id&gt;&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">unifi_forward_sftp_enable</span>: <span style="color:#e6db74">&#39;/bin/bash /config/scripts/unifi.sh portfwd &lt;user&gt; &lt;password&gt; &lt;forward_id&gt; true&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">unifi_forward_sftp_disable</span>: <span style="color:#e6db74">&#39;/bin/bash /config/scripts/unifi.sh portfwd &lt;user&gt; &lt;password&gt; &lt;forward_id&gt; false&#39;</span>
</span></span></code></pre></div><p>Replace <code>&lt;user&gt;</code>, <code>&lt;password&gt;</code> and <code>&lt;forward_id&gt;</code> with the login credentials and id of the forward you just created.</p>
<p>Next, add a command line switch definition to your <code>configuration.yaml</code> (or in my case to my <code>packages/network.yaml</code> file):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">switch</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">platform</span>: <span style="color:#ae81ff">command_line</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">switches</span>: 
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">sftp_port_forward</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">friendly_name</span>: <span style="color:#e6db74">&#34;SFTP Port Forward&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">command_state</span>: !<span style="color:#ae81ff">secret unifi_forward_sftp_check</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">command_on</span>: !<span style="color:#ae81ff">secret unifi_forward_sftp_enable</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">command_off</span>: !<span style="color:#ae81ff">secret unifi_forward_sftp_disable</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value_template</span>: <span style="color:#e6db74">&#39;{{ bool(value, false) }}&#39;</span>
</span></span></code></pre></div><p>Throw that somewhere on your dashboard, or alternatively tie it into some automation, and you&rsquo;re good to go!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Maybe a regular user suffices as well, I honestly can&rsquo;t remember, but I&rsquo;m using an admin user here.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to automatically sync screenshots from the Steamdeck to Google Drive</title><link>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-drive/</link><pubDate>Sat, 11 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-drive/</guid><description>&lt;p>I wanted to automatically sync the screenshots I take on my Steamdeck to some cloud, without having to manually do it for every single one in the Steamdeck&amp;rsquo;s own uploader.&lt;/p>
&lt;p>I came across &lt;a href="https://gist.github.com/pegasd/048bd5d53558f066765253d55a456306">this gist by pegasd&lt;/a> that accomplishes this via rclone, a path monitoring systemd service and some reconfiguration in Steam. However, I had to adjust things slightly for everything to really work - I could imagine that some past Steam update changed things slightly vs when the gist was created:&lt;/p></description><content:encoded><![CDATA[<p>I wanted to automatically sync the screenshots I take on my Steamdeck to some cloud, without having to manually do it for every single one in the Steamdeck&rsquo;s own uploader.</p>
<p>I came across <a href="https://gist.github.com/pegasd/048bd5d53558f066765253d55a456306">this gist by pegasd</a> that accomplishes this via rclone, a path monitoring systemd service and some reconfiguration in Steam. However, I had to adjust things slightly for everything to really work - I could imagine that some past Steam update changed things slightly vs when the gist was created:</p>
<ol>
<li>I had to make sure that I selected the option to make Steam create uncompressed screenshots.</li>
<li>I had to manually start the path watcher.</li>
</ol>
<p>Here&rsquo;s a quick summary of how I managed to make things work on my deck (all credits go to <a href="https://github.com/pegasd">@pegasd</a>, replicating things here mostly so they don&rsquo;t get lost in the future).</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>First of all I switched into Desktop mode.</p>
<p>Since I had not yet done this since upgrading my Deck&rsquo;s SSD, I set a password for my user account by opening a terminal and running <code>passwd</code>. I also made sure to install Firefox from the package manager.</p>
<h2 id="setting-up-the-screenshot-directory">Setting up the screenshot directory</h2>
<p>I opened a terminal and created a dedicated screenshot folder:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir ~/.steam_screenshots
</span></span></code></pre></div><p>Next I had to tell Steam to use this folder. Still in desktop mode I opened Steam settings and on the &ldquo;In-Game&rdquo; tab set the screenshot folder to the one I had just created. I also made sure to check &ldquo;Save uncompressed copy&rdquo;, as <a href="https://steamcommunity.com/discussions/forum/1/4329623982989743690/#c4329623982989971883">otherwise Steam won&rsquo;t use the just configured custom folder</a>.</p>
<h2 id="installing-and-configuring-rclone">Installing and configuring rclone</h2>
<p>Next, still on the deck, I <a href="https://rclone.org/downloads/">downloaded rclone</a> (&ldquo;Intel/AMD - 64 Bit / Linux&rdquo;). I opened the file browser, opened the archive I had just downloaded and extracted the <code>rclone</code> binary into <code>~/bin</code> (if that doesn&rsquo;t exist yet, just create it).</p>
<p>I then went back to the terminal, ran <code>~/bin/rclone config</code> and configured a new remote <code>gdrive</code> following <a href="https://rclone.org/drive/">these steps</a>. Quick summary:</p>
<ol>
<li><code>New remote</code></li>
<li><code>gdrive</code></li>
<li>Empty application ID and secret</li>
<li>Full access to all files</li>
<li>No service account credentials file</li>
<li>Use web browser to authenticate</li>
<li>Not configured as a shared drive</li>
</ol>
<h2 id="automatic-sync">Automatic sync</h2>
<p>I created a file <code>~/bin/sync_screenshots</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>RCLONE_BIN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/rclone&#34;</span>
</span></span><span style="display:flex;"><span>REMOTE_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;gdrive&#39;</span>
</span></span><span style="display:flex;"><span>REMOTE_DIR<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;Privat/Steamdeck/Screenshots&#39;</span>
</span></span><span style="display:flex;"><span>SOURCE_DIR<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.steam_screenshots&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">${</span>RCLONE_BIN<span style="color:#e6db74">}</span> sync <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SOURCE_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>REMOTE_NAME<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">${</span>REMOTE_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>and made it executable with <code>chmod +x ~/bin/sync_screenshots</code>.</p>
<p>Then I created a service file for it, <code>~/.config/systemd/user/sync_screenshots.service</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>[Unit]
</span></span><span style="display:flex;"><span>Description=Sync Steam Screenshots
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[Service]
</span></span><span style="display:flex;"><span>Type=oneshot
</span></span><span style="display:flex;"><span>ExecStart=%h/bin/sync_screenshots
</span></span></code></pre></div><p>and another file setting up a path watcher, <code>~/.config/systemd/user/sync_screenshots.path</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>[Unit]
</span></span><span style="display:flex;"><span>Description=Sync Steam Screenshots
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[Path]
</span></span><span style="display:flex;"><span>PathModified=%h/.steam_screenshots
</span></span><span style="display:flex;"><span>Unit=sync_screenshots.service
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[Install]
</span></span><span style="display:flex;"><span>WantedBy=default.target
</span></span></code></pre></div><p>I enabled the automation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl daemon-reload
</span></span><span style="display:flex;"><span>systemctl --user enable sync_screenshots.path
</span></span><span style="display:flex;"><span>systemctl --user start sync_screenshots.path
</span></span></code></pre></div><h2 id="a-quick-test">A quick test</h2>
<p>I checked that the path watcher was up and running with <code>systemctl --user status sync_screenshots.path</code>.</p>
<p>Then I created a quick test file in the synced folder</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>touch ~/.steam_screenshots/test
</span></span></code></pre></div><p>and verified it showed up in the target folder on my Google Drive.</p>
<p>Next I deleted the file on the deck and verified it got deleted in Google Drive.</p>
<p>Finally I booted back into Game mode, took a screenshot there as well with <code>Steam</code>+<code>R1</code> and verified this showed up on my Drive.</p>
<p><img alt="A freshly synced screenshot of my Steamdeck&rsquo;s home screen" loading="lazy" src="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-drive/screenshot.png"></p>
<p>Success!</p>
<p><em>Update 2023-02-19</em>: There is now also a <a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">TIL on how to do the same for Google Photos</a>.
<em>Update 2025-03-25</em>: And now there&rsquo;s also a <a href="/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/">TIL on how to do the same for Immich</a>.</p>
]]></content:encoded></item><item><title>How to trim screenshots via the commandline</title><link>https://foosel.net/til/how-to-trim-screenshots-via-the-commandline/</link><pubDate>Thu, 09 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-trim-screenshots-via-the-commandline/</guid><description>&lt;p>I just had to trim a bunch of screenshots that had some black borders around them. I didn&amp;rsquo;t want to do this manually or via a GUI, but ideally batch-able via the commandline. Thankfully, that&amp;rsquo;s one of the many things that &lt;a href="https://imagemagick.org/">ImageMagick&lt;/a> can do for you.&lt;/p>
&lt;p>I put all my screenshot PNGs into a folder, and then in that folder ran this &lt;a href="https://imagemagick.org/script/mogrify.php">&lt;code>mogrify&lt;/code>&lt;/a> command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>magick mogrify -trim -define trim:percent-background&lt;span style="color:#f92672">=&lt;/span>0% -background black -path output/ *.png
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I found that I had to add the &lt;code>-define trim:percent-background=0%&lt;/code> option to get rid of &lt;em>all&lt;/em> black borders, as otherwise on some of the images a very slim one ended up remaining. I also specified the background color with &lt;code>-background black&lt;/code> to make sure that it really only trimmed the black borders.&lt;/p></description><content:encoded><![CDATA[<p>I just had to trim a bunch of screenshots that had some black borders around them. I didn&rsquo;t want to do this manually or via a GUI, but ideally batch-able via the commandline. Thankfully, that&rsquo;s one of the many things that <a href="https://imagemagick.org/">ImageMagick</a> can do for you.</p>
<p>I put all my screenshot PNGs into a folder, and then in that folder ran this <a href="https://imagemagick.org/script/mogrify.php"><code>mogrify</code></a> command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>magick mogrify -trim -define trim:percent-background<span style="color:#f92672">=</span>0% -background black -path output/ *.png
</span></span></code></pre></div><p>I found that I had to add the <code>-define trim:percent-background=0%</code> option to get rid of <em>all</em> black borders, as otherwise on some of the images a very slim one ended up remaining. I also specified the background color with <code>-background black</code> to make sure that it really only trimmed the black borders.</p>
<p>I then could combine the resulting images into a PDF with <a href="https://pypi.org/project/img2pdf/">img2pdf</a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>img2pdf --output output.pdf *.png
</span></span></code></pre></div><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Look, a hidden bonus TIL!&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to quickly generate a QR Code with transparent background</title><link>https://foosel.net/til/how-to-quickly-generate-a-qr-code-with-transparent-background/</link><pubDate>Mon, 06 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-quickly-generate-a-qr-code-with-transparent-background/</guid><description>&lt;p>For an upcoming presentation I wanted to quickly generate a QR Code of my web site&amp;rsquo;s URL to include on the final slide. Since my slide theme has a green gradient background with white text, I wanted the QR Code to be white on a transparent background, as a PNG.&lt;/p>
&lt;p>Enter &lt;a href="https://github.com/soldair/node-qrcode">node-qrcode&lt;/a> which runs easily via &lt;code>npx&lt;/code>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx qrcode -o output.png -d FFFF -l &lt;span style="color:#ae81ff">0000&lt;/span> -w &lt;span style="color:#ae81ff">500&lt;/span> &lt;span style="color:#e6db74">&amp;#34;https://foosel.net&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>-o output.png&lt;/code> sets the output file&lt;/li>
&lt;li>&lt;code>-d FFFF&lt;/code> sets the dark color (usually black) to white with 100% opacity&lt;/li>
&lt;li>&lt;code>-l 0000&lt;/code> sets the light color (usually white) to black with 0% opacity - fully transparent&lt;/li>
&lt;li>&lt;code>-w 500&lt;/code> sets the size to 500px&lt;/li>
&lt;/ul>
&lt;p>For further options like error correction or QR code version, or how to use it as a library or in the browser, see node-qrcode&amp;rsquo;s repo linked above.&lt;/p></description><content:encoded><![CDATA[<p>For an upcoming presentation I wanted to quickly generate a QR Code of my web site&rsquo;s URL to include on the final slide. Since my slide theme has a green gradient background with white text, I wanted the QR Code to be white on a transparent background, as a PNG.</p>
<p>Enter <a href="https://github.com/soldair/node-qrcode">node-qrcode</a> which runs easily via <code>npx</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npx qrcode -o output.png -d FFFF -l <span style="color:#ae81ff">0000</span> -w <span style="color:#ae81ff">500</span>  <span style="color:#e6db74">&#34;https://foosel.net&#34;</span>
</span></span></code></pre></div><ul>
<li><code>-o output.png</code> sets the output file</li>
<li><code>-d FFFF</code> sets the dark color (usually black) to white with 100% opacity</li>
<li><code>-l 0000</code> sets the light color (usually white) to black with 0% opacity - fully transparent</li>
<li><code>-w 500</code> sets the size to 500px</li>
</ul>
<p>For further options like error correction or QR code version, or how to use it as a library or in the browser, see node-qrcode&rsquo;s repo linked above.</p>
<p>I for one am happy with the result of this little exercise:</p>
<p><img alt="The final slide of a presentation. It says &ldquo;Thank you for you attention!&rdquo;. Below that I&rsquo;ve listed my Mastodon account @foosel@chaos.social, my GitHub account @foosel and my website&rsquo;s address foosel.net. A big white QR Code is placed right underneath, a handdrawn arrow points from website to code." loading="lazy" src="/til/how-to-quickly-generate-a-qr-code-with-transparent-background/slide.png"></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Yes, there are also online services that can do this for you, no, I didn&rsquo;t want to look through dozens of them trying to find one that didn&rsquo;t attempt to make me subscribe to something just to change the color of the generated QR code. Local CLI ftw.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to use jq to extract new posts from a JSON Feed</title><link>https://foosel.net/til/how-to-use-jq-to-extract-new-posts-from-a-json-feed/</link><pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-use-jq-to-extract-new-posts-from-a-json-feed/</guid><description>&lt;p>I&amp;rsquo;m currently looking into ways to automate some stuff around new posts on this page (be it blog or TIL post) directly during the page build on GitHub Actions. For this, I first need to be able to reliably &lt;em>detect&lt;/em> new posts, from a bash run step. So here&amp;rsquo;s how to do that with &lt;a href="https://stedolan.github.io/jq/">&lt;code>jq&lt;/code>&lt;/a>.&lt;/p>
&lt;p>The idea is to get the current &lt;a href="https://foosel.net/til/how-to-add-json-feed-support-to-hugo/">&lt;code>feed.json&lt;/code>&lt;/a> prior to publishing the page, and then compare it to the one that was just generated during the build. If there are any differences, we know that there are new posts and can trigger further actions from there.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m currently looking into ways to automate some stuff around new posts on this page (be it blog or TIL post) directly during the page build on GitHub Actions. For this, I first need to be able to reliably <em>detect</em> new posts, from a bash run step. So here&rsquo;s how to do that with <a href="https://stedolan.github.io/jq/"><code>jq</code></a>.</p>
<p>The idea is to get the current <a href="/til/how-to-add-json-feed-support-to-hugo/"><code>feed.json</code></a> prior to publishing the page, and then compare it to the one that was just generated during the build. If there are any differences, we know that there are new posts and can trigger further actions from there.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Get current feed.json</span>
</span></span><span style="display:flex;"><span>curl -s https://foosel.net/til/feed.json &gt; feed.current.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Get new feed.json</span>
</span></span><span style="display:flex;"><span>cp public/til/feed.json feed.next.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Compare the two, this is where the magic happens</span>
</span></span><span style="display:flex;"><span>jq --slurpfile current til.current.json --slurpfile next til.next.json -n <span style="color:#e6db74">&#39;$next[0].items - $current[0].items&#39;</span> &gt; til.json
</span></span></code></pre></div><p>Let&rsquo;s go through this <code>jq</code> command there:</p>
<ul>
<li><code>--slurpfile &lt;variable&gt; &lt;file&gt;</code> reads in the given files and makes it accessible as an array contained in the given variable. In this case we read in <code>til.current.json</code> and make it accessible as <code>$current</code>, and also read in <code>til.next.json</code> and make it accessible as <code>$next</code>.</li>
<li><code>-n</code> doesn&rsquo;t wait for input on stdin.</li>
<li><code>'$next[0].items - $current[0].items'</code> subtracts the items from the new feed from the items in the current feed<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</li>
<li><code>&gt; til.json</code> writes the output to <code>til.json</code>.</li>
</ul>
<p><code>til.json</code> will then contain all new items (as long as there weren&rsquo;t more than the feed&rsquo;s item size), can be uploaded as an artifact and then used in further jobs<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The indexing (e.g. <code>$new[0]</code>) here is needed due to <code>--slurpfile</code> creating an array from the read file. I admittedly need to experiment more with this option to fully understand it, but for the purpose here it works.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>My current goal is to move my announcements on Mastodon for new posts from my NodeRED install got the page build, and also send any webmentions for links contained in new posts as well.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to grep a log for multiline errors</title><link>https://foosel.net/til/how-to-grep-a-log-for-multline-errors/</link><pubDate>Wed, 01 Feb 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-grep-a-log-for-multline-errors/</guid><description>&lt;p>I just found myself in the position to have to &lt;code>grep&lt;/code> an OctoPrint log file for error log entries with attached Python stack traces. I wanted to not only get the starting line where the exception log output starts, but the full stack trace up until the next regular log line.&lt;/p>
&lt;p>The format of the lines in &lt;code>octoprint.log&lt;/code> is a simple &lt;code>%(asctime)s - %(name)s - %(levelname)s - %(message)s&lt;/code>, so a log with an error and attached exception looks like this:&lt;/p></description><content:encoded><![CDATA[<p>I just found myself in the position to have to <code>grep</code> an OctoPrint log file for error log entries with attached Python stack traces. I wanted to not only get the starting line where the exception log output starts, but the full stack trace up until the next regular log line.</p>
<p>The format of the lines in <code>octoprint.log</code> is a simple <code>%(asctime)s - %(name)s - %(levelname)s - %(message)s</code>, so a log with an error and attached exception looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>2023-01-30 17:50:45,704 - octoprint.events.fire - DEBUG - Firing event: Disconnecting (Payload: None)
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,704 - octoprint.events - DEBUG - Sending action to &lt;bound method PrinterStateConnection._onEvent of &lt;octoprint.server.util.sockjs.PrinterStateConnection object at 0x000001635CB6EE50&gt;&gt;
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,705 - octoprint.plugin - DEBUG - Calling on_event on action_command_notification
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,705 - octoprint.plugin - DEBUG - Calling on_event on action_command_prompt
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,705 - octoprint.plugin - DEBUG - Calling on_event on announcements
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,705 - octoprint.plugin - DEBUG - Calling on_event on file_check
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,706 - octoprint.plugin - DEBUG - Calling on_event on firmware_check
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,706 - octoprint.plugin - DEBUG - Calling on_event on pluginmanager
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,706 - octoprint.plugin - DEBUG - Calling on_event on softwareupdate
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,706 - octoprint.plugin - DEBUG - Calling on_event on tracking
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,711 - octoprint.plugin - DEBUG - Calling on_event on mqtt
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,732 - octoprint.events.fire - DEBUG - Firing event: Disconnected (Payload: None)
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,735 - octoprint.events - DEBUG - Sending action to &lt;function Server.run.&lt;locals&gt;.&lt;lambda&gt; at 0x000001635BDB2CA0&gt;
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,750 - octoprint.events - ERROR - Got an exception while sending event Disconnected (Payload: None) to &lt;function Server.run.&lt;locals&gt;.&lt;lambda&gt; at 0x000001635BDB2CA0&gt;
</span></span><span style="display:flex;"><span>Traceback (most recent call last):
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\events.py&#34;, line 197, in _work
</span></span><span style="display:flex;"><span>    listener(event, payload)
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1212, in &lt;lambda&gt;
</span></span><span style="display:flex;"><span>    octoprint.events.Events.DISCONNECTED, lambda e, p: run_autorefresh()
</span></span><span style="display:flex;"><span>                                                       ^^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1195, in run_autorefresh
</span></span><span style="display:flex;"><span>    autorefresh.stop()
</span></span><span style="display:flex;"><span>    ^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>AttributeError: &#39;RepeatedTimer&#39; object has no attribute &#39;stop&#39;
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,753 - octoprint.events - DEBUG - Sending action to &lt;bound method PrinterStateConnection._onEvent of &lt;octoprint.server.util.sockjs.PrinterStateConnection object at 0x000001635CB6EE50&gt;&gt;
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,755 - octoprint.plugin - DEBUG - Calling on_event on action_command_notification
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,756 - octoprint.server.util.sockjs - DEBUG - Socket message held back until permissions cleared, added to backlog: plugin
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,758 - octoprint.plugins.action_command_notification - INFO - Notifications cleared
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,758 - octoprint.plugin - DEBUG - Calling on_event on action_command_prompt
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,758 - octoprint.plugin - DEBUG - Calling on_event on announcements
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,759 - octoprint.plugin - DEBUG - Calling on_event on file_check
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,759 - octoprint.plugin - DEBUG - Calling on_event on firmware_check
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,759 - octoprint.server.util.sockjs - DEBUG - Socket message held back until permissions cleared, added to backlog: plugin
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,763 - octoprint.plugin - DEBUG - Calling on_event on pluginmanager
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,764 - octoprint.plugin - DEBUG - Calling on_event on softwareupdate
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,764 - octoprint.plugin - DEBUG - Calling on_event on tracking
</span></span></code></pre></div><p>What I now wanted is for <code>grep</code> to spit out just the <code>ERROR</code> line and the attached stack trace:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>2023-01-30 17:50:45,750 - octoprint.events - ERROR - Got an exception while sending event Disconnected (Payload: None) to &lt;function Server.run.&lt;locals&gt;.&lt;lambda&gt; at 0x000001635BDB2CA0&gt;
</span></span><span style="display:flex;"><span>Traceback (most recent call last):
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\events.py&#34;, line 197, in _work
</span></span><span style="display:flex;"><span>    listener(event, payload)
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1212, in &lt;lambda&gt;
</span></span><span style="display:flex;"><span>    octoprint.events.Events.DISCONNECTED, lambda e, p: run_autorefresh()
</span></span><span style="display:flex;"><span>                                                       ^^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1195, in run_autorefresh
</span></span><span style="display:flex;"><span>    autorefresh.stop()
</span></span><span style="display:flex;"><span>    ^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>AttributeError: &#39;RepeatedTimer&#39; object has no attribute &#39;stop&#39;
</span></span></code></pre></div><p>For this I needed a way to set <code>grep</code> to match multiple lines and do a (non-matching) look ahead for the end. It turns out that the secret to success here is to treat the whole input as one line, use Perl compatible regex mode, and make sure to set the multiline flag. After some fiddling around on <a href="https://regex101.com/r/qYOrnT/1">regex101.com</a> and reading up on <a href="https://perldoc.perl.org/perlre#Extended-Patterns">Perl&rsquo;s regex options</a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, I came up with the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>grep -Pazo &#39;(?m)^\N+\- ERROR \-\N*\n(^\N*?\n)*?(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \- )&#39; octoprint.log
</span></span></code></pre></div><p>Let&rsquo;s walk through this:</p>
<ul>
<li><code>-P</code> enables Perl compatible regex mode</li>
<li><code>-a</code> enables text mode</li>
<li><code>-z</code> turns all newlines into null bytes and thus treats the whole input as a single line for finding matches<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
<li><code>-o</code> only outputs the matched part of the line (otherwise we&rsquo;d get the whole file printed out)</li>
<li><code>(?m)</code> enables multiline mode</li>
<li><code>^\N+\- ERROR \-\N*\n</code> matches the first line of the error, which is the one that starts with the timestamp and package and contains the word <code>ERROR</code></li>
<li><code>(^\N*?\n)*?</code> non-greedily matches all following lines of the error, which are anything but a newline followed by a newline</li>
<li><code>(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \- )</code> is a positive look-ahead that matches a line starting with a timestamp again, which signifies the end of the error&rsquo;s lines</li>
</ul>
<p>Hooray, it works 🥳:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-plain" data-lang="plain"><span style="display:flex;"><span>❯ grep -Pazo &#39;(?m)^\N+\- ERROR \-\N*\n(^\N*?\n)*?(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \- )&#39; octoprint.log
</span></span><span style="display:flex;"><span>2023-01-30 17:50:45,750 - octoprint.events - ERROR - Got an exception while sending event Disconnected (Payload: None) to &lt;function Server.run.&lt;locals&gt;.&lt;lambda&gt; at 0x000001635BDB2CA0&gt;
</span></span><span style="display:flex;"><span>Traceback (most recent call last):
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\events.py&#34;, line 197, in _work
</span></span><span style="display:flex;"><span>    listener(event, payload)
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1212, in &lt;lambda&gt;
</span></span><span style="display:flex;"><span>    octoprint.events.Events.DISCONNECTED, lambda e, p: run_autorefresh()
</span></span><span style="display:flex;"><span>                                                       ^^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>  File &#34;C:\Devel\OctoPrint\OctoPrint\src\octoprint\server\__init__.py&#34;, line 1195, in run_autorefresh
</span></span><span style="display:flex;"><span>    autorefresh.stop()
</span></span><span style="display:flex;"><span>    ^^^^^^^^^^^^^^^^
</span></span><span style="display:flex;"><span>AttributeError: &#39;RepeatedTimer&#39; object has no attribute &#39;stop&#39;
</span></span></code></pre></div><p>(And yes, I&rsquo;ve fixed the error that lead to this stack trace as well 😉)</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I don&rsquo;t know about you, but I always forget about positive/negative look-ahead/behind and pattern-match modifiers.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>The downside of this is that now <code>-n</code> (print line number of match) will not work anymore and just happily report line 1 for every single match.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to run Playwright on GitHub Actions</title><link>https://foosel.net/til/how-to-run-playwright-on-github-actions/</link><pubDate>Tue, 31 Jan 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-run-playwright-on-github-actions/</guid><description>&lt;p>Running Playwright on GitHub Actions is fairly straightforward at first glance, however it becomes a bit more tricky when you don&amp;rsquo;t want to download the whole browser binary zoo on every single CI build.&lt;/p>
&lt;p>Looking around a bit on how to go about caching these, I came across various approaches listed in &lt;a href="https://github.com/microsoft/playwright/issues/7249">this GitHub issue on the Playwright repo&lt;/a>. Below is the result of reading through most of them and figuring out what works best for me and my use case (OctoPrint&amp;rsquo;s E2E tests, &lt;code>npm&lt;/code> based test project).&lt;/p></description><content:encoded><![CDATA[<p>Running Playwright on GitHub Actions is fairly straightforward at first glance, however it becomes a bit more tricky when you don&rsquo;t want to download the whole browser binary zoo on every single CI build.</p>
<p>Looking around a bit on how to go about caching these, I came across various approaches listed in <a href="https://github.com/microsoft/playwright/issues/7249">this GitHub issue on the Playwright repo</a>. Below is the result of reading through most of them and figuring out what works best for me and my use case (OctoPrint&rsquo;s E2E tests, <code>npm</code> based test project).</p>
<p>These steps make sure to install Playwright, fetching the browser binaries from cache if possible, in any case installing the OS depencies, running the tests (replace <code>./path/to/tests</code> accordingly for your setup) and finally upload the generated report as artifact, regardless of whether the tests succeeded or not:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># Run npm ci and get Playwright version</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">🏗 Prepare Playwright env</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">./path/to/tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    npm ci
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output &#39;.dependencies[&#34;@playwright/test&#34;].version&#39;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    echo &#34;PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION&#34; &gt;&gt; $GITHUB_ENV</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Cache browser binaries, cache key is based on Playwright version and OS</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">🧰 Cache Playwright browser binaries</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/cache@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id</span>: <span style="color:#ae81ff">playwright-cache</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;~/.cache/ms-playwright&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restore-keys</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ${{ runner.os }}-playwright-</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Install browser binaries &amp; OS dependencies if cache missed</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">🏗 Install Playwright browser binaries &amp; OS dependencies</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.playwright-cache.outputs.cache-hit != &#39;true&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">./path/to/tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    npx playwright install --with-deps</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Install only the OS dependencies if cache hit</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">🏗 Install Playwright OS dependencies</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.playwright-cache.outputs.cache-hit == &#39;true&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">./path/to/tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    npx playwright install-deps</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">🚀 Run Playwright</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">./path/to/tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    npx playwright test</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">⬆ Upload report</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">playwright-report</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">tests/playwright/playwright-report</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title>How to add JSON Feed support to Hugo</title><link>https://foosel.net/til/how-to-add-json-feed-support-to-hugo/</link><pubDate>Sun, 29 Jan 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-json-feed-support-to-hugo/</guid><description>&lt;p>In order to add &lt;a href="https://www.jsonfeed.org/">JSON Feed 1.1 support&lt;/a> to &lt;a href="https://gohugo.io">Hugo&lt;/a> you need to first add a new &lt;code>jsonfeed&lt;/code> output format in &lt;code>config.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mediaTypes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">application/feed+json&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">suffixes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">json&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">outputFormats&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">jsonfeed&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">mediaType&lt;/span>: &lt;span style="color:#ae81ff">application/feed+json&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">baseName&lt;/span>: &lt;span style="color:#ae81ff">feed&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">rel&lt;/span>: &lt;span style="color:#ae81ff">alternate&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">isPlainText&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This adds a new media type &lt;code>application/feed+json&lt;/code> with the extension &lt;code>json&lt;/code> and creates a new output format &lt;code>jsonfeed&lt;/code> rendering into that media type with a base name of &lt;code>feed&lt;/code> (so &lt;code>feed.json&lt;/code> as &lt;a href="https://www.jsonfeed.org/version/1.1/#discovery">recommended by the JSON Feed spec&lt;/a>).&lt;/p>
&lt;p>This then needs to be added to the outputs it should be generated for - on this page I&amp;rsquo;ve only added it to &lt;code>section&lt;/code>s. Again, in &lt;code>config.yaml&lt;/code>:&lt;/p></description><content:encoded><![CDATA[<p>In order to add <a href="https://www.jsonfeed.org/">JSON Feed 1.1 support</a> to <a href="https://gohugo.io">Hugo</a> you need to first add a new <code>jsonfeed</code> output format in <code>config.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">mediaTypes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">application/feed+json</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">suffixes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">json</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">outputFormats</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jsonfeed</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mediaType</span>: <span style="color:#ae81ff">application/feed+json</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">baseName</span>: <span style="color:#ae81ff">feed</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rel</span>: <span style="color:#ae81ff">alternate</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">isPlainText</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>This adds a new media type <code>application/feed+json</code> with the extension <code>json</code> and creates a new output format <code>jsonfeed</code> rendering into that media type with a base name of <code>feed</code> (so <code>feed.json</code> as <a href="https://www.jsonfeed.org/version/1.1/#discovery">recommended by the JSON Feed spec</a>).</p>
<p>This then needs to be added to the outputs it should be generated for - on this page I&rsquo;ve only added it to <code>section</code>s. Again, in <code>config.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">outputs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">home</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">HTML</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">JSON</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">section</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">HTML</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">RSS</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">jsonfeed</span>
</span></span></code></pre></div><p>Finally, a template needs to be created so that Hugo can actually render something. I&rsquo;ve put this into <code>layouts/_default/list.jsonfeed.json</code> (following the expected naming scheme of <code>list.&lt;outputFormat&gt;.&lt;extension&gt;</code>):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go-text-template" data-lang="go-text-template"><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$pctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">.</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.IsHome</span> <span style="color:#75715e">-}}{{</span> <span style="color:#a6e22e">$pctx</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">site</span> <span style="color:#75715e">}}{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$pages</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">slice</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">or</span> <span style="color:#a6e22e">$.IsHome</span> <span style="color:#a6e22e">$.IsSection</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$pages</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">$pctx</span><span style="color:#a6e22e">.RegularPages</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">else</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$pages</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">$pctx</span><span style="color:#a6e22e">.Pages</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$limit</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">site</span><span style="color:#a6e22e">.Config.Services.RSS.Limit</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">ge</span> <span style="color:#a6e22e">$limit</span> <span style="color:#a6e22e">1</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$pages</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">$pages</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">first</span> <span style="color:#a6e22e">$limit</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">-}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$title</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">eq</span> <span style="color:#a6e22e">.Title</span> <span style="color:#a6e22e">.Site.Title</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$title</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">.Site.Title</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">else</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">.Title</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$title</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">print</span> <span style="color:#a6e22e">.</span> <span style="color:#e6db74">&#34; on &#34;</span><span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$title</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">print</span> <span style="color:#a6e22e">$title</span> <span style="color:#a6e22e">.Site.Title</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    &#34;version&#34;: &#34;https://jsonfeed.org/version/1.1&#34;,
</span></span><span style="display:flex;"><span>    &#34;title&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">$title</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>    &#34;home_page_url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Permalink</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">with</span>  <span style="color:#a6e22e">.OutputFormats.Get</span> <span style="color:#e6db74">&#34;jsonfeed&#34;</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    &#34;feed_url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Permalink</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span>  <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#f92672">(</span><span style="color:#66d9ef">or</span> <span style="color:#a6e22e">.Site.Params.author</span> <span style="color:#a6e22e">.Site.Params.author_url</span><span style="color:#f92672">)</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    &#34;authors&#34;: [{
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.Site.Params.author</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        &#34;name&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Site.Params.author</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.Site.Params.author_url</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        &#34;url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Site.Params.author_url</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    }],
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">$pages</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    &#34;items&#34;: [
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">$index</span><span style="color:#f92672">,</span> <span style="color:#a6e22e">$element</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">$pages</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">$element</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">$index</span> <span style="color:#75715e">}}</span>,<span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span> {
</span></span><span style="display:flex;"><span>            &#34;title&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Title</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            &#34;id&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Permalink</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            &#34;url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Permalink</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.Site.Params.showFullTextinJSONFeed</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            &#34;summary&#34;: <span style="color:#75715e">{{</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">.Description</span> <span style="color:#75715e">}}{{</span> <span style="color:#a6e22e">.</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}{{</span> <span style="color:#66d9ef">else</span> <span style="color:#75715e">}}{{</span> <span style="color:#a6e22e">.Summary</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}{{</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">-}}</span>,
</span></span><span style="display:flex;"><span>            &#34;content_html&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Content</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">else</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            &#34;content_text&#34;: <span style="color:#75715e">{{</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">.Description</span> <span style="color:#75715e">}}{{</span> <span style="color:#a6e22e">.</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}{{</span> <span style="color:#66d9ef">else</span> <span style="color:#75715e">}}{{</span> <span style="color:#a6e22e">.Summary</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}{{</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">-}}</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">.Params.cover.image</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#a6e22e">$cover</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">(</span><span style="color:#a6e22e">.Resources.ByType</span> <span style="color:#e6db74">&#34;image&#34;</span><span style="color:#f92672">)</span><span style="color:#a6e22e">.GetMatch</span> <span style="color:#f92672">(</span><span style="color:#66d9ef">printf</span> <span style="color:#e6db74">&#34;*%s*&#34;</span> <span style="color:#f92672">(</span><span style="color:#a6e22e">.Params.cover.image</span><span style="color:#f92672">))</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">$cover</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            &#34;image&#34;: <span style="color:#75715e">{{</span> <span style="color:#f92672">(</span><span style="color:#a6e22e">path</span><span style="color:#a6e22e">.Join</span> <span style="color:#a6e22e">.RelPermalink</span> <span style="color:#a6e22e">$cover</span><span style="color:#f92672">)</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">absURL</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>            &#34;date_published&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Date.Format</span> <span style="color:#e6db74">&#34;2006-01-02T15:04:05Z07:00&#34;</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>By default, this generates a feed with summaries only. If you want a full content feed, set <code>params.showFullTextinJSONFeed</code> to <code>true</code> in <code>config.yaml</code>.</p>
<p>The relevant docs for custom media types, output formats and template locations can be found <a href="https://gohugo.io/templates/output-formats/">here</a>.</p>
<p>On <a href="https://github.com/adityatelange/hugo-PaperMod">the Papermod theme</a> the above will automatically cause something like</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;alternate&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;application/feed+json&#34;</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://foosel.net/til/feed.json&#34;</span>&gt;
</span></span></code></pre></div><p>to be added to the <code>head</code> of the page, as needed for <a href="https://www.jsonfeed.org/version/1.1/#discovery">discovery</a>. In other themes you might have to do it yourself.</p>
<p>The result of all of this is something like <a href="https://foosel.net/til/feed.json">this</a>.</p>
]]></content:encoded></item><item><title>How to view the page source on Firefox and Chrome mobile</title><link>https://foosel.net/til/how-to-view-the-page-source-on-firefox-and-chrome-mobile/</link><pubDate>Sat, 28 Jan 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-view-the-page-source-on-firefox-and-chrome-mobile/</guid><description>&lt;p>Viewing the page source on Firefox and Chrome mobile is as easy as prepending &lt;code>view-source:&lt;/code> to the URL.&lt;/p>
&lt;p>Example: &lt;code>https://foosel.net&lt;/code> becomes &lt;code>view-source:https://foosel.net&lt;/code>.&lt;/p></description><content:encoded><![CDATA[<p>Viewing the page source on Firefox and Chrome mobile is as easy as prepending <code>view-source:</code> to the URL.</p>
<p>Example: <code>https://foosel.net</code> becomes <code>view-source:https://foosel.net</code>.</p>
]]></content:encoded></item><item><title>How to detect Termux in a script</title><link>https://foosel.net/til/how-to-detect-termux-in-a-script/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-detect-termux-in-a-script/</guid><description>&lt;p>If you need to detect whether you are running in Termux from a bash script, check if &lt;code>$PREFIX&lt;/code> contains the string &lt;code>com.termux&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>echo $PREFIX | grep -o &lt;span style="color:#e6db74">&amp;#34;com.termux&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This can also be used to set a variable in a &lt;a href="https://taskfile.dev">Taskfile&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">vars&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">TERMUX&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;{{and .PREFIX (contains &amp;#34;com.termux&amp;#34; .PREFIX)}}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://www.reddit.com/r/termux/comments/co46qw/how_to_detect_in_a_bash_script_that_im_in_termux/">Source&lt;/a>&lt;/p></description><content:encoded><![CDATA[<p>If you need to detect whether you are running in Termux from a bash script, check if <code>$PREFIX</code> contains the string <code>com.termux</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo $PREFIX | grep -o <span style="color:#e6db74">&#34;com.termux&#34;</span>
</span></span></code></pre></div><p>This can also be used to set a variable in a <a href="https://taskfile.dev">Taskfile</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">TERMUX</span>: <span style="color:#e6db74">&#39;{{and .PREFIX (contains &#34;com.termux&#34; .PREFIX)}}&#39;</span>
</span></span></code></pre></div><p><a href="https://www.reddit.com/r/termux/comments/co46qw/how_to_detect_in_a_bash_script_that_im_in_termux/">Source</a></p>
]]></content:encoded></item><item><title>How to open a file from Tasker in Markor</title><link>https://foosel.net/til/how-to-open-a-file-from-tasker-in-markor/</link><pubDate>Sat, 21 Jan 2023 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-open-a-file-from-tasker-in-markor/</guid><description>&lt;p>In order to open a file from Tasker in Markor (e.g. to edit a &lt;a href="https://foosel.net/blog/2023-01-21-hugo-meet-android/">newly created blog post&lt;/a>), create a &amp;ldquo;Send Intent&amp;rdquo; step with:&lt;/p>
&lt;ul>
&lt;li>Action: &lt;code>android.intent.action.SEND&lt;/code>&lt;/li>
&lt;li>Cat: &lt;code>None&lt;/code>&lt;/li>
&lt;li>Mime Type: &lt;code>text/plain&lt;/code>&lt;/li>
&lt;li>Data: &lt;code>content://net.dinglisch.android.taskerm.fileprovider/external_files/path/to/the/file&lt;/code> (be sure to replace &lt;code>/path/to/the/file&lt;/code> with the absolute path to the file you want to open)&lt;/li>
&lt;li>Package: &lt;code>net.gsantner.markor&lt;/code>&lt;/li>
&lt;li>Class: &lt;code>net.gsantner.markor.activity.DocumentActivity&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://www.reddit.com/r/tasker/comments/xbspjr/send_intent_to_markor/">Source&lt;/a>&lt;/p></description><content:encoded><![CDATA[<p>In order to open a file from Tasker in Markor (e.g. to edit a <a href="/blog/2023-01-21-hugo-meet-android/">newly created blog post</a>), create a &ldquo;Send Intent&rdquo; step with:</p>
<ul>
<li>Action: <code>android.intent.action.SEND</code></li>
<li>Cat: <code>None</code></li>
<li>Mime Type: <code>text/plain</code></li>
<li>Data: <code>content://net.dinglisch.android.taskerm.fileprovider/external_files/path/to/the/file</code> (be sure to replace <code>/path/to/the/file</code> with the absolute path to the file you want to open)</li>
<li>Package: <code>net.gsantner.markor</code></li>
<li>Class: <code>net.gsantner.markor.activity.DocumentActivity</code></li>
</ul>
<p><a href="https://www.reddit.com/r/tasker/comments/xbspjr/send_intent_to_markor/">Source</a></p>
]]></content:encoded></item><item><title>How to edit an STL file in FreeCAD</title><link>https://foosel.net/til/how-to-edit-an-stl-file-in-freecad/</link><pubDate>Fri, 30 Sep 2022 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-edit-an-stl-file-in-freecad/</guid><description>&lt;ol>
&lt;li>Create a new file&lt;/li>
&lt;li>&amp;ldquo;File&amp;rdquo; &amp;gt; &amp;ldquo;Import&amp;rdquo;, import the STL&lt;/li>
&lt;li>Select the Part workbench&lt;/li>
&lt;li>Select the imported model&lt;/li>
&lt;li>&amp;ldquo;Part&amp;rdquo; &amp;gt; &amp;ldquo;Create shape from mesh&amp;rdquo;. A tesselation distance of 0.10 should work. Delete or hide import.&lt;/li>
&lt;li>&amp;ldquo;Part&amp;rdquo; &amp;gt; &amp;ldquo;Convert to solid&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://all3dp.com/1/7-free-stl-editors-edit-repair-stl-files/">Source&lt;/a>&lt;/p></description><content:encoded><![CDATA[<ol>
<li>Create a new file</li>
<li>&ldquo;File&rdquo; &gt; &ldquo;Import&rdquo;, import the STL</li>
<li>Select the Part workbench</li>
<li>Select the imported model</li>
<li>&ldquo;Part&rdquo; &gt; &ldquo;Create shape from mesh&rdquo;. A tesselation distance of 0.10 should work. Delete or hide import.</li>
<li>&ldquo;Part&rdquo; &gt; &ldquo;Convert to solid&rdquo;</li>
</ol>
<p><a href="https://all3dp.com/1/7-free-stl-editors-edit-repair-stl-files/">Source</a></p>
]]></content:encoded></item><item><title>How to add an audio delay for video conferencing on Windows</title><link>https://foosel.net/til/how-to-add-an-audio-delay-for-video-conferencing-on-windows/</link><pubDate>Thu, 29 Sep 2022 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-add-an-audio-delay-for-video-conferencing-on-windows/</guid><description>&lt;h1 id="situation">Situation&lt;/h1>
&lt;p>&lt;a href="https://obsproject.com/">OBS&lt;/a> used for video conferences through the virtual camera. Audio virtualized and with active OBS filters applied (limiter, noise suppression) through means of setting the monitor device to a sink created with &lt;a href="https://vac.muzychenko.net/en/">VirtualCable&lt;/a> and using its source in the video conferences tools.&lt;/p>
&lt;h1 id="problem">Problem&lt;/h1>
&lt;p>The camera feed has a slight delay of 300-400ms. The audio is thus ahead.&lt;/p>
&lt;p>Adding a delay through OBS doesn&amp;rsquo;t get applied to the monitor device (&lt;a href="https://obsproject.com/forum/threads/connecting-obs-with-zoom-without-av-syncing-issues.123960/post-469274">Source&lt;/a>):&lt;/p></description><content:encoded><![CDATA[<h1 id="situation">Situation</h1>
<p><a href="https://obsproject.com/">OBS</a> used for video conferences through the virtual camera. Audio virtualized and with active OBS filters applied (limiter, noise suppression) through means of setting the monitor device to a sink created with <a href="https://vac.muzychenko.net/en/">VirtualCable</a> and using its source in the video conferences tools.</p>
<h1 id="problem">Problem</h1>
<p>The camera feed has a slight delay of 300-400ms. The audio is thus ahead.</p>
<p>Adding a delay through OBS doesn&rsquo;t get applied to the monitor device (<a href="https://obsproject.com/forum/threads/connecting-obs-with-zoom-without-av-syncing-issues.123960/post-469274">Source</a>):</p>
<blockquote>
<p>The sync offset that you apply in OBS only applies to either the recording from OBS or the output stream from OBS. It does not apply to the monitor. When you use a virtual cable, and you set it up as a monitor in OBS, you will hear the inputs without any sync delay. As far as I know there is no way to apply your delays to the audio monitor output.</p></blockquote>
<h1 id="solution">Solution</h1>
<p>Install <a href="https://www.daansystems.com/radiodelay/">RadioDelay</a>.</p>
<p>Create a second virtual cable device. Use its sink as the monitoring device in OBS, its source as source in RadioDelay, and set the sink of the first virtual cable as sink in RadioDelay. Apply the delay in RadioDelay.</p>
<p>The first cable now outputs delayed audio and can be used as audio source in the video conferencing tools.</p>
<p>You can also create a shortcut to fire up RadioDelay with the right devices selected, the delay applied and the output active with something like this:</p>
<pre tabindex="0"><code>&#34;C:\Program Files\Radiodelay\radiodelay.exe&#34; -delay 0.3 -in 5 -out 3
</code></pre><p><code>in</code> and <code>out</code> are the positions of the audio device in the drop-down list. <code>delay</code> is the delay in seconds.</p>
<p>Can of course also be used without OBS.</p>
<p><em>edit 2023-03-11:</em> <a href="../how-to-add-an-audio-delay-for-video-conferencing-on-linuxpulseaudio">Here&rsquo;s how I solved the same problem under Linux</a>.</p>
]]></content:encoded></item><item><title>How to determine an RPi kernel version and build without booting it</title><link>https://foosel.net/til/how-to-determine-an-rpi-kernel-version-and-build-without-booting-it/</link><pubDate>Thu, 16 Jun 2022 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-determine-an-rpi-kernel-version-and-build-without-booting-it/</guid><description>&lt;p>To figure out the kernel version and build without booting it, e.g. to install matching device drivers during an automated image build in something like &lt;a href="https://github.com/OctoPrint/CustoPiZer">CustoPiZer&lt;/a>, use something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">function&lt;/span> version_and_build_for_kernelimg&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernelimg&lt;span style="color:#f92672">=&lt;/span>$1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># uncompressed kernel?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>strings $kernelimg | grep &lt;span style="color:#e6db74">&amp;#39;Linux version&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span> echo&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">[&lt;/span> -z &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$output&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#f92672">]&lt;/span>; &lt;span style="color:#66d9ef">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># compressed kernel, needs more work, see https://raspberrypi.stackexchange.com/a/108107&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pos&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>LC_ALL&lt;span style="color:#f92672">=&lt;/span>C grep -P -a -b -m &lt;span style="color:#ae81ff">1&lt;/span> --only-matching &lt;span style="color:#e6db74">&amp;#39;\x1f\x8b\x08&amp;#39;&lt;/span> $kernelimg | cut -f &lt;span style="color:#ae81ff">1&lt;/span> -d :&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dd &lt;span style="color:#66d9ef">if&lt;/span>&lt;span style="color:#f92672">=&lt;/span>$kernelimg of&lt;span style="color:#f92672">=&lt;/span>kernel.gz skip&lt;span style="color:#f92672">=&lt;/span>$pos iflag&lt;span style="color:#f92672">=&lt;/span>skip_bytes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>gzip --decompress --stdout kernel.gz | strings | grep &lt;span style="color:#e6db74">&amp;#39;Linux version&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span> echo&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> version&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>echo $output | awk &lt;span style="color:#e6db74">&amp;#39;{print $3}&amp;#39;&lt;/span> | tr -d &lt;span style="color:#e6db74">&amp;#39;+&amp;#39;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> build&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>echo $output | awk -F&lt;span style="color:#e6db74">&amp;#34;#&amp;#34;&lt;/span> &lt;span style="color:#e6db74">&amp;#39;{print $NF}&amp;#39;&lt;/span> | awk &lt;span style="color:#e6db74">&amp;#39;{print $1}&amp;#39;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">[[&lt;/span> -n &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$version&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> -n &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$build&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#f92672">]]&lt;/span>; &lt;span style="color:#66d9ef">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> echo &lt;span style="color:#e6db74">&amp;#34;Version: &lt;/span>$kernel&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> echo &lt;span style="color:#e6db74">&amp;#34;Build: &lt;/span>$build&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> echo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> echo &lt;span style="color:#e6db74">&amp;#34;Cannot determine kernel version and build number for &lt;/span>$kernelimg&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note that this has only been tested with kernels on RaspberryPi OS images, YMMV.&lt;/p></description><content:encoded><![CDATA[<p>To figure out the kernel version and build without booting it, e.g. to install matching device drivers during an automated image build in something like <a href="https://github.com/OctoPrint/CustoPiZer">CustoPiZer</a>, use something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> version_and_build_for_kernelimg<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    kernelimg<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># uncompressed kernel?</span>
</span></span><span style="display:flex;"><span>    output<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>strings $kernelimg | grep <span style="color:#e6db74">&#39;Linux version&#39;</span> <span style="color:#f92672">||</span> echo<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$output<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># compressed kernel, needs more work, see https://raspberrypi.stackexchange.com/a/108107</span>
</span></span><span style="display:flex;"><span>        pos<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>LC_ALL<span style="color:#f92672">=</span>C grep -P -a -b -m <span style="color:#ae81ff">1</span> --only-matching <span style="color:#e6db74">&#39;\x1f\x8b\x08&#39;</span> $kernelimg | cut -f <span style="color:#ae81ff">1</span> -d :<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>        dd <span style="color:#66d9ef">if</span><span style="color:#f92672">=</span>$kernelimg of<span style="color:#f92672">=</span>kernel.gz skip<span style="color:#f92672">=</span>$pos iflag<span style="color:#f92672">=</span>skip_bytes
</span></span><span style="display:flex;"><span>        output<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>gzip --decompress --stdout kernel.gz | strings | grep <span style="color:#e6db74">&#39;Linux version&#39;</span> <span style="color:#f92672">||</span> echo<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    version<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo $output | awk <span style="color:#e6db74">&#39;{print $3}&#39;</span> | tr -d <span style="color:#e6db74">&#39;+&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    build<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo $output | awk -F<span style="color:#e6db74">&#34;#&#34;</span> <span style="color:#e6db74">&#39;{print $NF}&#39;</span> | awk <span style="color:#e6db74">&#39;{print $1}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -n <span style="color:#e6db74">&#34;</span>$version<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">&amp;&amp;</span> -n <span style="color:#e6db74">&#34;</span>$build<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;Version: </span>$kernel<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;Build: </span>$build<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>        echo
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;Cannot determine kernel version and build number for </span>$kernelimg<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Note that this has only been tested with kernels on RaspberryPi OS images, YMMV.</p>
]]></content:encoded></item><item><title>How to sync starred GitHub repos to Raindrop via NodeRED</title><link>https://foosel.net/til/how-to-sync-starred-github-repos-to-raindrop-via-nodered/</link><pubDate>Thu, 16 Jun 2022 00:00:00 +0000</pubDate><guid>https://foosel.net/til/how-to-sync-starred-github-repos-to-raindrop-via-nodered/</guid><description>&lt;p>The following flow syncs starred repos of a GitHub user to Raindrop.io every 10min and on trigger:&lt;/p>
&lt;p>&lt;img alt="Screenshot of a NodeRED flow. Two nodes &amp;ldquo;every 10min&amp;rdquo; and &amp;ldquo;timestamp&amp;rdquo; lead to a node &amp;ldquo;Starred repos for foosel&amp;rdquo;. That is wired to &amp;ldquo;Preprocess&amp;rdquo; which in turn is wired to &amp;ldquo;Save in Raindrop.io&amp;rdquo;" loading="lazy" src="https://foosel.net/til/how-to-sync-starred-github-repos-to-raindrop-via-nodered/nodered.png">&lt;/p>
&lt;p>&amp;ldquo;Every 10min&amp;rdquo; is a cron trigger node that fires every 10min. &amp;ldquo;timestamp&amp;rdquo; is an inject node for triggering the flow manually.&lt;/p></description><content:encoded><![CDATA[<p>The following flow syncs starred repos of a GitHub user to Raindrop.io every 10min and on trigger:</p>
<p><img alt="Screenshot of a NodeRED flow. Two nodes &ldquo;every 10min&rdquo; and &ldquo;timestamp&rdquo; lead to a node &ldquo;Starred repos for foosel&rdquo;. That is wired to &ldquo;Preprocess&rdquo; which in turn is wired to &ldquo;Save in Raindrop.io&rdquo;" loading="lazy" src="/til/how-to-sync-starred-github-repos-to-raindrop-via-nodered/nodered.png"></p>
<p>&ldquo;Every 10min&rdquo; is a cron trigger node that fires every 10min. &ldquo;timestamp&rdquo; is an inject node for triggering the flow manually.</p>
<p>&ldquo;Starred repos for foosel&rdquo; is an HTTP GET request against <code>https://api.github.com/users/foosel/starred</code>, set to return an object parsed from the response JSON. Change <code>foosel</code> to your own username.</p>
<p>The &ldquo;Preprocess&rdquo; function node has this source:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">key</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;feed_githubstars_repos&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">repos</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">flow</span>.<span style="color:#a6e22e">get</span>(<span style="color:#a6e22e">key</span>) <span style="color:#f92672">||</span> [];
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">added</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">filter</span>(<span style="color:#a6e22e">repo</span> =&gt; <span style="color:#f92672">!</span><span style="color:#a6e22e">repos</span>.<span style="color:#a6e22e">includes</span>(<span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">full_name</span>));
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">flow</span>.<span style="color:#a6e22e">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">repos</span>.<span style="color:#a6e22e">concat</span>(<span style="color:#a6e22e">added</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">repo</span> =&gt; <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">full_name</span>)));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">payload</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">items</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">added</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">repo</span> =&gt; {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">title</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">full_name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">description</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">link</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">repo</span>.<span style="color:#a6e22e">html_url</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">tags</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#34;github&#34;</span>, <span style="color:#e6db74">&#34;starred&#34;</span>]
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>&ldquo;Save in Raindrop.io&rdquo; performs an HTTP POST request against <code>https://api.raindrop.io/rest/v1/raindrops</code> with authentication type&quot;Bearer&quot; and an API token created <a href="https://app.raindrop.io/settings/integrations">here</a>.</p>
]]></content:encoded></item></channel></rss>