<?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>MacOS on kunat.dev</title>
    <link>https://kunat.dev/tags/macos/</link>
    <description>Recent content in MacOS on kunat.dev</description>
    <generator>Hugo -- 0.147.5</generator>
    <language>en-us</language>
    <copyright>2025 kunat.dev</copyright>
    <lastBuildDate>Thu, 30 Apr 2026 18:33:50 +0200</lastBuildDate>
    <atom:link href="https://kunat.dev/tags/macos/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PhotoSorter Released</title>
      <link>https://kunat.dev/notes/photosorter-release/</link>
      <pubDate>Thu, 30 Apr 2026 18:33:50 +0200</pubDate>
      <guid>https://kunat.dev/notes/photosorter-release/</guid>
      <description>&lt;p&gt;&lt;em&gt;tl;dr &lt;a href=&#34;https://github.com/bkunat/PhotoSorter&#34; target=&#34;_blank&#34; &gt;PhotoSorter&lt;/a&gt; is a tiny macOS utility for sorting loose images into folders with keyboard shortcuts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;PhotoSorter header&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/photosorter-release/photosorter-header.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;I built PhotoSorter for myself a few months ago because I needed a faster way to clean up folders full of random images.&lt;/p&gt;
&lt;p&gt;By &amp;ldquo;photo library&amp;rdquo; I mean photos managed by Photos on macOS, Google Photos, or any other actual photo management app. PhotoSorter is for everything outside of that. Screenshots, saved memes, random images from Downloads, and all the other stuff that somehow piles up on disk.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>tl;dr <a href="https://github.com/bkunat/PhotoSorter" target="_blank" >PhotoSorter</a> is a tiny macOS utility for sorting loose images into folders with keyboard shortcuts.</em></p>
<p><img alt="PhotoSorter header" loading="lazy" src="/notes/images/photosorter-release/photosorter-header.jpg"></p>
<p>I built PhotoSorter for myself a few months ago because I needed a faster way to clean up folders full of random images.</p>
<p>By &ldquo;photo library&rdquo; I mean photos managed by Photos on macOS, Google Photos, or any other actual photo management app. PhotoSorter is for everything outside of that. Screenshots, saved memes, random images from Downloads, and all the other stuff that somehow piles up on disk.</p>
<p>It does one thing: you point it at a folder, create a few categories, and sort images one by one using keyboard shortcuts.</p>
<video controls muted playsinline preload="metadata" poster="/notes/images/photosorter-release/photosorter-demo-poster.jpg" style="width: 100%; border-radius: 8px;">
  <source src="/notes/images/photosorter-release/photosorter-demo.mp4" type="video/mp4">
</video>
<p>Each shortcut moves the current image into its matching category folder and advances to the next one. That&rsquo;s it.</p>
<h2 id="why">Why</h2>
<p>Every few months I need to declutter my Desktop and Downloads folders from all kinds of random files, most of which are screenshots.</p>
<p>I&rsquo;m a hoarder. I don&rsquo;t delete stuff. I just put things away for later in case I ever need them.</p>
<p><img alt="A meme about keeping useful stuff because it might be useful someday" loading="lazy" src="/notes/images/photosorter-release/useful-meme.jpg"></p>
<p>Why shouldn&rsquo;t I. Storage is effectively free. At least it felt that way back in 2023 when I was setting up my NAS.</p>
<p><img alt="A screenshot of disk prices" loading="lazy" src="/notes/images/photosorter-release/disk-prices.png"></p>
<p>Technically I could add all those images to my photo library and let Photos sort them for me, but I prefer to keep them separate.</p>
<p>First, I like to keep my photo library clean. Adding hundreds of random screenshots is the opposite of that.</p>
<p>Second, while doing the quarterly image sorting, I often find stuff where some action is required: schedule an appointment, follow up with someone, create a Jira ticket, whatever. Most of it is just screenshots of tweets tho.</p>
<p><img alt="A random saved tweet screenshot" loading="lazy" src="/notes/images/photosorter-release/random-tweet.png"></p>
<p>Before PhotoSorter I did this manually by dragging and dropping images into different folders. At some point I tried using macOS tags in Finder, but those don&rsquo;t support keyboard shortcuts either. Both solutions required way too much mouse movement.</p>
<p>I couldn&rsquo;t find a good existing solution for this exact problem, so I built PhotoSorter.</p>
<p>There are probably a dozen people around the world who need this kind of tool, but I wanted to share it nonetheless. I&rsquo;ve been using it for a while now and it has already saved me a lot of time.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/bkunat/PhotoSorter" target="_blank" >PhotoSorter on GitHub</a></li>
</ul>
]]></content:encoded>
    </item>
    <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>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>Swift Package Manager Mirrors for Local Development</title>
      <link>https://kunat.dev/notes/spm-package-mirroring/</link>
      <pubDate>Tue, 23 Dec 2025 19:50:06 +0100</pubDate>
      <guid>https://kunat.dev/notes/spm-package-mirroring/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A dependency mirror refers to an alternate source location which exactly replicates the contents of the original source. [1]&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I often end up in the same situation: I need to tweak an internal package that a project depends on. The usual options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drag and drop the repo into Xcode (Xcode replaces the remote dependency with the local copy)&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;Package.swift&lt;/code&gt; to a local path dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both work, but I prefer a CLI-only workflow. Turns out SPM has a cleaner alternative: &lt;strong&gt;dependency mirroring!&lt;/strong&gt;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>A dependency mirror refers to an alternate source location which exactly replicates the contents of the original source. [1]</p></blockquote>
