<?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 kunat.dev</title>
    <link>https://kunat.dev/tags/til/</link>
    <description>Recent content in TIL on kunat.dev</description>
    <generator>Hugo -- 0.147.5</generator>
    <language>en-us</language>
    <copyright>2025 kunat.dev</copyright>
    <lastBuildDate>Thu, 12 Feb 2026 12:17:30 +0100</lastBuildDate>
    <atom:link href="https://kunat.dev/tags/til/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Convert PDF to Markdown Locally on macOS</title>
      <link>https://kunat.dev/notes/convert-pdf-to-markdown-on-macos/</link>
      <pubDate>Thu, 12 Feb 2026 12:17:30 +0100</pubDate>
      <guid>https://kunat.dev/notes/convert-pdf-to-markdown-on-macos/</guid>
      <description>&lt;p&gt;Today I needed to convert a PDF to Markdown on macOS. I tried CLIs I could install with &lt;code&gt;brew&lt;/code&gt;, but none of them gave me decent output for structure and formatting. &lt;a href=&#34;https://github.com/datalab-to/marker&#34; target=&#34;_blank&#34; &gt;&lt;code&gt;marker-pdf&lt;/code&gt;&lt;/a&gt; was the first one that consistently gave me clean Markdown, even though it required more work to set up.&lt;/p&gt;
