<?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: hiyoyo</title>
    <description>The latest articles on DEV Community by hiyoyo (@hiyoyok).</description>
    <link>https://dev.to/hiyoyok</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%2F3851832%2Fa2762ba1-e687-4ae9-901d-245b96cf95d6.jpg</url>
      <title>DEV Community: hiyoyo</title>
      <link>https://dev.to/hiyoyok</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hiyoyok"/>
    <language>en</language>
    <item>
      <title>Building an Android File Transfer Alternative for macOS: Why It Had to Be Rust</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Mon, 06 Jul 2026 05:47:41 +0000</pubDate>
      <link>https://dev.to/hiyoyok/building-an-android-file-transfer-alternative-for-macos-why-it-had-to-be-rust-1m0h</link>
      <guid>https://dev.to/hiyoyok/building-an-android-file-transfer-alternative-for-macos-why-it-had-to-be-rust-1m0h</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Android file transfer on macOS has been a broken experience for a decade — Google's official app is buggy, and most alternatives either wrap Electron (heavy) or bolt a Go MTP library onto a native Swift UI via CGO (fast UI, but a fragile bridge underneath). When I set out to build my own alternative, HiyokoMTP, I picked pure Rust + Tauri: one language from the USB layer to the transfer engine, no GC pauses, no cross-runtime signal handling headaches. This post walks through the real macOS/Android edge cases that made that choice non-negotiable — device claim conflicts with macOS's own background processes, MediaStore stalls on large batch transfers, and why "it connects" and "it transfers reliably" are two very different engineering problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem nobody really solves
&lt;/h2&gt;

&lt;p&gt;If you've ever plugged an Android phone into a Mac, you know the experience: Google's official Android File Transfer app is functionally frozen in time — 4GB file size limits, random disconnects, no rename support. So a small ecosystem of alternatives has grown up around this gap, mostly falling into two camps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Electron-based tools&lt;/strong&gt; — cross-platform, but heavy (200-400MB+ binaries) and never quite feel native on macOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Swift UI + Go MTP backend&lt;/strong&gt; — lightweight, native-feeling UI, but the actual device communication runs in a Go library compiled to a C archive and called through CGO.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went a third way: &lt;strong&gt;Rust end-to-end&lt;/strong&gt;, using &lt;a href="https://github.com/kevinmehall/nusb" rel="noopener noreferrer"&gt;nusb&lt;/a&gt; for USB access, wrapped in a Tauri shell with a React frontend. No CGO, no separate garbage-collected runtime living inside a native app, no C bridge translating between memory models.&lt;/p&gt;

&lt;p&gt;Here's what actually drove that decision — not language fandom, but specific failure modes I ran into while building this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode #1: macOS already has its hands on your device
&lt;/h2&gt;

&lt;p&gt;The most common — and most confusing — connection failure isn't a USB issue at all. macOS ships with its own background processes for handling imaging devices (the same machinery behind Image Capture and Preview's "import from device" feature). When an Android phone connects in MTP mode, these processes can grab the device's PTP/MTP interface &lt;em&gt;before your app gets a chance to&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The result, from the user's side: "Device busy" or a silent connection failure that looks like a bug in your app, when it's actually a resource contention issue with macOS itself.&lt;/p&gt;

&lt;p&gt;I didn't figure this out from documentation — I found it by comparing &lt;code&gt;system_profiler&lt;/code&gt;/IORegistry output and &lt;code&gt;lsof&lt;/code&gt;-style process inspection at the exact moment a connection failed, and noticing the same handful of Apple process names showing an open handle on the device every time. Once you know to look, the pattern is obvious; before that, it just looks like your USB code is broken.&lt;/p&gt;

&lt;p&gt;Handling this properly means your app has to actively check for and release these conflicting processes before attempting to claim the interface — and retry with backoff, because the timing of when macOS's own daemon grabs the device is not deterministic. This is the kind of edge case you only find by testing against real devices repeatedly, not by reading the MTP spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode #2: the Android side doesn't keep up
&lt;/h2&gt;

&lt;p&gt;The other class of failures happens &lt;em&gt;after&lt;/em&gt; a successful connection, during bulk transfers. Android's MediaStore (the system database that indexes media files) does write-side bookkeeping every time a file lands on the device. If you fire off writes faster than MediaStore can index them — which is easy to do once you're using a modern high-speed USB connection — the device-side MTP stack can stall waiting on the database, and the whole transfer session hangs.&lt;/p&gt;

&lt;p&gt;This one took longer to isolate — my first instinct was to blame USB bandwidth or cable quality, and chasing that dead end wasted a few days before I looked in the right place. It doesn't fail consistently either, because it depends on the phone model, how full its MediaStore index already is, and how many small files are in the batch. What eventually made it click was watching transfer speed graphs over long batch jobs: instead of a flat line, I'd see the throughput trail off and then flatline entirely partway through, always around the same file count on a given device. That "it always stalls around file N, not randomly" pattern is what pointed at a device-side bottleneck rather than a USB or driver issue.&lt;/p&gt;

&lt;p&gt;The fix isn't more raw throughput — it's pacing. Deliberately inserting small breathing gaps at intervals during large batch transfers, and being conservative about how often you refresh UI state (since a naive "re-list the folder after every file" approach re-triggers MTP traffic that competes with the actual transfer and makes things worse, not better).&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's the unglamorous, accumulated knowledge of "what actually breaks when real Android phones talk to a real Mac over real USB," and it only comes from hammering the thing with many devices over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the language choice actually matters here
&lt;/h2&gt;

&lt;p&gt;None of the above is language-specific in principle — you could handle process conflicts and write-pacing in Go just as well as Rust. But the &lt;em&gt;architecture&lt;/em&gt; the language choice forces you into matters a lot in practice. Laid out side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Electron&lt;/th&gt;
&lt;th&gt;Native Swift + Go core&lt;/th&gt;
&lt;th&gt;Rust end-to-end&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;Large (200–400MB+)&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;Small–medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC pauses in the transfer path&lt;/td&gt;
&lt;td&gt;Yes (JS)&lt;/td&gt;
&lt;td&gt;Yes (Go)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-runtime FFI boundary&lt;/td&gt;
&lt;td&gt;No (single JS runtime)&lt;/td&gt;
&lt;td&gt;Yes (Swift ↔ C ↔ Go)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal-handling conflicts&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Yes — Go's runtime installs its own handlers (SIGPIPE, SIGURG collisions when embedded via CGO)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapting to a new device quirk&lt;/td&gt;
&lt;td&gt;Easy (single runtime, but heavier at rest)&lt;/td&gt;
&lt;td&gt;Touches 2–3 layers&lt;/td&gt;
&lt;td&gt;One layer, one language&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that matters most day-to-day is the last one. Splitting the work across 2–3 layers doesn't just mean more surface area to touch — the boundary itself becomes a source of bugs. The SIGPIPE/SIGURG collision wasn't caused by writing new feature code; it was caused by the act of wiring two runtimes together in the first place. Rust end-to-end removes that boundary entirely: the USB layer, the transfer engine, and the pacing/retry logic all live in one memory model, with no FFI boundary to reason about and no second garbage collector running inside your process.&lt;/p&gt;

&lt;p&gt;The Tauri/React frontend is a tradeoff in the other direction — you give up a bit of native "feel" and binary size compared to pure SwiftUI. But for the part of the app that actually has to be &lt;em&gt;correct under real-world device weirdness&lt;/em&gt; — the USB and MTP layer — a single-language, GC-free stack removed an entire category of bugs before I even had to think about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The MTP spec itself was never the hard part. The hard part was the accumulated pile of "here's what actually happens with real devices" — and I didn't want to be debugging that pile through a C shim while also hoping Go's signal handlers stayed out of Swift's way. Keeping the whole USB-facing side of HiyokoMTP in one language, with no FFI boundary between me and the bug, was less about picking a "better" language and more about making sure that when something inevitably breaks in the field, I have one stack to look at instead of three.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;HiyokoMTP is a macOS Android file transfer app built with Rust + Tauri. If you've fought with MTP on macOS before, I'd love to hear your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>mac</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why I Chose Tauri over Electron for Hardware-Heavy Tools</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:52:52 +0000</pubDate>
      <link>https://dev.to/hiyoyok/why-i-chose-tauri-over-electron-for-hardware-heavy-tools-4759</link>
      <guid>https://dev.to/hiyoyok/why-i-chose-tauri-over-electron-for-hardware-heavy-tools-4759</guid>
      <description>&lt;h1&gt;
  
  
  Why I Chose Tauri over Electron for Hardware-Heavy Tools
&lt;/h1&gt;

&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). When your tools talk directly to USB hardware, the gap between Tauri and Electron isn't a preference—it's a performance cliff.&lt;/p&gt;

&lt;p&gt;When you're building tools that interact directly with hardware—Android devices via MTP, ADB over USB, real-time system monitoring—the bridge between your UI and the operating system is everything. I evaluated both Electron and Tauri for the Hiyoko Suite. On my 8-year-old MacBook Air with 8GB of RAM, Tauri wasn't just better. It was the only viable option.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tauri uses the system WebView (~25MB RAM) vs. Electron's bundled Chromium (~350MB RAM).&lt;/li&gt;
&lt;li&gt;Rust's direct memory management makes USB buffer handling safe and predictable—no GC pauses during file transfers.&lt;/li&gt;
&lt;li&gt;Binary sizes: Hiyoko apps average 8-12MB. An equivalent Electron app starts at 150MB+.&lt;/li&gt;
&lt;li&gt;Tauri v2's plugin system makes hardware permissions explicit and auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Memory Budget Problem
&lt;/h2&gt;