<p>I often end up in the same situation: I need to tweak an internal package that a project depends on. The usual options are:</p>
<ul>
<li>Drag and drop the repo into Xcode (Xcode replaces the remote dependency with the local copy)</li>
<li>Change <code>Package.swift</code> to a local path dependency</li>
</ul>
<p>Both work, but I prefer a CLI-only workflow. Turns out SPM has a cleaner alternative: <strong>dependency mirroring!</strong></p>
<h2 id="set-a-mirror">Set a Mirror</h2>
<p>Point the remote URL to a local repo:</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>swift package config set-mirror --original <span style="color:#e6db74">&#34;THE_ORIGINAL_URL&#34;</span> --mirror <span style="color:#e6db74">&#34;LOCAL_PATH&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Example</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># swift package config set-mirror --original &#34;https://github.com/apple/swift-argument-parser&#34; \</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   --mirror &#34;/Users/user/Developer/swift-argument-parser&#34;</span>
</span></span></code></pre></div><p>To confirm:</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>swift package config get-mirror --original <span style="color:#e6db74">&#34;https://github.com/apple/swift-argument-parser&#34;</span>
</span></span></code></pre></div><h2 id="important-notes">Important Notes</h2>
<ul>
<li>A mirror only changes where SPM fetches the repo from. It still resolves a version, branch, or revision.</li>
<li>If you want local edits to show up, you must commit them in the mirrored repo.</li>
<li>Use a branch or revision in <code>Package.swift</code>:</li>
</ul>
<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-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(url: <span style="color:#e6db74">&#34;https://github.com/apple/swift-argument-parser&#34;</span>, branch: <span style="color:#e6db74">&#34;main&#34;</span>),
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><ul>
<li>Mirrors are matched by the exact URL string. If your dependency uses the <code>.git</code> suffix, set a mirror for that URL too.</li>
</ul>
<h2 id="remove-the-mirror">Remove the Mirror</h2>
<p>Once you are done, remove 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-shell" data-lang="shell"><span style="display:flex;"><span>swift package config unset-mirror --original https://github.com/apple/swift-argument-parser
</span></span></code></pre></div><p>If you added a <code>.git</code> mirror, remove that one as well.</p>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>Mirrors are not a drop-in replacement for local path dependencies. You still need to commit changes, but in return you get a clean, repeatable CLI workflow that works across projects without touching Xcode.</p>
<h2 id="resources">Resources</h2>
<ul>
<li>[1] <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0219-package-manager-dependency-mirroring.md" target="_blank" >swift-evolution/proposals/0219-package-manager-dependency-mirroring.md at main - swiftlang/swift-evolution</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Alfred Snippet Converter v0.3.0</title>
      <link>https://kunat.dev/notes/alfredsnippetconverter-v2/</link>
      <pubDate>Mon, 01 Sep 2025 19:41:31 +0200</pubDate>
      <guid>https://kunat.dev/notes/alfredsnippetconverter-v2/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;hello there&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/alfredsnippetconverter-v2/alfredsnippetconverter-v2-header.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;Need your Alfred snippets on iPhone and iPad? Alfred Snippet Converter now supports zip archives, drag &amp;amp; drop, and batch conversion of multiple collections.&lt;/p&gt;
&lt;h2 id=&#34;whats-new&#34;&gt;What&amp;rsquo;s New&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Zip Archive Support&lt;/strong&gt; - Convert collections directly after exporting from Alfred without manual extraction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drag &amp;amp; Drop&lt;/strong&gt; - Simply drag your collection files onto the app window.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Batch Processing&lt;/strong&gt; - Convert all your Alfred snippet collections in one operation.&lt;/p&gt;
&lt;p&gt;The tool converts Alfred snippets into macOS text replacements, which sync across all your Apple devices via iCloud. Read the full setup guide in &lt;a href=&#34;https://kunat.dev/notes/alfred-snippets-on-ios/&#34; &gt;Alfred Snippets on iOS&lt;/a&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img alt="hello there" loading="lazy" src="/notes/images/alfredsnippetconverter-v2/alfredsnippetconverter-v2-header.jpg"></p>
<p>Need your Alfred snippets on iPhone and iPad? Alfred Snippet Converter now supports zip archives, drag &amp; drop, and batch conversion of multiple collections.</p>
<h2 id="whats-new">What&rsquo;s New</h2>
<p><strong>Zip Archive Support</strong> - Convert collections directly after exporting from Alfred without manual extraction.</p>
<p><strong>Drag &amp; Drop</strong> - Simply drag your collection files onto the app window.</p>
<p><strong>Batch Processing</strong> - Convert all your Alfred snippet collections in one operation.</p>
<p>The tool converts Alfred snippets into macOS text replacements, which sync across all your Apple devices via iCloud. Read the full setup guide in <a href="/notes/alfred-snippets-on-ios/" >Alfred Snippets on iOS</a>.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/bkunat/AlfredSnippetConverter" target="_blank" >Download Alfred Snippet Converter</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>Adding Swift Package Manager Support to a Legacy Objective-C Project</title>
      <link>https://kunat.dev/notes/spm-support-objc-project/</link>
      <pubDate>Tue, 03 Jun 2025 16:50:49 +0200</pubDate>
      <guid>https://kunat.dev/notes/spm-support-objc-project/</guid>
      <description>&lt;p&gt;I recently needed to add Swift Package Manager support to a legacy package that one of my projects was using. The package was originally distributed with CocoaPods. The primary reason for migrating from CocoaPods to SPM is that CocoaPods entered &lt;a href=&#34;https://blog.cocoapods.org/CocoaPods-Support-Plans/&#34; target=&#34;_blank&#34; &gt;maintenance mode&lt;/a&gt; a few months ago. Removing it will future-proof our projects and allow us to use the latest features, such as the &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; introduced in Xcode 16.&lt;/p&gt;
&lt;h2 id=&#34;assumptions&#34;&gt;Assumptions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Since this was an internal package, I did not need to maintain CocoaPods support. The goal was to replace it entirely with SPM. It is certainly possible to keep both, but that would require some additional work.&lt;/li&gt;
&lt;li&gt;The project I was working on is considered legacy; no one has touched it in about six years. Once this task is complete, I hope no one will need to update it for another half-dozen years. This allowed me to take some shortcuts, like integrating a third-party dependency directly into the codebase instead of spending time updating it to the latest version.&lt;/li&gt;
&lt;li&gt;All internal dependencies of your framework already support SPM.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that said, let&amp;rsquo;s get started.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently needed to add Swift Package Manager support to a legacy package that one of my projects was using. The package was originally distributed with CocoaPods. The primary reason for migrating from CocoaPods to SPM is that CocoaPods entered <a href="https://blog.cocoapods.org/CocoaPods-Support-Plans/" target="_blank" >maintenance mode</a> a few months ago. Removing it will future-proof our projects and allow us to use the latest features, such as the <a href="https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d" target="_blank" >buildable folders</a> introduced in Xcode 16.</p>
<h2 id="assumptions">Assumptions</h2>
<ul>
<li>Since this was an internal package, I did not need to maintain CocoaPods support. The goal was to replace it entirely with SPM. It is certainly possible to keep both, but that would require some additional work.</li>
<li>The project I was working on is considered legacy; no one has touched it in about six years. Once this task is complete, I hope no one will need to update it for another half-dozen years. This allowed me to take some shortcuts, like integrating a third-party dependency directly into the codebase instead of spending time updating it to the latest version.</li>
<li>All internal dependencies of your framework already support SPM.</li>
</ul>
<p>With that said, let&rsquo;s get started.</p>
<h2 id="how">How?</h2>
<p>I&rsquo;ll assume your framework has no internal dependencies that require the same SPM treatment. If it does, start with the bottom-most dependency and work your way up.</p>
<h3 id="external-dependencies">External Dependencies</h3>
<p>The first step is to update all external dependencies to use SPM.</p>
<p>In my case, one of the dependencies was <a href="https://github.com/mxcl/PromiseKit" target="_blank" >PromiseKit</a>, which was stuck six major versions behind the latest release. Since the project was considered legacy and no further development was planned, I decided to cut some corners.</p>
<p>Instead of spending time updating PromiseKit from version 1 through 7, I opted to add SPM support to the version my project was currently using. This approach required significantly less work and proved to be the right choice.</p>
<h3 id="removing-the-old-dependency-manager">Removing the Old Dependency Manager</h3>
<p>With all dependencies migrated to SPM, you should now be able to remove the old dependency manager from your project.</p>
<h3 id="adding-a-swift-package-manifest">Adding a Swift Package Manifest</h3>
<p>Next, initialize a new Swift package in your project&rsquo;s root directory:</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>swift package init
</span></span></code></pre></div><p>This will create a <code>Package.swift</code> file. Here is a basic configuration:</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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// swift-tools-version: 6.1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PackageDescription</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> package = Package(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>    products: [
</span></span><span style="display:flex;"><span>        .library(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>            targets: [<span style="color:#e6db74">&#34;MyPackage&#34;</span>]),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackageTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;MyPackage&#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></code></pre></div><p>Move your project&rsquo;s source files to <code>Sources/MyPackage</code> and try to build the project. You will likely encounter one of the following errors:</p>
<pre tabindex="0"><code>SomeHeaderFile.h:19:9 &#39;AnotherHeaderFile.h&#39; file not found
</code></pre><p>or</p>
<pre tabindex="0"><code>public headers (&#34;include&#34;) directory path for &#39;MyPackage&#39; is invalid or not contained in the target
</code></pre><p>These errors indicate that the compiler cannot locate your project&rsquo;s header files. By default, SPM looks for public headers in an <code>include</code> directory.</p>
<p>Create an <code>include</code> directory inside <code>Sources/MyPackage</code> and move all of your project&rsquo;s public headers there.</p>
<p>If this structure doesn&rsquo;t suit your project, you can define a custom path for your public headers by specifying <code>publicHeadersPath</code> in your <code>Package.swift</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-swift" data-lang="swift"><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>            publicHeadersPath: <span style="color:#e6db74">&#34;some/custom/path&#34;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackageTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;MyPackage&#34;</span>]
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span></code></pre></div><p><strong>How do I locate my project&rsquo;s public headers?</strong></p>
<p>Your project most likely had an Xcode project file (<code>.xcproj</code>) before you started adding SPM support. You can find a list of your public headers in the &ldquo;Build Phases&rdquo; tab, under the &ldquo;Headers&rdquo; section.</p>
<p><img alt="changedetection" loading="lazy" src="/notes/images/spm-objc-public-headers.jpg"></p>
<p>After correctly configuring the public header files, your project might build. If it still doesn&rsquo;t, you may have other header files that need to be discoverable. If that&rsquo;s the case, you&rsquo;ll need to add search paths for your private header files using <code>headerSearchPath</code> in your cSettings:</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-swift" data-lang="swift"><span style="display:flex;"><span>.target(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>    cSettings: [
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Payments&#34;</span>),
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Payments/ApplePayPaymentsService&#34;</span>),
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Transactions/Components&#34;</span>)
</span></span><span style="display:flex;"><span>    ]),
</span></span></code></pre></div><p>It&rsquo;s up to you whether you want to group all private header files in a single directory (<code>.headerSearchPath(&quot;Path/To/My/Private/Headers&quot;)</code>) or add all existing directories one by one using their current locations as I did in the snippet above.</p>
<p>With that done, you should now be able to build your project.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p>In my case, after migrating all dependencies to SPM and adding the package manifest, I was able to delete the Xcode project file entirely. If that is not the case for you, you might encounter additional issues.</p>
<p>One common problem is the error <code>fatal error: framework '&lt;some_framework&gt;' not found</code> when trying to import an SPM dependency into an Objective-C codebase in a project managed via an Xcode project file (<code>.xcproj</code>).</p>
<p>In this situation, you may need to experiment with your project settings. <a href="https://developer.apple.com/forums/thread/120152" target="_blank" >This thread</a> on the Apple Developer Forums is an good starting point.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Once these steps are completed, you should be able to build your project. If you add your new package as a dependency via SPM, you can import it like any other Swift package. The same rules apply.</p>
<p><strong>Swift:</strong></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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">MyPackage</span>
</span></span></code></pre></div><p><strong>Objective-C:</strong></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-objectivec" data-lang="objectivec"><span style="display:flex;"><span>@import MyPackage;
</span></span></code></pre></div><h2 id="resources">Resources</h2>
<p>What helped me the most was looking at examples of existing Objective-C projects with SPM support. The following repositories are worth exploring:</p>
<ul>
<li><a href="https://github.com/PSPDFKit/PDFXKit" target="_blank" >PSPDFKit/PDFXKit</a>: A drop-in replacement for Apple&rsquo;s PDFKit, powered by the PSPDFKit framework.</li>
<li><a href="https://github.com/AliSoftware/OHHTTPStubs" target="_blank" >AliSoftware/OHHTTPStubs</a>: A library to easily stub your network requests.</li>
<li><a href="https://github.com/Mantle/Mantle" target="_blank" >Mantle/Mantle</a>: Model framework for Cocoa and Cocoa Touch</li>
</ul>
]]></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>Create a Safari Extension to Extract Article Content</title>
      <link>https://kunat.dev/notes/safari-extension-reader-mode/</link>
      <pubDate>Wed, 18 Dec 2024 18:59:56 +0100</pubDate>
      <guid>https://kunat.dev/notes/safari-extension-reader-mode/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;safari-extension-reader-mode-preview&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/safari-extension-reader-mode-preview.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;In this article, I&amp;rsquo;ll show you how to access a web page&amp;rsquo;s content, alter it using JavaScript, and display it using your Safari extension&amp;rsquo;s UI. Specifically, I&amp;rsquo;ll demonstrate how to display an article&amp;rsquo;s content in a popover.&lt;/p&gt;