&lt;p&gt;I used this &lt;code&gt;convert.py&lt;/code&gt; script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; subprocess
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pathlib &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;convert_pdf_to_markdown&lt;/span&gt;(pdf_file: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pdf_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(pdf_file)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;expanduser()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;resolve()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; pdf_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FileNotFoundError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;PDF not found: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;pdf_path&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pdf_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parent
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    venv_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; root_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.venv&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; root_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    python_bin &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; venv_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bin&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pip_bin &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; venv_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bin&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    marker_bin &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; venv_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bin&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;marker_single&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(cmd: list[str]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;+&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(str(c) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; cmd))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        subprocess&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(cmd, check&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; python_bin&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        run([&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python3&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-m&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;venv&amp;#34;&lt;/span&gt;, str(venv_dir)])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; marker_bin&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        run([str(pip_bin), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;install&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;marker-pdf&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mkdir(parents&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, exist_ok&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            str(marker_bin),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            str(pdf_path),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--output_format&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;markdown&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--output_dir&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            str(output_dir),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; __name__ &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    convert_pdf_to_markdown(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;path/to/the/pdf/you/want/to/convert.pdf&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then run:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Today I needed to convert a PDF to Markdown on macOS. I tried CLIs I could install with <code>brew</code>, but none of them gave me decent output for structure and formatting. <a href="https://github.com/datalab-to/marker" target="_blank" ><code>marker-pdf</code></a> was the first one that consistently gave me clean Markdown, even though it required more work to set up.</p>
<p>I used this <code>convert.py</code> script:</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:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> subprocess
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pathlib <span style="color:#f92672">import</span> Path
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">convert_pdf_to_markdown</span>(pdf_file: str) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    pdf_path <span style="color:#f92672">=</span> Path(pdf_file)<span style="color:#f92672">.</span>expanduser()<span style="color:#f92672">.</span>resolve()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> pdf_path<span style="color:#f92672">.</span>exists():
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">FileNotFoundError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;PDF not found: </span><span style="color:#e6db74">{</span>pdf_path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    root_dir <span style="color:#f92672">=</span> pdf_path<span style="color:#f92672">.</span>parent
</span></span><span style="display:flex;"><span>    venv_dir <span style="color:#f92672">=</span> root_dir <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;.venv&#34;</span>
</span></span><span style="display:flex;"><span>    output_dir <span style="color:#f92672">=</span> root_dir <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;output&#34;</span>
</span></span><span style="display:flex;"><span>    python_bin <span style="color:#f92672">=</span> venv_dir <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;bin&#34;</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;python&#34;</span>
</span></span><span style="display:flex;"><span>    pip_bin <span style="color:#f92672">=</span> venv_dir <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;bin&#34;</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;pip&#34;</span>
</span></span><span style="display:flex;"><span>    marker_bin <span style="color:#f92672">=</span> venv_dir <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;bin&#34;</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">&#34;marker_single&#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">run</span>(cmd: list[str]) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;+&#34;</span>, <span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(str(c) <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> cmd))
</span></span><span style="display:flex;"><span>        subprocess<span style="color:#f92672">.</span>run(cmd, check<span style="color:#f92672">=</span><span style="color:#66d9ef">True</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">not</span> python_bin<span style="color:#f92672">.</span>exists():
</span></span><span style="display:flex;"><span>        run([<span style="color:#e6db74">&#34;python3&#34;</span>, <span style="color:#e6db74">&#34;-m&#34;</span>, <span style="color:#e6db74">&#34;venv&#34;</span>, str(venv_dir)])
</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> marker_bin<span style="color:#f92672">.</span>exists():
</span></span><span style="display:flex;"><span>        run([str(pip_bin), <span style="color:#e6db74">&#34;install&#34;</span>, <span style="color:#e6db74">&#34;marker-pdf&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    output_dir<span style="color:#f92672">.</span>mkdir(parents<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, exist_ok<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    run(
</span></span><span style="display:flex;"><span>        [
</span></span><span style="display:flex;"><span>            str(marker_bin),
</span></span><span style="display:flex;"><span>            str(pdf_path),
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;--output_format&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;markdown&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;--output_dir&#34;</span>,
</span></span><span style="display:flex;"><span>            str(output_dir),
</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><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    convert_pdf_to_markdown(<span style="color:#e6db74">&#34;path/to/the/pdf/you/want/to/convert.pdf&#34;</span>)
</span></span></code></pre></div><p>Then run:</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>cd path/to/the/folder/with/convert.py
</span></span><span style="display:flex;"><span>chmod +x convert.py
</span></span><span style="display:flex;"><span>./convert.py
</span></span></code></pre></div><p>Output goes to:</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>path/to/the/pdf/folder/output/&lt;pdf-name&gt;/&lt;pdf-name&gt;.md
</span></span></code></pre></div><p>First run will be slower because it creates <code>.venv</code>, installs <code>marker-pdf</code>, and downloads model files.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Synology DSM 7.x: qBittorrent WireGuard/NFTables Errors After Update</title>
      <link>https://kunat.dev/notes/synology-qbittorrent-wireguard-nftables/</link>
      <pubDate>Sat, 24 Jan 2026 13:35:18 +0100</pubDate>
      <guid>https://kunat.dev/notes/synology-qbittorrent-wireguard-nftables/</guid>
      <description>&lt;p&gt;Seeing &lt;code&gt;RTNETLINK answers: Not supported&lt;/code&gt; during &lt;code&gt;init-wireguard&lt;/code&gt;, or &lt;code&gt;Error: Could not process rule: Not supported&lt;/code&gt; with &lt;code&gt;add table inet hotio&lt;/code&gt; when starting &lt;code&gt;ghcr.io/hotio/qbittorrent&lt;/code&gt; on DSM 7.2/7.3? That&amp;rsquo;s Synology&amp;rsquo;s 4.4 kernel lacking nftables support after hotio dropped legacy iptables workarounds.&lt;/p&gt;
&lt;p&gt;Typical failure logs look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[VPN] Creating interface [wg0-fix].
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RTNETLINK answers: Not supported
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or later during firewall setup:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error: Could not process rule: Not supported
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;add table inet hotio
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Pin the image to the last known working WireGuard build from the &lt;a href=&#34;https://github.com/TRaSH-Guides/Synology-Templates/pull/222&#34; target=&#34;_blank&#34; &gt;TRaSH-Guides PR #222&lt;/a&gt;:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Seeing <code>RTNETLINK answers: Not supported</code> during <code>init-wireguard</code>, or <code>Error: Could not process rule: Not supported</code> with <code>add table inet hotio</code> when starting <code>ghcr.io/hotio/qbittorrent</code> on DSM 7.2/7.3? That&rsquo;s Synology&rsquo;s 4.4 kernel lacking nftables support after hotio dropped legacy iptables workarounds.</p>
<p>Typical failure logs look 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-text" data-lang="text"><span style="display:flex;"><span>[VPN] Creating interface [wg0-fix].
</span></span><span style="display:flex;"><span>RTNETLINK answers: Not supported
</span></span></code></pre></div><p>Or later during firewall setup:</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>Error: Could not process rule: Not supported
</span></span><span style="display:flex;"><span>add table inet hotio
</span></span></code></pre></div><h2 id="the-fix">The Fix</h2>
<p>Pin the image to the last known working WireGuard build from the <a href="https://github.com/TRaSH-Guides/Synology-Templates/pull/222" target="_blank" >TRaSH-Guides PR #222</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">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">qbittorrent</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/hotio/qbittorrent:release-e799f87</span>
</span></span></code></pre></div><p>Tradeoffs of pinning an old image:</p>
<ul>
<li>No security or feature updates.</li>
<li>Potential breakage when VPN providers change endpoints or auth flows.</li>
<li>You&rsquo;re stuck on older qBittorrent/libtorrent versions.</li>
<li>This is a temporary workaround, not a real fix.</li>
</ul>
<p>Long term, move the VPN to a dedicated container (for example, <a href="https://github.com/qdm12/gluetun" target="_blank" >gluetun</a> with OpenVPN) and run qBittorrent without VPN logic inside its container. That keeps your NAS kernel limitations out of the qBittorrent image and avoids the nftables requirement.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/TRaSH-Guides/Synology-Templates/pull/222" target="_blank" >TRaSH-Guides Synology Templates PR #222</a></li>
<li><a href="https://github.com/qdm12/gluetun" target="_blank" >gluetun</a></li>
<li><a href="https://hotio.dev/containers/qbittorrent" target="_blank" >hotio qBittorrent container docs</a></li>
</ul>
<p>If you&rsquo;re interested, check out how to set up qBittorrent with PIA on Synology, verify your qBittorrent VPN IP, or add a reverse proxy to your Synology setup:</p>
<ul>
<li><a href="/notes/synology-qbitorrent-vpn-pia/" >Setting Up qBittorrent with Private Internet Access (PIA) VPN on Synology NAS</a></li>
<li><a href="/notes/check-torrent-client-vpn-ip/" >Verifying VPN Status for Docker qBittorrent on Synology</a></li>
<li><a href="/notes/synology-caddy-reverse-proxy/" >Setting Up Caddy as a Reverse Proxy on Synology NAS</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Bypass Cisco Umbrella DNS Filtering on macOS</title>
      <link>https://kunat.dev/notes/cisco-umbrella-baypass/</link>
      <pubDate>Fri, 16 Jan 2026 21:08:01 +0100</pubDate>
      <guid>https://kunat.dev/notes/cisco-umbrella-baypass/</guid>
      <description>&lt;p&gt;Cisco Umbrella can block domains by returning fake DNS answers, which makes a site look down even though it works elsewhere. If you only need access to a few domains, bypass the DNS layer for those domains on macOS.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;cisco umbrella block screen&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/137073.jpg&#34; title=&#34;cisco umbrella block screen&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ Disclaimer: This guide is for educational purposes. Bypassing corporate security controls may violate your company&amp;rsquo;s IT policies. Use responsibly and at your own risk.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Cisco Umbrella can block domains by returning fake DNS answers, which makes a site look down even though it works elsewhere. If you only need access to a few domains, bypass the DNS layer for those domains on macOS.</p>
<p><img alt="cisco umbrella block screen" loading="lazy" src="/notes/images/137073.jpg" title="cisco umbrella block screen"></p>
<blockquote>
<p>⚠️ Disclaimer: This guide is for educational purposes. Bypassing corporate security controls may violate your company&rsquo;s IT policies. Use responsibly and at your own risk.</p></blockquote>
<p>First, confirm the block and grab the real IP from an external resolver:</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"># What your system resolves (potentially filtered)</span>
</span></span><span style="display:flex;"><span>dig +short example.com
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># What the real IP should be (Cloudflare DNS)</span>
</span></span><span style="display:flex;"><span>dig +short example.com @1.1.1.1
</span></span></code></pre></div><p>If the IPs differ, the domain is being filtered.</p>
<h2 id="the-fix">The Fix</h2>
<h3 id="option-1-etchosts-recommended">Option 1: <code>/etc/hosts</code> (recommended)</h3>
<p>Requires admin access. This bypasses DNS for all apps.</p>
<ol>
<li>
<p>Get the real IP:</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>dig +short store.steampowered.com @1.1.1.1
</span></span></code></pre></div></li>
<li>
<p>Add the mapping:</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 nano /etc/hosts
</span></span></code></pre></div><p>Example entry:</p>
<pre tabindex="0"><code>23.197.161.221 store.steampowered.com
</code></pre></li>
<li>
<p>Verify:</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>ping -c <span style="color:#ae81ff">1</span> store.steampowered.com
</span></span></code></pre></div></li>
</ol>
<h3 id="option-2-proxyman-dns-spoofing">Option 2: Proxyman DNS spoofing</h3>
<p>Useful if you want a toggle and do not want to touch system files. Only works for traffic routed through Proxyman.</p>
<ol>
<li>Open Proxyman and go to <strong>Tools → DNS Settings</strong>.</li>
<li>Add a custom mapping with the real IP.</li>
<li>Enable the mapping and ensure Proxyman is capturing traffic.</li>
<li>Test in your browser.</li>
</ol>
<h2 id="why-this-works">Why This Works</h2>
<p>Cisco Umbrella intercepts DNS on port 53 (often via a local proxy like:
<code>/opt/cisco/secureclient/bin/dnscryptproxy --listenPort=53 --listenAddress=127.0.0.1</code>).</p>
<p>Both methods avoid that lookup: <code>/etc/hosts</code> is checked before DNS, and Proxyman can rewrite DNS answers before they hit the network.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<ul>
<li>
<p>Confirm Umbrella is running: <code>ps aux | grep -i umbrella</code>.</p>
</li>
<li>
<p>Check your hosts entry: <code>grep -n &quot;steam&quot; /etc/hosts</code>.</p>
</li>
<li>
<p>Test a direct IP mapping with curl:</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 -I https://store.steampowered.com --resolve <span style="color:#e6db74">&#34;store.steampowered.com:443:23.197.161.221&#34;</span>
</span></span></code></pre></div></li>
<li>
<p>Flush DNS cache if needed:</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 dscacheutil -flushcache; sudo killall -HUP mDNSResponder
</span></span></code></pre></div></li>
</ul>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://docs.umbrella.com/" target="_blank" >Cisco Umbrella documentation</a></li>
<li><a href="https://1.1.1.1/dns/" target="_blank" >Cloudflare 1.1.1.1 DNS</a></li>
<li><a href="https://proxyman.io/dns-mapping" target="_blank" >Proxyman DNS Mapping</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Finding 300 GB in macOS System Data: Time Machine Local Snapshots</title>
      <link>https://kunat.dev/notes/time-machine-snapshots/</link>
      <pubDate>Sat, 27 Dec 2025 14:20:37 +0100</pubDate>
      <guid>https://kunat.dev/notes/time-machine-snapshots/</guid>
      <description>&lt;p&gt;Mail popped up an alert: “Mail cannot save information about your mailboxes because there isn’t enough space in your home folder.”&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;macOS alert&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/pasted-image-20251225155159.png&#34; title=&#34;mail alert saying there&amp;#39;s no space left on disk&#34;&gt;&lt;/p&gt;
&lt;p&gt;My first two suspects in these situations are always Xcode junk or a full Trash.&lt;/p&gt;
&lt;p&gt;I opened &lt;a href=&#34;https://www.omnigroup.com/more&#34; target=&#34;_blank&#34; &gt;OmniDiskSweeper&lt;/a&gt; and started scanning. I love it because there is no visual fluff. You scan, you delete, you move on. I cleaned up old runtimes, old Xcodes, and VM images. That saved ~70 GB.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Mail popped up an alert: “Mail cannot save information about your mailboxes because there isn’t enough space in your home folder.”</p>
<p><img alt="macOS alert" loading="lazy" src="/notes/images/pasted-image-20251225155159.png" title="mail alert saying there&#39;s no space left on disk"></p>
<p>My first two suspects in these situations are always Xcode junk or a full Trash.</p>
<p>I opened <a href="https://www.omnigroup.com/more" target="_blank" >OmniDiskSweeper</a> and started scanning. I love it because there is no visual fluff. You scan, you delete, you move on. I cleaned up old runtimes, old Xcodes, and VM images. That saved ~70 GB.</p>
<p>The mystery was not solved though. macOS Settings still showed ~300 GB of &ldquo;System Data.&rdquo; OmniDiskSweeper did not show it.</p>
<p>Next up I tried <a href="https://macpaw.com/cleanmymac" target="_blank" >CleanMyMac X</a>. I usually prefer OmniDiskSweeper, but I was out of ideas. It found some big caches, but nothing close to 300 GB.</p>
<p>So I kept googling and landed on <a href="https://daisydiskapp.com/" target="_blank" >DaisyDisk</a>. It was blazing fast. I also do not hate the UI, but I do not love it either. It is definitely original.</p>
<p>DaisyDisk solved the mystery tho: all that &ldquo;System Data&rdquo; was Time Machine local snapshots.</p>
<p>It clicked. A few weeks earlier I had re-set up my Time Machine backup to a remote drive. The backup started failing, I missed it, and the snapshots kept piling up.</p>
<p>I deleted the local snapshots and freed ~300 GB.</p>
<h2 id="remove-local-snapshots">Remove local snapshots</h2>
<p>I followed this thread:</p>
<p><a href="https://apple.stackexchange.com/questions/340905/how-to-delete-all-local-timemachine-snapshots" target="_blank" >How to delete all local TimeMachine snapshots</a></p>
<p>The commands 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-bash" data-lang="bash"><span style="display:flex;"><span>tmutil listlocalsnapshots /
</span></span><span style="display:flex;"><span>sudo tmutil deletelocalsnapshots &lt;snapshot-id&gt;
</span></span></code></pre></div><p>If you want to delete all of them, the thread includes a one-liner. I didn’t need anything local, so I wiped everything and got my disk space back.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Navigate Obsidian Notes with Arrow Keys</title>
      <link>https://kunat.dev/notes/obsidian-arrows-navigation/</link>
      <pubDate>Fri, 05 Sep 2025 17:13:19 +0200</pubDate>
      <guid>https://kunat.dev/notes/obsidian-arrows-navigation/</guid>
      <description>&lt;p&gt;A few months back I migrated my notes from Bear Notes to Obsidian. The switch has been great - I love what Obsidian offers.&lt;/p&gt;
&lt;p&gt;One feature I missed dearly from Bear Notes was the ability to quickly navigate between notes using up/down arrow keys. This couldn&amp;rsquo;t be replicated using Obsidian hotkeys or plugins, and other people were missing this feature too.&lt;/p&gt;
&lt;p&gt;Turns out Obsidian just added this in version 1.9.10!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months back I migrated my notes from Bear Notes to Obsidian. The switch has been great - I love what Obsidian offers.</p>
<p>One feature I missed dearly from Bear Notes was the ability to quickly navigate between notes using up/down arrow keys. This couldn&rsquo;t be replicated using Obsidian hotkeys or plugins, and other people were missing this feature too.</p>
<p>Turns out Obsidian just added this in version 1.9.10!</p>
<h2 id="the-fix">The Fix</h2>
<p>In Obsidian 1.9.10+ you can navigate through your notes using <strong>CMD + up/down arrows</strong>.</p>
<p>Make sure the File Explorer view is active, then hold Command/Ctrl while using the arrow keys to browse through files. The file under the cursor will open automatically.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://obsidian.md/changelog/" target="_blank" >Obsidian 1.9.10 Changelog</a></li>
<li><a href="https://www.reddit.com/r/ObsidianMD/comments/xrw3wf/navigate_notes_using_arrow_keys/" target="_blank" >Reddit discussion about missing arrow key navigation</a></li>
<li><a href="https://x.com/kepano/status/1900971833677279611" target="_blank" >Kepano&rsquo;s announcement</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Fix Plex 401 Unauthorized Error in Homarr Dashboard</title>
      <link>https://kunat.dev/notes/homarr-plex-unauthorized/</link>
      <pubDate>Tue, 12 Aug 2025 18:42:06 +0200</pubDate>
      <guid>https://kunat.dev/notes/homarr-plex-unauthorized/</guid>
      <description>&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/homarr-plex-unauthorized/homarr-plex-unauthorized-header.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;Seeing a red dot with &amp;ldquo;401 unauthorized&amp;rdquo; status for Plex in your Homarr dashboard, even though your Plex server runs perfectly fine? The issue is likely your URL format.&lt;/p&gt;
&lt;p&gt;Instead of using the standard IP:PORT format like other containers, Plex requires the full web interface path.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Change your Plex internal address from:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;192.168.1.100:32400
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;192.168.1.100:32400/web/index.html#!/
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;why-this-path-is-required&#34;&gt;Why This Path is Required&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Plex&amp;rsquo;s web interface uses a single-page application architecture with hash-based routing. The &lt;code&gt;/web/index.html#!/&lt;/code&gt; path serves as the entry point to the bundled Plex Web App. This specific URL structure is how Plex handles authentication and routing for external integrations like Homarr.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img loading="lazy" src="/notes/images/homarr-plex-unauthorized/homarr-plex-unauthorized-header.png"></p>
<p>Seeing a red dot with &ldquo;401 unauthorized&rdquo; status for Plex in your Homarr dashboard, even though your Plex server runs perfectly fine? The issue is likely your URL format.</p>
<p>Instead of using the standard IP:PORT format like other containers, Plex requires the full web interface path.</p>
<h2 id="the-fix">The Fix</h2>
<p>Change your Plex internal address from:</p>
<pre tabindex="0"><code>192.168.1.100:32400
</code></pre><p>To:</p>
<pre tabindex="0"><code>192.168.1.100:32400/web/index.html#!/
</code></pre><h2 id="why-this-path-is-required">Why This Path is Required</h2>
<blockquote>
<p>Plex&rsquo;s web interface uses a single-page application architecture with hash-based routing. The <code>/web/index.html#!/</code> path serves as the entry point to the bundled Plex Web App. This specific URL structure is how Plex handles authentication and routing for external integrations like Homarr.</p></blockquote>
<blockquote>
<p>Unlike other services that expose simple API endpoints at their base URL, Plex requires access through its web application interface to properly authenticate and respond to status checks.</p></blockquote>
<ul>
<li><a href="https://github.com/ajnart/homarr/discussions/1407" target="_blank" >Homarr Plex 401 Discussion</a></li>
<li><a href="https://support.plex.tv/articles/201638786-plex-media-server-url-commands/" target="_blank" >Plex Media Server URL Commands | Plex Support</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Fix Key Repeat Issues with Cursor&#39;s Vim Plugin</title>
      <link>https://kunat.dev/notes/cursor-vim-plugin/</link>
      <pubDate>Tue, 12 Aug 2025 18:23:34 +0200</pubDate>
      <guid>https://kunat.dev/notes/cursor-vim-plugin/</guid>
      <description>&lt;p&gt;When using Cursor&amp;rsquo;s Vim plugin on macOS, you might notice that holding down keys like &lt;code&gt;j&lt;/code&gt; or &lt;code&gt;k&lt;/code&gt; for navigation doesn&amp;rsquo;t repeat as expected. Instead of continuous movement, the system shows accent character options or simply doesn&amp;rsquo;t repeat at all.&lt;/p&gt;
&lt;p&gt;This happens because macOS has &amp;ldquo;Press and Hold&amp;rdquo; enabled by default, which interferes with Vim&amp;rsquo;s key repeat functionality.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Run this command in Terminal to disable Press and Hold specifically for Cursor:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When using Cursor&rsquo;s Vim plugin on macOS, you might notice that holding down keys like <code>j</code> or <code>k</code> for navigation doesn&rsquo;t repeat as expected. Instead of continuous movement, the system shows accent character options or simply doesn&rsquo;t repeat at all.</p>
<p>This happens because macOS has &ldquo;Press and Hold&rdquo; enabled by default, which interferes with Vim&rsquo;s key repeat functionality.</p>
<h2 id="the-fix">The Fix</h2>
<p>Run this command in Terminal to disable Press and Hold specifically for Cursor:</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>defaults write com.todesktop.230313mzl4w4u92 ApplePressAndHoldEnabled -bool false
</span></span></code></pre></div><p>Restart Cursor for the changes to take effect.</p>
<h2 id="finding-your-bundle-id">Finding Your Bundle ID</h2>
<p>If the command above doesn&rsquo;t work (Cursor updates might change the bundle ID), you can find the correct identifier:</p>
<ol>
<li>Right-click on Cursor in Applications</li>
<li>Select &ldquo;Show Package Contents&rdquo;</li>
<li>Open <code>Contents/Info.plist</code></li>
<li>Look for the <code>CFBundleIdentifier</code> value</li>
</ol>
<p>Replace the bundle ID in the command with your specific identifier.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://stackoverflow.com/questions/39972335/how-do-i-press-and-hold-a-key-and-have-it-repeat-in-vscode" target="_blank" >How do I press and hold a key and have it repeat in vscode? | Stack Overflow</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>How to Use Xcode 16&#39;s Buildable Folders with CocoaPods</title>
      <link>https://kunat.dev/notes/xcode16-folders-pods/</link>
      <pubDate>Wed, 11 Jun 2025 19:11:09 +0200</pubDate>
      <guid>https://kunat.dev/notes/xcode16-folders-pods/</guid>
      <description>&lt;p&gt;A few months ago, I decided to try Xcode&amp;rsquo;s new &lt;a href=&#34;https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d&#34; target=&#34;_blank&#34; &gt;buildable folders&lt;/a&gt; feature in my project. However, right after converting all my groups to folders, I discovered that CocoaPods had stopped working. Attempting to install project dependencies resulted in the following error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pod install
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;❌ ArgumentError - &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;Xcodeproj&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; Unable to find compatibility version string &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; object version &lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;70&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At the time, I updated CocoaPods to the latest version, confirmed my project format was set to the latest Xcode version (16.3), and tried again. When the issue persisted, I assumed CocoaPods was incompatible with buildable folders and that I would need to migrate to Swift Package Manager (SPM) to use them. This conclusion seemed to be confirmed by several closed GitHub issues where the suggested workaround was to manually convert folders back to groups.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months ago, I decided to try Xcode&rsquo;s new <a href="https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d" target="_blank" >buildable folders</a> feature in my project. However, right after converting all my groups to folders, I discovered that CocoaPods had stopped working. Attempting to install project dependencies resulted in the following error:</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>$ pod install
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>❌ ArgumentError - <span style="color:#f92672">[</span>Xcodeproj<span style="color:#f92672">]</span> Unable to find compatibility version string <span style="color:#66d9ef">for</span> object version <span style="color:#e6db74">`</span>70<span style="color:#e6db74">`</span>.
</span></span></code></pre></div><p>At the time, I updated CocoaPods to the latest version, confirmed my project format was set to the latest Xcode version (16.3), and tried again. When the issue persisted, I assumed CocoaPods was incompatible with buildable folders and that I would need to migrate to Swift Package Manager (SPM) to use them. This conclusion seemed to be confirmed by several closed GitHub issues where the suggested workaround was to manually convert folders back to groups.</p>
<p>However, it turns out you don&rsquo;t have to wait to use Xcode&rsquo;s buildable folders with CocoaPods! The solution is surprisingly simple: set the <strong>Project Format</strong> in your project&rsquo;s settings to <strong>Xcode 16.0</strong>. Any other selection will trigger the <code>Xcodeproj</code> error.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/xcode16-folders-pods-header.jpg"></p>
<p>With that single change, you can start taking full advantage of Xcode&rsquo;s buildable folders in your CocoaPods projects!</p>
<h3 id="references">References</h3>
<ul>
<li><a href="https://github.com/CocoaPods/Xcodeproj/pull/985" target="_blank" >Xcodeproj PR #985</a></li>
<li><a href="https://github.com/CocoaPods/CocoaPods/issues/12671#issuecomment-2467142931" target="_blank" >CocoaPods Issue #12671</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Region-Specific Language Control: Implementing Forced Localization in iOS Apps</title>
      <link>https://kunat.dev/notes/removing-unwanted-localization-files/</link>
      <pubDate>Wed, 02 Apr 2025 22:08:53 +0200</pubDate>
      <guid>https://kunat.dev/notes/removing-unwanted-localization-files/</guid>
      <description>&lt;p&gt;My current project has a single target and multiple schemes. Each scheme represents the same app for different countries/regions. It&amp;rsquo;s essentially the same application with minor adjustments made for each of the supported jurisdictions.&lt;/p&gt;
&lt;p&gt;In Xcode projects, you define supported languages at the project level (not scheme level). Because of this structure, each app variant inherits the same language settings.&lt;/p&gt;
&lt;p&gt;My task was to force each app variant to support only the language for the region it was released in. The French app should only support French, no matter what the user&amp;rsquo;s device language is set to, and so on. This decision was made because large portions of the app are web-based and only supported a single language.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>My current project has a single target and multiple schemes. Each scheme represents the same app for different countries/regions. It&rsquo;s essentially the same application with minor adjustments made for each of the supported jurisdictions.</p>
<p>In Xcode projects, you define supported languages at the project level (not scheme level). Because of this structure, each app variant inherits the same language settings.</p>
<p>My task was to force each app variant to support only the language for the region it was released in. The French app should only support French, no matter what the user&rsquo;s device language is set to, and so on. This decision was made because large portions of the app are web-based and only supported a single language.</p>
<h2 id="solution">Solution</h2>
<p>There are multiple ways to solve this problem. What follows is the approach that worked best for my particular use case, given the project&rsquo;s current structure:</p>
<p>First, create a <a href="https://nshipster.com/xcconfig/" target="_blank" >build configuration file</a> for each of the schemes and define the <code>LOCALIZATIONS_TO_KEEP</code> variable.</p>
<pre tabindex="0"><code class="language-plist" data-lang="plist">LOCALIZATIONS_TO_KEEP = fr
</code></pre><p>Then, add a new build phase called &ldquo;Remove unused localizations.&rdquo; Remember to position it after the &ldquo;Copy Bundle Resources&rdquo; build phase:</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-shell" data-lang="shell"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Keeping only </span><span style="color:#e6db74">${</span>LOCALIZATIONS_TO_KEEP<span style="color:#e6db74">}</span><span style="color:#e6db74">.lproj&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Application .app path: </span><span style="color:#e6db74">${</span>CODESIGNING_FOLDER_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Only search in the main app bundle, excluding </span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Frameworks directory and other dependency folders</span>
</span></span><span style="display:flex;"><span>find <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>CODESIGNING_FOLDER_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -type d -name <span style="color:#e6db74">&#34;*.lproj&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/Frameworks/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/PlugIns/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/Watch/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/SwiftPM/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | grep -v <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>LOCALIZATIONS_TO_KEEP<span style="color:#e6db74">}</span><span style="color:#e6db74">.lproj&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | xargs rm -rf
</span></span></code></pre></div><blockquote>
<p><code>CODESIGNING_FOLDER_PATH</code> is an environment variable used in Xcode build scripts that represents the path to the app bundle being built. It typically points to the directory where the compiled application and its resources are placed before code signing occurs.</p></blockquote>
<p>You&rsquo;ll want to exclude locations where third-party dependencies might be located, because you can&rsquo;t guarantee that they support the same language you want to force the app to use.</p>
<p>You can verify that the script works by navigating to ${CODESIGNING_FOLDER_PATH} in derived data and checking if the script successfully removed all other localization files.</p>
<h2 id="alternative-solutions">Alternative Solutions</h2>
<p>This issue could be solved by setting <code>CFBundleLocalizations</code> and <code>CFBundleDevelopmentRegion</code> IF my project used separate targets instead of separate schemes for different apps.</p>
<p>Alternatively, I could dynamically select Info.plist files for each of the schemes. I opted against this approach since the project already had build configurations defined for each of the schemes.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Creating a New Objective-C Project in Xcode 16</title>
      <link>https://kunat.dev/notes/xcode-objc-project-template/</link>
      <pubDate>Tue, 25 Mar 2025 17:52:35 +0100</pubDate>
      <guid>https://kunat.dev/notes/xcode-objc-project-template/</guid>
      <description>&lt;p&gt;Update: 26/03/25&lt;/p&gt;
&lt;p&gt;It looks like Objective-C is still an option in the default iOS project template. You can access it after setting the &lt;em&gt;Interface&lt;/em&gt; to &amp;ldquo;Storyboard&amp;rdquo;. I hadn&amp;rsquo;t realized these dropdown menus were interconnected.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;hello there&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/xcode-objc-project-template-2.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;All credit goes to &lt;a href=&#34;https://douglashill.co&#34; target=&#34;_blank&#34; &gt;Douglas&lt;/a&gt; for spotting this!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;tl;dr use macOS → Application → Game or Command Line Tool&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;hello there&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/objc-xcode-project-template.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;Recently I wanted to refresh my memory on Objective-C. My first thought was to open Xcode, create a new project with language set to Objective-C and play with it. To my surprise, none of the iOS Application project templates allow you to select any other language than Swift.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Update: 26/03/25</p>
<p>It looks like Objective-C is still an option in the default iOS project template. You can access it after setting the <em>Interface</em> to &ldquo;Storyboard&rdquo;. I hadn&rsquo;t realized these dropdown menus were interconnected.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/xcode-objc-project-template-2.jpg"></p>
<p>All credit goes to <a href="https://douglashill.co" target="_blank" >Douglas</a> for spotting this!</p>
<hr>
<p>tl;dr use macOS → Application → Game or Command Line Tool</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/objc-xcode-project-template.jpg"></p>
<p>Recently I wanted to refresh my memory on Objective-C. My first thought was to open Xcode, create a new project with language set to Objective-C and play with it. To my surprise, none of the iOS Application project templates allow you to select any other language than Swift.</p>
<p>I (mistakenly) assumed that Apple must have removed Objective-C from all templates altogether. I ended up searching for some open-source Objective-C project that I could use as a starting point.</p>
<p>Later on I learned that Objective-C language options are still alive and well in some templates for macOS.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Verifying VPN Status for Docker qBittorrent on Synology</title>
      <link>https://kunat.dev/notes/check-torrent-client-vpn-ip/</link>
      <pubDate>Sat, 30 Nov 2024 22:09:34 +0100</pubDate>
      <guid>https://kunat.dev/notes/check-torrent-client-vpn-ip/</guid>
      <description>&lt;p&gt;I run &lt;code&gt;qbittorrent&lt;/code&gt; in a Docker container on my Synology NAS, with VPN configured at the Synology system level (Control Panel -&amp;gt; Network -&amp;gt; Network Interface) rather than the container level. For setup instructions, you can follow &lt;a href=&#34;https://blog.held.codes/how-to-use-mullvad-vpn-on-synology-nas-ed7a2ceb9595&#34; target=&#34;_blank&#34; &gt;this guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Even with killswitch enabled, I wanted to verify beyond the UI&amp;rsquo;s &amp;ldquo;Firewalled&amp;rdquo; status that my torrent traffic was actually routing through the VPN.&lt;/p&gt;
&lt;p&gt;To check this, SSH into your Synology NAS and run:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I run <code>qbittorrent</code> in a Docker container on my Synology NAS, with VPN configured at the Synology system level (Control Panel -&gt; Network -&gt; Network Interface) rather than the container level. For setup instructions, you can follow <a href="https://blog.held.codes/how-to-use-mullvad-vpn-on-synology-nas-ed7a2ceb9595" target="_blank" >this guide</a>.</p>
<p>Even with killswitch enabled, I wanted to verify beyond the UI&rsquo;s &ldquo;Firewalled&rdquo; status that my torrent traffic was actually routing through the VPN.</p>
<p>To check this, SSH into your Synology NAS and run:</p>
<pre tabindex="0"><code>sudo docker exec qbittorrent curl ifconfig.me
</code></pre><p>This command retrieves the container&rsquo;s public IP address. If it matches your VPN IP address rather than your real IP, you&rsquo;ve confirmed that the container&rsquo;s traffic is properly routing through the VPN.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
