<?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: Sergio Farfan Cardenete</title>
    <description>The latest articles on DEV Community by Sergio Farfan Cardenete (@sergio_farfn_b071cafc7ed).</description>
    <link>https://dev.to/sergio_farfn_b071cafc7ed</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%2F3828298%2F93ba3193-a163-43de-b5a5-88d654bffd3d.jpg</url>
      <title>DEV Community: Sergio Farfan Cardenete</title>
      <link>https://dev.to/sergio_farfn_b071cafc7ed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sergio_farfn_b071cafc7ed"/>
    <language>en</language>
    <item>
      <title>RepoDeck: a native macOS dashboard for all your git repos</title>
      <dc:creator>Sergio Farfan Cardenete</dc:creator>
      <pubDate>Thu, 09 Jul 2026 05:11:59 +0000</pubDate>
      <link>https://dev.to/sergio_farfn_b071cafc7ed/repodeck-a-native-macos-dashboard-for-all-your-git-repos-472c</link>
      <guid>https://dev.to/sergio_farfn_b071cafc7ed/repodeck-a-native-macos-dashboard-for-all-your-git-repos-472c</guid>
      <description>&lt;p&gt;I have somewhere around thirty git repositories checked out on my Mac at any given time — side projects, forks, work I forgot to push before closing the laptop. Every few days I'd ask myself the same question: which of these have uncommitted changes? Which ones are behind their remote and need a pull? The honest answer was always "I don't know," because finding out meant opening each folder in an editor just to glance at the Source Control panel — one repo at a time, over and over.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;RepoDeck&lt;/strong&gt; — a native macOS dashboard that tracks a set of folders, recursively finds every git repository underneath them, and shows you at a glance which ones need attention.&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%2Fspke1tby7vk54w8nl3vy.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%2Fspke1tby7vk54w8nl3vy.png" alt="RepoDeck dashboard" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-repo dashboard&lt;/strong&gt; — track any set of folders; RepoDeck recursively discovers every git repo underneath them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live status via FSEvents&lt;/strong&gt; — no polling, no timers; the sidebar badge updates the moment a file changes on disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage, commit, and sync&lt;/strong&gt; — pull, push, and fetch per repo, plus &lt;strong&gt;Fetch All&lt;/strong&gt; / &lt;strong&gt;Pull All&lt;/strong&gt; across every tracked repo at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History search&lt;/strong&gt; by commit message, author, file path, or content (git's pickaxe search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar filter and pinning&lt;/strong&gt; for the repos you touch most often&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes&lt;/strong&gt; — System/Light/Dark, custom accent color, fonts, and font size (⌘,)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resizable Changes/History split&lt;/strong&gt;, so you can give more room to whichever pane you're using&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;RepoDeck is Swift Package Manager only — there's no &lt;code&gt;.xcodeproj&lt;/code&gt;, no &lt;code&gt;.pbxproj&lt;/code&gt; to merge-conflict over. &lt;code&gt;swift build&lt;/code&gt;, &lt;code&gt;swift test&lt;/code&gt;, &lt;code&gt;swift run RepoDeck&lt;/code&gt; are the entire dev loop.&lt;/p&gt;

&lt;p&gt;The UI is SwiftUI, state is &lt;code&gt;@Observable&lt;/code&gt;, and the codebase builds under Swift 6's strict concurrency checking.&lt;/p&gt;

&lt;p&gt;The one deliberate architectural choice worth calling out: RepoDeck shells out to the real &lt;code&gt;git&lt;/code&gt; binary instead of linking libgit2. That's slower per call, but it means every operation inherits the user's actual &lt;code&gt;~/.gitconfig&lt;/code&gt; — credential helpers, aliases, hooks, SSH config, everything — for free. It's the same reason VS Code's Source Control panel, GitHub Desktop, and &lt;code&gt;lazygit&lt;/code&gt; all do the same thing: reimplementing what real git already does correctly, and what your credential helper already knows how to do, is a losing trade.&lt;/p&gt;

&lt;p&gt;The package builds two targets: &lt;code&gt;RepoDeckKit&lt;/code&gt; — every bit of git, parsing, and filesystem-watching logic, with no SwiftUI imports — and &lt;code&gt;RepoDeck&lt;/code&gt;, the app target, which is just views and view models wired to it. Nothing in &lt;code&gt;RepoDeckKit&lt;/code&gt; touches a window or the screen; every git call and every filesystem event passes through a seam a test can drive directly. That split is why the code below is unit- and integration-testable at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parsing &lt;code&gt;git status --porcelain=v2 -z&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Status parsing runs on &lt;code&gt;git status --porcelain=v2 --branch --untracked-files=all -z&lt;/code&gt; — NUL-separated so filenames with spaces, newlines, or anything else don't need escaping. &lt;code&gt;PorcelainParser&lt;/code&gt; is a pure function over &lt;code&gt;Data&lt;/code&gt;, no &lt;code&gt;Process&lt;/code&gt;, no I/O, which makes it trivial to unit test without ever invoking git.&lt;/p&gt;

&lt;h3&gt;
  
  
  The XY fan-out
&lt;/h3&gt;

&lt;p&gt;Porcelain v2's ordinary-change records carry a two-letter &lt;code&gt;XY&lt;/code&gt; code: &lt;code&gt;X&lt;/code&gt; is the index status, &lt;code&gt;Y&lt;/code&gt; is the worktree status. A file that's staged &lt;em&gt;and&lt;/em&gt; has further unstaged edits is one record, but RepoDeck's UI wants it in two different sections — Staged and Changes. &lt;code&gt;appendFanOut&lt;/code&gt; does the split:&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;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;appendFanOut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="nv"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;inout&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;FileChange&lt;/span&gt;&lt;span class="p"&gt;])&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;letters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&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;indexStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;letters&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;worktreeStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;indexStatus&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FileChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&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="nv"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;staged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;statusLetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexStatus&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="n"&gt;worktreeStatus&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FileChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&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="nv"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unstaged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;statusLetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worktreeStatus&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One record becomes zero, one, or two &lt;code&gt;FileChange&lt;/code&gt; rows depending on which half of &lt;code&gt;XY&lt;/code&gt; isn't a dot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Renames consume the next token
&lt;/h3&gt;

&lt;p&gt;Rename and copy records (&lt;code&gt;2 ...&lt;/code&gt;) are the one record kind where a single logical event spans two NUL-delimited tokens: the record itself, then the original path as a separate token immediately after it. The dispatch loop has to know to look ahead and skip an extra slot:&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="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"2"&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;originalPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;records&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="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="nf"&gt;parseRenameOrCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;originalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;into&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;changes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;originalPath&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss that &lt;code&gt;index += 2&lt;/code&gt; and the parser starts reading the next file's rename-origin path as if it were a new status record — everything after the first rename in the list comes out garbled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Untracked files and merge conflicts
&lt;/h3&gt;

&lt;p&gt;Two more record kinds skip the fan-out entirely. Untracked files (&lt;code&gt;?&lt;/code&gt; records) are just the path after a fixed two-character prefix:&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;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;parseUntracked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="nv"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;inout&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;FileChange&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// drop "? "&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FileChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&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="nv"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;untracked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;statusLetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"U"&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;Unmerged conflicts (&lt;code&gt;u&lt;/code&gt; records, left behind by a failed merge or rebase) carry their own two-character conflict code and go straight into a dedicated &lt;code&gt;.unmerged&lt;/code&gt; area rather than through the staged/unstaged split — a conflicted file isn't meaningfully "staged," it's blocking, and the UI treats it that way.&lt;/p&gt;




&lt;h2&gt;
  
  
  ProcessRunner: One Subprocess Primitive for Everything
&lt;/h2&gt;

&lt;p&gt;Every git invocation in the app funnels through a single function: async, deadlock-free, cancellable, and globally bounded. Two rules made that worth centralizing instead of calling &lt;code&gt;Process&lt;/code&gt; ad hoc wherever a view model needed git.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipe-drain deadlock
&lt;/h3&gt;

&lt;p&gt;Pipes have a fixed OS buffer. Wait for a child process to exit &lt;em&gt;before&lt;/em&gt; reading its stdout, and if that process writes more output than the pipe can hold, the child blocks writing while you're blocked waiting for it to exit — a real deadlock, and it only shows up once &lt;code&gt;git status&lt;/code&gt; output gets big enough to fill the pipe. &lt;code&gt;ProcessRunner&lt;/code&gt; drains stdout and stderr concurrently, in a loop, while the process is still running:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;stderrData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drainAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderrStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;stdoutData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;truncated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stdoutStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stdoutData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxOutputBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdoutData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;truncated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;break&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;Only after both streams are fully drained does it wait for the actual process exit. The output cap lives in the same loop — once it's hit, the process is terminated on the spot rather than left to keep filling a pipe nobody will finish reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GIT_TERMINAL_PROMPT=0&lt;/code&gt; and a 6-slot semaphore
&lt;/h3&gt;

&lt;p&gt;Every subprocess runs with &lt;code&gt;GIT_TERMINAL_PROMPT=0&lt;/code&gt; so a missing credential fails fast with a real error instead of git silently hanging on a terminal prompt that will never appear in a GUI app:&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="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ProcessInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"GIT_TERMINAL_PROMPT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LC_ALL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And because bulk operations (Fetch All, Pull All) can fire dozens of git processes at once, a process-wide counting semaphore caps concurrency at 6:&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;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;concurrencyLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ConcurrencyLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;concurrencyLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;ProcessRunner.run&lt;/code&gt; call acquires a slot before launching and releases it on every exit path — success, failure, or cancellation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancellation kills the child, not just the &lt;code&gt;Task&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Cancelling the surrounding Swift &lt;code&gt;Task&lt;/code&gt; — closing a repo, navigating away mid-fetch — has to actually kill the git process, not just stop awaiting it and leave it running orphaned in the background:&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="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;onCancel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;SIGTERM&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;code&gt;Process&lt;/code&gt; itself isn't &lt;code&gt;Sendable&lt;/code&gt;, so only the &lt;code&gt;pid&lt;/code&gt; — captured before entering the cancellation handler — is allowed to cross into that &lt;code&gt;@Sendable&lt;/code&gt; closure. The drain loops then end naturally once the killed child closes its pipes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watching the Filesystem Without Hammering It
&lt;/h2&gt;

&lt;p&gt;RepoDeck doesn't poll. &lt;code&gt;RepoWatcher&lt;/code&gt; wraps the FSEvents C API directly and emits debounced events on an &lt;code&gt;AsyncStream&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring &lt;code&gt;index.lock&lt;/code&gt; churn
&lt;/h3&gt;

&lt;p&gt;Git itself writes and deletes &lt;code&gt;.git/index.lock&lt;/code&gt; constantly during normal operations — every &lt;code&gt;git add&lt;/code&gt;, every &lt;code&gt;git commit&lt;/code&gt; touches it. Without filtering that out, RepoDeck would refresh its own view of a repo's status in response to changes &lt;em&gt;caused by its own git calls&lt;/em&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="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;shouldIgnore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;rawPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&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;let&lt;/span&gt; &lt;span class="nv"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rawPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;separator&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="nv"&gt;omittingEmptySubsequences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"index.lock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".git"&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="kc"&gt;true&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;component&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".watchman-cookie"&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="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prunedNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&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="kc"&gt;true&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="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A 300ms debounce — no timer hammering your disk
&lt;/h3&gt;

&lt;p&gt;Saving a file, running a build, or checking out a branch can fire dozens of FSEvents callbacks in a fraction of a second. &lt;code&gt;RepoWatcher&lt;/code&gt; collapses a burst for the same repo into a single emission, 300ms after the last event in the burst — cancel-and-reschedule, not a recurring poll:&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;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WatchEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&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;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DispatchWorkItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&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;self&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;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stopped&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;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debounceInterval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;execute&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no timer running in the background polling anything. The app is silent until FSEvents says something changed, and even then it waits out the burst before doing a single &lt;code&gt;git status&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Truncation Seam
&lt;/h2&gt;

&lt;p&gt;This is the bug I'm most annoyed I didn't catch sooner, because both halves of it were individually correct — and individually unit tested.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ProcessRunner&lt;/code&gt;'s output cap protects against a repo with hundreds of thousands of untracked files turning &lt;code&gt;git status&lt;/code&gt; into an unbounded memory sink: past the cap, it sends &lt;code&gt;SIGTERM&lt;/code&gt; to the child and returns whatever was read, flagged as truncated. That's correct, and the unit tests for &lt;code&gt;ProcessRunner&lt;/code&gt; confirmed it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GitClient.status&lt;/code&gt; passes a truncated read through to &lt;code&gt;PorcelainParser&lt;/code&gt;, which drops the trailing partial record and sets &lt;code&gt;didHitLimit&lt;/code&gt; on the result. Also correct, also unit tested.&lt;/p&gt;

&lt;p&gt;Here's the seam: &lt;code&gt;SIGTERM&lt;/code&gt; makes the process's exit code come back as 15, not 0. And the shared helper that every &lt;code&gt;GitClient&lt;/code&gt; method funneled through had one guard for all of them:&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="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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;throw&lt;/span&gt; &lt;span class="kt"&gt;GitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;commandString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fullArguments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&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;A truncated status never reached &lt;code&gt;PorcelainParser&lt;/code&gt; at all — it was thrown as a generic &lt;code&gt;GitError&lt;/code&gt; before the parser ever saw the bytes. Which meant the "Too many changes — showing a partial list" banner in the Changes list, built and unit tested against a &lt;code&gt;RepoStatus&lt;/code&gt; with &lt;code&gt;didHitLimit == true&lt;/code&gt;, was &lt;strong&gt;unreachable&lt;/strong&gt; in the running app. Every real truncation just looked like git had failed outright.&lt;/p&gt;

&lt;p&gt;Nothing caught this in isolation, because nothing in isolation was wrong. &lt;code&gt;ProcessRunner&lt;/code&gt;'s truncation tests never touched &lt;code&gt;GitClient&lt;/code&gt;. &lt;code&gt;PorcelainParser&lt;/code&gt;'s truncation tests fed it pre-truncated bytes directly, never through a real process exit code. The only place the seam existed was the exact path connecting a real &lt;code&gt;SIGTERM&lt;/code&gt;, a real exit code 15, and that exit-code guard — and that only shows up in an end-to-end pass, not a unit test of either side alone.&lt;/p&gt;

&lt;p&gt;The fix was widening one guard: &lt;code&gt;result.exitCode == 0 || result.outputTruncated&lt;/code&gt;. Only &lt;code&gt;status&lt;/code&gt; ever passes an output cap, so no other command's real failures get masked. To lock the seam shut for good, the status output cap became an injectable public property (4 MB by default) so a test could shrink it to 64 bytes against a real temporary repo with real files on disk — forcing an actual truncation, actual &lt;code&gt;SIGTERM&lt;/code&gt;, actual exit 15, without generating megabytes of fixture data:&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="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;GitClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusOutputLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="cp"&gt;#expect(status.didHitLimit == true)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson stuck: when two components are each individually correct and each individually tested, that says nothing about the seam between them. A whole-codebase review pass is what caught it — not either of the unit suites, which had been green the entire time. If a bug can only exist in the handoff, only a test that exercises that exact handoff will ever find it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build &amp;amp; Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Easiest:&lt;/strong&gt; grab the &lt;code&gt;.dmg&lt;/code&gt; from the &lt;a href="https://github.com/sergio-farfan/repodeck/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;, open it, and drag &lt;strong&gt;RepoDeck&lt;/strong&gt; onto &lt;strong&gt;Applications&lt;/strong&gt;. It's ad-hoc signed, not notarized, so the first launch needs one manual nudge past Gatekeeper — right-click &lt;strong&gt;RepoDeck.app → Open&lt;/strong&gt;, or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-dr&lt;/span&gt; com.apple.quarantine /Applications/RepoDeck.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From source&lt;/strong&gt; — Swift Package Manager only, no Xcode project needed:&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 git@github.com:sergio-farfan/repodeck.git
&lt;span class="nb"&gt;cd &lt;/span&gt;repodeck
swift build
swift &lt;span class="nb"&gt;test
&lt;/span&gt;swift run RepoDeck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full build, packaging, and release commands are in the &lt;a href="https://github.com/sergio-farfan/repodeck#build-from-source" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;RepoDeck is on GitHub: &lt;a href="https://github.com/sergio-farfan/repodeck" rel="noopener noreferrer"&gt;github.com/sergio-farfan/repodeck&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Swift 6 and SwiftUI on macOS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>git</category>
      <category>macos</category>
      <category>opensource</category>
    </item>
    <item>
      <title>NoSleep: Prevent your Mac to go sleep. Period.</title>
      <dc:creator>Sergio Farfan Cardenete</dc:creator>
      <pubDate>Sun, 22 Mar 2026 05:09:02 +0000</pubDate>
      <link>https://dev.to/sergio_farfn_b071cafc7ed/nosleep-a-lightweight-macos-menu-bar-app-with-swiftui-5fap</link>
      <guid>https://dev.to/sergio_farfn_b071cafc7ed/nosleep-a-lightweight-macos-menu-bar-app-with-swiftui-5fap</guid>
      <description>&lt;p&gt;Have you ever been mid-presentation, watching a long build compile, or waiting for a large file to download — only for your Mac to decide it's nap time? macOS ships a built-in tool for exactly this: &lt;code&gt;caffeinate&lt;/code&gt;. But running it from the terminal every time is clunky.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;NoSleep&lt;/strong&gt; — a tiny macOS menu bar utility that wraps &lt;code&gt;caffeinate&lt;/code&gt; in a one-click toggle. No Dock icon. No main window. Just a cup icon in your menu bar.&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%2Ft47ow2zgmh97aypb2sg9.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%2Ft47ow2zgmh97aypb2sg9.png" alt="NoSleep menu bar dropdown" width="242" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update — v1.1.0:&lt;/strong&gt; NoSleep now ships as a downloadable, drag-to-install &lt;code&gt;.dmg&lt;/code&gt; (universal), activates the moment you pick a duration, shows a green active indicator with a readable countdown, and pops a notification with a one-tap &lt;strong&gt;Extend 1 hour&lt;/strong&gt; when a timed session ends. The new bits — and the async race the notification introduced — are covered in the v1.1.0 section below.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download &amp;amp; run&lt;/strong&gt; — grab the &lt;code&gt;.dmg&lt;/code&gt; from Releases and drag NoSleep to Applications (universal: Apple Silicon + Intel)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click toggle&lt;/strong&gt; — start/stop caffeinate from the menu bar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-activate&lt;/strong&gt; — pick a duration and it starts immediately, no extra click&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration presets&lt;/strong&gt; — 15 min, 30 min, 1 hr, 2 hr, 4 hr, 8 hr, 10 hr, or Indefinite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live countdown&lt;/strong&gt; — a green active dot and remaining time while active (e.g. &lt;code&gt;2h 34m&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion notification&lt;/strong&gt; — when a timed session ends, a notification offers a one-tap &lt;strong&gt;Extend 1 hour&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start at Login&lt;/strong&gt; — optional LaunchAgent so it auto-starts on boot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevents display + idle sleep&lt;/strong&gt; — uses &lt;code&gt;caffeinate -d -i&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swift 6.0&lt;/strong&gt; with strict concurrency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SwiftUI&lt;/strong&gt; + &lt;code&gt;MenuBarExtra&lt;/code&gt; (macOS 13+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UserNotifications&lt;/strong&gt; — for the session-complete alert and its Extend action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swift Package Manager&lt;/strong&gt; — no Xcode project file required; ships a &lt;strong&gt;universal binary&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Minimum target: &lt;strong&gt;macOS 14 (Sonoma)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  App Entry Point: MenuBarExtra
&lt;/h2&gt;

&lt;p&gt;The entire app lives in the menu bar, which SwiftUI makes surprisingly clean with &lt;code&gt;MenuBarExtra&lt;/code&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="kd"&gt;@main&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;NoSleepApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@StateObject&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;caffeinateManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CaffeinateManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;@StateObject&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;loginManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LoginItemManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;Scene&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;MenuBarExtra&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;MenuBarView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;caffeinateManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;loginManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;loginManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;caffeinateManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isActive&lt;/span&gt;
                  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"cup.and.saucer.fill"&lt;/span&gt;
                  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"cup.and.saucer"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole entry point. &lt;code&gt;MenuBarExtra&lt;/code&gt; handles all the menu bar plumbing — no &lt;code&gt;NSStatusItem&lt;/code&gt;, no AppKit boilerplate. The icon toggles between a filled and outlined cup based on whether caffeinate is running.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;LSUIElement: true&lt;/code&gt; in &lt;code&gt;Info.plist&lt;/code&gt; hides the Dock icon and removes the main window entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Logic: CaffeinateManager
&lt;/h2&gt;

&lt;p&gt;The heart of the app is &lt;code&gt;CaffeinateManager&lt;/code&gt; — an &lt;code&gt;@MainActor&lt;/code&gt; &lt;code&gt;ObservableObject&lt;/code&gt; that manages the &lt;code&gt;caffeinate&lt;/code&gt; child process and a countdown timer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spawning the Process
&lt;/h3&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;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&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;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executableURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fileURLWithPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/usr/bin/caffeinate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;selectedDuration&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indefinite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;selectedDuration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;remainingSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectedDuration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;

    &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminationHandler&lt;/span&gt; &lt;span class="o"&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;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleTermination&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;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;
    &lt;span class="n"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;selectedDuration&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indefinite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduledTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withTimeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-d -i&lt;/code&gt; flags&lt;/strong&gt; — &lt;code&gt;-d&lt;/code&gt; prevents the display from sleeping, &lt;code&gt;-i&lt;/code&gt; prevents idle sleep. Together they cover the common use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-t &amp;lt;seconds&amp;gt;&lt;/code&gt;&lt;/strong&gt; — when a duration is selected, caffeinate self-terminates after that many seconds. The app also runs a &lt;code&gt;Timer&lt;/code&gt; in parallel to track remaining time for the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;terminationHandler&lt;/code&gt;&lt;/strong&gt; — if caffeinate exits on its own (duration expired, or the system killed it), this handler fires and cleans up app state. The &lt;code&gt;Task { @MainActor in ... }&lt;/code&gt; pattern bridges from the background callback thread into the main actor, which Swift 6 strict concurrency requires. (In v1.1.0 this handler grew a &lt;em&gt;run-token&lt;/em&gt; guard — more on why below.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Duration Options
&lt;/h3&gt;

&lt;p&gt;Durations are a typed enum with raw values in seconds:&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;enum&lt;/span&gt; &lt;span class="kt"&gt;SleepDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CaseIterable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Identifiable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;fifteenMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;thirtyMin&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;oneHour&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;twoHours&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;fourHours&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14400&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;eightHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;28800&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;tenHours&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;36000&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;indefinite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selected duration is persisted in &lt;code&gt;UserDefaults&lt;/code&gt; so the preference survives app restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Login Item: LaunchAgent Plist
&lt;/h2&gt;

&lt;p&gt;Rather than using &lt;code&gt;SMAppService&lt;/code&gt; (which requires a sandboxed app), NoSleep writes a &lt;code&gt;LaunchAgent&lt;/code&gt; plist directly to &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.nosleep.app&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/Applications/NoSleep.app/Contents/MacOS/NoSleep&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works without sandboxing and gives full control over the plist.&lt;/p&gt;




&lt;h2&gt;
  
  
  v1.1.0: Auto-Activate, Completion Alerts, and a Real Download
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pick a duration → it just starts
&lt;/h3&gt;

&lt;p&gt;Originally you picked a duration and then clicked Start. Now selecting any preset activates immediately:&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;changeDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SleepDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;selectedDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// auto-activate on selection (re-selecting restarts the timer)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A completion notification you can act on
&lt;/h3&gt;

&lt;p&gt;When a timed session ends, NoSleep posts a notification with an &lt;strong&gt;Extend 1 hour&lt;/strong&gt; action, using &lt;code&gt;UserNotifications&lt;/code&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;extend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UNNotificationAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"EXTEND_1H"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Extend 1 hour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&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;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UNNotificationCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SESSION_COMPLETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                      &lt;span class="nv"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                                      &lt;span class="nv"&gt;intentIdentifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="kt"&gt;UNUserNotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNotificationCategories&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tapping &lt;strong&gt;Extend 1 hour&lt;/strong&gt; starts a fresh one-hour session. One gotcha: the delegate has to be registered &lt;strong&gt;at launch&lt;/strong&gt; — Apple requires it &lt;em&gt;before the app finishes launching&lt;/em&gt;, so doing it lazily when the menu first opens can drop the action response.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stale-termination trap
&lt;/h3&gt;

&lt;p&gt;Here's the interesting bug the notification surfaced. &lt;code&gt;caffeinate&lt;/code&gt; runs as a child process; when it exits, its &lt;code&gt;terminationHandler&lt;/code&gt; fires on a background thread. But that handler fires for &lt;em&gt;three&lt;/em&gt; different reasons: the timer expired (→ notify), the user hit Stop (→ don't notify), or a &lt;strong&gt;restart&lt;/strong&gt; replaced the process (→ don't notify, and don't clobber the new session's state).&lt;/p&gt;

&lt;p&gt;The restart case is a classic async race: &lt;code&gt;start()&lt;/code&gt; calls &lt;code&gt;stop()&lt;/code&gt; (terminating the old process), then launches a new one — but the old process's termination callback arrives &lt;em&gt;later&lt;/em&gt;, after the new session is already live. A naive "was this a user stop?" flag misfires.&lt;/p&gt;

&lt;p&gt;The fix is a monotonic &lt;strong&gt;run token&lt;/strong&gt;. Each &lt;code&gt;start()&lt;/code&gt; bumps a counter, and the termination handler captures the value it was launched with:&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;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;runToken&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runToken&lt;/span&gt;
    &lt;span class="c1"&gt;// ...spawn caffeinate...&lt;/span&gt;
    &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminationHandler&lt;/span&gt; &lt;span class="o"&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;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleTermination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&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="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleTermination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;runToken&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="c1"&gt;// stale (restarted) — ignore it&lt;/span&gt;
    &lt;span class="c1"&gt;// ...decide natural-expiry vs user-stop, then maybe post the notification&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the main actor runs &lt;code&gt;start()&lt;/code&gt; synchronously through the token bump, any stale handler that arrives afterward sees a token that no longer matches — and bails out before touching the new session or firing a notification. The whole "should this fire?" decision is a small pure function, which made it easy to unit-test in isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Download-and-run distribution
&lt;/h3&gt;

&lt;p&gt;The biggest change for users: NoSleep now ships a real &lt;code&gt;.dmg&lt;/code&gt;. The entire pipeline uses only tooling that's already on every Mac — no third-party dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App icon&lt;/strong&gt; — a small AppKit script renders the &lt;code&gt;cup.and.saucer.fill&lt;/code&gt; SF Symbol onto a gradient squircle, then &lt;code&gt;sips&lt;/code&gt; + &lt;code&gt;iconutil&lt;/code&gt; turn it into &lt;code&gt;AppIcon.icns&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal binary&lt;/strong&gt; — &lt;code&gt;swift build -c release --arch arm64 --arch x86_64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styled DMG&lt;/strong&gt; — &lt;code&gt;hdiutil&lt;/code&gt; plus a little AppleScript lay out the window: the app on the left, an arrow to an Applications drop-target, a background image, and a volume icon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's ad-hoc signed (not notarized), the first launch needs a one-time Gatekeeper nudge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-dr&lt;/span&gt; com.apple.quarantine /Applications/NoSleep.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Build &amp;amp; Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Easiest:&lt;/strong&gt; download &lt;code&gt;NoSleep-&amp;lt;version&amp;gt;.dmg&lt;/code&gt; from the &lt;a href="https://github.com/sergio-farfan/nosleep/releases" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;, open it, and drag &lt;strong&gt;NoSleep&lt;/strong&gt; onto Applications. On first launch, run the &lt;code&gt;xattr&lt;/code&gt; command above (or right-click → Open) once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From source&lt;/strong&gt; — the project uses Swift Package Manager, no &lt;code&gt;.xcodeproj&lt;/code&gt; needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build (universal binary, bundles, ad-hoc code signs)&lt;/span&gt;
./build.sh

&lt;span class="c"&gt;# Run&lt;/span&gt;
open NoSleep.app

&lt;span class="c"&gt;# Package a distributable .dmg&lt;/span&gt;
./package-dmg.sh

&lt;span class="c"&gt;# Install to ~/Applications (optional)&lt;/span&gt;
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements to build: Swift 6.0+, Xcode Command Line Tools, macOS 14+.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;NoSleep is open source under the GPLv3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/sergio-farfan/nosleep" rel="noopener noreferrer"&gt;github.com/sergio-farfan/nosleep&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contributions, issues, and stars are all welcome. If you run into any macOS quirks with &lt;code&gt;caffeinate&lt;/code&gt;, &lt;code&gt;MenuBarExtra&lt;/code&gt;, or notifications from an ad-hoc-signed app, feel free to open an issue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Swift 6 and SwiftUI on macOS Sonoma.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>swiftui</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Windows-Style Alt+Tab Window Switcher for macOS in Pure Swift</title>
      <dc:creator>Sergio Farfan Cardenete</dc:creator>
      <pubDate>Sun, 22 Mar 2026 04:16:11 +0000</pubDate>
      <link>https://dev.to/sergio_farfn_b071cafc7ed/i-built-a-windows-style-alttab-window-switcher-for-macos-in-pure-swift-4pp7</link>
      <guid>https://dev.to/sergio_farfn_b071cafc7ed/i-built-a-windows-style-alttab-window-switcher-for-macos-in-pure-swift-4pp7</guid>
      <description>&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Hold &lt;code&gt;Option&lt;/code&gt; and tap &lt;code&gt;Tab&lt;/code&gt; — a panel appears showing every open window across all your apps, on the screen where your mouse is. Cycle through them with &lt;code&gt;Tab&lt;/code&gt; / &lt;code&gt;Shift-Tab&lt;/code&gt; or the arrow keys, then release &lt;code&gt;Option&lt;/code&gt; to jump straight to the selected window. Click any thumbnail to switch instantly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Option-Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open switcher / next window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cycle forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift-Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cycle backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;← / →&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Navigate left / right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Escape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirm and switch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Click&lt;/td&gt;
&lt;td&gt;Select and switch immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The switcher includes minimized windows (they unminimize when selected), windows of &lt;code&gt;Cmd-H&lt;/code&gt;-hidden apps, and windows sitting on other Spaces. No Dock icon, no clutter — just a clean menu bar icon with a small preferences menu.&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%2F99zgbju36xqq3p9mz7g0.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%2F99zgbju36xqq3p9mz7g0.png" alt="AltTab menu bar menu" width="281" height="221"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's New in 1.3
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Liquid Glass.&lt;/strong&gt; On macOS 26 (Tahoe), the switcher can render on Apple's native glass material via &lt;code&gt;NSGlassEffectView&lt;/code&gt; — the real thing, not a blur imitation. Status menu → &lt;strong&gt;Background → Liquid Glass&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selectable backgrounds.&lt;/strong&gt; Three styles, applied on the next &lt;code&gt;Option-Tab&lt;/code&gt; with no relaunch: &lt;strong&gt;Solid&lt;/strong&gt; (default — opaque, theme-adaptive), &lt;strong&gt;Transparent&lt;/strong&gt; (the classic translucent HUD with vibrancy), and &lt;strong&gt;Liquid Glass&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appearance override.&lt;/strong&gt; The panel follows the OS Light/Dark theme by default (including scheduled Auto switching), and you can force Light or Dark from the menu regardless of the system setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WCAG-tested readability.&lt;/strong&gt; The text under each window is no longer hardcoded white on a translucent panel. Labels use semantic system colors, and on the default Solid background the label/background pair meets WCAG 2.x AA (contrast ≥ 4.5:1) — not as an aspiration, but verified by unit tests that resolve the &lt;em&gt;live&lt;/em&gt; system colors under both Light and Dark appearances and compute the actual contrast ratio. If Apple ever changes the palette in a way that breaks AA, the test suite fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's New in 1.2
&lt;/h2&gt;

&lt;p&gt;Instant-open switcher (cached window list, reconciled off the main thread), a 0.25s messaging timeout on every Accessibility call so one wedged app can't freeze the hotkey, opt-in live window previews via ScreenCaptureKit (macOS 14+, no Screen Recording prompt unless you enable it), click-to-switch fixed, multi-monitor support, and a unit-tested core.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; macOS 13 Ventura or later (Liquid Glass needs macOS 26)&lt;/p&gt;

&lt;h3&gt;
  
  
  Download (recommended)
&lt;/h3&gt;

&lt;p&gt;Grab the prebuilt universal DMG (Apple Silicon + Intel) from the &lt;a href="https://github.com/sergio-farfan/alttab-macos/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;, drag AltTab to Applications, and launch it. The build is not notarized yet, so on first launch: right-click &lt;strong&gt;AltTab.app → Open&lt;/strong&gt;, or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-dr&lt;/span&gt; com.apple.quarantine /Applications/AltTab.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build from source
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sergio-farfan/alttab-macos.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;alttab-macos &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./build.sh &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; open ~/Applications/AltTab.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./build.sh build          &lt;span class="c"&gt;# Build Release binary only&lt;/span&gt;
./build.sh &lt;span class="nb"&gt;install&lt;/span&gt;        &lt;span class="c"&gt;# Build and install to ~/Applications&lt;/span&gt;
./build.sh run            &lt;span class="c"&gt;# Build and launch immediately&lt;/span&gt;
./build.sh uninstall      &lt;span class="c"&gt;# Remove the installed app&lt;/span&gt;
swift &lt;span class="nb"&gt;test&lt;/span&gt;                &lt;span class="c"&gt;# Run the unit tests (32)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: building the Liquid Glass code requires Xcode 26+ (macOS 26 SDK); the app still runs on macOS 13+.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt; (required) — intercepts the &lt;code&gt;Option-Tab&lt;/code&gt; hotkey, reads window titles, raises windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Recording&lt;/strong&gt; (never prompted by default) — only requested if you explicitly enable "Show Window Previews" in the menu&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Global hotkey interception&lt;/strong&gt; — A CGEvent tap installed at the session level intercepts &lt;code&gt;Option-Tab&lt;/code&gt; system-wide before it reaches any app, without stealing keyboard focus. The tap plumbing only decodes events; the session logic lives in a pure &lt;code&gt;SwitcherStateMachine&lt;/code&gt; struct, unit-testable without synthesizing CGEvents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complete window discovery&lt;/strong&gt; — On-screen windows come from &lt;code&gt;CGWindowList&lt;/code&gt;. Everything else (titles, minimized windows, hidden apps, other Spaces) comes from a single AXUIElement pass per application, with a messaging timeout on every element. Fun fact: &lt;code&gt;AXUIElementSetMessagingTimeout&lt;/code&gt; is per-&lt;em&gt;element&lt;/em&gt; — setting it on the app element does nothing for the window elements it returns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selectable backgrounds&lt;/strong&gt; — The panel builds one of three root views per style: an opaque &lt;code&gt;NSBox&lt;/code&gt; (Solid), a &lt;code&gt;.hudWindow&lt;/code&gt; &lt;code&gt;NSVisualEffectView&lt;/code&gt; (Transparent — labels become effect-view descendants, so macOS renders them with vibrancy), or an &lt;code&gt;NSGlassEffectView&lt;/code&gt; (Liquid Glass) with the content embedded through its &lt;code&gt;contentView&lt;/code&gt; property, which is the only placement the SDK header guarantees. The persistent scroll view is re-parented between roots only when the preference actually changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MRU ordering&lt;/strong&gt; — Windows are sorted most-recently-used first via NSWorkspace notifications plus per-app AXObserver callbacks that catch intra-app switches (like &lt;code&gt;Cmd-`&lt;/code&gt;). Explicit activations are pinned so the asynchronous window-raise can't demote the window you just picked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-activating overlay&lt;/strong&gt; — The switcher is an &lt;code&gt;NSPanel&lt;/code&gt; with &lt;code&gt;.nonactivatingPanel&lt;/code&gt;, so it appears without stealing focus. Releasing &lt;code&gt;Option&lt;/code&gt; activates the target window, not AltTab.&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%2Fkrvt45fckzhioflvbrin.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%2Fkrvt45fckzhioflvbrin.png" alt="AltTab About dialog, version 1.3.0" width="325" height="282"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two More macOS Gotchas
&lt;/h2&gt;

&lt;p&gt;The 1.3 work surfaced two API behaviors worth knowing about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;withAlphaComponent()&lt;/code&gt; freezes dynamic colors.&lt;/strong&gt; macOS semantic colors like &lt;code&gt;labelColor&lt;/code&gt; are &lt;em&gt;dynamic&lt;/em&gt; — they resolve differently per appearance, at draw time. But call &lt;code&gt;withAlphaComponent(0.78)&lt;/code&gt; on one and you get back a &lt;strong&gt;static&lt;/strong&gt; color, resolved against whatever appearance happens to be current &lt;em&gt;at that call site&lt;/em&gt;. My unit test caught this before it shipped: the derived color measured 1.21:1 contrast in Dark mode — because it had frozen the &lt;em&gt;Light&lt;/em&gt; variant (black text) and composited it onto a dark background. The fix is &lt;code&gt;NSColor(name:dynamicProvider:)&lt;/code&gt;, which re-derives the color inside each appearance resolution. If you take one thing from this post: contrast assertions in unit tests that resolve live system colors are cheap, and they catch exactly this class of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#available&lt;/code&gt; gates runtime, not compile time.&lt;/strong&gt; The Liquid Glass code is properly wrapped in &lt;code&gt;#available(macOS 26.0, *)&lt;/code&gt; — and the release build still failed in CI with &lt;code&gt;cannot find 'NSGlassEffectView' in scope&lt;/code&gt;. Locally everything compiled fine. The difference: my Mac has the macOS 26 SDK; the CI runner had Xcode 16 with the macOS 15 SDK, where the &lt;em&gt;symbol doesn't exist at all&lt;/em&gt;. Availability checks assume the symbol is in the SDK you compile against — they only guard which OS executes it. Bumping the CI image to one with Xcode 26 fixed it. If you adopt Tahoe APIs behind &lt;code&gt;#available&lt;/code&gt;, check your CI's SDK before you tag the release.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Accessibility Toggle Lies
&lt;/h2&gt;

&lt;p&gt;(From the 1.2.1 investigation, still the best bug of this project.) After updating, &lt;code&gt;Option-Tab&lt;/code&gt; went dead while System Settings showed the Accessibility toggle confidently &lt;strong&gt;ON&lt;/strong&gt;. macOS pins every TCC permission grant to a &lt;em&gt;code signing requirement&lt;/em&gt; — for unsigned apps, essentially the cdhash of one exact build. My DMG script built with &lt;code&gt;CODE_SIGNING_ALLOWED=NO&lt;/code&gt;, which ships an &lt;strong&gt;unsealed&lt;/strong&gt; bundle, and macOS fabricates unpredictable code identities for those — so the recorded grant never matched the running binary. The toggle renders "an allowed entry exists"; enforcement asks "does this binary match the requirement". Different questions.&lt;/p&gt;

&lt;p&gt;Diagnosing it meant reading the &lt;code&gt;csreq&lt;/code&gt; blob out of &lt;code&gt;TCC.db&lt;/code&gt; with &lt;code&gt;sqlite3&lt;/code&gt;, decompiling it with &lt;code&gt;csreq -r -t&lt;/code&gt;, comparing cdhashes with &lt;code&gt;codesign -dvvv&lt;/code&gt;, and confirming with &lt;code&gt;CGGetEventTapList&lt;/code&gt; that no event tap existed. The fix: always apply an ad-hoc seal (&lt;code&gt;codesign --force --deep -s -&lt;/code&gt;) when no signing identity is available. If you ever hit a stuck grant on an unsigned app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tccutil reset Accessibility &amp;lt;bundle-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then relaunch and grant again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Stats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~2,100 lines&lt;/strong&gt; of Swift across 13 source files, plus a unit-tested core (32 tests, &lt;code&gt;swift test&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero external dependencies&lt;/strong&gt; — pure Swift + AppKit + ScreenCaptureKit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT licensed&lt;/strong&gt; — fork it, modify it, ship it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS 13+&lt;/strong&gt; (Ventura through Tahoe; Liquid Glass on 26+)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/sergio-farfan/alttab-macos" rel="noopener noreferrer"&gt;github.com/sergio-farfan/alttab-macos&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have been frustrated by macOS window switching, give it a try. And if you are curious about the low-level macOS APIs — CGEvent taps, AXUIElement, ScreenCaptureKit, NSGlassEffectView, or how TCC really decides whether your app is trusted — the codebase is small enough to read in an afternoon.&lt;/p&gt;

&lt;p&gt;Feedback, issues, and PRs are welcome!&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Generate OCI Architecture Diagrams from Terraform with One Claude Code Command</title>
      <dc:creator>Sergio Farfan Cardenete</dc:creator>
      <pubDate>Tue, 17 Mar 2026 01:16:57 +0000</pubDate>
      <link>https://dev.to/sergio_farfn_b071cafc7ed/generate-oci-architecture-diagrams-from-terraform-with-one-claude-code-command-1f4b</link>
      <guid>https://dev.to/sergio_farfn_b071cafc7ed/generate-oci-architecture-diagrams-from-terraform-with-one-claude-code-command-1f4b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (Jul 2026):&lt;/strong&gt; v1.1.0 is out — aspect-correct icons, the official Oracle v24.2 palette and container styles, orthogonal edge routing by default, and a shipped overlap checker. Existing scripts keep running unchanged. See "What's new in v1.1.0" below, or jump straight to the &lt;a href="https://github.com/sergio-farfan/OCI-draw.io-Architect/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Manually drawing OCI diagrams in draw.io is tedious...&lt;br&gt;
If you have ever had to document an OCI architecture, the process is familiar. You open draw.io, locate the correct Oracle icon set, drag shapes onto the canvas, manually wire up VCNs and subnets, nudge elements into alignment — then spend another 30 minutes reconciling colors against Oracle's official template, only to discover that the Terraform configuration changed the previous week and the diagram is already out of date.&lt;/p&gt;

&lt;p&gt;For cloud architects working with OCI, this is a recurring overhead on every project:&lt;/p&gt;
&lt;h2&gt;
  
  
  Diagrams drift from reality
&lt;/h2&gt;

&lt;p&gt;Terraform is the source of truth, but draw.io has no awareness of it. Every infrastructure change requires a manual diagram update — one that typically does not occur until someone requests it during a review.&lt;/p&gt;
&lt;h2&gt;
  
  
  The OCI icon set is not native to draw.io
&lt;/h2&gt;

&lt;p&gt;It must be located, imported, and mapped to the correct services across 15 categories and 220+ icons — a non-trivial exercise before any actual diagramming begins.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layout is time-consuming
&lt;/h2&gt;

&lt;p&gt;Correctly representing the &lt;code&gt;Region → VCN → Subnet → Service&lt;/code&gt; hierarchy, with proper spacing, non-overlapping containers, and Oracle's color scheme, requires significant effort even for experienced practitioners.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hub-and-spoke topologies are especially difficult
&lt;/h2&gt;

&lt;p&gt;Arranging 10–15 spoke VCNs connected through a DRG in a clean, readable layout is an hour-long exercise in manual positioning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Diagrams are created once and abandoned
&lt;/h2&gt;

&lt;p&gt;Because updates are costly, teams stop maintaining them. By the time a new team member onboards or an audit is conducted, the diagram reflects an architecture from two sprints prior.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;The root cause is that architecture diagrams are treated as design artifacts — something produced manually — rather than something derived directly from the infrastructure definition.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sergio-farfan/OCI-draw.io-Architect" rel="noopener noreferrer"&gt;https://github.com/sergio-farfan/OCI-draw.io-Architect&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Claude Code plugin — type /drawio-architect in any project...&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The plugin accepts three input types: a Terraform directory path — parsed to extract VCNs, subnets, gateways, and DRG attachments — a VCN name resolved against existing &lt;code&gt;.tfvars&lt;/code&gt; files, or a plain-text description of the target architecture.&lt;/p&gt;

&lt;p&gt;From any of these inputs, it computes a pixel-precise grid layout, calculating container bounding boxes to eliminate element overlap, and generates a Python script leveraging a custom &lt;code&gt;DrawioBuilder&lt;/code&gt; class &lt;strong&gt;backed by 220 bundled OCI SVG icons&lt;/strong&gt;. Executing the script produces a &lt;code&gt;.drawio&lt;/code&gt; file fully styled with Oracle's official color palette.&lt;/p&gt;

&lt;p&gt;The entire workflow is invoked within Claude Code via a single &lt;code&gt;/drawio-architect&lt;/code&gt; command.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step by Step Installation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; (CLI) installed and working&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 3.9+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;draw.io desktop&lt;/strong&gt; for viewing generated &lt;code&gt;.drawio&lt;/code&gt; files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin bundles 220 OCI SVG icons and auto-installs Pillow (Python imaging library) if missing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Step 1 - Download, Extract and Install
&lt;/h4&gt;

&lt;p&gt;One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/sergio-farfan/OCI-draw.io-Architect/releases/download/v1.1.0/oci-drawio-architect-v1.1.0.tar.gz | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xz&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./oci-drawio-architect/install.sh

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download, extract and execute the Installer script&lt;/li&gt;
&lt;li&gt;Check prerequisites (Python 3, Pillow)&lt;/li&gt;
&lt;li&gt;Install Pillow automatically if missing&lt;/li&gt;
&lt;li&gt;Create a local marketplace at &lt;code&gt;~/.claude/plugins/marketplaces/local/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the plugin files into the marketplace&lt;/li&gt;
&lt;li&gt;Verify all components (5 checks)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 2 - Register in Claude Code
&lt;/h4&gt;

&lt;p&gt;Open Claude Code and run these two commands:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/plugin marketplace add ~/.claude/plugins/marketplaces/local&lt;/code&gt;&lt;br&gt;
&lt;code&gt;/plugin install oci-drawio-architect@local&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Exit Claude Code and reopen it for the plugin to load.&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 3 - Verify
&lt;/h4&gt;

&lt;p&gt;Run inside Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/drawio-architect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command should prompt you for what to diagram.&lt;/p&gt;

&lt;p&gt;Example Snippet:&lt;br&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%2Fpdqpe2sucr46xr68jp8g.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%2Fpdqpe2sucr46xr68jp8g.png" alt=" " width="559" height="566"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What's new in v1.1.0
&lt;/h2&gt;

&lt;p&gt;I've kept using this plugin since I posted this, and v1.1.0 is a big step up in rendering fidelity and workflow safety. Here's what changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aspect-correct icons.&lt;/strong&gt; v1.0.0 forced every icon into a fixed 75x95 slot, which stretched non-square icons by 15-20% on one axis. v1.1.0 reads each SVG's native aspect ratio and derives the cell size from it — fixed 95px height, width computed from the icon's own proportions (or the other way around if you only pass &lt;code&gt;w&lt;/code&gt; or &lt;code&gt;h&lt;/code&gt; to &lt;code&gt;add_icon()&lt;/code&gt;). Icons render true to their actual shape now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Styles aligned to Oracle's official v24.2 toolkit, plus 5 new container types.&lt;/strong&gt; The color palette and every container style are now extracted directly from Oracle's official OCI Architecture Diagram Toolkit (v24.2): dotted-Sienna compartments with Bark labels, a &lt;code&gt;#9E9892&lt;/code&gt; 2pt services-panel border, centered region labels, plain dashed connectors. Five new group types fill out the container vocabulary: &lt;code&gt;tenancy&lt;/code&gt;, &lt;code&gt;availability_domain&lt;/code&gt;, &lt;code&gt;fault_domain&lt;/code&gt;, &lt;code&gt;oracle_services_network&lt;/code&gt;, and &lt;code&gt;onprem&lt;/code&gt; (the old &lt;code&gt;hub&lt;/code&gt; type still works — it's now a deprecated alias of &lt;code&gt;onprem&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orthogonal edge routing by default.&lt;/strong&gt; Port-less &lt;code&gt;add_edge()&lt;/code&gt; calls now hand routing to draw.io's own orthogonal router instead of pinning a fixed exit/entry side. You get cleaner, auto-routed connectors without touching your script.&lt;/p&gt;

&lt;p&gt;Before (v1.0.0, pinned ports on every edge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vcn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;exit_x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exit_y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry_x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry_y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (v1.1.0, default — let draw.io route it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vcn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scripts that still pass &lt;code&gt;exit_x&lt;/code&gt;/&lt;code&gt;exit_y&lt;/code&gt;/&lt;code&gt;entry_x&lt;/code&gt;/&lt;code&gt;entry_y&lt;/code&gt; or &lt;code&gt;waypoints&lt;/code&gt; keep their exact v1.0.0 pinned behavior — nothing breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shipped overlap checker, now a workflow gate.&lt;/strong&gt; &lt;code&gt;scripts/check_overlaps.py&lt;/code&gt; is a standalone CLI (exit &lt;code&gt;0&lt;/code&gt; clean, &lt;code&gt;1&lt;/code&gt; overlaps found, &lt;code&gt;2&lt;/code&gt; usage/parse error), backed by &lt;code&gt;DrawioBuilder.check_overlaps()&lt;/code&gt;. The &lt;code&gt;/drawio-architect&lt;/code&gt; command now runs it as a mandatory gate before handing you the &lt;code&gt;.drawio&lt;/code&gt; file, so overlapping containers get caught before you open draw.io, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata and tooltips.&lt;/strong&gt; &lt;code&gt;add_icon()&lt;/code&gt; and &lt;code&gt;add_group()&lt;/code&gt; take optional &lt;code&gt;metadata=&lt;/code&gt; and &lt;code&gt;tooltip=&lt;/code&gt; arguments now, stored as draw.io &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; attributes. Hover a shape in draw.io to see the tooltip, or open Edit Data to see the metadata — useful for attaching OCIDs, workload tags, or anything else you want to keep with a shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;detect_settings&lt;/code&gt; hardening.&lt;/strong&gt; The Terraform-directory and tenancy-detection logic got a pass: the tenancy-OCID regex is now bounded (no more cross-variable false matches), Terraform-directory discovery is deterministic, OCI CLI JSON handling is more robust, and there's an optional plugin-local &lt;code&gt;logos/&lt;/code&gt; directory for embedding your own logo in a diagram.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real demo.&lt;/strong&gt; &lt;code&gt;examples/generate_demo_diagram.py&lt;/code&gt; builds a full sample architecture — tenancy → on-prem + region → availability/fault domains, compartment → VCN → subnets, a services panel, an Oracle Services Network panel — exercising every container type, the main icon-sizing and edge modes, metadata/tooltips, and the overlap gate in one script. It doubles as a post-install smoke test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python 3.9 is now the floor&lt;/strong&gt; (was 3.8).&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibility
&lt;/h3&gt;

&lt;p&gt;Existing v1.0.0 scripts run unchanged — positional arguments, keyword ports plus waypoints, and the &lt;code&gt;hub&lt;/code&gt; group type are all preserved. Regenerated diagrams will pick up the refreshed v24.2 styling and aspect-correct icons automatically.&lt;/p&gt;

&lt;p&gt;Full release notes: &lt;a href="https://github.com/sergio-farfan/OCI-draw.io-Architect/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;https://github.com/sergio-farfan/OCI-draw.io-Architect/releases/tag/v1.1.0&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>oci</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
