<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Im Woojin</title>
    <description>The latest articles on DEV Community by Im Woojin (@rakkunn).</description>
    <link>https://dev.to/rakkunn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3860147%2F8f75007d-4931-48ca-881c-915ce86430f2.jpeg</url>
      <title>DEV Community: Im Woojin</title>
      <link>https://dev.to/rakkunn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rakkunn"/>
    <language>en</language>
    <item>
      <title>Migrating from LocalStorage to Native FS in Tauri: Repo-rter v0.4.1 Technical Deep Dive</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Wed, 15 Jul 2026 02:34:25 +0000</pubDate>
      <link>https://dev.to/rakkunn/migrating-from-localstorage-to-native-fs-in-tauri-repo-rter-v041-technical-deep-dive-5hja</link>
      <guid>https://dev.to/rakkunn/migrating-from-localstorage-to-native-fs-in-tauri-repo-rter-v041-technical-deep-dive-5hja</guid>
      <description>&lt;p&gt;A few days ago, I introduced &lt;strong&gt;Repo-rter&lt;/strong&gt;—a local-first desktop app built with Next.js and Tauri v2 to bypass GitHub’s annoying 14-day traffic history limit.&lt;/p&gt;

&lt;p&gt;While the launch was successful, we quickly hit some critical architectural bottlenecks in production. In our latest &lt;strong&gt;v0.4.1&lt;/strong&gt; release, we completely refactored the data layer. &lt;/p&gt;

&lt;p&gt;Here is a technical breakdown of how we migrated from WebView &lt;code&gt;localStorage&lt;/code&gt; to a native file-system cache, bypassed API rate limits, and implemented automatic data purging.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛑 The Problem: Why &lt;code&gt;localStorage&lt;/code&gt; is Not a Database
&lt;/h2&gt;

&lt;p&gt;In our initial release, we relied on the browser-level &lt;code&gt;localStorage&lt;/code&gt; inside the WebView to store merged historical traffic logs. This was a bad engineering decision for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OS Cache Purging&lt;/strong&gt;: WebView storage is tied to browser caches. On macOS and Windows, if the system disk runs low, the OS daemon (&lt;code&gt;cache_delete&lt;/code&gt;) can automatically purge WebView folders without warning. Cache-cleaning utilities (like CCleaner or CleanMyMac) also wipe these out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 5MB Limit&lt;/strong&gt;: Standard &lt;code&gt;localStorage&lt;/code&gt; has a hard limit of 5MB. If a user tracked 20+ repositories over a year, the JSON string would hit the quota limit, throwing &lt;code&gt;QuotaExceededError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volatile Backups&lt;/strong&gt;: We provided JSON exports, but without a native filesystem reader/writer, restoring those backups across different devices was painful.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛠️ The Solution: Tauri Rust-backed Native File Storage
&lt;/h2&gt;

&lt;p&gt;To solve this, we bypassed the WebView entirely for data persistence and wrote a custom Rust file cache. &lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Rust Backend Commands
&lt;/h3&gt;

&lt;p&gt;We implemented two native Tauri commands in Rust to read and write repository statistics directly to the OS Application Data directory (which is safe from OS cache sweepers):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src-tauri/src/lib.rs&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;save_traffic_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.app_data_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create_dir_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Escape slashes to make safe filenames (e.g., owner/repo -&amp;gt; owner__repo)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;safe_repo_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo_key&lt;/span&gt;&lt;span class="nf"&gt;.replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"__"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"traffic_{}_{}.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe_repo_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Asynchronous Frontend Caching Layer
&lt;/h3&gt;