&lt;p&gt;On an 8GB machine, memory allocation is a zero-sum game. Here's my typical development session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS + WindowServer:    ~2.5 GB
VS Code + extensions:    ~800 MB
Terminal + rust-analyzer: ~600 MB
Physical Android device:  (USB, no memory cost)
─────────────────────────────────
Available for apps:      ~4.1 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single Electron app consuming 350MB would eat 8.5% of my total RAM. With 10 apps in the suite, that's 3.5GB—leaving nothing for actual work. Tauri apps at 25MB each use 250MB total. That 3.25GB difference is the difference between a working machine and a frozen one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust Backend for Hardware I/O
&lt;/h2&gt;

&lt;p&gt;The critical advantage of Tauri isn't just memory—it's the backend language. USB device communication involves raw buffer management, concurrent I/O streams, and hardware interrupts. These are Rust's strengths and Node.js's weaknesses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified USB bulk transfer in Rust — zero-copy, no GC pauses&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;nusb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RequestBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_bulk_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;nusb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pre-allocate buffer — no GC will move this during transfer&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;RequestBuffer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Submit USB transfer — OS handles DMA directly&lt;/span&gt;
    &lt;span class="k"&gt;let&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;interface&lt;/span&gt;
        &lt;span class="nf"&gt;.bulk_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HiyokoError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;UsbTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="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;In an Electron app, this same operation would require:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Node.js native module (C++ addon) for USB access.&lt;/li&gt;
&lt;li&gt;A bridge layer to pass buffers between V8's managed heap and native memory.&lt;/li&gt;
&lt;li&gt;Hope that V8's garbage collector doesn't pause during a critical transfer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With Rust, the buffer stays exactly where I put it. No GC pauses. No bridge overhead. The USB transfer runs at the speed the hardware allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cold Start and Binary Size
&lt;/h2&gt;

&lt;p&gt;Users notice launch time. On my MacBook Air's SATA SSD (not the fast NVMe in newer Macs), the difference is measurable:&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;# Cold start comparison (measured with `time`)&lt;/span&gt;
&lt;span class="c"&gt;# HiyokoMTP (Tauri v2):&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;open &lt;span class="nt"&gt;-a&lt;/span&gt; HiyokoMTP
&lt;span class="c"&gt;# real 0.8s — app window visible and responsive&lt;/span&gt;

&lt;span class="c"&gt;# Equivalent Electron app:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;open &lt;span class="nt"&gt;-a&lt;/span&gt; ElectronMTPTool
&lt;span class="c"&gt;# real 3.2s — Chromium initialization + V8 warmup&lt;/span&gt;

&lt;span class="c"&gt;# Binary size on disk:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /Applications/HiyokoMTP.app
&lt;span class="c"&gt;# 11M    /Applications/HiyokoMTP.app&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /Applications/ElectronMTPTool.app
&lt;span class="c"&gt;# 187M   /Applications/ElectronMTPTool.app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 0.8s launch time matters because HiyokoShot (the screenshot tool) needs to feel instant—click the menu bar icon, and it should be ready. A 3-second cold start would make it feel like a web page loading, not a native tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit Hardware Permissions
&lt;/h2&gt;

&lt;p&gt;Tauri v2's capability system maps cleanly to hardware access patterns. Each app declares exactly what it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hardware-access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USB and filesystem permissions for MTP transfers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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="s2"&gt;"fs:allow-read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs:allow-write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"shell:allow-execute"&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;In Electron, Node.js has ambient access to everything—filesystem, network, child processes—with no granular permission model. For hardware-heavy tools that users run with sensitive data, explicit permissions aren't just nice to have. They're how you build trust with security-conscious developers.&lt;/p&gt;

&lt;p&gt;If you're building specialized utilities—especially anything that touches hardware—Tauri with Rust gives you native performance, memory safety, and a small footprint that Electron can't match. The trade-off is learning Rust, but for hardware I/O, that's an investment that pays for itself immediately.&lt;/p&gt;

&lt;p&gt;Have you built hardware-interfacing desktop apps? What framework did you use, and how did you handle the native bridge?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2e966b64-554e-42a0-b865-4240281978a1" rel="noopener noreferrer"&gt;HiyokoMTP&lt;/a&gt;&lt;/strong&gt; — Android↔Mac MTP file transfer with custom nusb-based stack.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>electron</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From Zero to 10 Apps: My Multi-App Productivity System</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:10:26 +0000</pubDate>
      <link>https://dev.to/hiyoyok/from-zero-to-10-apps-my-multi-app-productivity-system-47pd</link>
      <guid>https://dev.to/hiyoyok/from-zero-to-10-apps-my-multi-app-productivity-system-47pd</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). Over the past three years, I've built and released 10 specialized macOS apps for Android developers—not by working harder, but by building a system that makes shipping the next app easier than the last.&lt;/p&gt;

&lt;p&gt;Many developers struggle to finish a single side project. I've shipped 10 production apps while working solo on aging hardware. The secret isn't discipline or caffeine—it's architecture. Each app I build makes the next one faster, because 80% of the plumbing is already solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Micro-App" architecture keeps each codebase small, focused, and independently deployable.&lt;/li&gt;
&lt;li&gt;A shared internal crate (&lt;code&gt;hiyoko-helper&lt;/code&gt;) provides licensing, auto-start, and path management—write once, use in all 10 apps.&lt;/li&gt;
&lt;li&gt;Shared CSS tokens and React components eliminate UX decision-making for new apps.&lt;/li&gt;
&lt;li&gt;On a dual-core Intel MacBook Air, small codebases mean fast compile times—under 2 minutes per app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Micro-App Philosophy
&lt;/h2&gt;

&lt;p&gt;The temptation is to build one "Ultimate Android Suite" that does everything. I tried that approach early on and abandoned it. A monolithic app means a single bug can block the entire release. On my 8GB machine, a large codebase means 15+ minute compile times.&lt;/p&gt;

&lt;p&gt;Instead, each Hiyoko app solves exactly one problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HiyokoMTP       → File transfer (MTP protocol)
HiyokoShot      → Screenshot capture + transfer
HiyokoLogcat    → Real-time log viewer + AI diagnostics
HiyokoPDFVault  → PDF tools + encryption
HiyokoBar       → System HUD in menu bar
HiyokoClip      → Clipboard manager
HiyokoSync      → Differential file sync
HiyokoAutoSync  → Zero-touch wireless sync
HiyokoKit       → All-in-one bundle
HiyokoLogcat    → Free OSS log viewer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a bug appears in HiyokoMTP's file transfer logic, I can fix and ship a patch in 30 minutes without touching (or risking) the PDF encryption code. Small blast radius, fast iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 80% Reuse Strategy
&lt;/h2&gt;

&lt;p&gt;Every app needs the same foundational behaviors: license checking, auto-start registration, macOS path resolution, update checking. Building this from scratch 10 times would be insanity.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hiyoko-helper&lt;/code&gt; crate handles all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every app's main.rs starts with the same 3 lines:&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hiyoko_helper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;init_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-mtp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_PKG_VERSION"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to initialize app"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.manage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.invoke_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_handler!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* app-specific commands */&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_context!&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error running app"&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;When I build a new app, the first thing I do is add &lt;code&gt;hiyoko-helper = { path = "../hiyoko-helper" }&lt;/code&gt; to &lt;code&gt;Cargo.toml&lt;/code&gt;. Licensing, auto-start, and path management work immediately. I can focus 100% on the app's unique logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eliminating UX Decisions
&lt;/h2&gt;

&lt;p&gt;Decision fatigue is a real productivity killer. For a solo developer, every decision you &lt;em&gt;don't&lt;/em&gt; have to make is time saved.&lt;/p&gt;

&lt;p&gt;I eliminated UX decisions by standardizing everything visual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Import once, design is done */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;'../shared/hiyoko-tokens.css'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Each app only sets its accent color */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4a9eff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* HiyokoMTP = blue */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Everything else — backgrounds, typography, spacing,
   glassmorphism, popovers — is inherited from tokens */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// New app scaffold — takes ~10 minutes&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Toolbar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../shared/components/Toolbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SettingsModal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../shared/components/SettingsModal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Toast&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../shared/components/Toast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-app"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Toolbar&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"HiyokoNewApp"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* App-specific content goes here */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Toast&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;The result: building a new app's UI scaffold takes about 10 minutes. The design already looks like it belongs in the Hiyoko Suite because it &lt;em&gt;is&lt;/em&gt; the Hiyoko Suite—same tokens, same components, different accent color.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compound Effect
&lt;/h2&gt;

&lt;p&gt;Each app I ship makes the next one faster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App #1 (HiyokoMTP): 3 months. Built everything from scratch.&lt;/li&gt;
&lt;li&gt;App #3 (HiyokoShot): 3 weeks. Shared crate and components existed.&lt;/li&gt;
&lt;li&gt;App #7 (HiyokoClip): 10 days. The pattern was fully established.&lt;/li&gt;
&lt;li&gt;App #10 (HiyokoKit): 2 weeks. Mostly integration of existing engines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This compound effect is the real productivity system. It's not about working harder—it's about making each unit of work count toward every future project.&lt;/p&gt;

&lt;p&gt;Productivity for a solo developer isn't about typing faster. It's about making fewer decisions and reusing more solutions. Build the system first, then the apps practically build themselves.&lt;/p&gt;

