<?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: Rutvik Panchal</title>
    <description>The latest articles on DEV Community by Rutvik Panchal (@rutvik_panchal_b48b8efc56).</description>
    <link>https://dev.to/rutvik_panchal_b48b8efc56</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%2F3879181%2Fdf440e23-a2c0-4f49-a15c-7f7dc87e40a3.jpg</url>
      <title>DEV Community: Rutvik Panchal</title>
      <link>https://dev.to/rutvik_panchal_b48b8efc56</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rutvik_panchal_b48b8efc56"/>
    <language>en</language>
    <item>
      <title>I built a MacBook notch app in SwiftUI. Here's what the notch taught me</title>
      <dc:creator>Rutvik Panchal</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:28:09 +0000</pubDate>
      <link>https://dev.to/rutvik_panchal_b48b8efc56/i-built-a-macbook-notch-app-in-swiftui-heres-what-the-notch-taught-me-nmo</link>
      <guid>https://dev.to/rutvik_panchal_b48b8efc56/i-built-a-macbook-notch-app-in-swiftui-heres-what-the-notch-taught-me-nmo</guid>
      <description>&lt;p&gt;The notch on my MacBook Pro was just sitting there, a black rectangle in the middle of the menu bar. Meanwhile I spent my days doing two things over and over: checking when my next call starts, and digging for something I copied ten minutes ago.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;NotchDesk&lt;/strong&gt;: hover the notch and you get your next meeting with a live countdown and a Join button, plus a private clipboard history you can paste from in any app. It's a native Mac app (SwiftUI + AppKit, macOS 14+), and it launches this week.&lt;/p&gt;

&lt;p&gt;This post isn't a sales pitch. It's the handful of things I had to figure out that I couldn't find written down in one place. If you're thinking of building anything that lives in or around the notch, I hope it saves you a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Finding the notch (and measuring it)
&lt;/h2&gt;

&lt;p&gt;There's no &lt;code&gt;NSScreen.hasNotch&lt;/code&gt;. What you do get, since macOS 12, is &lt;code&gt;safeAreaInsets&lt;/code&gt; and two "auxiliary" areas: the strips of menu bar to the left and right of the camera housing. The notch is whatever's left in the middle:&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;if&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safeAreaInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;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;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auxiliaryTopLeftArea&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auxiliaryTopRightArea&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;right&lt;/span&gt;
    &lt;span class="n"&gt;notchSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safeAreaInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No notch: external display, lid closed, or an older Mac.&lt;/span&gt;
    &lt;span class="c1"&gt;// Draw a small "island" under the menu bar instead.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;menuBarHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleFrame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxY&lt;/span&gt;
    &lt;span class="n"&gt;notchSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuBarHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&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;Two things bit me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The notch moves between screens.&lt;/strong&gt; Close the lid with an external display attached and the notched screen disappears. Listen for &lt;code&gt;NSApplication.didChangeScreenParametersNotification&lt;/code&gt; and re-place your window every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plenty of your users don't have a notch.&lt;/strong&gt; M1 Airs, external displays, the new MacBook Neo. Decide early what your app looks like without one. I went with a small island at the top centre of the main display, and it ended up being a setting people actually use on their external monitors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. A window that lives above the menu bar
&lt;/h2&gt;