&lt;p&gt;We refactored &lt;code&gt;mergeTrafficData&lt;/code&gt; in TypeScript to be asynchronous. When running inside Tauri, it invokes the Rust backend to read/write JSON files. For web browser development environments, it gracefully falls back to &lt;code&gt;localStorage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/storage.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isTauri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tauri-apps/api/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeTrafficData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;views&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clones&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;incomingData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrafficDataPoint&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;storedData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrafficDataPoint&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runningInTauri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isTauri&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runningInTauri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storedStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load_traffic_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;repoKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;storedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;storedStr&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storedStr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Fallback&lt;/span&gt;
      &lt;span class="nx"&gt;storedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`gittraffic_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repoKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// O(1) merge logic using a Map...&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runningInTauri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save_traffic_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;repoKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mergedData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mergedData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔄 Bypassing GitHub's API Rate Limits: Rotational Syncing
&lt;/h2&gt;

&lt;p&gt;GitHub fine-grained PATs are limited to &lt;strong&gt;5,000 requests per hour&lt;/strong&gt;. In our initial background sync setup, we synced the top 10 repositories. But for power users with 50+ repositories, the remaining 40 repos would eventually fall off the 14-day cliff because syncing everything at once risked hitting rate limits.&lt;/p&gt;

&lt;p&gt;To solve this, we implemented a &lt;strong&gt;sliding-window rotational queue&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/hooks/useBackgroundSync.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BATCH_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastSyncedIndexStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background_sync_last_index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastSyncedIndexStr&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastSyncedIndexStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Sort consistently so rotation mappings remain stable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortedRepos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stargazers_count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stargazers_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;BATCH_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sortedRepos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchRepos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sortedRepos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Sync only this batch...&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batchRepos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRepoTrafficViews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRepoTrafficClones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Save next offset index&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextStartIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;sortedRepos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background_sync_last_index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextStartIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple shift ensures that &lt;strong&gt;100% of a user's repositories are eventually backed up&lt;/strong&gt;, rotating through the list silently in the background without trigger limits.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧹 Zero-Dependency Data Purging in Rust
&lt;/h2&gt;

&lt;p&gt;We also added a &lt;strong&gt;Custom Data Retention Policy&lt;/strong&gt; (&lt;code&gt;Keep Forever&lt;/code&gt;, &lt;code&gt;30 Days&lt;/code&gt;, &lt;code&gt;90 Days&lt;/code&gt;, etc.). &lt;/p&gt;

&lt;p&gt;To keep the Rust binary lean, we wanted to avoid importing heavy dates/time crates like &lt;code&gt;chrono&lt;/code&gt;. Because ISO-8601 strings (&lt;code&gt;YYYY-MM-DDTHH:MM:SSZ&lt;/code&gt;) sort lexicographically, we wrote a zero-dependency date filter in Rust using simple string comparisons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src-tauri/src/lib.rs&lt;/span&gt;
&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="nf"&gt;.retain&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// String comparison is fully compatible with ISO-8601 sorting!&lt;/span&gt;
        &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold_str&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We calculate the ISO threshold string in JavaScript, pass it to Rust, and let Rust do the heavy lifting of reading, filtering, and rewriting the files.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏷️ Dyn-Binding App Versioning
&lt;/h2&gt;

&lt;p&gt;We also got rid of hardcoded version numbers in the UI. By enabling &lt;code&gt;resolveJsonModule: true&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt;, we dynamically import the version directly from our Node configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;packageInfo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// In UI rendering&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;packageInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, version numbers in our About dialog, Node environment, and Tauri configs are always perfectly in sync.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Conclusion &amp;amp; v0.4.1 Release
&lt;/h2&gt;

&lt;p&gt;Tauri v2 combined with React Query makes building robust, local-first applications incredibly fun. Moving away from &lt;code&gt;localStorage&lt;/code&gt; to native Rust file I/O has made Repo-rter significantly faster, immune to OS-level cache wipes, and ready for long-term historical logging.&lt;/p&gt;

&lt;p&gt;You can check out the source code, see the Tauri setup, or download the latest release (v0.4.1) here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/RAKKUNN/Repo-rter" rel="noopener noreferrer"&gt;Repo-rter GitHub Repository&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts on this architecture! How do you handle sync and local storage in your desktop apps? Let me know in the comments! 💻👇&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I got tired of GitHub deleting my traffic stats after 14 days, so I built a local-first alternative 🚀</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:56:07 +0000</pubDate>
      <link>https://dev.to/rakkunn/i-got-tired-of-github-deleting-my-traffic-stats-after-14-days-so-i-built-a-local-first-alternative-307h</link>
      <guid>https://dev.to/rakkunn/i-got-tired-of-github-deleting-my-traffic-stats-after-14-days-so-i-built-a-local-first-alternative-307h</guid>
      <description>&lt;p&gt;Hey DEV community! 👋&lt;/p&gt;

&lt;p&gt;If you maintain open-source projects on GitHub, you probably love checking your repository's "Insights" tab. Seeing people clone, view, and star your project is an amazing feeling.&lt;/p&gt;

&lt;p&gt;But there are two catches that have always frustrated me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Tedious Click-Fest: To see how your projects are doing, you have to manually open GitHub in your browser, navigate to each repository individually, click "Insights", and then click "Traffic". If you maintain 5+ repos, this becomes a chore real quick.&lt;/li&gt;
&lt;li&gt;The 14-Day Limit: Even worse, GitHub only keeps your traffic data for exactly 14 days.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you don't check your stats within that window, that data is gone forever. If you want a unified view and historical data, you either have to manually scrape it yourself, write a cron job, or pay a monthly subscription for a third-party SaaS tool.&lt;/p&gt;

&lt;p&gt;I didn't want to do any of those. So, I built my own solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌟 Enter: Repo-rter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rakkunn.github.io/Repo-rter/" rel="noopener noreferrer"&gt;Repo-rter&lt;/a&gt; is a completely free, 100% open-source desktop application available for Windows, macOS, and Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmtth4sqisi7aevjqoma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmtth4sqisi7aevjqoma.png" alt=" " width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It fetches your GitHub traffic data and caches it locally on your machine, meaning you never lose your historical stats again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TIP&lt;br&gt;
Privacy First: Unlike SaaS alternatives, Repo-rter doesn't store your Personal Access Token (PAT) on any server. Everything runs locally on your machine, so your data remains strictly yours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ✨ Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Infinite History: Automatically merges new traffic data with your local cache. Say goodbye to the 14-day limit!&lt;/li&gt;
&lt;li&gt;Release Downloads Tracker: Wondering how many people downloaded your .exe or .dmg? Repo-rter tracks total and individual asset downloads across all your releases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3kouvllgslpzc13zhei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3kouvllgslpzc13zhei.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neo-Brutalist UI: I wanted the app to be fun to use, so it features a vibrant, gamified Neo-Brutalist design.&lt;/li&gt;
&lt;li&gt;Export to Markdown: Need to show off your stats? Generate and download a beautiful Markdown report of your repo's health and traffic with one click.&lt;/li&gt;
&lt;li&gt;Cross-Platform: Built with Tauri, it's incredibly lightweight and runs natively on Windows, macOS (Apple Silicon &amp;amp; Intel), and Linux.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛠️ The Tech Stack
&lt;/h2&gt;

&lt;p&gt;I wanted to build something fast, lightweight, and modern. Here is what powers Repo-rter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Framework: Next.js (App Router) + React&lt;/li&gt;
&lt;li&gt;Desktop Runtime: Tauri v2&lt;/li&gt;
&lt;li&gt;Styling: TailwindCSS (with custom Neo-Brutalist utility classes)&lt;/li&gt;
&lt;li&gt;Data Fetching: React Query&lt;/li&gt;
&lt;li&gt;Charts: Recharts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using &lt;strong&gt;Tauri&lt;/strong&gt; instead of Electron was a game-changer. The app bundle size is incredibly small (around ~15MB), and it uses the native OS webview (WebKit on Mac, WebView2 on Windows), making memory consumption practically non-existent compared to heavy Electron apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Try it out!
&lt;/h2&gt;

&lt;p&gt;You can grab the latest version (v0.3.0) for your OS directly from the Releases page.&lt;/p&gt;

&lt;p&gt;Setup takes exactly 30 seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download and open the app.&lt;/li&gt;
&lt;li&gt;Generate a fine-grained GitHub PAT (Read-only access to Metadata &amp;amp; Administration).&lt;/li&gt;
&lt;li&gt;Paste it in, and watch your dashboard light up!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Let me know what you think! 👇
&lt;/h3&gt;

&lt;p&gt;I built this tool to solve my own itch, but I really hope it helps other open-source maintainers keep track of their hard work.&lt;/p&gt;

&lt;p&gt;I would absolutely love your feedback. What features should I add next? If you find it useful, a Star ⭐️ on GitHub would make my day!&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://rakkunn.github.io/Repo-rter/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;rakkunn.github.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Happy coding! 💻✨&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>github</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I made my Markdown Editor "AI-Ready": MarkSmith v0.3.0</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Thu, 28 May 2026 18:51:39 +0000</pubDate>
      <link>https://dev.to/rakkunn/i-made-my-markdown-editor-ai-ready-marksmith-v030-2pdb</link>
      <guid>https://dev.to/rakkunn/i-made-my-markdown-editor-ai-ready-marksmith-v030-2pdb</guid>
      <description>&lt;p&gt;Hey DEV community! 👋 &lt;/p&gt;

&lt;p&gt;A few days ago, I built a VS Code extension called &lt;strong&gt;Marksmith&lt;/strong&gt; to fix the most annoying parts of writing Markdown (like pasting Excel tables and syncing preview scrolls). &lt;/p&gt;

&lt;p&gt;But recently, I noticed a huge shift in my own workflow: &lt;strong&gt;Half the Markdown I write isn't for humans anymore.&lt;/strong&gt; It’s being fed directly into Claude, ChatGPT, or Gemini as prompts and context.&lt;/p&gt;

&lt;p&gt;When you're constantly stuffing docs into context windows, two things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You worry about hitting context limits (or racking up API costs).&lt;/li&gt;
&lt;li&gt;You waste time dealing with AI "hallucinations" when you ask it to generate docs back for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, for the &lt;strong&gt;v0.3.0 release&lt;/strong&gt;, &lt;br&gt;
I decided to pivot Marksmith into something new: &lt;strong&gt;An Agent AI-Ready Markdown Toolkit.&lt;/strong&gt; 🚀&lt;/p&gt;

&lt;p&gt;Here is what I added to survive the AI era:&lt;/p&gt;




&lt;h3&gt;
  
  
  📊 1. Real-time LLM Token Estimator
&lt;/h3&gt;

&lt;p&gt;Instead of just counting words, Marksmith’s &lt;em&gt;Document X-Ray&lt;/em&gt; sidebar now includes a &lt;strong&gt;Heuristic Token Estimator&lt;/strong&gt; for GPT, Claude, and Gemini. &lt;/p&gt;

&lt;p&gt;Before you copy-paste that massive README into your AI assistant, you can see exactly how "heavy" it is in terms of tokens right inside your editor. No more guessing if you're about to blow past your context limit!&lt;/p&gt;

&lt;h3&gt;
  
  
  ✂️ 2. Copy Optimized for AI (1-Click Minify)
&lt;/h3&gt;

&lt;p&gt;Formatting is great for humans, but LLMs don't need all those extra spaces, perfectly aligned markdown tables, or empty lines. &lt;/p&gt;

&lt;p&gt;I added a &lt;code&gt;CodeLens&lt;/code&gt; button at the top of your files. Click it, and Marksmith instantly &lt;strong&gt;minifies&lt;/strong&gt; your Markdown (compresses tables, strips blanks) and copies it to your clipboard. &lt;br&gt;
&lt;em&gt;Result:&lt;/em&gt; You save significant tokens and API costs without ruining your beautiful local &lt;code&gt;.md&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  🕵️ 3. Hallucination Quick Fix
&lt;/h3&gt;

&lt;p&gt;Ever ask an AI to write documentation, and it leaves behind a bunch of &lt;code&gt;[TODO: Insert link here]&lt;/code&gt; or makes up a fake local image path? &lt;/p&gt;

&lt;p&gt;Marksmith now automatically scans your document and puts a red squiggly line under &lt;strong&gt;AI placeholders and broken local links&lt;/strong&gt;. Click the 💡 icon, and you can instantly strip them out or fix them. It acts as a safety net before you commit AI-generated docs.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛡️ 4. Security Hardening (SSRF / RCE blocked)
&lt;/h3&gt;

&lt;p&gt;Wait, security in a Markdown extension? Yes! &lt;br&gt;
If you are previewing AI-generated Markdown, you are essentially rendering untrusted input. Maliciously crafted Markdown can trigger SSRF (Server-Side Request Forgery) by trying to fetch internal network metadata, or even execute code during PDF exports. &lt;/p&gt;

&lt;p&gt;I completely locked down the extension. It uses strict &lt;code&gt;DOMPurify&lt;/code&gt; sandboxing and actively blocks attempts to ping internal/cloud IPs.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎨 Bonus: A 90s Retro Redesign
&lt;/h3&gt;

&lt;p&gt;Because coding should be fun, I spent the weekend completely rebuilding the landing page using a &lt;strong&gt;Mac OS Classic / Neo-Brutalism&lt;/strong&gt; theme. Let me know what you think of the vibes! 💾&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Open VSX / Cursor Support
&lt;/h3&gt;

&lt;p&gt;For all my Cursor, Windsurf, and VSCodium users out there—thank you for the support! The extension has been doing surprisingly well on &lt;strong&gt;Open VSX&lt;/strong&gt;, and it's officially verified there too.&lt;/p&gt;

&lt;p&gt;👇 &lt;strong&gt;Check it out here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Website &amp;amp; Demo:&lt;/strong&gt; &lt;a href="https://rakkunn.github.io/MarkSmith/" rel="noopener noreferrer"&gt;rakkunn.github.io/MarkSmith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/rakkunn/MarkSmith" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is 100% free and open-source. If you are building with AI or just write a ton of docs, I hope this saves you as much time as it saves me. &lt;/p&gt;

&lt;p&gt;Would love to hear your feedback in the comments! Happy coding! 💻✨&lt;/p&gt;

</description>
      <category>ai</category>
      <category>markdown</category>
      <category>productivity</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Building Marksmith: lessons from making Markdown bearable in VS Code</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sun, 24 May 2026 10:04:35 +0000</pubDate>
      <link>https://dev.to/rakkunn/building-marksmith-lessons-from-making-markdown-bearable-in-vs-code-a1d</link>
      <guid>https://dev.to/rakkunn/building-marksmith-lessons-from-making-markdown-bearable-in-vs-code-a1d</guid>
      <description>&lt;p&gt;I'll start with the moment that pushed me to build this.&lt;/p&gt;

&lt;p&gt;I was editing a 1,200-line README. Spotted a typo in the preview pane. Scrolled the source to find it. Lost my place. Scrolled more. Pasted a table from a Google Sheet and got back a wall of tab characters. Closed VS Code in frustration and went to make coffee.&lt;/p&gt;

&lt;p&gt;That week I started building &lt;strong&gt;Marksmith&lt;/strong&gt;, a VS Code extension that tries to fix the small things that quietly drain you when writing Markdown. This is a retrospective: what I shipped, the parts that were harder than expected, and the security work that ended up being the largest chunk of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't Markdown — it's the workflow around it
&lt;/h2&gt;

&lt;p&gt;The syntax is fine. The friction lives next to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasting a table from Excel or Sheets gives you tab-separated garbage&lt;/li&gt;
&lt;li&gt;Pasting a URL means manually typing &lt;code&gt;[text](url)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Editor and preview scroll independently, so a long file turns into needle-in-haystack&lt;/li&gt;
&lt;li&gt;You have no idea how heavy your doc is until you paste it into Claude or GPT and watch the token count blow up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is small. Together they're death by a thousand context switches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Paste: making the clipboard do the work
&lt;/h2&gt;

&lt;p&gt;The first thing I built was clipboard interception. When you paste, Marksmith inspects the clipboard before VS Code's default handler runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePaste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextEditor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Tab-separated, multi-line → Excel/Sheets table&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isTabularData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;convertToMarkdownTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// URL pasted over a text selection → auto-link&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Excel-to-Markdown conversion is the one users mention most. It's not complicated — split by tabs, normalize column widths, generate the &lt;code&gt;|---|&lt;/code&gt; separator. But the impact on writing flow is disproportionate to the implementation effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bi-directional sync: scrolling that actually works
&lt;/h2&gt;

&lt;p&gt;This one took a few iterations.&lt;/p&gt;

&lt;p&gt;The first version synced scroll position by percentage. Useless for long files, because Markdown blocks have wildly different rendered heights (a one-line image reference is tiny in source, huge in preview).&lt;/p&gt;

&lt;p&gt;The fix was source-mapping. When rendering Markdown to HTML, attach a &lt;code&gt;data-line&lt;/code&gt; attribute to every top-level element pointing back to the source line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;data-line=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bi-directional sync&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;data-line=&lt;/span&gt;&lt;span class="s"&gt;"44"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This one took a few iterations.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll handlers on both sides then walk the DOM and align based on those anchors. Clicking any element in the preview jumps the cursor to that source line. Not novel — most modern Markdown editors do something similar — but getting it smooth inside a VS Code webview took some debouncing to avoid feedback loops where each side's scroll event triggered the other infinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document X-Ray: knowing the cost before you paste
&lt;/h2&gt;

&lt;p&gt;If you draft in Markdown and feed it to an LLM, you eventually hit context limits. Marksmith has a sidebar showing word count, readability score, and an estimated LLM token count.&lt;/p&gt;

&lt;p&gt;The token estimate is a heuristic — running a real tokenizer in the extension host would be too heavy on every keystroke. Instead it uses a character-and-whitespace approximation calibrated against &lt;code&gt;tiktoken&lt;/code&gt; output on a documentation corpus. It gets within ~5% on doc-style text, which is enough to catch "this prompt is too big" before you actually paste it somewhere.&lt;/p&gt;

&lt;p&gt;I'd like to move this to a real tokenizer in a worker thread eventually. For now the heuristic is good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I underestimated: security
&lt;/h2&gt;

&lt;p&gt;I thought I was building an editor extension. I ended up spending more time on security than on features. Three classes of vulnerability mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSS in the webview.&lt;/strong&gt; Markdown allows raw HTML, so a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in someone's README would execute in the webview context. I run all rendered HTML through DOMPurify with a strict allowlist, while preserving the elements I actually want (Mermaid SVG, syntax-highlighted spans):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ALLOWED_TAGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;defaultTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;ALLOWED_ATTR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;defaultAttrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-line&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;FORBID_TAGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iframe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RCE through PDF export.&lt;/strong&gt; Marksmith uses Puppeteer to render to PDF. In the first version I hadn't verified the Chromium sandbox was actually engaging on user machines. A crafted Markdown file with malicious HTML could, in theory, escape the renderer. I now explicitly verify sandbox mode at launch and fail closed if it isn't available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSRF in URL preview.&lt;/strong&gt; Smart Paste optionally fetches the title of a URL you paste. Without filtering, a user could be tricked into pasting &lt;code&gt;http://169.254.169.254/...&lt;/code&gt; (the AWS metadata endpoint) or &lt;code&gt;http://192.168.1.1/admin&lt;/code&gt;, and the extension would dutifully fetch it. The fix is IP filtering before the request, plus a redirect cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPrivateIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isLoopback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blocked: private/loopback address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* + cap follows */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this is novel security work. What surprised me was how much attack surface a "simple" Markdown extension exposes once it starts being helpful — fetching URLs, rendering arbitrary HTML, spawning headless browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;A few things in hindsight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Threat-model first, features second.&lt;/strong&gt; I retrofitted protections; should have designed for them from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token counter should have been a worker from the start.&lt;/strong&gt; Moving it off the main thread later was awkward and broke a few assumptions in the sidebar UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on Windows earlier.&lt;/strong&gt; Path handling for PDF export gave me grief that I could have caught much sooner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/rakkunn/MarkSmith" rel="noopener noreferrer"&gt;Github&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Homepage: &lt;a href="https://rakkunn.github.io/MarkSmith/" rel="noopener noreferrer"&gt;Marksmith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VSCode Marketplace: 
&lt;a href="https://marketplace.visualstudio.com/items?itemName=rakkunn.marksmith" rel="noopener noreferrer"&gt;Marksmith on VS Code Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenVSX Marketplace:
&lt;a href="https://open-vsx.org/extension/rakkunn/marksmith" rel="noopener noreferrer"&gt;Marksmith on OpenVSX&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped a VS Code extension and have war stories about webview security or Puppeteer sandboxing, I'd genuinely like to hear them — drop a comment.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>markdown</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DualClip v1.2.6 — Stability Fix, Homebrew</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Tue, 14 Apr 2026 07:57:34 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-v126-stability-fix-homebrew-47ef</link>
      <guid>https://dev.to/rakkunn/dualclip-v126-stability-fix-homebrew-47ef</guid>
      <description>&lt;p&gt;It's been a busy couple of days for DualClip. What started as a frustrating crash investigation turned into a series of meaningful improvements — from fixing a deep resource-bundling bug to shipping Homebrew support. Here's a rundown of everything that changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crash That Wouldn't Quit
&lt;/h2&gt;

&lt;p&gt;Shortly after v1.2.0, DualClip started crashing on launch for some users. The crash report pointed to Bundle.module inside the KeyboardShortcuts dependency — a fatalError triggered because the app couldn't locate its resource bundle at runtime.&lt;/p&gt;

&lt;p&gt;The root cause turned out to be a subtle mismatch in how Swift Package Manager bundles resources for standalone .app builds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPM generates a file called resource_bundle_accessor.swift that looks for resources at Bundle.main.bundleURL — which resolves to the .app/ root directory.&lt;/li&gt;
&lt;li&gt;But macOS codesigning requires all resources to live inside Contents/Resources/, not the app root. Placing bundles at the root causes an "unsealed contents" signing error.&lt;/li&gt;
&lt;li&gt;So the resources were in Contents/Resources/, but the code was looking at .app/ — and finding nothing.
The fix was surgical: after the initial build, we patch the generated accessor to use Bundle.main.resourceURL! (which points to Contents/Resources/), then recompile just the affected module and re-link the executable. SPM regenerates the accessor on every full build, so we had to carefully avoid triggering a rebuild after patching. The final binary is verified with strings to confirm the patch is baked in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This fix ships in v1.2.6 and the crash is fully resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrew Support
&lt;/h2&gt;

&lt;p&gt;DualClip is now installable via Homebrew:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brew install RAKKUNN/tap/dualclip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We set up a dedicated Homebrew Tap with a Cask formula that points to the latest signed and notarized release. The CI pipeline automatically updates the formula (version + SHA256) whenever a new release is tagged — zero manual steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes DualClip Different
&lt;/h2&gt;

&lt;p&gt;After analyzing 57 macOS clipboard managers, one thing stood out: every single one is a clipboard history manager. DualClip is the only one that uses dedicated slots.&lt;/p&gt;

&lt;p&gt;The difference matters:&lt;/p&gt;

&lt;p&gt;History managers record everything you copy and let you scroll back through it. Great for recall, but adds complexity — databases, search UI, sync, storage limits.&lt;br&gt;
DualClip gives you 3 fixed slots with instant keyboard access. You decide where to copy, not when to find it later. No history, no scrolling, no search.&lt;br&gt;
It's also one of the few clipboard tools that stores absolutely nothing to disk. Everything lives in RAM and vanishes on quit. No network access, no telemetry, no cloud sync. &lt;/p&gt;
&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;brew install RAKKUNN/tap/dualclip&lt;/code&gt;&lt;br&gt;
Or grab the latest release from GitHub.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/RAKKUNN" rel="noopener noreferrer"&gt;
        RAKKUNN
      &lt;/a&gt; / &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;
        DualClip
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A lightweight macOS app for multi-slot clipboard management
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/icon.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ficon.png" alt="DualClip Icon" width="128" height="128"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DualClip&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;b&gt;English&lt;/b&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-ko.md" rel="noopener noreferrer"&gt;한국어&lt;/a&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-zh.md" rel="noopener noreferrer"&gt;中文&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  A lightweight macOS menu bar app that provides &lt;b&gt;multi-slot clipboard management&lt;/b&gt;.&lt;br&gt;
  Unlike history-based clipboard managers, DualClip gives you instant access to dedicated clipboard slots via customizable keyboard shortcuts
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://rakkunn.github.io/DualClip/" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;🌐 Website&lt;/b&gt;&lt;/a&gt;
  &amp;nbsp;·&amp;nbsp;
  &lt;a href="https://github.com/RAKKUNN/DualClip/releases/latest" rel="noopener noreferrer"&gt;&lt;b&gt;⬇ Download&lt;/b&gt;&lt;/a&gt;
  &amp;nbsp;·&amp;nbsp;
  &lt;a href="https://github.com/RAKKUNN/DualClip#installation" rel="noopener noreferrer"&gt;&lt;b&gt;🍺 Homebrew&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/actions/workflows/ci.yml/badge.svg"&gt;&lt;img src="https://github.com/RAKKUNN/DualClip/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/6bffd51139ffc392eddcd88884109841bddeb7332c97055a073d66df7fadf064/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31332532422d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/6bffd51139ffc392eddcd88884109841bddeb7332c97055a073d66df7fadf064/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31332532422d626c7565" alt="macOS 13+"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2b1ee465df76b12ec13951241072e303f4518d7562c9ae3abcf609d074419bd6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4170706c6525323053696c69636f6e2d72657175697265642d626c61636b3f6c6f676f3d6170706c65"&gt;&lt;img src="https://camo.githubusercontent.com/2b1ee465df76b12ec13951241072e303f4518d7562c9ae3abcf609d074419bd6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4170706c6525323053696c69636f6e2d72657175697265642d626c61636b3f6c6f676f3d6170706c65" alt="Apple Silicon"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8c972106af96ab0d4f2778157576ab9728bc8b0735969ac8963a503781b0e62b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53776966742d352e392532422d6f72616e6765"&gt;&lt;img src="https://camo.githubusercontent.com/8c972106af96ab0d4f2778157576ab9728bc8b0735969ac8963a503781b0e62b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53776966742d352e392532422d6f72616e6765" alt="Swift 5.9+"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 Clipboard Slots&lt;/strong&gt;: Slot A (system default), Slot B, and Slot C&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable Shortcuts&lt;/strong&gt;: No hardcoded key conflicts — configure your own shortcuts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Paste&lt;/strong&gt;: Seamlessly pastes from any slot without corrupting your system clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menu Bar Popover&lt;/strong&gt;: Quick-glance view of all slot contents with previews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy First&lt;/strong&gt;: All data lives in RAM only — nothing is persisted to disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Network Access&lt;/strong&gt;: No telemetry, no analytics, no internet communication&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/test_dualclip.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ftest_dualclip.gif" alt="DualClip Demo" width="600"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/test_dualclip_image.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ftest_dualclip_image.gif" alt="DualClip Image Support Demo" width="600"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Default Shortcuts&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Action&lt;/th&gt;

&lt;th&gt;Shortcut&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;Copy to Slot B&lt;/td&gt;

&lt;td&gt;⌥⌘C&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Paste from Slot B&lt;/td&gt;

&lt;td&gt;⌥⌘V&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Copy to Slot C&lt;/td&gt;

&lt;td&gt;⌃⌘C&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Paste from Slot C&lt;/td&gt;

&lt;td&gt;⌃⌘V&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;All shortcuts are…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&gt;
Signed and notarized by Apple — no Gatekeeper warnings.

&lt;p&gt;DualClip is open source under the MIT license. Issues, feedback, and contributions are welcome.&lt;/p&gt;

&lt;p&gt;Thank you, have a nice day!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DualClip - Update_2026.04.12.</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:48:23 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-update20260412-4pb7</link>
      <guid>https://dev.to/rakkunn/dualclip-update20260412-4pb7</guid>
      <description>&lt;p&gt;Last week, I shared the first round of updates for DualClip, my open-source macOS clipboard manager. Since then, I’ve crossed off three major items from the roadmap—including the one I’m most excited about: you can finally download DualClip without needing to build it from source.&lt;/p&gt;

&lt;p&gt;Here is the breakdown of what’s new in v1.1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔒 Secure Input Field Detection
&lt;/h2&gt;

&lt;p&gt;This was at the top of my "What’s Next" list for a reason: Privacy.&lt;/p&gt;

&lt;p&gt;When macOS has a password field focused, the system activates a mode called Secure Event Input. DualClip now checks for this state before every copy and paste operation. If you're typing a password into Safari, 1Password, or a terminal sudo prompt, DualClip silently steps back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Swift&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;isSecureInputActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;IsSecureEventInputEnabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard is dead simple—one line in handleCopy and one in handlePaste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Swift&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;AccessibilityService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSecureInputActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s the kind of feature that, when it works correctly, you never notice. And that’s exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Prebuilt Binaries (No Xcode Required!)
&lt;/h2&gt;

&lt;p&gt;The biggest friction point in my original post was: "There's no prebuilt binary yet." Starting with v1.1.0, every release is signed with a Developer ID certificate and notarized by Apple. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No more Gatekeeper warnings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No "Open Anyway" dance in System Settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Just download, drag to Applications, and go.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗 The CI/CD Saga: 3 Bugs and a Notarization Pipeline
&lt;/h2&gt;

&lt;p&gt;Getting the automated release pipeline working was... an adventure. I’m using GitHub Actions to handle the heavy lifting. The workflow triggers on version tags (v*.&lt;em&gt;.&lt;/em&gt;) and follows this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Build: swift build -c release on a macOS 14 ARM runner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import: Decode the .p12 certificate from GitHub Secrets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bundle: Assemble the .app structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sign: codesign with hardened runtime and a timestamp.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notarize: Submit to Apple, poll for completion, and staple the ticket.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sounds clean on paper. In practice, it took four attempts and taught me three very humbling lessons:&lt;/p&gt;

&lt;p&gt;Bug 1: The One-Character Typo&lt;br&gt;
I spent 10 minutes staring at a 401 Unauthorized error from Apple’s API. It turned out to be a typo in my app-specific password secret: fjhk vs tihk. CI/CD reminds you that your eyes see what they want to see, not what’s actually there.&lt;/p&gt;

&lt;p&gt;Bug 2: The Service Outage&lt;br&gt;
Three consecutive submissions got stuck at "In Progress" for hours. I thought my script was hung. It turns out Apple's notarization service was actually having a global outage. Even the best pipeline can’t fix a broken cloud.&lt;/p&gt;

&lt;p&gt;Bug 3: The awk Failure&lt;br&gt;
My polling script used awk '{print $2}' to extract the status from notarytool info. However, the status string for a pending job is In Progress (two words). awk captured only "In," which matched nothing in my logic.&lt;br&gt;
The Fix: Switched to sed 's/.*status: //' to grab the full string.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔄 Updated Roadmap
&lt;/h2&gt;

&lt;p&gt;Feature Status&lt;br&gt;
Secure input field detection    ✅ Shipped&lt;br&gt;
RAM zeroing on termination  ✅ Shipped&lt;br&gt;
Image/rich text support ✅ Shipped&lt;br&gt;
GitHub Actions CI/CD + Notarization ✅ Shipped&lt;br&gt;
Homebrew Cask distribution  🔜 Next&lt;br&gt;
Sparkle auto-update 📅 Planned&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;If you’ve been waiting for a downloadable build, it’s ready for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to the Latest Releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download DualClip-1.1.0-arm64.zip.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unzip, move to Applications, and launch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grant Accessibility permission when prompted.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find a bug or have a feature request, feel free to open an issue or a PR!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
    </item>
    <item>
      <title>DualClip - Update_2026.04.04.</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sat, 04 Apr 2026 10:54:46 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-update20260404-39lp</link>
      <guid>https://dev.to/rakkunn/dualclip-update20260404-39lp</guid>
      <description>&lt;h1&gt;
  
  
  DualClip Update — Beyond Text, and a 150ms Correction
&lt;/h1&gt;

&lt;p&gt;A few hours ago, I shared &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;DualClip&lt;/a&gt;, a slot-based clipboard manager for macOS. Thank you to everyone who checked it out!&lt;/p&gt;

&lt;p&gt;Since then, I've shipped a few meaningful updates that I didn't cover in the original post. I also owe you a small correction. Let me walk through both.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 First, a correction: 50ms → 150ms
&lt;/h2&gt;

&lt;p&gt;In my first post, I wrote that the Atomic Paste operation completes "in less than 50ms." That was wrong. The actual restore delay is &lt;strong&gt;150ms&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// 150ms is an empirically safe value to prevent race conditions.&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;restoreDelayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;150ms was chosen empirically to avoid a race condition — if the clipboard is restored before the target application finishes reading it, the paste silently fails. 50ms sounded cooler, but 150ms is what actually works reliably. Sorry about that.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ How Atomic Paste actually works
&lt;/h2&gt;

&lt;p&gt;Since this is the core mechanic of DualClip, I figured it deserves a proper breakdown. When you press the hotkey to paste from Slot B, here's what happens under the hood:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Backup&lt;/strong&gt; — The entire system clipboard (Slot A) is deep-copied into a temporary buffer. Not just text — all &lt;code&gt;NSPasteboardItem&lt;/code&gt; types and their raw data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Swap&lt;/strong&gt; — Slot B's content is written to the system clipboard, replacing whatever was there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Simulate&lt;/strong&gt; — A &lt;code&gt;CGEvent&lt;/code&gt;-based ⌘V keystroke is posted to the HID system. This is why DualClip requires Accessibility permission — it's literally pressing keys on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Restore&lt;/strong&gt; — After 150ms, the backup is written back to the system clipboard. Your original ⌘C content is untouched.&lt;/p&gt;

&lt;p&gt;The entire flow is invisible to the user. You press a hotkey, text appears, and your clipboard stays exactly as it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  🖼 Multi-content type support
&lt;/h2&gt;

&lt;p&gt;The first post only showed text workflows, which gave the impression that DualClip is text-only. It's not — it now supports &lt;strong&gt;four content types&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plain text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code snippets, URLs, API keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rich text (RTF)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Formatted text with bold, colors, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screenshots, design assets from Figma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File URLs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File paths copied from Finder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each slot stores the raw &lt;code&gt;NSPasteboardItem&lt;/code&gt; data faithfully, so what you copy is exactly what you get back — formatting, metadata, and all.&lt;/p&gt;

&lt;p&gt;This means you can now keep a screenshot in Slot B and a HEX code in Slot C, then paste them alternately into your editor without touching the mouse.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 RAM Zeroing on termination
&lt;/h2&gt;

&lt;p&gt;In the first post, I mentioned that all data lives in RAM only. That's still true, but I've taken it a step further.&lt;/p&gt;

&lt;p&gt;When DualClip quits, it doesn't just release memory — it &lt;strong&gt;overwrites every byte with zeros&lt;/strong&gt; before deallocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;secureWipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pasteboardItems&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withUnsafeBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;baseAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseAddress&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mutating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is called on &lt;code&gt;applicationWillTerminate&lt;/code&gt; for all three slots. If you temporarily stored an API key or password in a slot, it won't linger in physical memory after the app closes.&lt;/p&gt;

&lt;p&gt;To summarize the privacy model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No disk writes.&lt;/strong&gt; Nothing is persisted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network.&lt;/strong&gt; Zero external communication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM zeroing.&lt;/strong&gt; Memory is scrubbed on exit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source.&lt;/strong&gt; You can verify all of the above yourself.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏗 CI pipeline
&lt;/h2&gt;

&lt;p&gt;I've added a GitHub Actions workflow that runs on every push and PR to &lt;code&gt;main&lt;/code&gt;. It builds the project in both Debug and Release configurations on macOS 14 with Swift 5.9.&lt;/p&gt;

&lt;p&gt;It's a simple setup — no notarization or automatic releases yet — but it catches build regressions before they land.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Building from source
&lt;/h2&gt;

&lt;p&gt;There's no prebuilt binary yet, but building is straightforward if you have Xcode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/RAKKUNN/DualClip.git
&lt;span class="nb"&gt;cd &lt;/span&gt;DualClip
open Package.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit ▶ Run in Xcode. On first launch, macOS will ask for Accessibility permission — this is required for the CGEvent keystroke simulation that powers Atomic Paste.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Secure input field detection (auto-disable in password fields)&lt;/li&gt;
&lt;li&gt;Homebrew Cask distribution&lt;/li&gt;
&lt;li&gt;Sparkle auto-update&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This is still a small, single-purpose tool, but I'm trying to get the details right. If you have feedback or want to contribute, the repo is open.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
    </item>
    <item>
      <title>DualClip: multi-slot clipboard manager for macOS</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Fri, 03 Apr 2026 23:09:10 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-multi-slot-clipboard-manager-44f5</link>
      <guid>https://dev.to/rakkunn/dualclip-multi-slot-clipboard-manager-44f5</guid>
      <description>&lt;p&gt;&lt;strong&gt;First project with macOS &amp;amp; Swift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to share &lt;strong&gt;DualClip&lt;/strong&gt;, a native macOS menu bar app I’ve been working on.&lt;/p&gt;

&lt;p&gt;While there are many great clipboard managers like Maccy or Paste, I found that most of them focus on "History"—searching through a vertical list of everything you've copied.&lt;/p&gt;

&lt;p&gt;I built DualClip because I needed something that works more like a "Workbench." Instead of picking from a menu, DualClip gives you dedicated slots (A, B, and C) that you can access instantly via global hotkeys. &lt;/p&gt;

&lt;p&gt;🚀 How it differs from history-based managers:&lt;/p&gt;

&lt;p&gt;No List Selection: You don't have to break your flow to search or click an item from a list. You use ⌥⌘C to save to Slot B and ⌥⌘V to paste it instantly. &lt;/p&gt;

&lt;p&gt;Atomic Paste: When you trigger a secondary slot, the app performs a high-speed "injection"—swapping the system clipboard, pasting, and restoring the original content in less than 50ms. &lt;/p&gt;

&lt;p&gt;Parallel Workflow: Perfect for developers moving IDs and Emails simultaneously, or translators working with source and target text in two separate slots. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzfbwy1oifgvlbmx1f7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzfbwy1oifgvlbmx1f7n.png" alt=" " width="566" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo0pbvbliklcfzzhgc7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo0pbvbliklcfzzhgc7e.png" alt=" " width="714" height="510"&gt;&lt;/a&gt;&lt;br&gt;
🔐 Privacy:&lt;/p&gt;

&lt;p&gt;As a security enthusiast, I designed this with transparency in mind:&lt;/p&gt;

&lt;p&gt;In-Memory Only: Clipboard data is stored strictly in RAM and is never written to disk.&lt;/p&gt;

&lt;p&gt;Zero Network Access: The app has no network permissions. No telemetry, no analytics, no external communication. &lt;/p&gt;

&lt;p&gt;🛠 Tech Stack:&lt;/p&gt;

&lt;p&gt;Language: Swift 5.9+ / SwiftUI &amp;amp; AppKit Hybrid&lt;/p&gt;

&lt;p&gt;The project is licensed under MIT, and I’d love to get some feedback or contributions from this community!&lt;/p&gt;

&lt;p&gt;Thank you for reading my small project!&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repository: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