&lt;p&gt;How do you handle the overhead of multiple projects? Do you invest in shared infrastructure, or do you prefer each project to be fully independent?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2c94dd0f-e28a-4a17-8efc-7bd93087d46d" rel="noopener noreferrer"&gt;HiyokoKit&lt;/a&gt;&lt;/strong&gt; — all-in-one Android dev toolkit (MTP file manager, Logcat+AI, ADB Tools, Monitor).&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>mac</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Business on LemonSqueezy: My Solo Dev Setup</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:54:32 +0000</pubDate>
      <link>https://dev.to/hiyoyok/building-a-business-on-lemonsqueezy-my-solo-dev-setup-22eg</link>
      <guid>https://dev.to/hiyoyok/building-a-business-on-lemonsqueezy-my-solo-dev-setup-22eg</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). My entire business infrastructure—payments, taxes, licensing—runs through a single platform, and the integration code fits in about 200 lines of Rust.&lt;/p&gt;

&lt;p&gt;Being a great developer is only half of building a software business. You also need to handle global payments, tax compliance, and license management. When I started selling the Hiyoko Suite internationally, I needed a payment system that handled the complexity of VAT, GST, and sales tax in 100+ countries without requiring me to become a tax lawyer. LemonSqueezy's Merchant of Record model solved this completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Merchant of Record (MoR) services like LemonSqueezy handle international tax compliance—you receive net payouts, they handle the rest.&lt;/li&gt;
&lt;li&gt;Per-app licensing with unique checkout links lets users buy only what they need.&lt;/li&gt;
&lt;li&gt;License validation happens via a simple API call on app startup, with offline grace periods for reliability.&lt;/li&gt;
&lt;li&gt;The checkout flow runs in a secure Tauri webview window—zero native payment UI code needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Merchant of Record Matters
&lt;/h2&gt;

&lt;p&gt;If you sell software directly to 100+ countries, you're technically responsible for collecting and remitting VAT (EU), GST (Australia, India), sales tax (US states), and consumption tax (Japan). The compliance burden alone would consume my entire development time.&lt;/p&gt;

&lt;p&gt;LemonSqueezy acts as the legal seller. They collect payment, handle all tax obligations, and send me a net payout. From a tax perspective, I have one "customer" (LemonSqueezy), not thousands of individual buyers across dozens of tax jurisdictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-App Checkout Links
&lt;/h2&gt;

&lt;p&gt;Each Hiyoko app has its own LemonSqueezy checkout URL. This supports a per-app pricing model where users buy only the tools they need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// License configuration — each app maps to its own checkout&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;AppLicense&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;checkout_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;trial_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LICENSES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppLicense&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;AppLicense&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hiyoko-mtp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;checkout_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://hiyokomtp.lemonsqueezy.com/checkout/buy/2e966b64-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;trial_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;AppLicense&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hiyoko-shot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;checkout_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://hiyokomtp.lemonsqueezy.com/checkout/buy/6cd85b5d-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;trial_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 8 more apps&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HiyokoKit serves as the "All-in-One" bundle—a single purchase that unlocks the full suite at a discount. For users who only need screenshot transfers, HiyokoShot is available standalone for $7.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkout in a Tauri Webview
&lt;/h2&gt;

&lt;p&gt;Integrating a payment flow into a desktop app doesn't require building native payment UI. I open the LemonSqueezy hosted checkout in a secure Tauri webview window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;open_checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkout_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Open checkout in a dedicated window&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;WebviewWindowBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;WebviewUrl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;External&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkout_url&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{e}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Purchase — Hiyoko Suite"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.inner_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;480.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;720.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.resizable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.center&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checkout page is fully hosted by LemonSqueezy—responsive design, payment method selection, tax calculation, and receipt generation all happen on their end. My app just opens a window and waits for the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  License Validation
&lt;/h2&gt;

&lt;p&gt;After purchase, the user receives a license key. On app startup, the &lt;code&gt;hiyoko-helper&lt;/code&gt; crate validates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;LicenseResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;license_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LicenseKeyData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;LicenseKeyData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// "active", "expired", "disabled"&lt;/span&gt;
    &lt;span class="n"&gt;activation_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;activations_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/// Validate a license key against LemonSqueezy's API&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;validate_license&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;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LicenseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
        &lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.lemonsqueezy.com/v1/licenses/validate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="s"&gt;"license_key"&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="s"&gt;"instance_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_machine_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="py"&gt;.valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="py"&gt;.license_key.status&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LicenseState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LicenseState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Expired&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;                &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LicenseState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Invalid&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="cd"&gt;/// Startup check with offline grace period&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;check_license_on_startup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_stored_license&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;validate_license_sync&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;stored&lt;/span&gt;&lt;span class="py"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;cache_validation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&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;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Network failure — use cached result with 7-day grace&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_cached_validation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="nf"&gt;.age_days&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="py"&gt;.state&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LicenseState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RequiresOnlineCheck&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;&lt;em&gt;The actual implementation handles additional edge cases not shown here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The offline grace period is critical. My users are mobile developers who might be on a plane, in a subway, or in a building with no internet. Requiring a network connection just to open a file transfer tool would be hostile UX. The 7-day cache means the app works reliably offline while still preventing license abuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works for Solo Devs
&lt;/h2&gt;

&lt;p&gt;The entire business stack—checkout, licensing, tax compliance, receipt generation—is handled by LemonSqueezy plus about 200 lines of Rust in &lt;code&gt;hiyoko-helper&lt;/code&gt;. I don't run a payment server. I don't manage a customer database. I don't file VAT returns in 27 EU member states.&lt;/p&gt;

&lt;p&gt;My 8-year-old MacBook Air doesn't need to be a commerce platform. It just needs to build great software and call a simple API. Focus on your code, let the platforms handle the complexity.&lt;/p&gt;

&lt;p&gt;What payment/licensing setup do you use for your indie projects? Has anyone tried the Merchant of Record model versus handling taxes yourself?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2c94dd0f-e28a-4a17-8efc-7bd93087d46d" rel="noopener noreferrer"&gt;HiyokoKit&lt;/a&gt;&lt;/strong&gt; — all-in-one Android dev toolkit (MTP file manager, Logcat+AI, ADB Tools, Monitor).&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>developer</category>
      <category>webdev</category>
      <category>rust</category>
      <category>tauri</category>
    </item>
    <item>
      <title>The Ethics of "Offline-First" AI: Privacy as a Competitive Advantage</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:06:53 +0000</pubDate>
      <link>https://dev.to/hiyoyok/the-ethics-of-offline-first-ai-privacy-as-a-competitive-advantage-1k5p</link>
      <guid>https://dev.to/hiyoyok/the-ethics-of-offline-first-ai-privacy-as-a-competitive-advantage-1k5p</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). When I added AI diagnostics to my logcat viewer, the first question wasn't "which model"—it was "how do I keep user data off someone else's server."&lt;/p&gt;

&lt;p&gt;In the age of cloud-everything, privacy has become a luxury. When I built the AI features for HiyokoLogcat and HiyokoPDFVault, I faced a real engineering challenge: how do you give developers the power of LLMs without exposing their sensitive log data? The answer lies in an "Offline-First" architecture with rigorous local masking.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Logcat output contains sensitive data (IPs, tokens, emails) that must be masked before any external API call.&lt;/li&gt;
&lt;li&gt;A Rust-based regex masking pipeline runs locally, replacing sensitive patterns with placeholders before data reaches Gemini.&lt;/li&gt;
&lt;li&gt;HiyokoPDFVault uses &lt;code&gt;candle-core&lt;/code&gt; for fully local semantic search and OCR—no server involved.&lt;/li&gt;
&lt;li&gt;Users get explicit, opt-in consent dialogs before any data leaves the device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem: Logs Are Full of Secrets
&lt;/h2&gt;

&lt;p&gt;Logcat output from an Android device is a debugging goldmine, but it routinely contains data that should never leave the developer's machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Typical logcat output with sensitive data
D/NetworkManager: Connected to 192.168.1.105:5555
D/AuthService: Bearer token: eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...
I/UserManager: Logged in user: dev@company-internal.com
W/CrashHandler: Stack trace uploaded to https://internal-sentry.corp.net/api/12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sending this directly to a cloud AI for analysis is a security incident waiting to happen. IP addresses reveal network topology. Bearer tokens grant access. Email addresses identify individuals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Masking Pipeline
&lt;/h2&gt;