&lt;p&gt;The notch overlay is a borderless &lt;code&gt;NSPanel&lt;/code&gt; that never steals focus:&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;panel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nv"&gt;styleMask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;borderless&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nonactivatingPanel&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="nv"&gt;backing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSWindow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSWindow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Level&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mainMenu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collectionBehavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canJoinAllSpaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stationary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullScreenAuxiliary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignoresCycle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isOpaque&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;
&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasShadow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.nonactivatingPanel&lt;/code&gt; means clicking the notch doesn't pull focus away from the app you're typing in. Essential if you ever want to paste into that app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.canJoinAllSpaces&lt;/code&gt; + &lt;code&gt;.stationary&lt;/code&gt; keep it on every Space without animating along with Mission Control.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.ignoresCycle&lt;/code&gt; keeps it out of ⌘-backtick window cycling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part is &lt;strong&gt;mouse pass-through&lt;/strong&gt;. When the notch is expanded, the panel is bigger than the shape the user sees, and you don't want it swallowing clicks meant for menu bar items next to it. I poll the mouse position every 50 ms while it's open and flip &lt;code&gt;ignoresMouseEvents&lt;/code&gt; depending on whether the pointer is over the drawn shape. Crude, but rock solid, and cheaper than it sounds because the timer returns immediately unless the panel is expanded.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Clipboard history: there is no "clipboard changed" notification
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NSPasteboard&lt;/code&gt; has no notification for changes. Every clipboard manager on the Mac polls &lt;code&gt;changeCount&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="n"&gt;pollTimer&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="mf"&gt;0.4&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;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assumeIsolated&lt;/span&gt; &lt;span class="p"&gt;{&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;poll&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;func&lt;/span&gt; &lt;span class="nf"&gt;poll&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;pasteboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changeCount&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;lastChangeCount&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="n"&gt;lastChangeCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pasteboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changeCount&lt;/span&gt;
    &lt;span class="c1"&gt;// read the new item…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.4 s is a good balance: fast enough that nothing feels missed, cheap enough to be invisible in Activity Monitor.&lt;/p&gt;

&lt;p&gt;The part that matters more is &lt;strong&gt;what you refuse to record&lt;/strong&gt;. There's a long-standing convention at &lt;a href="http://nspasteboard.org/" rel="noopener noreferrer"&gt;nspasteboard.org&lt;/a&gt;: apps add marker types to tell clipboard managers how to treat a copy.&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;ignoredTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"org.nspasteboard.ConcealedType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// passwords: never store&lt;/span&gt;
    &lt;span class="s"&gt;"org.nspasteboard.TransientType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// only there for a moment&lt;/span&gt;
    &lt;span class="s"&gt;"org.nspasteboard.AutoGeneratedType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// not copied by the user&lt;/span&gt;
    &lt;span class="s"&gt;"com.agilebits.onepassword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// 1Password's own marker&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of these are on the item, skip it. I also exclude password apps by bundle ID (1Password, Bitwarden, Apple Passwords, Keychain Access and friends) because not every app sets the markers, and I run a small on-device detector for things that &lt;em&gt;look&lt;/em&gt; like secrets (card numbers, one-time codes, API keys) so they're masked and auto-deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The dictation trap
&lt;/h2&gt;

&lt;p&gt;This one came from a user. Dictation apps like Wispr Flow "type" by putting your words on the clipboard, pasting, and then putting back whatever was there before. A naive clipboard manager sees two new copies: the dictation and the restore. Worse: if you'd copied a password just before dictating, the restored copy comes back &lt;strong&gt;without&lt;/strong&gt; its Concealed marker, so it gets saved in plain text.&lt;/p&gt;

&lt;p&gt;The fix is a small guard: when a private (Concealed) write is followed quickly by an unmarked write, treat the second one as a restore and don't record it. Getting the timing window right took a test harness that replays the exact sequence of pasteboard writes, which I'd recommend for anything clipboard-related. It's very hard to reason about by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Finding a screenshot by what it says
&lt;/h2&gt;

&lt;p&gt;Every image and screenshot in the history goes through Vision on the Mac:&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VNRecognizeTextRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recognitionLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accurate&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usesLanguageCorrection&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;try&lt;/span&gt; &lt;span class="kt"&gt;VNImageRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cgImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;request&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topCandidates&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it off the main thread, store the text next to the clip, and searching "gate 22" finds the boarding pass. It's all on device, which made the privacy story easy: nothing leaves the Mac.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Shipping outside the App Store
&lt;/h2&gt;

&lt;p&gt;A notch overlay, global shortcuts and pasting into other apps don't fit the sandbox, so NotchDesk ships as a Developer ID app, notarized by Apple. A few things I learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notarize the DMG, then staple it&lt;/strong&gt; (&lt;code&gt;xcrun notarytool submit --wait&lt;/code&gt;, then &lt;code&gt;xcrun stapler staple&lt;/code&gt;) so it opens even offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store notary credentials in a keychain profile&lt;/strong&gt; and check it exists before you start a release build. Mine vanished twice. I'm moving to an App Store Connect API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't send your DMG over WhatsApp.&lt;/strong&gt; One tester got "NotchDesk can't be opened" with no Open button. The kernel log said the file was quarantined by WhatsApp and "created without user consent", because the chat app saved it automatically. macOS refuses to run those at all, notarized or not. A browser download works fine. I added that to the support page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;NotchDesk launches on &lt;strong&gt;Thursday 15 October&lt;/strong&gt;: meeting countdown and one-click Join (Meet, Zoom, Teams, Webex, FaceTime, Slack huddles), clipboard history with Quick Paste (⌃⌘V), a file shelf with AirDrop, and a few small tools in the notch. No account, no analytics. There's a 14-day free trial; the first 100 buyers get a lifetime license.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://notchdesk.com/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;https://notchdesk.com/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=launch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've built something for the notch, or you've fought &lt;code&gt;NSPasteboard&lt;/code&gt; yourself, I'd love to hear how you handled it in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: an AI assistant drafted this post from NotchDesk's real code and my notes, and I used AI coding assistants while building the app. The code snippets and the war stories are from the real project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Adding 70-language translation to an image API without paying per word</title>
      <dc:creator>Rutvik Panchal</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:03:04 +0000</pubDate>
      <link>https://dev.to/rutvik_panchal_b48b8efc56/adding-70-language-translation-to-an-image-api-without-paying-per-word-1o99</link>
      <guid>https://dev.to/rutvik_panchal_b48b8efc56/adding-70-language-translation-to-an-image-api-without-paying-per-word-1o99</guid>
      <description>&lt;p&gt;I run &lt;a href="https://pixeldrive.pro" rel="noopener noreferrer"&gt;PixelDrive&lt;/a&gt;, an API + editor that turns templates&lt;br&gt;
into branded images. You design once, mark layers as variables, then POST data&lt;br&gt;
and get a PNG. The most common request was localization: teams were making the&lt;br&gt;
same graphic in 10 languages by hand.&lt;/p&gt;

&lt;p&gt;Here's how I built translation into the render pipeline, and why I self-hosted&lt;br&gt;
it instead of calling a cloud translation API.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Translation here is not "translate documents." It's &lt;strong&gt;short, repetitive design&lt;br&gt;
copy&lt;/strong&gt; (headlines, CTAs, labels) rendered onto images, on a server that's&lt;br&gt;
already CPU-bound doing the actual rendering. Two facts drove every decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The text is tiny and cacheable. Translate &lt;code&gt;"Spring Sale" -&amp;gt; es&lt;/code&gt; once, store
it, and every future use is a sub-millisecond cache hit.&lt;/li&gt;
&lt;li&gt;The box has no spare CPU and no GPU. Anything I run competes with the image
renderer.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Why self-host
&lt;/h2&gt;

&lt;p&gt;With caching, the per-word cost of a paid API basically disappears, so cost was&lt;br&gt;
not the deciding factor. The deciding factors were control and not wanting a&lt;br&gt;
network hop in the render path. The trade-off is you have to pick a model that&lt;br&gt;
is small, fast on CPU, and good on short copy.&lt;/p&gt;

&lt;p&gt;The sweet spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;facebook/nllb-200-distilled-1.3B&lt;/code&gt; (200 languages, purpose-built
for translation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; &lt;a href="https://github.com/OpenNMT/CTranslate2" rel="noopener noreferrer"&gt;CTranslate2&lt;/a&gt; with int8
quantization. This is the key piece. It shrinks the model to ~1.3 GB and runs
CPU inference fast. Do &lt;strong&gt;not&lt;/strong&gt; run raw &lt;code&gt;transformers&lt;/code&gt; on CPU for this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A multi-stage Docker build does the conversion once, so the runtime image&lt;br&gt;
carries no torch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# stage 1: convert + quantize (needs torch, thrown away)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;converter&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ctranslate2 &lt;span class="s2"&gt;"transformers[sentencepiece]"&lt;/span&gt; torch &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://download.pytorch.org/whl/cpu
&lt;span class="k"&gt;RUN &lt;/span&gt;ct2-transformers-converter &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--model&lt;/span&gt; facebook/nllb-200-distilled-1.3B &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--quantization&lt;/span&gt; int8 &lt;span class="nt"&gt;--output_dir&lt;/span&gt; /models/nllb-ct2

&lt;span class="c"&gt;# stage 2: runtime (ctranslate2 + tokenizer only)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ctranslate2 &lt;span class="s2"&gt;"transformers[sentencepiece]"&lt;/span&gt; fastapi &lt;span class="s2"&gt;"uvicorn[standard]"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=converter /models /models&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service itself is a tiny FastAPI app with &lt;code&gt;/translate&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;/translate_batch&lt;/code&gt;. NLLB uses FLORES-200 codes (&lt;code&gt;spa_Latn&lt;/code&gt;), so I map ISO codes&lt;br&gt;
(&lt;code&gt;es&lt;/code&gt;) and normalize typographic punctuation (em dashes, curly quotes) that the&lt;br&gt;
tokenizer would otherwise drop as &lt;code&gt;&amp;lt;unk&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into rendering
&lt;/h2&gt;

&lt;p&gt;The render service is the only place that needs translation, and it already has&lt;br&gt;
a Redis/Kvrocks cache. The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A text field can carry a language: &lt;code&gt;{ "headline": { "text": "Spring Sale", "lang": "es" } }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The render cache key already includes the full payload (so &lt;code&gt;es&lt;/code&gt; and &lt;code&gt;fr&lt;/code&gt; are
different cache entries). On a &lt;strong&gt;cache miss only&lt;/strong&gt;, translate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;translatePayload()&lt;/code&gt; collects every text value with a &lt;code&gt;lang&lt;/code&gt;, checks the
cache (&lt;code&gt;tr:&amp;lt;lang&amp;gt;:&amp;lt;sha(text)&amp;gt;&lt;/code&gt;), batches the misses to the translation
service, writes results back to the cache, and swaps the text in.&lt;/li&gt;
&lt;li&gt;The harness draws the translated text. The same cache key is used by the
editor, so a translation done in the editor is reused at render time and
vice versa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two operational details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The translation container is &lt;strong&gt;CPU-capped&lt;/strong&gt; (a couple of cores). Even a burst
of cache misses can't starve the renderer.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;best-effort&lt;/strong&gt;: if the service is unavailable, it falls back to the
original text. Translation never breaks a render.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/render&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to our spring sale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;renders &lt;strong&gt;"Bienvenido a nuestra venta de primavera"&lt;/strong&gt; onto the image. In the&lt;br&gt;
editor there's a one-click "translate the whole template" button with a&lt;br&gt;
searchable 70-language picker, and the bulk-CSV flow supports a per-row &lt;code&gt;lang&lt;/code&gt;&lt;br&gt;
column so a single upload can render a batch across markets.&lt;/p&gt;

&lt;p&gt;Because every &lt;code&gt;(text, language)&lt;/code&gt; pair is cached forever, the model only runs on&lt;br&gt;
genuinely new strings, which for marketing copy is rare after warmup. A small&lt;br&gt;
CPU model plus aggressive caching turned out to be a better fit than a cloud API&lt;br&gt;
for this particular shape of problem.&lt;/p&gt;

&lt;p&gt;If you want to see it in action: &lt;a href="https://pixeldrive.pro" rel="noopener noreferrer"&gt;pixeldrive.pro&lt;/a&gt;.&lt;br&gt;
Happy to answer questions about the setup in the comments.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building an Open-Source Polotno Alternative (Canva-like Editor)</title>
      <dc:creator>Rutvik Panchal</dc:creator>
      <pubDate>Tue, 14 Apr 2026 18:48:45 +0000</pubDate>
      <link>https://dev.to/rutvik_panchal_b48b8efc56/building-an-open-source-polotno-alternative-canva-like-editor-53pp</link>
      <guid>https://dev.to/rutvik_panchal_b48b8efc56/building-an-open-source-polotno-alternative-canva-like-editor-53pp</guid>
      <description>&lt;p&gt;In the past few weeks, I built an open-source design editor that works similarly to Polotno — and decided to open-source it as OpenPolotno.&lt;/p&gt;

&lt;p&gt;The goal was simple: create a Canva-like editor that developers can fully control, customize, and self-host.&lt;/p&gt;

&lt;p&gt;🚀 Why I Built This&lt;/p&gt;

&lt;p&gt;Polotno is a great tool, especially for quickly building design editors.&lt;/p&gt;

&lt;p&gt;But while working with it, I felt a few limitations:&lt;/p&gt;

&lt;p&gt;Limited control over customization&lt;br&gt;
Dependency on a proprietary ecosystem&lt;br&gt;
Constraints when scaling or extending features&lt;/p&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;p&gt;Gives full ownership of the editor&lt;br&gt;
Allows deep customization&lt;br&gt;
Can be self-hosted without restrictions&lt;/p&gt;

&lt;p&gt;So I decided to build an alternative — compatible with Polotno, but fully open.&lt;/p&gt;

&lt;p&gt;🧠 The Idea&lt;/p&gt;

&lt;p&gt;Instead of reinventing everything, I focused on:&lt;/p&gt;

&lt;p&gt;👉 Compatibility&lt;/p&gt;

&lt;p&gt;If developers are already using Polotno, they shouldn’t need to rewrite everything.&lt;/p&gt;

&lt;p&gt;So OpenPolotno supports:&lt;/p&gt;

&lt;p&gt;Similar API structure&lt;br&gt;
Template compatibility&lt;br&gt;
Familiar data format&lt;/p&gt;

&lt;p&gt;This makes migration or integration much easier.&lt;/p&gt;

&lt;p&gt;✨ Features&lt;/p&gt;

&lt;p&gt;Here’s what the editor currently supports:&lt;/p&gt;

&lt;p&gt;🎨 Drag-and-drop canvas editor&lt;br&gt;
🖼️ Text, images, and layer management&lt;br&gt;
🧩 Polotno-compatible templates&lt;br&gt;
🔌 Similar API structure&lt;br&gt;
💾 JSON-based import/export&lt;br&gt;
⚡ Fast and lightweight performance&lt;br&gt;
🔓 Fully open-source&lt;br&gt;
🧱 Challenges I Faced&lt;/p&gt;

&lt;p&gt;Building a design editor is not trivial. Some of the biggest challenges were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Canvas Rendering Performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Handling multiple elements (text, images, layers) while keeping the editor smooth required careful optimization.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Data Structure Compatibility&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Making the system compatible with Polotno templates while keeping it flexible internally was tricky.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UX Complexity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even small features (like dragging, resizing, layering) require a lot of edge-case handling.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Designing the architecture so it can grow into a full product (plugins, extensions, etc.) took planning.&lt;/p&gt;

&lt;p&gt;🎯 What This Can Be Used For&lt;/p&gt;

&lt;p&gt;OpenPolotno can be used to build:&lt;/p&gt;

&lt;p&gt;Canva-like SaaS products&lt;br&gt;
Social media design tools&lt;br&gt;
Internal design platforms&lt;br&gt;
Marketing automation tools&lt;/p&gt;

&lt;p&gt;Basically, anything that requires a visual editor.&lt;/p&gt;

&lt;p&gt;🔗 Links&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/therutvikp/OpenPolotno" rel="noopener noreferrer"&gt;https://github.com/therutvikp/OpenPolotno&lt;/a&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/openpolotno" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/openpolotno&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💬 Feedback Welcome&lt;/p&gt;

&lt;p&gt;This is just the beginning. I’d love feedback from developers who:&lt;/p&gt;

&lt;p&gt;Have used Polotno&lt;br&gt;
Are building design tools&lt;br&gt;
Need a customizable editor&lt;/p&gt;

&lt;p&gt;If you have suggestions or ideas, feel free to share 🙌&lt;/p&gt;

</description>
      <category>design</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