&lt;p&gt;I often use Large Language Models (LLMs) to summarize articles when I&amp;rsquo;m unsure if they&amp;rsquo;re worth reading. Without the extension we&amp;rsquo;re about to build, I&amp;rsquo;d need to manually trigger Safari&amp;rsquo;s reader mode (⌘ + ⇧ + R), copy all content, and paste it into Claude or ChatGPT. This extension is a handy tool that reduces friction!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img alt="safari-extension-reader-mode-preview" loading="lazy" src="/notes/images/safari-extension-reader-mode-preview.png"></p>
<p>In this article, I&rsquo;ll show you how to access a web page&rsquo;s content, alter it using JavaScript, and display it using your Safari extension&rsquo;s UI. Specifically, I&rsquo;ll demonstrate how to display an article&rsquo;s content in a popover.</p>
<p>I often use Large Language Models (LLMs) to summarize articles when I&rsquo;m unsure if they&rsquo;re worth reading. Without the extension we&rsquo;re about to build, I&rsquo;d need to manually trigger Safari&rsquo;s reader mode (⌘ + ⇧ + R), copy all content, and paste it into Claude or ChatGPT. This extension is a handy tool that reduces friction!</p>
<p>Start by creating a new project using the &ldquo;Safari App Extension&rdquo; template, or add a Safari Extension target to your project. When prompted, set <code>type</code> to Safari App Extension (the default at the time of writing).</p>
<p>After the initial setup is complete, follow <a href="https://www.polpiella.dev/safari-extensions-swiftui" target="_blank" >this guide</a> to modify the template so that you can use SwiftUI to build the UI. Follow all the instructions except for one: do not delete <code>script.js</code>. We&rsquo;ll need it to capture the web page&rsquo;s content.</p>
<p>Once that&rsquo;s done, we&rsquo;ll proceed to get the current web page&rsquo;s content, modify it to extract the article content, and display it. You can check out the complete sample project <a href="https://github.com/bkunat/SafariReaderModeSample" target="_blank" >here</a>.</p>
<ol>
<li><strong>Getting Active Safari Page</strong> (<code>getActiveSafariPage</code>)</li>
</ol>
<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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> page = <span style="color:#66d9ef">try</span> await getActiveSafariPage()
</span></span></code></pre></div><ul>
<li>Uses Safari&rsquo;s extension API to retrieve the current window, tab, and page</li>
<li>Returns a <code>SFSafariPage</code> object representing the active browser page</li>
</ul>
<ol start="2">
<li><strong>Requesting Content</strong> (<code>requestAndReceiveContent</code>)</li>
</ol>
<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-swift" data-lang="swift"><span style="display:flex;"><span>page.dispatchMessageToScript(
</span></span><span style="display:flex;"><span>    withName: SafariExtensionMessage.Name.getContent.rawValue,
</span></span><span style="display:flex;"><span>    userInfo: <span style="color:#66d9ef">nil</span>)
</span></span></code></pre></div><ul>
<li>Sends a &ldquo;getContent&rdquo; message to the injected content script</li>
</ul>
<ol start="3">
<li><strong>Content Script Processing</strong> (<code>script.js</code>)</li>
</ol>
<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-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getPageContent</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Tries to extract content in this order:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 1. Structured article data (JSON-LD)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 2. Article elements with specific selectors
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 3. Fallback to body text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">title</span><span style="color:#f92672">:</span> document.<span style="color:#a6e22e">title</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">getReaderContent</span>().<span style="color:#a6e22e">trim</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:#a6e22e">safari</span>.<span style="color:#a6e22e">extension</span>.<span style="color:#a6e22e">dispatchMessage</span>(<span style="color:#e6db74">&#34;pageContent&#34;</span>, { <span style="color:#a6e22e">content</span> });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol start="4">
<li><strong>Message Handling</strong> (<code>SafariExtensionHandler</code>)</li>
</ol>
<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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">case</span> .pageContent:
</span></span><span style="display:flex;"><span>    NotificationCenter.<span style="color:#66d9ef">default</span>.post(
</span></span><span style="display:flex;"><span>        name: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue),
</span></span><span style="display:flex;"><span>        object: <span style="color:#66d9ef">nil</span>,
</span></span><span style="display:flex;"><span>        userInfo: userInfo
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><ul>
<li>Receives the content message from the script</li>
<li>Posts it to NotificationCenter</li>
</ul>
<ol start="5">
<li><strong>Content Reception</strong> (<code>PopoverViewModel</code>)</li>
</ol>
<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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> await notification <span style="color:#66d9ef">in</span> NotificationCenter.<span style="color:#66d9ef">default</span>
</span></span><span style="display:flex;"><span>    .notifications(named: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue))
</span></span><span style="display:flex;"><span>    .compactMap({ notification <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Converts notification data to WebPageContent</span>
</span></span><span style="display:flex;"><span>    })
</span></span></code></pre></div><ul>
<li>Listens for the notification with the page content</li>
<li>Converts the received data into a <code>WebPageContent</code> struct</li>
<li>Returns the structured content to be displayed in the popover</li>
</ul>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.polpiella.dev/safari-extensions-swiftui" target="_blank" >How to build a Safari extension with SwiftUI</a></li>
<li><a href="https://github.com/bkunat/SafariReaderModeSample" target="_blank" >SafariReaderModeSample | GitHub</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Single-Key App Launching with Karabiner and Hammerspoon</title>
      <link>https://kunat.dev/notes/hammerspoon-applauncher-setup/</link>
      <pubDate>Fri, 13 Sep 2024 16:56:19 +0200</pubDate>
      <guid>https://kunat.dev/notes/hammerspoon-applauncher-setup/</guid>
      <description>&lt;p&gt;For years, I wanted a way to launch my most-used apps with single-key shortcuts. While I knew tools like Hammerspoon could make this possible, I never found a key combination that felt right. I&amp;rsquo;d always end up disabling the setup, missing the functionality of keys like right control, caps lock, or escape.&lt;/p&gt;