&lt;p&gt;Before a single log line is sent to the Gemini API, HiyokoLogcat runs a Rust-based masking pipeline. The pipeline is fast enough to process thousands of lines per second on my dual-core MacBook Air:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;once_cell&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Pre-compiled regex patterns for performance&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;MASKING_RULES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MaskRule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;MaskRule&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?\b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"[IP_MASKED]"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;MaskRule&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"[EMAIL_MASKED]"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;MaskRule&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;r"(?i)(bearer|token|api[_-]?key|password|secret)\s*[:=]\s*\S+"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"[CREDENTIAL_MASKED]"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;MaskRule&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;r"https?://[^\s]+(?:internal|corp|staging|local)[^\s]*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"[INTERNAL_URL_MASKED]"&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;struct&lt;/span&gt; &lt;span class="n"&gt;MaskRule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;MaskRule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&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;MaskRule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid regex pattern"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;replacement&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="cd"&gt;/// Mask all sensitive patterns in a log line&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;mask_sensitive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.to_string&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;rule&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MASKING_RULES&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&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;rule&lt;/span&gt;&lt;span class="py"&gt;.pattern&lt;/span&gt;&lt;span class="nf"&gt;.replace_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="py"&gt;.replacement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_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;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After masking, the same logcat output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Masked output — safe to send to Gemini API
D/NetworkManager: Connected to [IP_MASKED]
D/AuthService: [CREDENTIAL_MASKED]
I/UserManager: Logged in user: [EMAIL_MASKED]
W/CrashHandler: Stack trace uploaded to [INTERNAL_URL_MASKED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI can still diagnose the issue ("NetworkManager connected successfully, then AuthService sent a token, then the user logged in") without ever seeing the actual sensitive values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fully Local AI with candle-core
&lt;/h2&gt;

&lt;p&gt;For HiyokoPDFVault, I went further. PDF documents can contain attorney-client privileged material, medical records, or financial data. Sending these to any cloud API—even with masking—is unacceptable for many users.&lt;/p&gt;

&lt;p&gt;Instead, HiyokoPDFVault uses &lt;code&gt;candle-core&lt;/code&gt; to run a small embedding model locally on the GPU via Metal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;candle_core&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cd"&gt;/// Initialize the local embedding engine on macOS Metal GPU&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;init_local_engine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmbeddingEngine&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use Metal backend on macOS for GPU acceleration&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_metal&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="nf"&gt;.unwrap_or_else&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cpu&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// fallback to CPU&lt;/span&gt;

    &lt;span class="c1"&gt;// Load a compact embedding model (~50MB)&lt;/span&gt;
    &lt;span class="c1"&gt;// Small enough for an 8-year-old MacBook Air&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_embedding_model&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;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmbeddingEngine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/// Generate embeddings for semantic search — fully local&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;embed_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&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;EmbeddingEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="py"&gt;.device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="py"&gt;.model&lt;/span&gt;&lt;span class="nf"&gt;.forward&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;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Normalize for cosine similarity search&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;l2_normalize&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;embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="nf"&gt;.to_vec1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The actual implementation handles additional edge cases not shown here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The result: users can search their PDF library semantically ("find contracts mentioning penalty clauses") without a single byte leaving their machine. On my MacBook Air's integrated GPU, this runs at about 200ms per query—not instant, but fast enough for interactive use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit User Consent
&lt;/h2&gt;

&lt;p&gt;Privacy shouldn't be a black box. When HiyokoLogcat or HiyokoBar is about to call the Gemini API, the user sees exactly what's happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A dialog shows the masked log content that will be sent.&lt;/li&gt;
&lt;li&gt;The user can review, edit, or cancel before any network request fires.&lt;/li&gt;
&lt;li&gt;The API key is stored in the macOS Keychain, never in a plain config file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This opt-in model means the default state is "nothing leaves your machine." External AI is an explicit action, not an ambient feature.&lt;/p&gt;

&lt;p&gt;Privacy isn't a checkbox—it's an architecture. By building masking, local inference, and explicit consent into the foundation, "Offline-First" AI becomes a genuine competitive advantage, not a marketing claim.&lt;/p&gt;

&lt;p&gt;How do you handle sensitive data when integrating AI into developer tools? Is local masking enough, or do you avoid cloud AI entirely?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/a1b4d7d8-a4b8-433c-9d36-abdc0f3937c3" rel="noopener noreferrer"&gt;HiyokoPDFVault&lt;/a&gt;&lt;/strong&gt; — PDF tools with AES-256 encryption, OCR, watermark, merge/split/compress, Bates numbering.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>ai</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Designing consistent UX across 10 different macOS Utility Apps</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:17:54 +0000</pubDate>
      <link>https://dev.to/hiyoyok/designing-consistent-ux-across-10-different-macos-utility-apps-19fc</link>
      <guid>https://dev.to/hiyoyok/designing-consistent-ux-across-10-different-macos-utility-apps-19fc</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). When 10 apps share the same machine, they'd better share the same design language—otherwise every context switch costs mental energy.&lt;/p&gt;

&lt;p&gt;The Hiyoko Suite isn't one app; it's 10 specialized tools. From MTP file transfers to PDF encryption to AI-powered log analysis, each app solves a different problem. But if a user opens HiyokoMTP and then switches to HiyokoShot, the experience should feel like opening a different room in the same house—not walking into a different building.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Shared CSS custom properties (design tokens) enforce visual consistency across all 10 apps with minimal overhead.&lt;/li&gt;
&lt;li&gt;A reusable React component library (Toolbar, SettingsModal, Toast) ensures identical interaction patterns.&lt;/li&gt;
&lt;li&gt;Glassmorphism with &lt;code&gt;backdrop-filter&lt;/code&gt; creates a native macOS feel without native-only code.&lt;/li&gt;
&lt;li&gt;Per-app accent colors give each tool its own identity within the shared system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Design Token Foundation
&lt;/h2&gt;

&lt;p&gt;Every Hiyoko app imports a shared CSS file with the core design tokens. This isn't a framework—it's 40 lines of CSS custom properties that define the entire visual system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* shared/hiyoko-tokens.css — imported by all 10 apps */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Background layers */&lt;/span&gt;
  &lt;span class="py"&gt;--bg-app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--bg-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--bg-elevated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c"&gt;/* Typography */&lt;/span&gt;
  &lt;span class="py"&gt;--font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;-apple-system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlinkMacSystemFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"SF Pro Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-size-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-size-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-size-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e4e4e4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#7a7a7a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Spacing scale (4px base) */&lt;/span&gt;
  &lt;span class="py"&gt;--space-xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--space-xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Borders and shapes */&lt;/span&gt;
  &lt;span class="py"&gt;--radius-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--radius-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--radius-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border-subtle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.06&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;Each app then sets its own accent color on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* HiyokoMTP */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4a9eff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;158&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* HiyokoLogcat */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffd44a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;212&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* HiyokoShot */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff6b8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;107&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;138&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern means 90% of the UI is identical across apps. Only the accent-colored elements—active tabs, selected items, progress bars—change per app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Translucent Popover Pattern
&lt;/h2&gt;

&lt;p&gt;Several Hiyoko apps (HiyokoShot, HiyokoBar, HiyokoClip) live in the macOS menu bar. They share an identical popover component that uses glassmorphism to feel native to macOS Sonoma and Sequoia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Menu bar popover — shared across HiyokoShot, HiyokoBar, HiyokoClip */&lt;/span&gt;
&lt;span class="nc"&gt;.menubar-popover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.78&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;32px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;180%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;32px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;180%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0.5px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;40px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Content area within the popover */&lt;/span&gt;
&lt;span class="nc"&gt;.menubar-popover__content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;480px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scrollbar-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;thin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scrollbar-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;blur(32px)&lt;/code&gt; value was tuned on my 8-year-old MacBook Air. Higher values look better but cause perceptible lag on the integrated GPU. 32px is the sweet spot between visual quality and performance on Intel Iris graphics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared React Components
&lt;/h2&gt;

&lt;p&gt;Beyond CSS, the suite shares a React component library. Here are the key pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shared/components/Toolbar.tsx — identical toolbar across all apps&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ToolbarProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Toolbar&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ToolbarProps&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-toolbar"&lt;/span&gt; &lt;span class="na"&gt;data-tauri-drag-region&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-toolbar__title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-toolbar__actions"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// shared/components/Toast.tsx — notification toast with "Piyo" sound&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ToastProps&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`hiyoko-toast hiyoko-toast--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-toast__icon"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;iconMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hiyoko-toast__message"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;When a user who knows HiyokoMTP opens HiyokoShot for the first time, the toolbar is in the same place, the settings icon opens the same modal layout, and the toast notifications look identical. The learning curve for app #2 through #10 is nearly zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency Saves Development Time
&lt;/h2&gt;

&lt;p&gt;The practical benefit beyond UX: when I build a new app, the design phase takes hours instead of days. Import the tokens, drop in the shared components, set the accent color, and the app already looks like it belongs in the suite.&lt;/p&gt;

&lt;p&gt;A bug fix in a shared component propagates to all 10 apps on the next build. A visual refinement to the popover border radius applies everywhere simultaneously. For a solo developer, this multiplication effect is the only way to maintain quality across a growing ecosystem.&lt;/p&gt;

&lt;p&gt;A suite of tools should feel like a well-organized toolbox, not a drawer full of mismatched screwdrivers. Investing in a shared design language upfront saves exponentially more time than it costs.&lt;/p&gt;

&lt;p&gt;How do you handle visual consistency across multiple projects? Do you use a formal design system or more of an ad-hoc approach?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2c94dd0f-e28a-4a17-8efc-7bd93087d46d" rel="noopener noreferrer"&gt;HiyokoKit&lt;/a&gt;&lt;/strong&gt; — all-in-one Android dev toolkit (MTP file manager, Logcat+AI, ADB Tools, Monitor).&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>mac</category>
      <category>android</category>
      <category>tauri</category>
      <category>rust</category>
    </item>
    <item>
      <title>I Asked an AI to "Tidy Up My Folders" — It Deleted a Month of My Work</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Sat, 27 Jun 2026 06:57:37 +0000</pubDate>
      <link>https://dev.to/hiyoyok/i-asked-an-ai-to-tidy-up-my-folders-it-deleted-a-month-of-my-work-423a</link>
      <guid>https://dev.to/hiyoyok/i-asked-an-ai-to-tidy-up-my-folders-it-deleted-a-month-of-my-work-423a</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Asked the Gemini extension in VS Code to "tidy up the folder order"&lt;/li&gt;
&lt;li&gt;It decided deleting files counted as "tidying"&lt;/li&gt;
&lt;li&gt;One month of research data and article drafts: gone&lt;/li&gt;
&lt;li&gt;Tried everything — Time Machine, Trash, iCloud, conversation history — no luck&lt;/li&gt;
&lt;li&gt;Plot twist: my memory turned out to be stronger than the documents anyway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Background: AI Agents Are Everywhere Now
&lt;/h2&gt;

&lt;p&gt;As a solo indie macOS developer, I use AI tools constantly. Code generation, article drafts, brainstorming — AI has become a genuine part of my workflow.&lt;/p&gt;

&lt;p&gt;All tests run on an 8-year-old MacBook Air. I ship apps. I write articles. I move fast.&lt;/p&gt;

&lt;p&gt;Sometimes too fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;My project folder was a little messy. Nothing broken — just the folder &lt;em&gt;order&lt;/em&gt; looked slightly off. You know that feeling.&lt;/p&gt;

&lt;p&gt;So I opened the Gemini extension in VS Code and typed, in the most casual way possible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Tidy up the folder order"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. I wanted the folders sorted. Alphabetically, by function, whatever. Just make it look neat.&lt;/p&gt;

&lt;p&gt;A few minutes later, Gemini reported back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The project folder reorganization is complete. I've reorganized everything into three functional categories: 'ocr-products', 'discord-products', and 'resources'. However, I must sincerely apologize — during the reorganization process, documents that were in the root 'articles' folder were accidentally deleted. Despite an extensive search, I was unable to recover them."&lt;/p&gt;
&lt;/blockquote&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%2F6dgxwme6d6i0s6lv31no.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%2F6dgxwme6d6i0s6lv31no.png" alt=" " width="524" height="996"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;I'm sorry, &lt;strong&gt;deleted&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;I said &lt;em&gt;tidy up the order&lt;/em&gt;. That's it. &lt;strong&gt;Why are files being deleted?!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Lost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~1 month of research notes&lt;/strong&gt; (data summaries from my information-gathering phase)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple article drafts&lt;/strong&gt; (works in progress)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Various documents&lt;/strong&gt; from the root 'articles' folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To its credit, Gemini tried hard to recover everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listed all Time Machine local snapshots&lt;/li&gt;
&lt;li&gt;Recursively searched every directory&lt;/li&gt;
&lt;li&gt;Checked conversation artifacts&lt;/li&gt;
&lt;li&gt;Searched the system Trash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Unfortunately, I was unable to recover the deleted documents. I sincerely apologize for the loss of such important data."&lt;/p&gt;
&lt;/blockquote&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%2Fdr0cn739gd5og3wg0wov.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%2Fdr0cn739gd5og3wg0wov.png" alt=" " width="506" height="832"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A very polite apology for a very unasked-for deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plot Twist: I'm Actually Fine
&lt;/h2&gt;

&lt;p&gt;Here's the thing I didn't expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm not that upset.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The article drafts? I wrote them — the content is in my head. The research summaries? What actually mattered wasn't the raw data, it was &lt;em&gt;my interpretation&lt;/em&gt; of that data, and that's still in my brain.&lt;/p&gt;

&lt;p&gt;Turns out &lt;strong&gt;human memory and understanding is more durable than documents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Losing the research data stings a little. But I can re-gather it. The real value — the thinking, the insights, the conclusions — never lived in those files to begin with.&lt;/p&gt;

&lt;p&gt;Still. One month of work vanishing in an instant is objectively not great.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Went Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The instruction was ambiguous (to a machine)
&lt;/h3&gt;

&lt;p&gt;"Tidy up the folder order" is completely unambiguous to a human. We instinctively know it means &lt;em&gt;rearrange&lt;/em&gt;, not &lt;em&gt;delete&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But an AI agent interprets instructions by reasoning toward a goal. The chain probably looked something like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Tidy up"&lt;/strong&gt; → &lt;strong&gt;"Make it cleaner"&lt;/strong&gt; → &lt;strong&gt;"Removing things makes it cleaner"&lt;/strong&gt; → &lt;strong&gt;💥 deletion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It optimized for the goal without respecting the implicit constraints a human would naturally apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  File operations are irreversible
&lt;/h3&gt;

&lt;p&gt;Code generation? Easy to redo. Writing? Regenerate it. File deletion? Gone.&lt;/p&gt;

&lt;p&gt;There's a fundamental asymmetry here: the cost of getting a file operation wrong is catastrophically higher than getting a text generation wrong. AI agents don't always account for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  No confirmation step
&lt;/h3&gt;

&lt;p&gt;The agent made a decision, executed it, and informed me afterward.&lt;/p&gt;

&lt;p&gt;No "I'm about to delete some files — is that okay?" No checkpoint. Just action, then apology.&lt;/p&gt;

&lt;p&gt;Human-in-the-loop isn't just a buzzword. This is why it exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rules I'm Following From Now On
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Explicitly forbid deletion in any file organization prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Reorganize the folder structure. Do NOT delete, move, or modify 
any files — only rename folders if absolutely necessary. 
Confirm before making any changes."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Mark critical folders as off-limits upfront
&lt;/h3&gt;

&lt;p&gt;Tell the agent which folders are untouchable &lt;em&gt;before&lt;/em&gt; it starts working. Don't assume it'll infer this.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Backup is non-negotiable
&lt;/h3&gt;

&lt;p&gt;Time Machine should have caught this. It didn't, in this case. An external backup would have. Lesson learned (again).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Include what you &lt;em&gt;don't&lt;/em&gt; want, not just what you do
&lt;/h3&gt;

&lt;p&gt;"Tidy up the folder order, without deleting or moving any files" — that's the instruction I should have given.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keep the important stuff in your head too
&lt;/h3&gt;

&lt;p&gt;Half joking. Half not. Documents are memory aids, not memory replacements. The real value is your understanding — and that can't be deleted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI agents are powerful. Genuinely, impressively powerful.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;"common sense" is not a given&lt;/strong&gt;. What's blindingly obvious to a human — "tidy up" means &lt;em&gt;rearrange&lt;/em&gt;, obviously not &lt;em&gt;delete&lt;/em&gt; — can be interpreted very differently by a machine optimizing for a goal.&lt;/p&gt;

&lt;p&gt;The data is gone. The lesson remains. And honestly, the whole incident turned into the most interesting piece of content I've produced this month.&lt;/p&gt;

&lt;p&gt;Thanks, Gemini. Unintentionally, you delivered.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a solo indie developer building macOS utilities for Android developers — the &lt;a href="https://hiyoyoko.github.io/hiyoko-site/" rel="noopener noreferrer"&gt;Hiyoko suite&lt;/a&gt;. I use AI tools daily in my workflow and occasionally suffer the consequences. All apps tested on an 8-year-old MacBook Air.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>gemini</category>
      <category>devstory</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Handling macOS Gatekeeper as an Unsigned Indie Dev: The `xattr` Struggle</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Sat, 27 Jun 2026 05:03:51 +0000</pubDate>
      <link>https://dev.to/hiyoyok/handling-macos-gatekeeper-as-an-unsigned-indie-dev-the-xattr-struggle-1028</link>
      <guid>https://dev.to/hiyoyok/handling-macos-gatekeeper-as-an-unsigned-indie-dev-the-xattr-struggle-1028</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). Every Hiyoko app ships unsigned, so I've become intimately familiar with every way macOS can block an app from launching.&lt;/p&gt;

&lt;p&gt;Building a great app is only half the battle. If you're a solo developer who isn't paying Apple's $99/year Developer Program fee, your users are greeted with "App is damaged" or "Developer cannot be verified" dialogs. This is the reality of distributing unsigned macOS software in 2026. Here's the full picture of what happens, why, and how to handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;macOS attaches a &lt;code&gt;com.apple.quarantine&lt;/code&gt; extended attribute to every downloaded file—this triggers Gatekeeper.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xattr -cr /path/to/App.app&lt;/code&gt; is the reliable fix, but users need clear instructions.&lt;/li&gt;
&lt;li&gt;Privacy permissions (Full Disk Access, USB) require separate handling in System Settings.&lt;/li&gt;
&lt;li&gt;Entitlements and &lt;code&gt;Info.plist&lt;/code&gt; keys are essential even for unsigned apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Gatekeeper Actually Works
&lt;/h2&gt;

&lt;p&gt;When a user downloads any file through Safari, Chrome, or curl, macOS adds an extended attribute called &lt;code&gt;com.apple.quarantine&lt;/code&gt;. For signed and notarized apps, Gatekeeper verifies the signature and removes the flag silently. For unsigned apps, this flag triggers the "damaged" or "unverified developer" error.&lt;/p&gt;

&lt;p&gt;You can see it for yourself:&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;# Check quarantine status of a downloaded app&lt;/span&gt;
xattr &lt;span class="nt"&gt;-l&lt;/span&gt; /Applications/HiyokoMTP.app

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# com.apple.quarantine: 0083;66a1b2c3;Safari;F2E4A6B8-...&lt;/span&gt;

&lt;span class="c"&gt;# The quarantine attribute contains:&lt;/span&gt;
&lt;span class="c"&gt;# - flags (0083 = downloaded + executed check pending)&lt;/span&gt;
&lt;span class="c"&gt;# - timestamp&lt;/span&gt;
&lt;span class="c"&gt;# - download source (browser name)&lt;/span&gt;
&lt;span class="c"&gt;# - download UUID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;xattr&lt;/code&gt; Fix
&lt;/h2&gt;

&lt;p&gt;The most reliable way to clear Gatekeeper for your users is a single terminal command:&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;# Clear ALL extended attributes recursively from the app bundle&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;xattr &lt;span class="nt"&gt;-cr&lt;/span&gt; /Applications/HiyokoMTP.app

&lt;span class="c"&gt;# -c = clear all attributes (not just quarantine)&lt;/span&gt;
&lt;span class="c"&gt;# -r = recursive (covers every file inside the .app bundle)&lt;/span&gt;
&lt;span class="c"&gt;# sudo = required because some attributes are root-owned&lt;/span&gt;

&lt;span class="c"&gt;# Verify it worked:&lt;/span&gt;
xattr &lt;span class="nt"&gt;-l&lt;/span&gt; /Applications/HiyokoMTP.app
&lt;span class="c"&gt;# (should output nothing)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I include this command in every Hiyoko app's README, download page, and first-launch error dialog. The key is making it impossible for users to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Gatekeeper: Privacy Permissions
&lt;/h2&gt;

&lt;p&gt;Clearing &lt;code&gt;xattr&lt;/code&gt; gets the app to launch, but apps like HiyokoMTP need deeper system access. macOS requires explicit user consent for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Disk Access&lt;/strong&gt; — needed for file transfer across all volumes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt; — needed for keyboard-driven automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USB Device Access&lt;/strong&gt; — needed for MTP and ADB connections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These permissions must be declared in your &lt;code&gt;Info.plist&lt;/code&gt;, even for unsigned apps:&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="c"&gt;&amp;lt;!-- Info.plist — privacy usage descriptions --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSAppleEventsUsageDescription&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;HiyokoMTP needs automation access for device detection.&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;NSDesktopFolderUsageDescription&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;HiyokoMTP needs access to transfer files to/from your Desktop.&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;NSDocumentsFolderUsageDescription&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;HiyokoMTP needs access to transfer files to/from Documents.&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;NSRemovableVolumesUsageDescription&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;HiyokoMTP needs access to external drives for file transfers.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without these keys, macOS silently denies the access request—no error, no dialog, just a silent failure. I spent two days debugging a "file not found" error that turned out to be a missing &lt;code&gt;Info.plist&lt;/code&gt; entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entitlements for Unsigned Apps
&lt;/h2&gt;

&lt;p&gt;Even without code signing, you can embed entitlements in your app bundle. Tauri v2 handles this through the build configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&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;"macOS"&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;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./Entitlements.plist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimumSystemVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.15"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Entitlements.plist --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "..."&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;com.apple.security.cs.allow-unsigned-executable-memory&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;key&amp;gt;&lt;/span&gt;com.apple.security.device.usb&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;h2&gt;
  
  
  macOS Version Differences
&lt;/h2&gt;

&lt;p&gt;Gatekeeper behavior varies across macOS versions, and my 8-year-old MacBook Air has seen several:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS 12 (Monterey)&lt;/strong&gt; — "Cannot verify developer" dialog with an "Open Anyway" button in System Preferences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS 13 (Ventura)&lt;/strong&gt; — "Open Anyway" moved to Privacy &amp;amp; Security → Security section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS 14+ (Sonoma/Sequoia)&lt;/strong&gt; — Stricter enforcement. Some users report needing to run &lt;code&gt;xattr&lt;/code&gt; even after clicking "Open Anyway."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I document all three paths in my support guides, with screenshots for each macOS version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Sign?
&lt;/h2&gt;

&lt;p&gt;Fair question. Apple's Developer Program costs $99/year and requires identity verification. For some solo developers, that's fine. For others—especially those distributing free tools or working outside the US—the bureaucracy and ongoing cost aren't justified.&lt;/p&gt;

&lt;p&gt;By being transparent about the unsigned status and providing clear, tested instructions, you build trust through honesty rather than through corporate certificates. Every Hiyoko download page includes a "First Launch Guide" section that walks users through the &lt;code&gt;xattr&lt;/code&gt; process step by step.&lt;/p&gt;

&lt;p&gt;Gatekeeper is designed to keep users safe, but it shouldn't be a wall between independent developers and their users. Master the tooling, document it clearly, and your users will appreciate the transparency.&lt;/p&gt;

&lt;p&gt;Have you dealt with Gatekeeper rejection for your indie apps? Did you eventually sign, or did you find workarounds that stuck?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2e966b64-554e-42a0-b865-4240281978a1" rel="noopener noreferrer"&gt;HiyokoMTP&lt;/a&gt;&lt;/strong&gt; — Android↔Mac MTP file transfer with custom nusb-based stack.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>mac</category>
      <category>tauri</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Rust is the Secret Weapon for Solo Indie Developers in 2026</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Fri, 26 Jun 2026 01:37:09 +0000</pubDate>
      <link>https://dev.to/hiyoyok/why-rust-is-the-secret-weapon-for-solo-indie-developers-in-2026-30pn</link>
      <guid>https://dev.to/hiyoyok/why-rust-is-the-secret-weapon-for-solo-indie-developers-in-2026-30pn</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). As a solo developer shipping 10 production macOS apps, Rust isn't just a language choice—it's my entire QA department.&lt;/p&gt;

&lt;p&gt;Being a solo developer means you're the architect, the coder, the tester, and the support staff. You can't afford to spend nights chasing memory leaks or debugging race conditions in production. After three years and 10 shipped apps, I'm convinced that Rust is the highest-leverage language a solo developer can choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rust's borrow checker eliminates entire categories of production bugs—memory leaks, data races, null pointer crashes.&lt;/li&gt;
&lt;li&gt;Fearless concurrency made 6-lane parallel file transfers possible without a single deadlock in production.&lt;/li&gt;
&lt;li&gt;Native performance lets me ship high-quality software that runs on 8-year-old hardware.&lt;/li&gt;
&lt;li&gt;The steep learning curve pays dividends: near-zero "random crash" support tickets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Borrow Checker Is Your Pair Programmer
&lt;/h2&gt;

&lt;p&gt;When you're solo, there's no second pair of eyes on your code. The Rust compiler fills that role—aggressively.&lt;/p&gt;

&lt;p&gt;In HiyokoMTP, I manage USB device connections that must be exclusive. In C or C++, a use-after-free on a device handle could corrupt data or crash the system. In Rust, the compiler catches this at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The compiler prevents use-after-free on device handles&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;transfer_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UsbDevice&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="nf"&gt;.open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// handle owns the connection&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_file&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="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="nf"&gt;.write_bulk&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="nf"&gt;.close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// handle is consumed here&lt;/span&gt;

    &lt;span class="c1"&gt;// handle.write_bulk(&amp;amp;[])?;&lt;/span&gt;
    &lt;span class="c1"&gt;// ^^^ compile error: "use of moved value: `handle`"&lt;/span&gt;
    &lt;span class="c1"&gt;// This bug would be a runtime crash in C. In Rust, it's a build failure.&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've lost count of how many bugs the borrow checker has caught before they reached users. For a solo developer with no QA team, this is the difference between shipping confidently and shipping anxiously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fearless Concurrency in Practice
&lt;/h2&gt;

&lt;p&gt;In HiyokoMTP, file transfers run across up to 6 parallel "lanes" using Tokio channels. In Go or Python, managing 6 concurrent file streams with shared state is a recipe for deadlocks and data races. In Rust, if it compiles, the thread safety is guaranteed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;TransferJob&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parallel_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TransferJob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lane_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TransferResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lane_count&lt;/span&gt; &lt;span class="o"&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;// Spawn N transfer lanes&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lane_count&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;job&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;permit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.acquire_owned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_single_transfer&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;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransferResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="py"&gt;.source&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.is_ok&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// release lane for next job&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// close sender so receiver knows when all jobs finish&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Update UI progress bar for each completed transfer&lt;/span&gt;
        &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transfer {}: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.path&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;result&lt;/span&gt;&lt;span class="py"&gt;.success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"OK"&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="s"&gt;"FAILED"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The actual implementation handles additional edge cases not shown here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This pattern—bounded semaphore + channel-based result collection—runs in production with zero deadlock incidents. On my dual-core MacBook Air, 6 lanes might sound excessive, but the bottleneck is USB I/O, not CPU. The lanes keep the USB bus saturated while the CPU barely notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance on a Budget
&lt;/h2&gt;

&lt;p&gt;Rust compiles to native machine code with no runtime garbage collector. On an 8GB machine with a dual-core i5, this matters enormously.&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;# Real memory usage comparison on MacBook Air (Intel, 8GB)&lt;/span&gt;
&lt;span class="c"&gt;# Task: Monitor 500 logcat lines/sec + transfer 2GB file&lt;/span&gt;

&lt;span class="c"&gt;# Node.js equivalent:  ~180MB RSS, GC pauses every ~3 seconds&lt;/span&gt;
&lt;span class="c"&gt;# Rust (Tokio):         ~22MB RSS, zero GC pauses&lt;/span&gt;

&lt;span class="c"&gt;# For a suite of 10 apps that might all run simultaneously:&lt;/span&gt;
&lt;span class="c"&gt;# Node.js: 10 × 180MB = 1.8GB just for backends&lt;/span&gt;
&lt;span class="c"&gt;# Rust:    10 × 22MB  = 220MB total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The zero-GC-pause characteristic is critical for real-time applications like logcat streaming. When HiyokoLogcat is processing 500+ lines per second, a 50ms GC pause means dropped log lines. With Rust, the processing pipeline runs at a consistent latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Learning Curve Pays for Itself
&lt;/h2&gt;

&lt;p&gt;Rust's learning curve is real. The first month is frustrating—fighting the borrow checker, understanding lifetimes, learning to think in terms of ownership. But here's the ROI calculation for a solo developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Month 1-2&lt;/strong&gt;: Slower than Python/TypeScript. Fighting the compiler constantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month 3-6&lt;/strong&gt;: Breaking even. Code compiles slower, but bugs in production drop to near zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month 6+&lt;/strong&gt;: Net positive. Support tickets for "random crashes" essentially disappear. Refactoring is safe because the compiler catches regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After three years, I receive almost zero support tickets for memory-related crashes across all 10 apps. That's time I can spend building features instead of debugging production fires.&lt;/p&gt;

&lt;p&gt;Rust isn't just about speed—it's about confidence. For a solo developer competing against entire teams, that confidence is what lets you ship fast without shipping broken.&lt;/p&gt;

&lt;p&gt;What's keeping you from trying Rust for your next side project? Is it the learning curve, the ecosystem, or something else?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/2e966b64-554e-42a0-b865-4240281978a1" rel="noopener noreferrer"&gt;HiyokoMTP&lt;/a&gt;&lt;/strong&gt; — Android↔Mac MTP file transfer with custom nusb-based stack.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>mtp</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a "Cute" Brand for Serious Developer Tools: The Hiyoko Story</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Thu, 25 Jun 2026 03:44:00 +0000</pubDate>
      <link>https://dev.to/hiyoyok/building-a-cute-brand-for-serious-developer-tools-the-hiyoko-story-44om</link>
      <guid>https://dev.to/hiyoyok/building-a-cute-brand-for-serious-developer-tools-the-hiyoko-story-44om</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). The apps are lightweight and fast, but they're also designed to make you smile while debugging at 2 AM.&lt;/p&gt;

&lt;p&gt;Developer tools have an aesthetic problem. They're either sterile gray panels or overwhelming dashboards crammed with every possible option. When I started building macOS utilities for Android development, I made a deliberate choice: these tools would be technically superior &lt;em&gt;and&lt;/em&gt; visually friendly. "Hiyoko" (ひよこ) means "chick" in Japanese—small, agile, and approachable. That name became a design philosophy.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Cute" branding reduces cognitive fatigue during long debugging sessions—it's a UX decision, not just decoration.&lt;/li&gt;
&lt;li&gt;Consistent design tokens (colors, spacing, typography) across 10 apps create a recognizable ecosystem.&lt;/li&gt;
&lt;li&gt;Custom audio feedback ("Piyo!" chirp) turns mundane notifications into moments of delight.&lt;/li&gt;
&lt;li&gt;Glassmorphism and translucent popovers make tools feel native to modern macOS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Developer Tools Don't Have to Be Ugly
&lt;/h2&gt;

&lt;p&gt;Most developer tools default to one of two extremes: enterprise gray (think Jenkins) or terminal minimalism (plain CLI output). Both are functional, but neither respects the fact that developers spend 8+ hours a day staring at these interfaces.&lt;/p&gt;

&lt;p&gt;Research on developer experience shows that visual comfort directly impacts sustained focus. A tool that feels pleasant to use gets used more consistently. A tool that feels like a chore gets replaced by a terminal alias.&lt;/p&gt;

&lt;p&gt;I chose a third path: tools that look approachable but don't compromise on technical depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Token System
&lt;/h2&gt;

&lt;p&gt;Every Hiyoko app shares a core set of CSS custom properties. This isn't just for visual consistency—it cuts development time when building new apps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* hiyoko-design-tokens.css — shared across all 10 apps */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Suite-wide base palette */&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-bg-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-bg-glass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-border-subtle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e8e8e8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-text-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#8b8b8b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-radius-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--hiyoko-shadow-popover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c"&gt;/* Per-app accent colors */&lt;/span&gt;
  &lt;span class="py"&gt;--accent-mtp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4a9eff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* HiyokoMTP — blue */&lt;/span&gt;
  &lt;span class="py"&gt;--accent-logcat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffd44a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* HiyokoLogcat — yellow */&lt;/span&gt;
  &lt;span class="py"&gt;--accent-shot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff6b8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* HiyokoShot — pink */&lt;/span&gt;
  &lt;span class="py"&gt;--accent-pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#7c5cfc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* HiyokoPDFVault — purple */&lt;/span&gt;
  &lt;span class="py"&gt;--accent-sync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4adb8b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* HiyokoAutoSync — green */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Glassmorphism popover — reused in HiyokoShot, HiyokoBar, HiyokoClip */&lt;/span&gt;
&lt;span class="nc"&gt;.hiyoko-popover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--hiyoko-bg-glass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;180%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;saturate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;180%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--hiyoko-border-subtle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--hiyoko-radius-md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--hiyoko-shadow-popover&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;When I build a new app, I import this token file and the base layout is already 80% done. Each app gets its own accent color, but the typography, spacing, and glass effects are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Piyo" Notification
&lt;/h2&gt;

&lt;p&gt;Here's a small detail that gets more positive feedback than almost any feature: custom notification sounds.&lt;/p&gt;

&lt;p&gt;When HiyokoShot finishes capturing a screenshot, instead of the default macOS "ding," users hear a soft "Piyo!" chirp. It's a tiny MP3, but the effect is outsized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Playing the "Piyo" chirp on task completion&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;play_notification_sound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resource_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
        &lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.resource_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sounds/piyo.mp3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Use macOS native audio playback via NSSound&lt;/span&gt;
    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"afplay"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.arg&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;resource_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// half volume — subtle, not jarring&lt;/span&gt;
        &lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision: the sound is &lt;em&gt;subtle&lt;/em&gt;. Half volume, short duration. It signals "done" without demanding attention. Users can disable it in settings, but almost nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  UX over Feature Count
&lt;/h2&gt;

&lt;p&gt;"Serious" tools often compete on feature count. Hiyoko tools take the opposite approach: prioritize the 3-5 actions developers do most, and make those actions frictionless.&lt;/p&gt;

&lt;p&gt;For HiyokoShot, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click the menu bar icon → screenshot captured and transferred → "Piyo!" → done.&lt;/li&gt;
&lt;li&gt;No configuration wizard. No "choose your output format" dialog on first launch.&lt;/li&gt;
&lt;li&gt;Advanced options exist, but they're one level deep in a settings panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This philosophy comes from a practical place: on my 8-year-old MacBook Air, every unnecessary UI element is wasted RAM and CPU. Minimal UI isn't just aesthetic—it's a performance requirement.&lt;/p&gt;

&lt;p&gt;You don't need a cold, gray interface to build "real" tools. Developers are humans who respond to thoughtful design. The Hiyoko Suite proves that approachability and technical depth aren't mutually exclusive—they reinforce each other.&lt;/p&gt;

&lt;p&gt;Do your favorite developer tools have any personality, or are they all function-over-form? I'd love to hear what makes a tool "feel right" to you.&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/6cd85b5d-e0a2-4e3c-8eb0-5016bf70a6ea" rel="noopener noreferrer"&gt;HiyokoShot&lt;/a&gt;&lt;/strong&gt; — Android screenshot transfer via USB or Wi-Fi.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>developer</category>
      <category>rust</category>
      <category>tauri</category>
      <category>hiyoko</category>
    </item>
    <item>
      <title>Tauri v1 to v2: Lessons from Migrating 10 Production Apps</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Wed, 24 Jun 2026 01:00:23 +0000</pubDate>
      <link>https://dev.to/hiyoyok/tauri-v1-to-v2-lessons-from-migrating-10-production-apps-2n58</link>
      <guid>https://dev.to/hiyoyok/tauri-v1-to-v2-lessons-from-migrating-10-production-apps-2n58</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). Migrating a single app is a weekend project. Migrating ten production apps simultaneously is a three-week marathon that tests every assumption you've made about your codebase.&lt;/p&gt;

&lt;p&gt;Tauri v2 brings plugin modularity, a new permission system, and mobile support. But the migration path from v1 is non-trivial, especially at scale. Here's what I learned migrating my entire Hiyoko Suite—10 Rust + React macOS apps—from Tauri v1.5 to v2.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The v1→v2 IPC layer changed completely: &lt;code&gt;tauri::command&lt;/code&gt; stays, but frontend &lt;code&gt;invoke&lt;/code&gt; calls need path updates.&lt;/li&gt;
&lt;li&gt;Plugin-based architecture replaces monolithic &lt;code&gt;tauri.conf.json&lt;/code&gt; capabilities with per-plugin permission manifests.&lt;/li&gt;
&lt;li&gt;Migrated 200+ IPC commands across 10 apps by building a centralized API wrapper pattern.&lt;/li&gt;
&lt;li&gt;Mobile support in v2 opens future doors, but macOS-specific APIs (&lt;code&gt;macos-private-api&lt;/code&gt;) need careful feature-gating.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  IPC Layer: The Biggest Breaking Change
&lt;/h2&gt;

&lt;p&gt;In Tauri v1, invoking a Rust command from the frontend was straightforward. In v2, the invoke path and error handling changed. Multiply that by 200+ commands across 10 apps, and you have a real migration project.&lt;/p&gt;

&lt;p&gt;Here's the before/after pattern I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// === Tauri v1 ===&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tauri-apps/api/tauri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sdcard/DCIM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// === Tauri v2 ===&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tauri-apps/api/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sdcard/DCIM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Import path changed: api/tauri → api/core&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The import path change seems small, but with 200+ &lt;code&gt;invoke&lt;/code&gt; calls scattered across 10 React codebases, a find-and-replace wasn't enough. Some apps had aliased imports, some used dynamic &lt;code&gt;invoke&lt;/code&gt; wrappers. I ended up building a centralized API layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/api/tauri-bridge.ts — centralized wrapper for all IPC calls&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tauri-apps/api/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tauriInvoke&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Centralized error handling — log to HiyokoBar if available&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[IPC] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage across all apps:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tauriInvoke&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FileEntry&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sdcard/DCIM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tauriInvoke&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SyncStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_sync_status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wrapper gave me a single point of control for logging, error formatting, and any future IPC changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission Manifests: Security Gets Granular
&lt;/h2&gt;

&lt;p&gt;Tauri v2 introduces a capability-based permission system. In v1, you declared broad access in &lt;code&gt;tauri.conf.json&lt;/code&gt;. In v2, each plugin declares its own permissions, and your app explicitly opts in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src-tauri/capabilities/default.json&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;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Default permissions for HiyokoMTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"windows"&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="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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="s2"&gt;"core:default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs:default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs:allow-read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fs:allow-write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"shell:allow-open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"dialog:default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"store:default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"notification:default"&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;For apps like HiyokoMTP that need deep filesystem access, defining granular permissions was initially tedious. But the payoff is real—users can now see exactly what each app accesses, and I can audit permissions per-app instead of relying on a monolithic config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugin System: From Monolith to Modules
&lt;/h2&gt;

&lt;p&gt;In v1, features like &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;dialog&lt;/code&gt;, and &lt;code&gt;store&lt;/code&gt; were built into the Tauri core. In v2, they're standalone plugins you explicitly register:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src-tauri/src/main.rs — Tauri v2 plugin registration&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_shell&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_dialog&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_store&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_notification&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.invoke_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_handler!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;list_files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;transfer_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;get_device_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_context!&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error running HiyokoMTP"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Cargo.toml&lt;/code&gt; dependencies change accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;tauri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"macos-private-api"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tauri-plugin-shell&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;tauri-plugin-dialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;tauri-plugin-store&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;tauri-plugin-fs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;tauri-plugin-notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration forced me to audit which plugins each app actually used. Turns out, three of my apps were pulling in &lt;code&gt;shell&lt;/code&gt; capabilities they never used—dead weight that v2's explicit model exposed instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Strategy for Multiple Apps
&lt;/h2&gt;

&lt;p&gt;With 10 apps to migrate, I couldn't do them all at once. Here's the order that worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the simplest app&lt;/strong&gt; (HiyokoShot — minimal IPC surface). Prove the migration pattern works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the centralized API wrapper&lt;/strong&gt; in the first app, then copy it to all others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate shared crates first&lt;/strong&gt; (&lt;code&gt;hiyoko-helper&lt;/code&gt;), then update all downstream apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save the most complex app for last&lt;/strong&gt; (HiyokoMTP — 60+ IPC commands, deep USB access).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process took about three weeks of focused work. The first app took 4 days. By app number 10, I had the pattern down to half a day.&lt;/p&gt;

&lt;p&gt;Tauri v2 is a significant upgrade for desktop (and now mobile) development. The migration was a marathon, but the resulting codebase is cleaner, more secure, and ready for whatever comes next.&lt;/p&gt;

&lt;p&gt;Have you migrated a Tauri v1 app to v2 yet? What was the most unexpected breaking change you hit?&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/20c922f1-ca45-4f77-aeb2-04e34aad2fb4" rel="noopener noreferrer"&gt;HiyokoAutoSync&lt;/a&gt;&lt;/strong&gt; — wireless zero-touch Android↔Mac file sync via ADB.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>mac</category>
      <category>tauri</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>Developing for Android on an 8-Year-Old MacBook Air: A Survivor's Guide</title>
      <dc:creator>hiyoyo</dc:creator>
      <pubDate>Tue, 23 Jun 2026 01:55:16 +0000</pubDate>
      <link>https://dev.to/hiyoyok/developing-for-android-on-an-8-year-old-macbook-air-a-survivors-guide-jeb</link>
      <guid>https://dev.to/hiyoyok/developing-for-android-on-an-8-year-old-macbook-air-a-survivors-guide-jeb</guid>
      <description>&lt;p&gt;All tests run on an 8-year-old MacBook Air (Intel). This isn't a nostalgic flex—it's a constraint that shaped every architectural decision in my toolchain.&lt;/p&gt;

&lt;p&gt;"Your hardware is too old." I've heard this from every tool's system requirements page. But an Intel MacBook Air from 2018 with 8GB of RAM still has a dual-core i5 and an SSD. The machine isn't the problem—the software running on it is. Here's how I develop Android tools full-time on hardware most people consider obsolete.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tauri v2 uses ~25MB RAM per app vs. Electron's ~350MB—critical when you only have 8GB total.&lt;/li&gt;
&lt;li&gt;Custom Rust engines avoid the overhead of general-purpose libraries, cutting compile times by 40%.&lt;/li&gt;
&lt;li&gt;Built HiyokoBar specifically to monitor system resources in real-time from the menu bar.&lt;/li&gt;
&lt;li&gt;Profiling with &lt;code&gt;sysinfo&lt;/code&gt; crate and &lt;code&gt;Activity Monitor&lt;/code&gt; keeps memory leaks from killing productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Know Your Budget
&lt;/h2&gt;

&lt;p&gt;On an 8GB machine, every megabyte matters. Here's my typical memory breakdown during a development session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS system:       ~2.5 GB
Xcode/Terminal:     ~1.2 GB
VS Code:            ~800 MB
Android Emulator:   NOT AN OPTION (2GB+ alone)
Remaining for apps: ~3.5 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means my entire suite of development tools needs to fit in that 3.5GB window alongside whatever I'm actually working on. An Electron app eating 350MB just to show a file list is out of the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lean Backends: Tauri over Electron
&lt;/h2&gt;

&lt;p&gt;The single biggest decision was choosing Tauri v2 over Electron. On my machine, the difference isn't marginal—it's the difference between a usable system and a frozen one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tauri.conf.json — optimized for minimal footprint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"windows"&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="s"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"resizable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"decorations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="s"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"dmg"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"minimumSystemVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"10.15"&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;Tauri uses the system WebView (WKWebView on macOS), so the binary ships without a bundled browser engine. My apps average 8-12MB on disk. An equivalent Electron app starts at 150MB before you write a single line of business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Engines over Bloated Libraries
&lt;/h2&gt;

&lt;p&gt;Standard libraries often include everything. When you're building an MTP file manager, you don't need a library that also supports PTP, USB audio, and printer protocols. By writing focused Rust engines, I only pay for the features I actually use.&lt;/p&gt;

&lt;p&gt;The compile-time impact matters too. On my dual-core i5:&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;# Compile time comparison on MacBook Air (Intel i5, 2 cores)&lt;/span&gt;
&lt;span class="c"&gt;# Full libmtp wrapper crate:  ~4 min 20 sec (pulls in C dependencies)&lt;/span&gt;
&lt;span class="c"&gt;# Custom focused MTP engine:  ~2 min 35 sec (pure Rust, nusb-based)&lt;/span&gt;

&lt;span class="c"&gt;# For 10 apps sharing a workspace, this adds up fast&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; hiyoko-mtp  &lt;span class="c"&gt;# 2m 35s&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; hiyoko-shot &lt;span class="c"&gt;# 1m 48s&lt;/span&gt;
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; hiyoko-clip &lt;span class="c"&gt;# 1m 22s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-Time Monitoring with HiyokoBar
&lt;/h2&gt;

&lt;p&gt;I built HiyokoBar out of necessity. When a background process starts leaking memory, I need to know before the UI starts lagging—not after the system forces a reboot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;sysinfo&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;collect_system_metrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SystemSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="nf"&gt;.refresh_all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpu_usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="nf"&gt;.global_cpu_usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;used_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="nf"&gt;.used_memory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// in bytes&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="nf"&gt;.total_memory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Trigger warning when memory pressure exceeds 85%&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mem_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;used_mem&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_mem&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pressure_alert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mem_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;SystemSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cpu_usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mem_used_mb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;used_mem&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_048_576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mem_total_mb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_mem&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_048_576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pressure_alert&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;HiyokoBar polls every 2 seconds and displays CPU, memory, and disk I/O in a translucent menu bar popover. When memory pressure crosses 85%, it highlights red. On an 8GB machine, this early warning has saved me from hard resets more times than I can count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Survival Tips
&lt;/h2&gt;

&lt;p&gt;After three years of daily-driving this setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never run the Android Emulator.&lt;/strong&gt; Use a physical device over USB or Wi-Fi ADB. The emulator alone consumes more RAM than my entire toolkit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill IDE language servers you're not using.&lt;/strong&gt; Rust-analyzer is heavy. If I'm in a React-only editing session, I disable it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile before optimizing.&lt;/strong&gt; The &lt;code&gt;sysinfo&lt;/code&gt; crate and &lt;code&gt;instruments&lt;/code&gt; on macOS tell you what's actually slow, not what you think is slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental builds only.&lt;/strong&gt; Full &lt;code&gt;cargo clean &amp;amp;&amp;amp; cargo build&lt;/code&gt; is a 15-minute commitment. Structure your workspace so incremental builds touch only the crate you changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't let your hardware define your productivity. With the right architecture and a focus on native performance, an 8-year-old Mac can outperform a modern machine drowning in unoptimized software.&lt;/p&gt;

&lt;p&gt;What's the oldest hardware you're still actively developing on? I'm curious how others handle the constraints.&lt;/p&gt;




&lt;p&gt;If this was helpful, check out &lt;strong&gt;&lt;a href="https://hiyokomtp.lemonsqueezy.com/checkout/buy/f9b85321-6878-40aa-a472-ff748d6de2d5" rel="noopener noreferrer"&gt;HiyokoBar&lt;/a&gt;&lt;/strong&gt; — macOS menubar HUD for engineers with Gemini AI error analysis.&lt;/p&gt;

&lt;p&gt;Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>android</category>
      <category>mac</category>
    </item>
  </channel>
</rss>