&lt;p&gt;Everything changed when I discovered Karabiner Elements&amp;rsquo; ability to create distinct mappings for different key events, such as short presses and long presses. The setup I&amp;rsquo;m about to share lets you launch any mapped app by pressing caps lock plus a specific letter. The best part? You don&amp;rsquo;t lose the caps lock functionality—it still works as normal when pressed on its own.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>For years, I wanted a way to launch my most-used apps with single-key shortcuts. While I knew tools like Hammerspoon could make this possible, I never found a key combination that felt right. I&rsquo;d always end up disabling the setup, missing the functionality of keys like right control, caps lock, or escape.</p>
<p>Everything changed when I discovered Karabiner Elements&rsquo; ability to create distinct mappings for different key events, such as short presses and long presses. The setup I&rsquo;m about to share lets you launch any mapped app by pressing caps lock plus a specific letter. The best part? You don&rsquo;t lose the caps lock functionality—it still works as normal when pressed on its own.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>This setup uses two tools: Karabiner Elements for setting up a hyper key binding and Hammerspoon for setting up the key mapping for specific apps. There&rsquo;s a multitude of solutions to this problem, but I&rsquo;ve found this one to work for me. It&rsquo;s fairly simple and easy to set up in minutes.</p>
<ol>
<li>Install <a href="https://karabiner-elements.pqrs.org/" target="_blank" >Karabiner Elements</a></li>
<li>Install <a href="https://www.hammerspoon.org/" target="_blank" >Hammerspoon</a></li>
</ol>
<h2 id="setting-up-karabiner-elements">Setting up Karabiner Elements</h2>
<p>Navigate to the Complex Modifications tab and install this <a href="https://ke-complex-modifications.pqrs.org/#caps_lock_to_hyper_or_esc_if_alone" target="_blank" >rule</a>. This rule maps the caps lock key to hyper or escape when pressed. You can modify it to your liking.</p>
<p><img alt="description" loading="lazy" src="/notes/images/hammerspoon-applauncher-setup/karabiner-elements-complex-modifications.png"></p>
<h2 id="setting-up-hammerspoon">Setting up Hammerspoon</h2>
<p>Paste the below into your Hammerspoon config and reload 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-lua" data-lang="lua"><span style="display:flex;"><span>hs.loadSpoon(<span style="color:#e6db74">&#34;AppLauncher&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> hyper <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;control&#34;</span>, <span style="color:#e6db74">&#34;option&#34;</span>, <span style="color:#e6db74">&#34;cmd&#34;</span>, <span style="color:#e6db74">&#34;shift&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>spoon.AppLauncher.modifiers <span style="color:#f92672">=</span> hyper
</span></span><span style="display:flex;"><span>spoon.AppLauncher:bindHotkeys(
</span></span><span style="display:flex;"><span>   {
</span></span><span style="display:flex;"><span>    c <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Terminal&#34;</span>,
</span></span><span style="display:flex;"><span>    x <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Xcode&#34;</span>,
</span></span><span style="display:flex;"><span>    f <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Firefox&#34;</span>,
</span></span><span style="display:flex;"><span>    s <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Slack&#34;</span>,
</span></span><span style="display:flex;"><span>    m <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Music&#34;</span>,
</span></span><span style="display:flex;"><span>    v <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Visual Studio Code&#34;</span>,
</span></span><span style="display:flex;"><span>    t <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tower&#34;</span>,
</span></span><span style="display:flex;"><span>    p <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Proxyman&#34;</span>,
</span></span><span style="display:flex;"><span>    o <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Obsidian&#34;</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></span></code></pre></div><p>This config uses the <a href="https://www.hammerspoon.org/Spoons/AppLauncher.html" target="_blank" >AppLauncher</a> spoon to set up hotkeys for specific apps. You can adjust them to your liking.</p>
<h2 id="notes">Notes</h2>
<h3 id="single-tool-for-the-job">Single Tool for the Job</h3>
<p>I know it is technically possible to achieve the same result using Hammerspoon alone, but it didn&rsquo;t seem like the right tool for the job to me. If that&rsquo;s not an issue for you, you might want to use this as your starting point:</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-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#66d9ef">local</span> hyper <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;ctrl&#34;</span>, <span style="color:#e6db74">&#34;alt&#34;</span>, <span style="color:#e6db74">&#34;cmd&#34;</span>, <span style="color:#e6db74">&#34;shift&#34;</span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> sendEscape <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> lastMods <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Function to check if only caps lock was pressed</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">onlyCapsWasPressed</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> k, v <span style="color:#66d9ef">in</span> pairs(lastMods) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> v <span style="color:#66d9ef">then</span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Create and start the eventtap</span>
</span></span><span style="display:flex;"><span>eventtap <span style="color:#f92672">=</span> hs.eventtap.new({hs.eventtap.event.types.flagsChanged, hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, <span style="color:#66d9ef">function</span>(event)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">local</span> newMods <span style="color:#f92672">=</span> event:getFlags()
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> event:getKeyCode() <span style="color:#f92672">==</span> <span style="color:#ae81ff">57</span> <span style="color:#66d9ef">then</span> <span style="color:#75715e">-- Caps Lock key</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">-- Check if fn is pressed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> newMods.fn <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">-- Allow normal caps lock behavior when fn is pressed</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">-- On key down</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> lastMods.capslock <span style="color:#f92672">and</span> newMods.capslock <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>            sendEscape <span style="color:#f92672">=</span> onlyCapsWasPressed()
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">-- Set Hyper key</span>
</span></span><span style="display:flex;"><span>            hs.eventtap.keyStroke(hyper, <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">-- On key up</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elseif</span> lastMods.capslock <span style="color:#f92672">and</span> <span style="color:#f92672">not</span> newMods.capslock <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> sendEscape <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>                hs.eventtap.keyStroke({}, <span style="color:#e6db74">&#34;escape&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    lastMods <span style="color:#f92672">=</span> newMods
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>)
</span></span><span style="display:flex;"><span>eventtap:start()
</span></span></code></pre></div><h3 id="running-out-of-keys">Running out of Keys</h3>
<p>Ideally, I&rsquo;d want this setup to support two-letter combinations, but I couldn&rsquo;t find a straightforward solution. One idea would be to completely ditch the AppLauncher and use something like 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-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#66d9ef">local</span> hyper <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;cmd&#34;</span>, <span style="color:#e6db74">&#34;alt&#34;</span>, <span style="color:#e6db74">&#34;ctrl&#34;</span>, <span style="color:#e6db74">&#34;shift&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> appShortcuts <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    te <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Terminal&#34;</span>,
</span></span><span style="display:flex;"><span>    to <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Tower&#34;</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">local</span> lastKeyPress <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> lastKeyPressTime <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>hs.hotkey.bind(hyper, <span style="color:#e6db74">&#34;t&#34;</span>, <span style="color:#66d9ef">function</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">local</span> now <span style="color:#f92672">=</span> hs.timer.secondsSinceEpoch()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> lastKeyPress <span style="color:#f92672">and</span> (now <span style="color:#f92672">-</span> lastKeyPressTime) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0.5</span> <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">local</span> combo <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t&#34;</span> <span style="color:#f92672">..</span> lastKeyPress
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> appShortcuts[combo] <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>            hs.application.launchOrFocus(appShortcuts[combo])
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>        lastKeyPress <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>        lastKeyPress <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    lastKeyPressTime <span style="color:#f92672">=</span> now
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> k, _ <span style="color:#66d9ef">in</span> pairs(appShortcuts) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">local</span> secondKey <span style="color:#f92672">=</span> string.sub(k, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    hs.hotkey.bind(hyper, secondKey, <span style="color:#66d9ef">function</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">local</span> now <span style="color:#f92672">=</span> hs.timer.secondsSinceEpoch()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> lastKeyPress <span style="color:#f92672">and</span> (now <span style="color:#f92672">-</span> lastKeyPressTime) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0.5</span> <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">local</span> combo <span style="color:#f92672">=</span> lastKeyPress <span style="color:#f92672">..</span> secondKey
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> appShortcuts[combo] <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>                hs.application.launchOrFocus(appShortcuts[combo])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>        lastKeyPress <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Xcode: Missing Package Product (local package)</title>
      <link>https://kunat.dev/notes/xcode-missing-package-product/</link>
      <pubDate>Mon, 24 Jun 2024 18:48:01 +0200</pubDate>
      <guid>https://kunat.dev/notes/xcode-missing-package-product/</guid>
      <description>&lt;p&gt;When adding a local Swift package to your Xcode project, you might encounter the following error:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Missing package product `PACKAGE_NAME`
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/xcode-missing-package-product.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;The issue often occurs when the package you&amp;rsquo;re trying to add is already open in Xcode. To resolve:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Close the package in Xcode.&lt;/li&gt;
&lt;li&gt;Close your main project.&lt;/li&gt;
&lt;li&gt;Reopen your main project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;m not sure what might be causing this issue or why reopening the project is required (a clean build won&amp;rsquo;t do).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When adding a local Swift package to your Xcode project, you might encounter the following error:</p>
<pre tabindex="0"><code>Missing package product `PACKAGE_NAME`
</code></pre><p><img loading="lazy" src="/notes/images/xcode-missing-package-product.jpg"></p>
<p>The issue often occurs when the package you&rsquo;re trying to add is already open in Xcode. To resolve:</p>
<ol>
<li>Close the package in Xcode.</li>
<li>Close your main project.</li>
<li>Reopen your main project.</li>
</ol>
<p>I&rsquo;m not sure what might be causing this issue or why reopening the project is required (a clean build won&rsquo;t do).</p>
<p>To learn more about local Swift packages read <a href="../swift-local-packages" >Working with Local Swift Packages</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Keeping Safari Windows Open: A Tab Group Workaround</title>
      <link>https://kunat.dev/notes/keep-safar-window-open/</link>
      <pubDate>Sat, 18 May 2024 18:59:10 +0200</pubDate>
      <guid>https://kunat.dev/notes/keep-safar-window-open/</guid>
      <description>&lt;p&gt;&lt;em&gt;tldr: In order to keep the current Safari window open when you close the last tab, use tab groups.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Last month, I once again switched my default web browser to Safari. It has its quirks, but I missed it in a weird way.&lt;/p&gt;
&lt;p&gt;I initially switched to Firefox because of its stellar &lt;a href=&#34;https://addons.mozilla.org/en-US/firefox/addon/vimium-ff/&#34; target=&#34;_blank&#34; &gt;support for vim keybindings&lt;/a&gt; and some other extensions I started to enjoy. Even with all those improvements, I began to miss some niceties that Safari has built-in.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>tldr: In order to keep the current Safari window open when you close the last tab, use tab groups.</em></p>
<p>Last month, I once again switched my default web browser to Safari. It has its quirks, but I missed it in a weird way.</p>
<p>I initially switched to Firefox because of its stellar <a href="https://addons.mozilla.org/en-US/firefox/addon/vimium-ff/" target="_blank" >support for vim keybindings</a> and some other extensions I started to enjoy. Even with all those improvements, I began to miss some niceties that Safari has built-in.</p>
<blockquote>
<p>With my current setup, I still use Firefox for some websites. Some sites are just hard to use without uBlock Origin. None of Safari&rsquo;s content blockers match its performance.</p></blockquote>
<p>I learned to live with all of Safari&rsquo;s shortcomings, but there is one UX issue that keeps me up at night: <strong>The Safari window always gets closed when you close the last tab</strong> (<code>⌘ + w</code>). As of macOS 14, there seems to be no way to change this default behavior without introducing some other issues.</p>
<p>What irks me even more is that this behavior does not exist on iPadOS. On iPads, the <code>⌘ + w</code> shortcut closes the open window only if the current window has a single empty tab. In all other cases, the Safari window will remain open.</p>
<p>What I&rsquo;ve found recently is that this behavior is different for tab groups. When you close the last tab in a Safari tab group, the window will remain open. Since then, I created my &ldquo;main&rdquo; tab group:</p>
<p><img alt="Safari tab groups with the &ldquo;main&rdquo; tab group" loading="lazy" src="/notes/images/keep-safar-window-open.png"></p>
<blockquote>
<p>This solution has the nice benefit of keeping all your current tabs in sync across all your devices. I know that by default you can still access all your open tabs from any device, but it was always an extra hassle for me to go through the &ldquo;From &lt;device_name&gt;&rdquo; section.</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Open Any Xcode Project Type with a Shell Script</title>
      <link>https://kunat.dev/notes/shell-script-for-opening-xcode-projects/</link>
      <pubDate>Mon, 29 Apr 2024 19:19:18 +0200</pubDate>
      <guid>https://kunat.dev/notes/shell-script-for-opening-xcode-projects/</guid>
      <description>&lt;p&gt;In my current role, we have around 30 projects distributed across roughly 20 different repositories. Most of them are Swift packages for various features used in our apps.&lt;/p&gt;
&lt;p&gt;This setup requires me to frequently switch between multiple projects. Just a few days ago, I was working on the login flow. This involved modifying two Swift packages and one Xcode project.&lt;/p&gt;
&lt;p&gt;Remembering whether a given project is a Swift package, a workspace (&lt;code&gt;.xcworkspace&lt;/code&gt;), or a standard project (&lt;code&gt;.xcodeproj&lt;/code&gt;) is exhausting. This adds unnecessary complexity and slows down the workflow. It would be better if we had a single command to open all of them at once. Here&amp;rsquo;s a script that does just that:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In my current role, we have around 30 projects distributed across roughly 20 different repositories. Most of them are Swift packages for various features used in our apps.</p>
<p>This setup requires me to frequently switch between multiple projects. Just a few days ago, I was working on the login flow. This involved modifying two Swift packages and one Xcode project.</p>
<p>Remembering whether a given project is a Swift package, a workspace (<code>.xcworkspace</code>), or a standard project (<code>.xcodeproj</code>) is exhausting. This adds unnecessary complexity and slows down the workflow. It would be better if we had a single command to open all of them at once. Here&rsquo;s a script that does just 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-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># The osp (Open Swift project) function first attempts to open a Package.swift file in the current directory; if it doesn&#39;t exist, it then searches for and opens either a .xcworkspace or a .xcodeproj directory, or outputs an error message if none are found.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>osp<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check for Package.swift in the current directory</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;Package.swift&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;Package.swift&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</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 style="color:#75715e"># Look for .xcworkspace directories in the current directory</span>
</span></span><span style="display:flex;"><span>local workspace<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>find . -maxdepth <span style="color:#ae81ff">1</span> -name <span style="color:#e6db74">&#34;*.xcworkspace&#34;</span> -type d | head -n 1<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>$workspace<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;</span>$workspace<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</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 style="color:#75715e"># Look for .xcodeproj directories if no .xcworkspace directories were found</span>
</span></span><span style="display:flex;"><span>local project<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>find . -maxdepth <span style="color:#ae81ff">1</span> -name <span style="color:#e6db74">&#34;*.xcodeproj&#34;</span> -type d | head -n 1<span style="color:#66d9ef">)</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>$project<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;</span>$project<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</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 style="color:#75715e"># Output error message if neither file type was found</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;No project or workspace files found&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>To use it, simply append it to your <code>.zshrc</code> file and run <code>source ~/.zshrc</code> or restart the shell.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Working with Local Swift Packages</title>
      <link>https://kunat.dev/notes/swift-local-packages/</link>
      <pubDate>Tue, 27 Feb 2024 21:49:35 +0100</pubDate>
      <guid>https://kunat.dev/notes/swift-local-packages/</guid>
      <description>&lt;p&gt;Imagine you&amp;rsquo;re working on Project A, which relies on Package B, a remote Swift package. To incorporate modifications in B and assess their impact on A, the standard approach requires committing and pushing the changes in B, updating B&amp;rsquo;s version in A, and then rebuilding A to observe the outcomes. This method is slow and cumbersome. There must be a better way!&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Project A
  |
  └──&amp;gt; Swift Package B
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To speed up and improve the workflow, you can convert the remote dependency B to a local one. This method works whether Project A is a Swift package or an Xcode project (xcodeproj or xcworkspace).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Imagine you&rsquo;re working on Project A, which relies on Package B, a remote Swift package. To incorporate modifications in B and assess their impact on A, the standard approach requires committing and pushing the changes in B, updating B&rsquo;s version in A, and then rebuilding A to observe the outcomes. This method is slow and cumbersome. There must be a better way!</p>
<pre tabindex="0"><code>Project A
  |
  └──&gt; Swift Package B
</code></pre><p>To speed up and improve the workflow, you can convert the remote dependency B to a local one. This method works whether Project A is a Swift package or an Xcode project (xcodeproj or xcworkspace).</p>
<h2 id="xcode-project">Xcode Project</h2>
<p>In this case, Project A has an Xcode project file.</p>
<ol>
<li>Remove the remote dependency from your project.</li>
<li>Re-add it as a local package.</li>
</ol>
<p><img alt="hello there" loading="lazy" src="/notes/images/local_packages_1.png"></p>
<blockquote>
<p>This might seem excessive, as Xcode also supports just dragging and dropping the package into your project, but I’ve found this solution to be flaky. Explicitly removing it works every time, most of the time.</p></blockquote>
<h2 id="swift-package">Swift Package</h2>
<p>If Project A is a Swift package, you&rsquo;ll need to convert B&rsquo;s dependency declaration:</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-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(url: <span style="color:#e6db74">&#34;https://github.com/TinyCorp/B&#34;</span>, exact: <span style="color:#e6db74">&#34;0.52.3&#34;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...</span>
</span></span></code></pre></div><p>&hellip; into 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-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(path: <span style="color:#e6db74">&#34;path/to/B&#34;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...</span>
</span></span></code></pre></div><p>Example: given the following file structure, <code>path</code> is <code>../B</code>:</p>
<pre tabindex="0"><code>.
├── A
│   └── Package.swift (we&#39;re here)
└── B
    └── Package.swift
</code></pre><hr>
<p>With this setup, you’re now able to edit dependency B directly inside of A. I was genuinely surprised by how well Xcode handles this workflow.</p>
<h2 id="notes">Notes</h2>
<blockquote>
<p>Make sure the Swift package that you plan to add to your project is not opened in another Xcode window. In some cases, Xcode might fail with a cryptic error when trying to add the local package.</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Alfred Snippet Converter</title>
      <link>https://kunat.dev/notes/alfred-snippet-converter/</link>
      <pubDate>Wed, 27 Dec 2023 19:50:24 +0100</pubDate>
      <guid>https://kunat.dev/notes/alfred-snippet-converter/</guid>
      <description>&lt;p&gt;A few months ago, I wrote about &lt;a href=&#34;https://kunat.dev/notes/alfred-snippets-on-ios/&#34; &gt;Alfred Snippets on iOS&lt;/a&gt;. In that article, I described the manual process of converting Alfred text snippets into macOS text replacements.&lt;/p&gt;
&lt;p&gt;Since then, I&amp;rsquo;ve had time to refine this project and add some new features. You can download the final Command Line Interface (CLI) or the macOS app from this link.&lt;/p&gt;
&lt;p&gt;Next on my list is the complete automation of the process of synchronizing Alfred text snippets with macOS text replacements. I&amp;rsquo;m not sure if it&amp;rsquo;s even possible, but there&amp;rsquo;s only one way to find out. I&amp;rsquo;m keeping my fingers crossed that it will take me less time than it did before.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months ago, I wrote about <a href="/notes/alfred-snippets-on-ios/" >Alfred Snippets on iOS</a>. In that article, I described the manual process of converting Alfred text snippets into macOS text replacements.</p>
<p>Since then, I&rsquo;ve had time to refine this project and add some new features. You can download the final Command Line Interface (CLI) or the macOS app from this link.</p>
<p>Next on my list is the complete automation of the process of synchronizing Alfred text snippets with macOS text replacements. I&rsquo;m not sure if it&rsquo;s even possible, but there&rsquo;s only one way to find out. I&rsquo;m keeping my fingers crossed that it will take me less time than it did before.</p>
<p>For anyone interested, the tool is available <a href="https://github.com/bkunat/AlfredSnippetConverter" target="_blank" >here</a>!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Automating Adding SPM Plugins</title>
      <link>https://kunat.dev/notes/spm-plugin-update/</link>
      <pubDate>Mon, 18 Dec 2023 18:43:35 +0100</pubDate>
      <guid>https://kunat.dev/notes/spm-plugin-update/</guid>
      <description>&lt;p&gt;&lt;em&gt;How to automate adding plugins to all of your existing (and future) targets in a Swift package&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;TL;DR&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-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;package.targets = package.targets.map { target &lt;span style=&#34;color:#66d9ef&#34;&gt;in&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;var&lt;/span&gt; plugins = target.plugins ?? []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    plugins.append(.plugin(name: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SwiftLintPlugin&amp;#34;&lt;/span&gt;, package: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SwiftLint&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    target.plugins = plugins
&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;return&lt;/span&gt; target
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Last month, I was tasked with adding SwiftFormat and SwiftLint to one of the projects my team maintains. The project is medium-sized and uses Swift Package Manager (SPM) for modularization. It has a separate SPM package for each module. For example:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>How to automate adding plugins to all of your existing (and future) targets in a Swift package</em></p>
<p>TL;DR</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-swift" data-lang="swift"><span style="display:flex;"><span>package.targets = package.targets.map { target <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> plugins = target.plugins ?? []
</span></span><span style="display:flex;"><span>    plugins.append(.plugin(name: <span style="color:#e6db74">&#34;SwiftLintPlugin&#34;</span>, package: <span style="color:#e6db74">&#34;SwiftLint&#34;</span>))
</span></span><span style="display:flex;"><span>    target.plugins = plugins
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> target
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="the-problem">The Problem</h2>
<p>Last month, I was tasked with adding SwiftFormat and SwiftLint to one of the projects my team maintains. The project is medium-sized and uses Swift Package Manager (SPM) for modularization. It has a separate SPM package for each module. For example:</p>
<ul>
<li>FeatureModules // Directory
<ul>
<li>Login // Swift Package</li>
<li>MyAccount // Swift Package</li>
<li>Dashboard // Swift Package</li>
</ul>
</li>
<li>Core // Directory
<ul>
<li>DomainModels // Swift Package</li>
<li>Networking // Swift Package</li>
</ul>
</li>
</ul>
<p>Each module typically consists of three targets (e.g., Login, LoginTests, LoginTestUtils). The third target&rsquo;s sole purpose is to avoid duplicating testing doubles. You can read more about it <a href="/notes/duplicate-testing-doubles/" >here</a>.</p>
<p>Adding SwiftFormat to the main project and all SPM packages was pretty straightforward. I decided to integrate it at the CI level to ensure all new code changes are properly formatted. Locally, I left this choice up to the maintainers. They can use it through a post-commit git hook or as an Xcode Extension. Depending on your needs, both solutions work great.</p>
<p>Integrating SwiftLint into the base project was also a breeze. What became problematic was integrating it into all the Swift packages. As I mentioned before, we were using a Swift package for each module. As per the documentation:</p>
<blockquote>
<p>Due to limitations with Swift Package Manager plugins, this is only recommended for projects that have a SwiftLint configuration in their root directory, as there is currently no way to pass any additional options to the SwiftLint executable.</p></blockquote>
<p>This limitation would require us to duplicate our SwiftLint config for each package, making future updates to the SwiftLint config error-prone and time-consuming. The same goes for updating the plugin itself.</p>
<h2 id="looking-for-a-solution">Looking For a Solution</h2>
<p>I had two problems to solve:</p>
<ol>
<li>Automating the updating of the config file(s).</li>
<li>Automating the addition of the SwiftLint plugin to all new Swift packages in the future.</li>
</ol>
<p>The first problem is easily solvable with a simple bash script. It would still require writing documentation, but let’s say I’m okay with the trade-off.</p>
<p>The second problem got me looking at Sourcery. However, this solution seemed like overkill from the beginning. Introducing Sourcery to a project just because of SPM limitations seemed like a bad idea.</p>
<p>Back to square one!</p>
<h2 id="solution">Solution</h2>
<p>I started experimenting with the project structure. Instead of creating a new Swift package for each module, I decided to create a single package that worked as a container for all existing modules. Having all the modules defined in a single <code>Package.swift</code> file makes automating the SwiftLint plugin trivial.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/spm-plugin-update-update-project-structure.png"></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-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// Inject base plugins into each target</span>
</span></span><span style="display:flex;"><span>package.targets = package.targets.map { target <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> plugins = target.plugins ?? []
</span></span><span style="display:flex;"><span>    plugins.append(.plugin(name: <span style="color:#e6db74">&#34;SwiftLintPlugin&#34;</span>, package: <span style="color:#e6db74">&#34;SwiftLint&#34;</span>))
</span></span><span style="display:flex;"><span>    target.plugins = plugins
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> target
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This approach not only solved all of my problems but also required very little work. It involved merging all package definitions into a single one and moving some files around.</p>
<p>This solution won’t work for larger projects. Since my app had only a handful of modules, it worked wonderfully. If you’re working on a mid-sized project with tens of packages, you might consider grouping your modules into a handful of packages like FeatureModules, Models, etc.</p>
<p>I hope this issue will have been resolved long before we hit the wall due to the growing number of modules!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.swiftystack.com/" target="_blank" >Swifty Stack</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>My Alfred Setup</title>
      <link>https://kunat.dev/notes/my-alfred-setup/</link>
      <pubDate>Sun, 13 Aug 2023 19:53:05 +0200</pubDate>
      <guid>https://kunat.dev/notes/my-alfred-setup/</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 13/08/23&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I’ve relied on Alfred as my daily driver for boosting productivity for years. However, I didn’t switch from Apple’s Spotlight overnight. It took three attempts before I truly embraced its capabilities. In this article I’ll share my Alfred setup and explain my most frequent workflows. Let’s dive in!&lt;/p&gt;
&lt;h2 id=&#34;the-initial-setup&#34;&gt;The Initial Setup&lt;/h2&gt;
&lt;p&gt;I’ve already described the initial setup in detail in &lt;a href=&#34;https://kunat.dev/notes/new-mac-setup/#web&#34; &gt;New Mac Setup&lt;/a&gt; article, so be sure to check it if you’re interested.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>Last updated: 13/08/23</em></p>
<p>I’ve relied on Alfred as my daily driver for boosting productivity for years. However, I didn’t switch from Apple’s Spotlight overnight. It took three attempts before I truly embraced its capabilities. In this article I’ll share my Alfred setup and explain my most frequent workflows. Let’s dive in!</p>
<h2 id="the-initial-setup">The Initial Setup</h2>
<p>I’ve already described the initial setup in detail in <a href="/notes/new-mac-setup/#web" >New Mac Setup</a> article, so be sure to check it if you’re interested.</p>
<h2 id="snippets-efficiency-at-its-best">Snippets: Efficiency at Its Best</h2>
<p>One of my absolute favorite features of Alfred is Snippets. I rely on them extensively, whether it’s for a quick access to emojis, executing frequent commands, or crafting boilerplate messages. Let’s explore those use cases!</p>
<h3 id="emojis">Emojis</h3>
<p>While there are several popular workflows for emojis, I&rsquo;ve chosen to create snippets for the ones I use most frequently. It&rsquo;s a highly efficient approach, and for those less common emojis, we&rsquo;ll explore workflows later. Curious about using your snippets on mobile devices? Don&rsquo;t miss my guide on <del><a href="/notes/alfred-snippets-on-ios/" >Alfred Snippets on iOS</a></del>.</p>
<h3 id="commands">Commands</h3>
<p>I&rsquo;ve compiled a set of frequently used commands, primarily for directory navigation and running common scripts. Although these tasks could be handled with shell aliases, I prefer having everything neatly organized within Alfred. This setup has proven to be reliable and convenient. It just works.</p>
<h3 id="note-taking">Note Taking</h3>
<p>Markdown is my go-to format for note-taking, and I&rsquo;ve leveraged snippets to automate repetitive tasks. Here are some noteworthy examples:</p>
<ul>
<li><strong>!cb</strong> - Inserts a markdown code block.</li>
<li><strong>!scb</strong> - Inserts a markdown code block with Swift syntax highlighting.</li>
<li><strong>!ds</strong> - Generates a line separator followed by today&rsquo;s date, perfect for organizing notes.</li>
<li><strong>!nd</strong> - Initiates a new daily entry in my TODOs note.</li>
</ul>
<h3 id="and-more">And More</h3>
<p>Snippets also come in handy for quick message replies, composing emails, and inserting code snippets.</p>
<h2 id="workflows-unleash-productivity">Workflows: Unleash Productivity</h2>
<p>The most exhilarating aspect of Alfred is its workflows – a feature that can supercharge your productivity. For a deeper dive into workflows, you can explore them further <del><a href="https://www.alfredapp.com/workflows/" target="_blank" >here</a></del>.</p>
<h3 id="note-taking-1">Note Taking</h3>
<p>Community-built Alfred workflows have streamlined note-taking with popular apps. Personally, I rely on <del><a href="https://github.com/drgrib/alfred-bear" target="_blank" >alfred-bear</a></del> for Bear Notes (personal notes) and <del><a href="https://github.com/hauselin/obsidian-alfred" target="_blank" >obsidian-alfred</a></del> for Obsidian (work-related notes).</p>
<blockquote>
<p>Ideally, I&rsquo;d prefer keeping all my notes in a single place. However, Bear Notes restricts data to your Apple ID, making cross-account sharing a challenge. Obsidian, with its raw markdown files, easily accommodates iCloud folder sharing. Initially, I thought about migrating my personal notes to Obsidian, but the recent major release of <del><a href="https://blog.bear.app/2023/07/bear-2-is-here/" target="_blank" >Bear Notes</a></del> swayed my decision. With new features, it’s just too good of an app to quit using!</p></blockquote>
<p>Both workflows offer similar features (Obsidian one is a bit more complex). They both excel at accessing notes and creating new ones on the fly.</p>
<h3 id="launchers-ready-set-go">Launchers: Ready, Set, Go!</h3>
<p>Launchers are my custom workflows that I use to <em>launch</em> a set of apps for a particular activity:</p>
<ul>
<li>Development: Launches a suite of essential tools and utilities.</li>
<li>Writing: Opens a predefined set of notes and tools.</li>
<li>Investing: Initiates my brokerage account, bank account, and a useful Excel sheet for tracking investments.</li>
</ul>
<p>While I could manage without these, they reduce friction when switching between tasks, ensuring I&rsquo;m just a click away from my next endeavor.</p>
<p><img loading="lazy" src="/notes/images/my-alfred-setup-1.png">
<img loading="lazy" src="/notes/images/my-alfred-setup-2.png"></p>
<h3 id="emoji-beyond-snippets">Emoji: Beyond Snippets</h3>
<p>As previously mentioned, snippets handle the most common emojis, but for the rest, there&rsquo;s <del><a href="https://github.com/jsumners/alfred-emoji/releases" target="_blank" >this</a></del> workflow. It may be a tad slower than snippets, but it&rsquo;s significantly quicker than macOS&rsquo;s built-in emoji picker. Give it a try – you won&rsquo;t want to go back.</p>
<h3 id="shortcuts-effortless-automation">Shortcuts: Effortless Automation</h3>
<p>Certain tasks are best automated using macOS Shortcuts.app. There’s one problem: there’s no easy way to launch them without clicking through the app. However, <a href="https://www.alfredapp.com/blog/guides-and-tutorials/macos-shortcuts-with-alfred-workflow/" target="_blank" >this</a> shortcut allows you to initiate them directly from Alfred.</p>
<h3 id="more-in-store">More in Store</h3>
<p>Explore these miscellaneous workflows for even more functionality:</p>
<ul>
<li><a href="https://github.com/dotson/iTunes-Airplay-Toggle" target="_blank" >AirPlay Toggle</a> - utility for switching between airplay devices</li>
<li><a href="https://github.com/vitorgalvao/link-clean-workflow" target="_blank" >LinkClean</a> - tool for removing extraneous information from URLs</li>
<li><a href="https://github.com/alfredapp/system-settings-workflow/" target="_blank" >System Settings</a> - utility for opening macOS system settings panes</li>
</ul>
<h2 id="wrap-up">Wrap Up</h2>
<p>That concludes my Alfred setup – a straightforward yet highly efficient automation of my daily tasks (at least the most repetitive ones). While there are a few improvements I&rsquo;m considering, the core concept will remain unchanged.</p>
]]></content:encoded>
    </item>
    <item>
      <title>New Mac Setup</title>
      <link>https://kunat.dev/notes/new-mac-setup/</link>
      <pubDate>Mon, 15 May 2023 20:16:21 +0200</pubDate>
      <guid>https://kunat.dev/notes/new-mac-setup/</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 20/05/25&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This note describes the steps I take when setting up a new Mac. This process could be mostly automated, but I can’t get myself to do it. It takes me around 90 minutes to go through all the steps. Let’s get started!&lt;/p&gt;
&lt;h2 id=&#34;i-prerequisite-files-to-import&#34;&gt;I. Prerequisite Files to Import&lt;/h2&gt;
&lt;p&gt;Before diving into installations, I make sure to have these configuration files ready:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitconfig&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.zshrc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;ii-software-installation&#34;&gt;II. Software Installation&lt;/h2&gt;
&lt;p&gt;Apps marked with “🌕” emoji are optional (it depends if it’s a work or a private Mac).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>Last updated: 20/05/25</em></p>
<p>This note describes the steps I take when setting up a new Mac. This process could be mostly automated, but I can’t get myself to do it. It takes me around 90 minutes to go through all the steps. Let’s get started!</p>
<h2 id="i-prerequisite-files-to-import">I. Prerequisite Files to Import</h2>
<p>Before diving into installations, I make sure to have these configuration files ready:</p>
<ul>
<li><code>.gitconfig</code></li>
<li><code>.zshrc</code></li>
</ul>
<h2 id="ii-software-installation">II. Software Installation</h2>
<p>Apps marked with “🌕” emoji are optional (it depends if it’s a work or a private Mac).</p>
<h3 id="a-homebrew--command-line-tools">A. Homebrew &amp; Command-Line Tools</h3>
<ol>
<li><strong>Install <a href="https://brew.sh/" target="_blank" >HomeBrew</a></strong> (from Web)</li>
<li><strong>Install CLI tools via Brew:</strong></li>
</ol>
<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>// Set shell to zsh
</span></span><span style="display:flex;"><span>chsh -s /bin/zsh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>brew install zsh-autosuggestions
</span></span><span style="display:flex;"><span>brew install zsh-syntax-highlighting
</span></span><span style="display:flex;"><span>brew install pure
</span></span><span style="display:flex;"><span>brew install jesseduffield/lazygit/lazygit
</span></span><span style="display:flex;"><span>brew install xcodesorg/made/xcodes
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>// Optional
</span></span><span style="display:flex;"><span>brew install rbenv
</span></span><span style="display:flex;"><span>brew install swiftlint
</span></span><span style="display:flex;"><span>brew install node
</span></span></code></pre></div><h3 id="b-app-store-applications">B. App Store Applications</h3>
<ul>
<li><a href="https://harshil.net/pause" target="_blank" >Pause</a></li>
<li><a href="https://apps.apple.com/pl/app/dynamo/id1445910651?mt=12" target="_blank" >Dynamo</a></li>
<li><a href="https://apps.apple.com/pl/app/pixelmator-pro/id1289583905?mt=12" target="_blank" >Pixelmator Pro</a></li>
</ul>
<h3 id="c-gui-applications-from-web">C. GUI Applications (from Web)</h3>
<ul>
<li><a href="https://www.alfredapp.com" target="_blank" >Alfred</a></li>
<li><a href="https://rectangleapp.com/" target="_blank" >Rectangle</a></li>
<li><a href="https://www.hammerspoon.org/" target="_blank" >Hammerspoon</a></li>
<li><a href="https://karabiner-elements.pqrs.org/" target="_blank" >Karabiner-Elements</a></li>
<li><a href="https://ghostty.org/" target="_blank" >Ghostty</a></li>
<li><a href="https://obsidian.md/download" target="_blank" >Obsidian</a></li>
<li><a href="https://toggl.com/track/time-tracking-mac/" target="_blank" >Toggl</a></li>
<li><a href="https://icemenubar.app/" target="_blank" >Ice</a></li>
<li><a href="https://developer.apple.com/download/all/" target="_blank" >Xcode</a></li>
<li><a href="https://code.visualstudio.com/download" target="_blank" >Visual Studio Code</a></li>
<li><a href="https://freemacsoft.net/appcleaner/" target="_blank" >AppCleaner</a></li>
<li><a href="https://www.logitech.com/en-us/software/logi-options-plus.html" target="_blank" >Logi Options+</a></li>
<li><a href="https://github.com/MonitorControl/MonitorControl" target="_blank" >Monitor Control</a></li>
<li><a href="https://pdfexpert.com/blog/meet-pdf-expert-2-the-best-pdf-editor-for-your-mac" target="_blank" >PDF Expert</a></li>
<li><a href="https://proxyman.com/" target="_blank" >ProxyMan</a></li>
<li><a href="https://boltai.com/" target="_blank" >BoltAI</a></li>
<li>🌕 <a href="https://www.omnigroup.com/more" target="_blank" >OmniDiskSweeper</a></li>
<li>🌕 <a href="https://monodraw.helftone.com" target="_blank" >Monodraw</a></li>
</ul>
<h2 id="iii-application-configuration">III. Application Configuration</h2>
<p>Now, let&rsquo;s configure the installed applications.</p>
<ul>
<li><strong>Alfred</strong>:
<ul>
<li>Disable both shortcuts for Spotlight (Keyboard -&gt; Keyboard Shortcuts &hellip; -&gt; Spotlight -&gt; disable both)</li>
<li>Import preferences from <code>/Users/$USER/Library/Application Support/Alfred/Alfred.alfredpreferences</code></li>
<li>Set global shortcut (⌘+Space)</li>
<li>Enable Clipboard History</li>
<li>Enable: Features -&gt; Snippets “Automatically expand snippets by keyword”</li>
<li>Enable: Features -&gt; Web Bookmarks -&gt; Safari Bookmarks</li>
</ul>
</li>
<li><strong>Rectangle</strong>:
<ul>
<li>Import config</li>
</ul>
</li>
<li><strong>Hammerspoon</strong>:
<ul>
<li>Import config</li>
<li>Install <a href="https://www.hammerspoon.org/Spoons/AppLauncher.html" target="_blank" >AppLauncher</a></li>
</ul>
</li>
<li><strong>Karabiner-Elements</strong>:
<ul>
<li>Import config</li>
</ul>
</li>
<li><strong>Ghostty</strong>:
<ul>
<li>Import config</li>
</ul>
</li>
<li><strong>Obsidian</strong>:
<ul>
<li>In order to make <code>git</code> plugin work:
<ul>
<li><code>git config --global credential.helper osxkeychain</code></li>
<li><code>git pull</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Visual Studio Code</strong>:
<ul>
<li>Log in to sync plugins and preferences</li>
<li>Fix holding motion keys not working with <code>vim</code> plugin:
<ul>
<li><code>defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Logi Options+</strong>:
<ul>
<li>Restore from backup</li>
</ul>
</li>
<li><strong>ProxyMan</strong>:
<ul>
<li>Import mapped requests</li>
</ul>
</li>
</ul>
<h2 id="iv-system-preferences-settings">IV. System <del>Preferences</del> Settings</h2>
<p>Let&rsquo;s tweak the system settings.</p>
<ul>
<li>Set desktop background</li>
</ul>
<h3 id="general">General</h3>
<ul>
<li>Set accent color</li>
</ul>
<h3 id="language--region">Language &amp; Region</h3>
<ul>
<li>Set English (United States) as primary language</li>
</ul>
<blockquote>
<p>I simply can’t stand Trash being called Bin.</p></blockquote>
<h3 id="displays">Displays</h3>
<ul>
<li>Adjust display resolution</li>
</ul>
<h3 id="desktop--dock">Desktop &amp; Dock</h3>
<ul>
<li>Desktop &amp; Dock -&gt; Desktop &amp; Stage Manager -&gt; Click wallpaper to reveal desktop -&gt; <code>Only in Stage Manager</code></li>
</ul>
<h3 id="keyboard">Keyboard</h3>
<ul>
<li>Keyboard -&gt; Keyboard Navigation -&gt; <code>Enable</code></li>
</ul>
<blockquote>
<p>This setting enables option selection with the Tab key.</p></blockquote>
<ul>
<li>Keyboard -&gt; Keyboard Shortcuts -&gt; Modifier Keys -&gt; Map Caps Lock to Escape</li>
</ul>
<h3 id="trackpad">Trackpad</h3>
<ul>
<li>Trackpad -&gt; Point &amp; Click -&gt; Tap to click</li>
<li>Trackpad -&gt; Point &amp; Click -&gt; Tracking Speed (increase)</li>
</ul>
<h3 id="sharing">Sharing</h3>
<ul>
<li>Change machine&rsquo;s name</li>
</ul>
<h3 id="login-password">Login Password</h3>
<ul>
<li>Enable “Use Apple Watch to unlock your applications and your Mac”</li>
</ul>
<h3 id="security--privacy">Security &amp; Privacy</h3>
<ul>
<li>Disable “Password Autofill”</li>
<li>Add fingers to Touch ID</li>
</ul>
<h3 id="general-system-settings-panel">General (System Settings Panel)</h3>
<ul>
<li>Set Login Items</li>
</ul>
<h2 id="v-core-macos-application--utility-configuration">V. Core macOS Application &amp; Utility Configuration</h2>
<h3 id="archive-utility">Archive Utility</h3>
<ul>
<li>Preferences -&gt; After expanding: move to trash</li>
</ul>
<h3 id="messages">Messages</h3>
<ul>
<li>Enable message forwarding on iPhone</li>
<li>Enable iCloud sync</li>
</ul>
<h3 id="dock">Dock</h3>
<ul>
<li>Move to right</li>
<li>Adjust zoom and size</li>
<li>Rearrange apps</li>
<li>Enable “Show Percentage” (Battery settings, if referring to menu bar)</li>
</ul>
<h3 id="mail">Mail</h3>
<ul>
<li>Take all accounts online</li>
<li>Rename and rearrange</li>
</ul>
<h3 id="finder">Finder</h3>
<ul>
<li>Rearrange sidebar</li>
<li>Set “Search the current folder”</li>
<li>“Show path bar” (it’s in the menu bar)</li>
<li>Enable “Snap to grid” (right click on empty Desktop, Show View Options -&gt; Sort By -&gt; Snap to Grid)</li>
<li>Disable “Show warning before removing from iCloud Drive”</li>
<li>Set Preferences -&gt; General -&gt; New Finder windows show … -&gt; Desktop</li>
</ul>
<h3 id="time-machine">Time Machine</h3>
<ul>
<li>Create backup</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>That&rsquo;s it. That&rsquo;s the entire setup process. I know there are tools to automate ~90% of it. I plan to do it someday. In the meantime, my next project is to automate syncing things like Alfred preferences and system settings across all of my Macs. Fingers crossed, I&rsquo;ll sit down and do it this summer!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Connectivity Issues with Logitech MX Keys on macOS Ventura</title>
      <link>https://kunat.dev/notes/mx-keys-ventura/</link>
      <pubDate>Tue, 04 Apr 2023 12:49:12 +0200</pubDate>
      <guid>https://kunat.dev/notes/mx-keys-ventura/</guid>
      <description>&lt;p&gt;After installing macOS Ventura, I began experiencing connectivity issues when using MX Keys keyboard through Bluetooth. The keyboard would disconnect from my Mac whenever it was inactive for a few minutes.&lt;/p&gt;
&lt;p&gt;What made this issue especially frustrating was that it didn&amp;rsquo;t happen every time, and the time it took for the keyboard to reconnect varied widely. It drove me insane.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been following several threads in &lt;a href=&#34;https://www.reddit.com/r/logitech/&#34; target=&#34;_blank&#34; &gt;r/logitech&lt;/a&gt;, reaching out to Logitech support multiple times, and submitting bug reports through the Logi Options+ app. However, none of these attempts at resolution have been successful.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After installing macOS Ventura, I began experiencing connectivity issues when using MX Keys keyboard through Bluetooth. The keyboard would disconnect from my Mac whenever it was inactive for a few minutes.</p>
<p>What made this issue especially frustrating was that it didn&rsquo;t happen every time, and the time it took for the keyboard to reconnect varied widely. It drove me insane.</p>
<p>I&rsquo;ve been following several threads in <a href="https://www.reddit.com/r/logitech/" target="_blank" >r/logitech</a>, reaching out to Logitech support multiple times, and submitting bug reports through the Logi Options+ app. However, none of these attempts at resolution have been successful.</p>
<p>The solution? Purchase the Logi Bolt USB Receiver. It&rsquo;s disappointing that there are no other options available, and it&rsquo;s frustrating that Logitech is ignoring the complaints. But unfortunately, that&rsquo;s the way it is.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Alfred Snippets on iOS</title>
      <link>https://kunat.dev/notes/alfred-snippets-on-ios/</link>
      <pubDate>Wed, 25 Jan 2023 23:59:59 +0100</pubDate>
      <guid>https://kunat.dev/notes/alfred-snippets-on-ios/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://www.alfredapp.com&#34; target=&#34;_blank&#34; &gt;Alfred&lt;/a&gt; allows you to save your frequently used text clips as Snippets. Over the past few years I’ve created hundreds of them.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;hello there&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/alfred-snippets.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;There’s one problem. Alfred snippets only work on your Mac. Today we’ll focus on how we can convert them into macOS snippets. That way, you’ll be able to use them across all your Apple devices!&lt;/p&gt;
&lt;h2 id=&#34;how&#34;&gt;How&lt;/h2&gt;
&lt;p&gt;Alfred stores snippets as a Zip archive. On macOS, you can open it with the Archive Utility app that’s shipped with the system. After successfully exporting it, you’ll need to convert it from Alfred’s data format to the one used by the system snippets. macOS allows to import keyboard snippets in bulk with Property List format files (&lt;code&gt;plist&lt;/code&gt;).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://www.alfredapp.com" target="_blank" >Alfred</a> allows you to save your frequently used text clips as Snippets. Over the past few years I’ve created hundreds of them.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/alfred-snippets.png"></p>
<p>There’s one problem. Alfred snippets only work on your Mac. Today we’ll focus on how we can convert them into macOS snippets. That way, you’ll be able to use them across all your Apple devices!</p>
<h2 id="how">How</h2>
<p>Alfred stores snippets as a Zip archive. On macOS, you can open it with the Archive Utility app that’s shipped with the system. After successfully exporting it, you’ll need to convert it from Alfred’s data format to the one used by the system snippets. macOS allows to import keyboard snippets in bulk with Property List format files (<code>plist</code>).</p>
<p>Here’s how Alfred stores snippets:</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;alfredsnippet&#34;</span> : {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;snippet&#34;</span> : <span style="color:#e6db74">&#34;🐛&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;uid&#34;</span> : <span style="color:#e6db74">&#34;81F59CDA-FD1E-4D71-AB4C-E4C47212023B&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;name&#34;</span> : <span style="color:#e6db74">&#34;bug&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;keyword&#34;</span> : <span style="color:#e6db74">&#34;bug&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>… and here’s how macOS stores snippets:</p>
<pre tabindex="0"><code class="language-plist" data-lang="plist">&lt;dict&gt;
	&lt;key&gt;phrase&lt;/key&gt;
	&lt;string&gt;🐛&lt;/string&gt;
	&lt;key&gt;shortcut&lt;/key&gt;
	&lt;string&gt;:bug&lt;/string&gt;
&lt;/dict&gt;
</code></pre><h3 id="steps">Steps</h3>
<ol>
<li>Export a snippet collection from Alfred
<ul>
<li>Alfred -&gt; Features -&gt; Snippets -&gt; Collection -&gt; <em>right-click</em> -&gt; Export</li>
</ul>
</li>
</ol>
<blockquote>
<p>macOS does not allow us to group snippets into collections</p></blockquote>
<ol start="2">
<li>Open the exported file using the Archive Utility app</li>
</ol>
<ul>
<li><em>right-click</em> -&gt; Open With -&gt; Archive Utility</li>
</ul>
<ol start="3">
<li>Convert files from Alfred’s format (<code>json</code>) to macOS format (<code>plist</code>)</li>
</ol>
<blockquote>
<p>For this step I use a script written in Swif. You can download it here: <a href="https://github.com/bkunat/snippet-converter" target="_blank" >bkunat/snippet-converter</a>. Should you decide to use it, please follow instructions in the <code>main.swift</code> file.</p></blockquote>
<ol start="4">
<li>Import the snippets into <code>/System/Library/PreferencePanes/Keyboard.prefPane</code></li>
</ol>
<ul>
<li>System Settings -&gt; Keyboard -&gt; Text Replacements -&gt; <em>drag&amp;drop the created <code>plist</code> file</em></li>
</ul>
<h2 id="notes">Notes</h2>
<ul>
<li>All devices need to be signed into a single iCloud account. Otherwise, macOS won’t sync your snippets.</li>
<li>macOS 13.2</li>
<li>Alfred 5.0.6</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
