<?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: yyzTools</title>
    <description>The latest articles on DEV Community by yyzTools (@jearry).</description>
    <link>https://dev.to/jearry</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%2F4087854%2F6e862585-d7ef-4a3f-aace-1c7b111228c5.png</url>
      <title>DEV Community: yyzTools</title>
      <link>https://dev.to/jearry</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jearry"/>
    <language>en</language>
    <item>
      <title>You Can Write Desktop Tools in Plain HTML/CSS/JS — Here's the SDK That Lets You</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Mon, 07 Sep 2026 02:36:52 +0000</pubDate>
      <link>https://dev.to/jearry/you-can-write-desktop-tools-in-plain-htmlcssjs-heres-the-sdk-that-lets-you-5597</link>
      <guid>https://dev.to/jearry/you-can-write-desktop-tools-in-plain-htmlcssjs-heres-the-sdk-that-lets-you-5597</guid>
      <description>&lt;p&gt;&lt;em&gt;I build &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyzTools&lt;/a&gt; — a free, local-first Windows productivity suite (command palette, clipboard history, OCR, file preview, batch processing — 40+ tools in one install). Today's post isn't about the app itself; it's about the module SDK I just opened up, because the development model is the kind of thing frontend developers keep telling me should be impossible: **one static webpage + one JSON file = a native desktop tool window.&lt;/em&gt;* No C++. No build step. No packaging.*&lt;/p&gt;




&lt;h2&gt;
  
  
  The demo first
&lt;/h2&gt;

&lt;p&gt;Download the sample project, unzip it, and you get two folders — &lt;code&gt;Web/&lt;/code&gt; and &lt;code&gt;Modules/&lt;/code&gt;. Copy them next to &lt;code&gt;yyzTools.exe&lt;/code&gt;, restart, and hit Alt+Space. Type &lt;code&gt;sdk&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;A 760×540 native window opens. It's a full-disk file search app — instant results, Everything-style syntax (&lt;code&gt;ext:pdf size:&amp;gt;100mb&lt;/code&gt;), double-click to open files.&lt;/p&gt;

&lt;p&gt;Behind that window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Web/SDK_Sample/
  index.html       # plain DOM, data-i18n attributes
  filefinder.js    # all the logic (vanilla JS, zero deps)
  filefinder.css   # styled entirely with --zen-* variables
  i18n.js          # string dictionary (zh_cn / en)
  lib/
    zen_api.js     # the ONLY library — the native API wrapper
Modules/SDK_Sample.zenmod   # one JSON declaring the window
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;package.json&lt;/code&gt;. No &lt;code&gt;node_modules&lt;/code&gt;. No bundler. It's 2026 and I'm shipping an SDK whose sample runs without a toolchain — deliberately, and the reason matters.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl07cwvthggdiy6mkhav.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%2Fcl07cwvthggdiy6mkhav.png" alt=" " width="799" height="568"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm644ly0xfszkrrjuwr6.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%2Fcm644ly0xfszkrrjuwr6.png" alt=" " width="799" height="568"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How the window gets born: it's declared, not built
&lt;/h2&gt;

&lt;p&gt;The part of desktop development that scares everyone off was never business logic — it's windows. Message loops, DPI awareness, taskbar integration, hotkey registration. The SDK's answer is to make the host a data file:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sdk_sample"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SDK_Sample/index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;760&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;540&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"layoutType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fas fa-search"&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;The host reads this at startup and creates a native window with a WebView2 control pointing at your page. And because the module now exists in the app's registry, it inherits the whole suite's infrastructure for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command palette search&lt;/strong&gt; — localized across all 12 UI languages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global hotkey binding&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dock mounting&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme integration&lt;/strong&gt; — the host injects &lt;code&gt;--zen-*&lt;/code&gt; CSS variables; your page follows the app's light/dark mode with zero &lt;code&gt;prefers-color-scheme&lt;/code&gt; media queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write the feature. The host absorbs the decade of Win32 plumbing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1ieo8ffkeo9vnhefkoz.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%2Fl1ieo8ffkeo9vnhefkoz.png" alt=" " width="800" height="71"&gt;&lt;/a&gt;## Talking to the system: the ZenAPI bridge&lt;/p&gt;

&lt;p&gt;Your webpage gets system powers through a &lt;code&gt;window.Zen&lt;/code&gt; bridge, consumed via an official &lt;code&gt;ZenAPI&lt;/code&gt; wrapper class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ZenAPI&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;./lib/zen_api.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Full-disk search (async — runs on a background thread)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;ZenAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;searchFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice ext:pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ZenAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isOk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* unified error check */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Open the file / reveal in Explorer&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ZenAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ZenAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFileLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Persist your own config (narrow-grained patch, not full rewrite)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ZenAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;sdk_sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One design detail worth stealing for anyone building a JS bridge: &lt;code&gt;searchFile&lt;/code&gt; is asynchronous, and the native side implements a &lt;strong&gt;search slot&lt;/strong&gt; — a new request &lt;em&gt;displaces&lt;/em&gt; any in-flight one (the old Promise resolves empty). Combined with 300ms input debounce and a monotonically increasing sequence number on the frontend to discard out-of-order responses, rapid typing neither floods the background thread nor flashes stale results. In interactive search, the correct semantic for concurrent requests is &lt;em&gt;overwrite&lt;/em&gt;, not &lt;em&gt;queue&lt;/em&gt; — the opposite of what server-side intuition tells you.&lt;/p&gt;

&lt;p&gt;The API surface goes well beyond search: file dialogs, chunked reads for large files, clipboard, screenshot → OCR pipeline, spawning external programs, invoking other module windows. The full reference is published in 12 languages — including which interface returns &lt;code&gt;isDir&lt;/code&gt; as a 1/0 &lt;em&gt;number&lt;/em&gt; rather than a boolean, because hand-rolled JSON paths and property-tree paths serialize differently. Documenting the type traps is the difference between docs people trust and docs people tolerate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging: just open it in a browser
&lt;/h2&gt;

&lt;p&gt;This is the part that sells frontend developers. Double-click &lt;code&gt;index.html&lt;/code&gt; — it opens in your normal browser. There's no &lt;code&gt;window.Zen&lt;/code&gt; there, so &lt;code&gt;ZenAPI.call&lt;/code&gt; goes down the exception path and returns &lt;code&gt;error: -1&lt;/code&gt;, but your layout, styles, debounce, and rendering logic all work normally. Iterate the UI in the browser; hop into the real host only for native integration. The degradation path was designed in from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest security section
&lt;/h2&gt;

&lt;p&gt;The native API is fully open to module developers: file read/write/delete, process creation, window control, clipboard. &lt;strong&gt;No permission prompts, no sandbox, no undo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a trust-model decision, not an oversight. This SDK targets developers building tools &lt;em&gt;for themselves or their team&lt;/em&gt; — the deployment unit is "copy a folder", the distribution radius is people who already trust you. If an attacker can write to your modules directory, they can already drop an exe; disguising it as a &lt;code&gt;.zenmod&lt;/code&gt; gains them nothing. Sandboxing buys you nothing here and costs every module author a permissions negotiation they don't need.&lt;/p&gt;

&lt;p&gt;If I ever build a public module store, this design gets rebuilt — different trust model, different security requirements. Match the security to the trust model, not to a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;If you write web pages at all, here's the pitch: your existing skillset now produces desktop tools with a native window, global hotkeys, tray integration, and a theming system — for the cost of one folder and one JSON. The internal tool you'd otherwise ship as "open this localhost link" can be a real window in the palette.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs (quick start + full API reference): &lt;a href="https://yyztools.com/sdk.html" rel="noopener noreferrer"&gt;yyztools.com/sdk.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample project download: same page&lt;/li&gt;
&lt;li&gt;The app: free forever, no accounts, no telemetry, Windows 10/11&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sample is a genuinely pleasant read — four files, an hour, including the debounce/race handling. If you build something with it, drop it in the comments; good ones may make it into the built-in catalog.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The Local-First Tax: What It Actually Costs to Refuse the Cloud</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:03:38 +0000</pubDate>
      <link>https://dev.to/jearry/the-local-first-tax-what-it-actually-costs-to-refuse-the-cloud-bnp</link>
      <guid>https://dev.to/jearry/the-local-first-tax-what-it-actually-costs-to-refuse-the-cloud-bnp</guid>
      <description>&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyzTools&lt;/a&gt; — a Windows productivity suite (clipboard history, OCR, file search, app usage stats) that is aggressively local-first by design. No accounts, no telemetry, no cloud sync. This post isn't about the product. It's about the real costs of the stance.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch vs. the tax
&lt;/h2&gt;

&lt;p&gt;"Local-first" is a compelling pitch: your data stays on your machine, no cloud dependencies, no breach exposure, works offline. It resonates with privacy-conscious users and with developers who've seen what happens when a cloud service sunsets your data.&lt;/p&gt;

&lt;p&gt;But the pitch hides real costs. After building a local-first tool for years, here's the tax I've paid—and what I'm still not sure is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The costs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Multi-device sync is genuinely gone
&lt;/h3&gt;

&lt;p&gt;"Copy something at work, see it at home" is the cloud clipboard's killer feature. I can't offer it without violating the core principle (data leaves the machine). My users—and I—work around it by manually syncing the local storage directory with a file-sync tool.&lt;/p&gt;

&lt;p&gt;That feels like 2005. The cloud clipboard isn't popular by accident; cross-device sync is genuinely useful. Telling users "just sync the DB folder yourself" is a real regression in convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Local models are heavy
&lt;/h3&gt;

&lt;p&gt;The OCR engine (RapidOCR's ONNX models) ships in the installer. The app is ~600MB. A cloud OCR tool is a 5MB app + a server. Users with small SSDs or slow connections notice.&lt;/p&gt;

&lt;p&gt;There's no way around this with local-first: a capable local model has weight. The trade is "data stays local" for "installer is large." I've made peace with it, but it's a tax.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No network effects
&lt;/h3&gt;

&lt;p&gt;Cloud tools get sticky because your data is there, your collaborators are there, your history is there. A local tool has none of that. Retention is purely "does this work well enough day to day?"—which is fragile. A user can walk away any time, taking nothing with them.&lt;/p&gt;

&lt;p&gt;For a solo dev without a marketing budget, this is a slow grind. Cloud tools compound (data lock-in); local tools don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The convenience/privacy framing is mostly a false binary
&lt;/h3&gt;

&lt;p&gt;"People will trade privacy for convenience" is the industry line. In my experience, users don't perceive the trade at all. They hit "allow" and use the cloud tool. The local-first stance is &lt;strong&gt;invisible until something goes wrong&lt;/strong&gt; (a breach, a policy change, a sunset), at which point it's too late—the data is already gone or compromised.&lt;/p&gt;

&lt;p&gt;You're selling insurance against a low-probability event. Most users don't buy insurance they can't see.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The user is responsible for backups
&lt;/h3&gt;

&lt;p&gt;Cloud tools hide backup responsibility. Local-first puts it on the user. I document the storage path and recommend syncing it, but I can't force it. If a user's drive dies and they didn't sync, their clipboard history is gone.&lt;/p&gt;

&lt;p&gt;This is philosophically correct ("it's your data, you own the responsibility"), but operationally it's a support burden and a UX cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I drew the line
&lt;/h2&gt;

&lt;p&gt;Not everything can be local. Translation, downloads, opening URLs—these are network by nature. The question is how to handle the network features honestly.&lt;/p&gt;

&lt;p&gt;My rule: &lt;strong&gt;the network feature's data destination is explicit and user-chosen.&lt;/strong&gt; Translation sends data to &lt;em&gt;the service the user picks in settings&lt;/em&gt; (DeepL, Google, etc.), not a fixed yyzTools server. The user knows where their text goes because they chose the service. No hidden relay.&lt;/p&gt;

&lt;p&gt;This is a middle path: local-first for the data that can be local (clipboard, OCR, search, stats), explicit-and-chosen for the data that can't (translation). It's not pure, but it's honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The no-account stance
&lt;/h2&gt;

&lt;p&gt;Accounts aren't just "friction." They're a privacy design decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account → behavior tied to identity&lt;/strong&gt; → you're processing personal information, with all the compliance that implies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No account → no identity&lt;/strong&gt; → behavior can't be linked to a person → no personal information processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;yyzTools has no accounts, no telemetry, no behavioral collection. This isn't "saving a registration step"—it's a structural choice that removes the &lt;em&gt;possibility&lt;/em&gt; of identity-behavior linkage. For users in regulated environments (enterprise, healthcare, government), this is a feature, not a limitation.&lt;/p&gt;

&lt;p&gt;For personal users, it's invisible. Nobody installs a clipboard manager thinking "glad there's no account." They notice the absence only when a cloud tool's breach headlines hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it defensible?
&lt;/h2&gt;

&lt;p&gt;I keep going back and forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The case for:&lt;/strong&gt; privacy regulations are tightening (GDPR, data residency laws). As breaches accumulate and cloud trust erodes, the local-first stance ages into relevance. The users who care are a minority, but they're loyal. The stance differentiates in a market where "free + cloud + ads" is the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The case against:&lt;/strong&gt; 95% of users don't value it until it's too late. The convenience tax (no sync, large installer, no collaboration) is felt daily; the privacy benefit is felt never (until a breach, which probably won't happen to them). You're paying a real daily cost for an insurance most users will never collect on.&lt;/p&gt;

&lt;p&gt;I don't have a conclusion. I have a stance and a tax bill, and I'm still calculating whether the exchange is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want to ask
&lt;/h2&gt;

&lt;p&gt;I'd love to hear from both sides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For users who've tried local-first tools and reverted to cloud:&lt;/strong&gt; what was the breaking point? Sync? Collaboration? The lack of a web UI?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For users who run local-first and wouldn't go back:&lt;/strong&gt; what makes it worth the convenience cost for you?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For builders:&lt;/strong&gt; is local-first a defensible long-term position, or a boutique stance that 95% of users don't value? Does it only make sense in regulated industries, or is there a broader market?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The middle path&lt;/strong&gt;—local for what can be local, explicit-chosen cloud for what can't—is it meaningfully different from a cloud tool, or just a cloud tool with better labeling?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not here to sell you on local-first. I'm here because I'm honestly uncertain whether it's a defensible position or a philosophical luxury, and I'd value outside perspective.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building yyzTools — a free, local-first Windows productivity suite. No accounts, no telemetry, no cloud sync. Website: &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;. The "no sync" tax is real; I use a file-sync tool myself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>privacy</category>
      <category>software</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why I Replaced Everything With a Homegrown MFT Search Engine (Memory: Hundreds of MB a Few MB)</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:21:47 +0000</pubDate>
      <link>https://dev.to/jearry/why-i-replaced-everything-with-a-homegrown-mft-search-engine-memory-hundreds-of-mb-a-few-mb-p87</link>
      <guid>https://dev.to/jearry/why-i-replaced-everything-with-a-homegrown-mft-search-engine-memory-hundreds-of-mb-a-few-mb-p87</guid>
      <description>&lt;p&gt;&lt;em&gt;I build &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyzTools&lt;/a&gt; — a free, local-first Windows productivity suite that folds 40+ tools (command palette, clipboard history, OCR, file preview, batch processing) into one installer. This post is about the 1.0.5 release, where I did something that sounds insane: I removed my dependency on Everything — the gold standard of Windows file search — and wrote my own engine.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;For about two years, file search in yyzTools was powered by Everything. And let me be clear: Everything is phenomenal. Sub-second full-disk search, a mature query syntax (&lt;code&gt;ext:pdf size:&amp;gt;100mb dm:today&lt;/code&gt;), battle-tested by millions of users. Integrating it was one of the best decisions I made early on — instant world-class search for free.&lt;/p&gt;

&lt;p&gt;But there was a catch that grew more annoying with every release: &lt;strong&gt;Everything is designed to be used standalone.&lt;/strong&gt; Its index lives in its own service process. On a machine with a lot of files, that index holds &lt;strong&gt;hundreds of MB of RAM&lt;/strong&gt;, resident from boot to shutdown.&lt;/p&gt;

&lt;p&gt;For a standalone search tool, that's a perfectly reasonable price. But yyzTools is a &lt;em&gt;resident&lt;/em&gt; suite — the whole point is that it sits quietly in your tray until you hit Alt+Space. Every MB of resident memory lowers the threshold at which users uninstall you. And search is the heaviest feature in the suite.&lt;/p&gt;

&lt;p&gt;So the conflict was structural: &lt;strong&gt;the dimension Everything optimizes least for (being embedded in someone else's memory-sensitive app) was exactly my core requirement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In 1.0.5, I replaced it. Index memory went from hundreds of MB to &lt;strong&gt;a few MB&lt;/strong&gt;. Query syntax stayed Everything-compatible. Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn #1: the "official" API is a trap
&lt;/h2&gt;

&lt;p&gt;When you start researching full-disk enumeration on Windows, you find &lt;code&gt;FSCTL_ENUM_USN_DATA&lt;/code&gt;. Documented, blessed, gives you every file's FRN (file reference number) and parent FRN. Seems like the obvious choice.&lt;/p&gt;

&lt;p&gt;I implemented it. &lt;strong&gt;1.34 million files, 120 seconds to enumerate.&lt;/strong&gt; Rejected.&lt;/p&gt;

&lt;p&gt;The problem: USN records are a &lt;em&gt;change journal&lt;/em&gt;, not a catalog. They don't carry file size or timestamps — so for every single file you have to call back with &lt;code&gt;FSCTL_GET_NTFS_FILE_RECORD&lt;/code&gt; to fetch the full MFT record. A million random reads by FRN. That's not slow code; that's a wrong-shaped data channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Everything way: read $MFT directly
&lt;/h2&gt;

&lt;p&gt;Everything and WizFile don't use that API for full enumeration. They open the volume device (&lt;code&gt;\\.\C:&lt;/code&gt;, read-only) and &lt;strong&gt;read the $MFT byte stream directly&lt;/strong&gt;. The Master File Table is NTFS's census book: every file and directory is one fixed 1024-byte record, stored sequentially, containing name, size, and timestamps. Scan it linearly and you get everything in one pass of sequential IO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Parse the $DATA run list of $MFT's own record (VCN→LCN mapping)&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ParseMftRuns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BYTE&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mftRec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;lcns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. ReadFile the volume handle in chunks, per run&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Fix up each 1024-byte record, extract $FILE_NAME / $DATA attributes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth knowing if you go this route:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard links&lt;/strong&gt;: one MFT record can carry multiple $FILE_NAME attributes (one per link, each pointing at its own parent directory). Each one must become a separate row in your index, or link results silently go missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt;: reading the volume device needs more than a standard user token. That's why the engine runs as a separate process backed by a Windows service — the service holds the volume handle, the main app consumes queries over IPC, and the GUI never elevates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the memory went: columnar storage
&lt;/h2&gt;

&lt;p&gt;The full-enumeration part was honestly the easier half. The interesting question was: you now have 1.34 million rows of metadata. How do you hold them in RAM for &lt;strong&gt;a few MB&lt;/strong&gt; instead of a hundred-plus?&lt;/p&gt;

&lt;p&gt;The naive layout is an array of objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FileEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;frn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parentFrn&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;// 24 B&lt;/span&gt;
    &lt;span class="kt"&gt;int64_t&lt;/span&gt;  &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// 8 B&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// 32 B + heap allocation per file&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// 1 B + 7 B padding&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Padding, per-string heap allocations, allocator headers, terrible cache locality. That's your hundred-plus MB right there — not because anyone wrote bad code, but because &lt;strong&gt;the layout itself is that size&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The engine stores rows columnar instead — separate tight arrays, one per attribute, plus an interned name pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;frns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// full 64-bit FRNs (sequence&amp;lt;&amp;lt;48 | index)&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mtimes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Unix seconds — 1970..2106 fits in 4 bytes&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nameOffs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// offset into the blob&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nameLens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;isDirs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;     &lt;span class="n"&gt;nameBlob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// UTF-8 pool, no NUL terminators, deduplicated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every frontend developer knows this instinctively: it's the difference between &lt;code&gt;[{name, size, mtime}, ...]&lt;/code&gt; and one TypedArray per column. SoA beats AoS when you scan.&lt;/p&gt;

&lt;p&gt;The merciless details that stack up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interned names.&lt;/strong&gt; "New folder", "package.json", "index.js" appear tens of thousands of times each. One copy in the blob; every row is just an 8-byte offset+length slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;uint32&lt;/code&gt; Unix-seconds mtimes.&lt;/strong&gt; Halves the time column; 1970–2106 is plenty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lowercase names that aren't there.&lt;/strong&gt; Case-insensitive search needs a lowercased name — but most filenames are &lt;em&gt;already&lt;/em&gt; lowercase. Each row carries a flag bit: if the folded name is byte-identical to the original, the lowercase view just reads the original blob. Saves nearly an entire second name pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived columns aren't persisted.&lt;/strong&gt; The parent FRN you need for path reconstruction is derivable from the parent-row column at query time, so the snapshot simply doesn't contain it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same data, different layout: hundreds of MB becomes a few MB. Structure, not optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying fresh: USN journal catch-up — and a subtle trap
&lt;/h2&gt;

&lt;p&gt;Full builds are one thing; filesystems keep changing. NTFS ships a change journal (USN records, monotonically increasing). The engine stores an anchor &lt;code&gt;(JournalId, NextUsn)&lt;/code&gt; and periodically replays new records.&lt;/p&gt;

&lt;p&gt;The trap I hit so you don't have to: &lt;strong&gt;the anchor must be taken &lt;em&gt;before&lt;/em&gt; enumeration starts, not after it finishes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first version grabbed the current USN after the full scan — logically airtight, right? Wrong. The scan takes seconds. Save a file &lt;em&gt;during&lt;/em&gt; the scan and it may not be on disk when your read passes its directory, yet its USN is already below your anchor. The journal replay skips it &lt;em&gt;forever&lt;/em&gt;. The index is silently missing a file, no error, no recovery.&lt;/p&gt;

&lt;p&gt;Store the pre-enumeration anchor. Replay may redundantly re-apply a change that the scan already caught — idempotent, harmless. Missing is not.&lt;/p&gt;

&lt;p&gt;Also: journals get trimmed and recreated. The anchor's JournalId detects that; on mismatch, abandon incremental catch-up and do a full rebuild. Correctness always outranks cleverness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence: mmap snapshots
&lt;/h2&gt;

&lt;p&gt;Rebuilding on every boot is wasteful, so the index snapshots to disk per volume and loads via &lt;strong&gt;mmap&lt;/strong&gt; — O(1) open, column pointers point straight at mapped pages, zero copies. The query-accelerating structure (an ASCII bigram inverted index, VByte-delta compressed) is &lt;em&gt;materialized into the snapshot&lt;/em&gt;, so loading requires zero rebuilding.&lt;/p&gt;

&lt;p&gt;Two Windows-specific footnotes for anyone doing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After mmap-loading, call &lt;code&gt;PrefetchVirtualMemory&lt;/code&gt;. Otherwise the first full scan pulls tens of MB in random 4 KB page faults (measured: ~6 s on my C: volume) while the OS pages things in lazily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A file you have mmap'd cannot be replaced.&lt;/strong&gt; Atomic snapshot replacement (&lt;code&gt;MoveFileEx&lt;/code&gt; over the old path) fails with &lt;code&gt;gle=5&lt;/code&gt; while your own process still holds a mapping of the old file — even though you're "just" swapping the path. Release your own mapping first, and guard the column-pointer swap with a reader-writer lock so in-flight queries don't race the swap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The scorecard
&lt;/h2&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;Bundled Everything&lt;/th&gt;
&lt;th&gt;Homegrown (1.0.5)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Index memory&lt;/td&gt;
&lt;td&gt;Hundreds of MB (external service)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A few MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependency&lt;/td&gt;
&lt;td&gt;Third-party install/bundle&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query syntax&lt;/td&gt;
&lt;td&gt;Everything's&lt;/td&gt;
&lt;td&gt;Compatible (&lt;code&gt;ext:pdf&lt;/code&gt;, &lt;code&gt;size:&amp;gt;100mb&lt;/code&gt;, &lt;code&gt;dm:today&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control&lt;/td&gt;
&lt;td&gt;Black box&lt;/td&gt;
&lt;td&gt;End-to-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Was it worth it? For this product, yes — search is the palette's core experience, and resident memory is the metric that decides whether a tray app survives on someone's machine. The rule of thumb I'd offer from this round:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't build a wheel where the existing one is already optimized for you. Build it when your core requirement is exactly the dimension the dependency doesn't optimize for.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything is perfectly optimized for being Everything. It was never going to be optimized for being &lt;em&gt;inside&lt;/em&gt; something else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqc3bp9zohozgd0dn0vrn.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%2Fqc3bp9zohozgd0dn0vrn.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This shipped in yyzTools 1.0.5 — free, no accounts, no telemetry, Windows 10/11, at &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;. The same release also added a quick OCR-translate pipeline (screenshot → recognize → translate, all offline via the local RapidOCR engine). Happy to dig deeper into any part of this in the comments — the parallel-scan worker pool has a genuinely fun stack-lifetime crash story if anyone wants it.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>productivity</category>
      <category>programming</category>
      <category>search</category>
    </item>
    <item>
      <title>yyzTools: 40+ Windows Productivity Tools in One Install — Local-First, Free Forever</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:14:35 +0000</pubDate>
      <link>https://dev.to/jearry/yyztools-40-windows-productivity-tools-in-one-install-local-first-free-forever-43e0</link>
      <guid>https://dev.to/jearry/yyztools-40-windows-productivity-tools-in-one-install-local-first-free-forever-43e0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tired of juggling a dozen separate utilities — a launcher, a clipboard manager, a translator, an OCR tool, a file previewer, a screenshot app, a batch renamer...? yyzTools fuses them all into a single, native Windows application with a shared data layer and unified keyboard shortcuts. No plugins. No accounts. No telemetry.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;If you're a Windows power user, your efficiency toolkit probably looks something like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Typical tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App launching&lt;/td&gt;
&lt;td&gt;PowerToys Run / Flow Launcher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File search&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clipboard history&lt;/td&gt;
&lt;td&gt;Ditto / Windows built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;DeepL + browser extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR&lt;/td&gt;
&lt;td&gt;Online tool / Umi-OCR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File preview&lt;/td&gt;
&lt;td&gt;QuickLook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;Snipaste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch renaming&lt;/td&gt;
&lt;td&gt;Separate utility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON formatting&lt;/td&gt;
&lt;td&gt;Browser tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex testing&lt;/td&gt;
&lt;td&gt;regex101.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base64 encoding&lt;/td&gt;
&lt;td&gt;Browser tab&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each tool has its own shortcut, its own settings panel, its own update channel — and crucially, &lt;strong&gt;none of them talk to each other&lt;/strong&gt;. You can't pipe clipboard history into OCR, or send OCR output directly to translation, or push translation results back to the clipboard. Every cross-tool workflow requires manual copy-paste intermediation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyzTools&lt;/a&gt; (Yes Your Zen Tools) takes a different approach: &lt;strong&gt;fuse 40+ productivity tools into a single install&lt;/strong&gt;, sharing one data layer, one set of keyboard shortcuts, and one visual language.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Current version&lt;/strong&gt;: v1.0.3.1300&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform&lt;/strong&gt;: Windows 10 1809+ / 11, 64-bit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price&lt;/strong&gt;: Free forever, no account required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages&lt;/strong&gt;: 12 (English, Simplified Chinese, Traditional Chinese, Japanese, Korean, French, German, Spanish, Russian, Arabic, Portuguese, Italian)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture: C++ + WebView2
&lt;/h2&gt;

&lt;p&gt;yyzTools uses a hybrid architecture that's worth understanding as a developer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│     Frontend (Alpine.js + vanilla JS)   │
│   cmdplate / ocr / translate / ...      │
│   Unified via web/lib/zen_api.js        │
├─────────────────────────────────────────┤
│      WebView2 Bridge Layer              │
│   BindSync → synchronous return          │
│   BindAsync → async callback (OCR only)  │
├─────────────────────────────────────────┤
│         C++ NativeApi Layer              │
│  FileManager / ProcessManager /          │
│  ClipboardManager / OcrManager /        │
│  DownloadManager / CmdManager ...        │
├─────────────────────────────────────────┤
│            Win32 System API              │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebView2 over Electron&lt;/strong&gt;: Uses the Windows-built-in Edge engine, no bundled Chromium, lower memory footprint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine.js (3KB) over React/Vue&lt;/strong&gt;: No virtual DOM overhead, no heavy build chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite multi-entry&lt;/strong&gt;: Each feature page is an independent bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend updates without recompiling C++&lt;/strong&gt;: UI changes ship as frontend dist, no .exe rebuild needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JS ↔ C++ bridge is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Frontend calls go through ZenAPI wrapper, never raw window.Zen&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ZenAPI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;isOk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Normalizes both number 0 and string "0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getConfig&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="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Zen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConfig&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// C++ registration in NativeApi::SetupBindings()&lt;/span&gt;
&lt;span class="n"&gt;m_bindManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BindSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getConfig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;m_bindManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ocrRecognition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bind&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;NativeApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OcrRecognition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All methods return a JSON object &lt;code&gt;{ error, errorMsg, ... }&lt;/code&gt;. &lt;code&gt;error === 0&lt;/code&gt; means success.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Command Palette — One Search Box for Everything
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Alt + Space&lt;/code&gt; brings up the unified entry point. Prefix-based domain switching:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Searches&lt;/th&gt;
&lt;th&gt;Replaces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;Apps, system commands, settings, command modules&lt;/td&gt;
&lt;td&gt;PowerToys Run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All files (Everything SDK)&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Running processes&lt;/td&gt;
&lt;td&gt;Task Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser bookmarks&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clipboard history&lt;/td&gt;
&lt;td&gt;Ditto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command line&lt;/td&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also includes &lt;strong&gt;instant math&lt;/strong&gt;: type &lt;code&gt;128*0.85&lt;/code&gt; or &lt;code&gt;sqrt(144)+3^2&lt;/code&gt; and the result appears in real-time, press Enter to copy.&lt;/p&gt;

&lt;p&gt;Supports &lt;strong&gt;pinyin search&lt;/strong&gt; for Chinese app names — typing &lt;code&gt;weixin&lt;/code&gt; or &lt;code&gt;wx&lt;/code&gt; finds WeChat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clipboard History + Cross-Feature Pipeline
&lt;/h3&gt;

&lt;p&gt;Text and image clipboard history with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin images to desktop as floating windows (great for reference screenshots)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline&lt;/strong&gt;: select a text entry → send to translation; select an image → send to OCR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the "fusion" advantage — no manual copy-paste between tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local OCR (RapidOCR)
&lt;/h3&gt;

&lt;p&gt;Screenshot-to-text using the local RapidOCR engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully offline&lt;/strong&gt; — no cloud API, no upload, no latency&lt;/li&gt;
&lt;li&gt;Chinese-English mixed recognition&lt;/li&gt;
&lt;li&gt;Pipeline: Screenshot OCR → one-click translate → one-click copy to clipboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  File Preview
&lt;/h3&gt;

&lt;p&gt;Spacebar to preview without launching any app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Word / Excel / PowerPoint / PDF&lt;/li&gt;
&lt;li&gt;Images / Videos / Audio&lt;/li&gt;
&lt;li&gt;Code files (syntax highlighted)&lt;/li&gt;
&lt;li&gt;Archive files (browse directory tree)&lt;/li&gt;
&lt;li&gt;DXF drawings&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Developer Tools
&lt;/h3&gt;

&lt;p&gt;Built-in utilities that replace browser tabs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regex tester&lt;/td&gt;
&lt;td&gt;Real-time match highlighting, group viewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encode/Decode&lt;/td&gt;
&lt;td&gt;Base64 / URL / HTML / Unicode / Hex / Morse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash calculator&lt;/td&gt;
&lt;td&gt;MD5 / SHA-1 / SHA-256 / SHA-512 for text and files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;OpenSSL-based symmetric/asymmetric encryption, key generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data conversion&lt;/td&gt;
&lt;td&gt;JSON ↔ YAML ↔ XML ↔ TOML ↔ CSV, hex/case/width conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data generation&lt;/td&gt;
&lt;td&gt;UUID v4, ULID, random strings, batch test data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Batch Processing — Three Heavy-Duty Engines
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Powers&lt;/th&gt;
&lt;th&gt;Capabilities&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ImageMagick&lt;/td&gt;
&lt;td&gt;Image batch&lt;/td&gt;
&lt;td&gt;Resize, crop, format convert, watermark, compress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FFmpeg&lt;/td&gt;
&lt;td&gt;Video batch&lt;/td&gt;
&lt;td&gt;Transcode, trim, extract frames, compress, extract audio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfcpu + Ghostscript&lt;/td&gt;
&lt;td&gt;PDF batch&lt;/td&gt;
&lt;td&gt;Merge, split, watermark, compress, encrypt/decrypt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus a batch rename tool with preview: sequential numbering, find-replace, regex, case, EXIF date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots + Recording
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Region screenshot&lt;/strong&gt;: Select area, copy or save&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrolling screenshot&lt;/strong&gt;: Scroll to capture long pages/documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin to screen&lt;/strong&gt;: Pin screenshots as desktop sticky notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen recording&lt;/strong&gt;: Record screen + audio, optional webcam overlay, auto zoom-follow on mouse, keystroke display, one-click export&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Command Module System (.zenmod)
&lt;/h3&gt;

&lt;p&gt;379 built-in command modules across 5 categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System&lt;/td&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;td&gt;All Windows Control Panel functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Tools&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;ChatGPT, DeepSeek, Gemini, Claude, Qwen, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;Volume control, shutdown, screenshot, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;Translation, OCR, download, calculator, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;OneNote, Microsoft To-Do&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Extending is trivial&lt;/strong&gt; — drop a JSON file into the &lt;code&gt;Modules/&lt;/code&gt; directory:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&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;"en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zh_cn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"我的工具"&lt;/span&gt;&lt;span class="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;"desc"&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;"en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Quick launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zh_cn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"快速启动"&lt;/span&gt;&lt;span class="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;"cmdPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yyzBrowser.exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cmdArgs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--url=https://example.com --size=800x600"&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;No compilation, no restart. The command appears in the palette immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Wallpaper + Desktop Effects
&lt;/h3&gt;

&lt;p&gt;Windows Composition API places web pages and videos &lt;strong&gt;below the desktop icon layer&lt;/strong&gt; — no icon obstruction, no click interference. Game mode pauses all effects to free resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Philosophy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local-First
&lt;/h3&gt;

&lt;p&gt;All data processed locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clipboard history → local storage&lt;/li&gt;
&lt;li&gt;OCR → local RapidOCR model, no cloud API&lt;/li&gt;
&lt;li&gt;File search → local Everything index&lt;/li&gt;
&lt;li&gt;App usage stats → local records&lt;/li&gt;
&lt;li&gt;File preview → local parsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only translation (calls translation service API) and update checks require network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free Forever
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No paywalls, no premium tier, no trial countdown&lt;/li&gt;
&lt;li&gt;No account registration required&lt;/li&gt;
&lt;li&gt;No identity collection, no user profiling, no behavioral telemetry&lt;/li&gt;
&lt;li&gt;No ads&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Delta Updates
&lt;/h3&gt;

&lt;p&gt;Each component (main binary, frontend pages, command modules, FFmpeg, OCR models) is packaged independently. Updates download only the changed sub-packages — a frontend-only update might be a few hundred KB, not a full 1GB reinstall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&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;yyzTools&lt;/th&gt;
&lt;th&gt;PowerToys&lt;/th&gt;
&lt;th&gt;uTools&lt;/th&gt;
&lt;th&gt;Flow Launcher&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;C++ + WebView2&lt;/td&gt;
&lt;td&gt;C# + WPF&lt;/td&gt;
&lt;td&gt;Electron&lt;/td&gt;
&lt;td&gt;C# + WPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature source&lt;/td&gt;
&lt;td&gt;All built-in&lt;/td&gt;
&lt;td&gt;All built-in&lt;/td&gt;
&lt;td&gt;Plugin market&lt;/td&gt;
&lt;td&gt;Plugin market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR&lt;/td&gt;
&lt;td&gt;Built-in (offline)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch processing&lt;/td&gt;
&lt;td&gt;ImageMagick/FFmpeg/pdfcpu&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File preview&lt;/td&gt;
&lt;td&gt;Office/PDF/code/archive&lt;/td&gt;
&lt;td&gt;SVG/PDF/MD&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data pipeline&lt;/td&gt;
&lt;td&gt;Clipboard→OCR→Translate→Copy&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No shared data layer&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension method&lt;/td&gt;
&lt;td&gt;JSON file (zero compile)&lt;/td&gt;
&lt;td&gt;C# plugin&lt;/td&gt;
&lt;td&gt;Plugin market&lt;/td&gt;
&lt;td&gt;C# plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Paid tier&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key difference isn't feature count — it's &lt;strong&gt;integration&lt;/strong&gt;. yyzTools features share one data layer and can pipe into each other. The others are either loose collections of independent tools (PowerToys) or plugin platforms where plugins don't share state (uTools).&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt;: &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requirements&lt;/strong&gt;: Windows 10 1809+ / 11, 64-bit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: Free, no registration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages&lt;/strong&gt;: 12 (English, Chinese Simplified/Traditional, Japanese, Korean, French, German, Spanish, Russian, Arabic, Portuguese, Italian)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quick start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download and install from &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Alt + Space&lt;/code&gt; to open the command palette&lt;/li&gt;
&lt;li&gt;Type a feature name or app name to search&lt;/li&gt;
&lt;li&gt;To add custom commands, drop a &lt;code&gt;.zenmod&lt;/code&gt; JSON file into the &lt;code&gt;Modules/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you use 3+ of the tools yyzTools covers (clipboard manager, translator, OCR, file previewer, batch renamer, screenshot tool, regex tester, etc.), it's worth trying as a single replacement rather than maintaining a fragmented toolkit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Website: &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt; · Free forever · Local-first · 12 languages&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Shipping Stock CLIs as Subprocess Instead of Static-Linking SDKs</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Sun, 23 Aug 2026 06:30:53 +0000</pubDate>
      <link>https://dev.to/jearry/shipping-stock-clis-as-subprocess-instead-of-static-linking-sdks-1jl8</link>
      <guid>https://dev.to/jearry/shipping-stock-clis-as-subprocess-instead-of-static-linking-sdks-1jl8</guid>
      <description>&lt;p&gt;I'm building yyzTools, which bundles 9 third-party engines (OpenSSL, FFmpeg, ImageMagick, pdfcpu, Aria2, 7-Zip, RapidOCR, Everything...). I chose to spawn them as subprocesses rather than static-link their SDKs. Here's why—and the cost.&lt;/p&gt;

&lt;p&gt;The conventional approach&lt;br&gt;
When your app needs OpenSSL crypto, FFmpeg video processing, ImageMagick image ops—you reach for the SDK. Link libssl, link libav*, link libMagick. One binary, no external deps, fast function calls. It's the textbook answer.&lt;/p&gt;

&lt;p&gt;I did the opposite. yyzTools ships the stock CLI binaries (openssl.exe, ffmpeg.exe, magick.exe, pdfcpu, aria2c, 7z) and spawns them as subprocesses. The C++ layer is a thin loop: build args → CreateProcess → read stdout → wrap as JSON → return. It doesn't know what -gravity southeast or sm4-cbc means. It just passes the algorithm name through.&lt;/p&gt;

&lt;p&gt;Why I went this way&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upgrades without recompiling
This is the big one for a desktop app. OpenSSL ships a CVE, or adds sm2/sm3/sm4 support in 3.x. If you've static-linked, you recompile the whole app, run full regression, re-release, and every user reinstalls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With the subprocess model, I drop in a new openssl.exe. Zero C++ changes. The update is a few-MB delta, not a full reinstall. For a product where users won't tolerate reinstalling for a library bump, this is the deciding factor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No symbol conflicts&lt;br&gt;
OpenSSL, zlib, libpng—multiple libraries want to own these symbols. Static linking them all into one binary is a recipe for "which inflate did I just call?" With subprocess CLIs, each tool brings its own dependencies in its own process. No conflict.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transparent supply chain&lt;br&gt;
openssl version, ffmpeg -version—auditing which version of each tool is live is trivial. It's an independent binary. Far easier than digging symbols out of a statically-linked blob.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Free crash isolation&lt;br&gt;
If ffmpeg.exe misbehaves, it exits non-zero and my host wraps that as an error. My main process keeps running. A static-linked bug can take down the whole app. The process boundary is a free fault domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Algorithm support is "front-end list + docs", not C++ rebuild&lt;br&gt;
Because the C++ side just passes algorithm names through, adding a new hash algorithm means adding a line to the front-end's ALGORITHMS list and updating docs. NativeApi doesn't change. I covered sm2/sm3/sm4 in a day this way.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cost&lt;br&gt;
This isn't free. Let's be honest:&lt;/p&gt;

&lt;p&gt;Per-call fork overhead&lt;br&gt;
Spawning a process is ~tens of ms. For "compress this folder" or "hash this file," fine. For "verify 10,000 HMACs in a loop," brutal. You'd never build a high-throughput crypto service this way. I knew this going in—yyzTools hashes one file at a time, not a stream of requests. Low frequency is the prerequisite for this pattern.&lt;/p&gt;

&lt;p&gt;Large-file hashing is slower&lt;br&gt;
A static SHA256() call streams bytes in-process. The subprocess model means bytes cross the process boundary. For a 4GB ISO, you feel it. Acceptable for my use case; unacceptable for a backup service.&lt;/p&gt;

&lt;p&gt;The stdout contract is the weak point&lt;br&gt;
CLIs emit human-readable output, not machine-readable JSON. I hand-rolled a stable parser for each CLI's output. When a CLI upgrades and changes its output format, the parser can break. Easier to debug than a static ABI change (the CLI just fails loudly), but it's a maintenance surface.&lt;/p&gt;

&lt;p&gt;I wish there were a standard "CLI emits JSON" convention. Some tools have it (gh, kubectl), many don't (openssl enc's output is a mess to parse). The inconsistency is the real friction.&lt;/p&gt;

&lt;p&gt;Process lifecycle management&lt;br&gt;
You own timeouts, zombie handling, cancellation. A static function call doesn't hang. A subprocess can. I had to build a work-queue with concurrency limits (don't spawn 200 magick.exe for 200 images) and cancellation. That's extra code you don't write for a static link.&lt;/p&gt;

&lt;p&gt;Where I'd draw the line&lt;br&gt;
This pattern fits when:&lt;/p&gt;

&lt;p&gt;Low-frequency calls — desktop tools, not services&lt;br&gt;
Fast upstream upgrades matter — security CVEs, new algorithms&lt;br&gt;
You want transparent supply chain — auditable binaries&lt;br&gt;
The CLI is mature and stable — FFmpeg, OpenSSL have decades of CLI stability&lt;br&gt;
It fails when:&lt;/p&gt;

&lt;p&gt;High throughput — a TLS server, a per-request crypto service&lt;br&gt;
Large streaming I/O — backup, media transcoding at scale&lt;br&gt;
The CLI output is unstable — you'd spend your life chasing format changes&lt;br&gt;
I use the subprocess model for the 9 bundled CLIs in yyzTools. If I were building a media server or a crypto API gateway, I'd static-link (or use a proper service). The pattern is frequency-dependent.&lt;/p&gt;

&lt;p&gt;A pattern worth naming&lt;br&gt;
I haven't seen this called out as a named pattern, but it shows up: VS Code ships ripgrep and calls it. Many Electron apps bundle CLIs. The principle—"ship the stock CLI, spawn it, parse stdout"—is a middle ground between static linking and microservices.&lt;/p&gt;

&lt;p&gt;Position on the dependency spectrum:&lt;/p&gt;

&lt;p&gt;Static link Subprocess CLI  gRPC service&lt;br&gt;
Perf    fastest (function call) medium (fork + IPC) slowest (network)&lt;br&gt;
Upgrade cost    recompile + regression  drop a file redeploy service&lt;br&gt;
Crash isolation none    process boundary    network boundary&lt;br&gt;
Cross-language  compile-time binding    natural natural&lt;br&gt;
Best for    high-throughput core    low-freq trusted capability cross-machine&lt;br&gt;
The subprocess-CLI slot is underused. For a desktop app bundling many third-party capabilities, it's often the right slot.&lt;/p&gt;

&lt;p&gt;The honest takeaway&lt;br&gt;
If your tool calls a third-party engine a few times a session (not thousands of times a second), and you care about fast upgrades and transparent supply chain, shipping the stock CLI as a subprocess beats static-linking. You pay per-call latency and a parser to maintain; you gain zero-recompile upgrades and crash isolation.&lt;/p&gt;

&lt;p&gt;Don't use it for hot paths. Do use it for "I need OpenSSL/FFmpeg/ImageMagick occasionally, and I don't want to recompile when they patch a CVE."&lt;/p&gt;

&lt;p&gt;I'm building yyzTools — a free, local-first Windows productivity suite. Website: yyztools.com. The 9 bundled CLIs are OpenSSL, 7-Zip, FFmpeg, ImageMagick, pdfcpu, Aria2, RapidOCR, Everything, and Ghostscript.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cli</category>
      <category>cpp</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why I Built a Windows Desktop App with C++ + WebView2 Instead of Electron</title>
      <dc:creator>yyzTools</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:40:17 +0000</pubDate>
      <link>https://dev.to/jearry/why-i-built-a-windows-desktop-app-with-c-webview2-instead-of-electron-2o69</link>
      <guid>https://dev.to/jearry/why-i-built-a-windows-desktop-app-with-c-webview2-instead-of-electron-2o69</guid>
      <description>&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyzTools&lt;/a&gt;, a Windows productivity suite with 40+ tools fused into one install. This is the architecture decision I get asked about most.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The default path
&lt;/h2&gt;

&lt;p&gt;When a developer today wants to build a desktop app, the default reflex is Electron. It's rational: HTML/CSS/JS, cross-platform, mature ecosystem (VS Code, Slack, Discord proved it works). If your team is frontend-heavy, it's almost a non-decision.&lt;/p&gt;

&lt;p&gt;I didn't take that path. yyzTools is C++ (Win32) + WebView2 + Alpine.js + Vite. Windows-only. No bundled Chromium.&lt;/p&gt;

&lt;p&gt;This post isn't "Electron is bad." It's "for a resident, lightweight, multi-feature desktop tool, Electron's costs outweigh its benefits, and here's what I picked instead."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Electron didn't fit this project
&lt;/h2&gt;

&lt;p&gt;The key word is &lt;strong&gt;resident&lt;/strong&gt;. yyzTools is a tool you install once and it sits in your tray, running every time you hit &lt;code&gt;Alt+Space&lt;/code&gt; for the command palette, grab from clipboard history, fire OCR. It's not an app you open and close—it's always there.&lt;/p&gt;

&lt;p&gt;Electron's model is "bundle a full Chromium per app." For an app you open occasionally, the ~200MB memory and multi-second startup is tolerable. For something that's &lt;strong&gt;always resident&lt;/strong&gt;, it's a tax you pay every second the machine is on.&lt;/p&gt;

&lt;p&gt;Nobody wants a clipboard manager + launcher eating 400MB of RAM just by being installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternative: WebView2 + a C++ host
&lt;/h2&gt;

&lt;p&gt;Microsoft's WebView2 is a control that renders web content using the &lt;strong&gt;Edge engine already installed on Windows&lt;/strong&gt;. No bundled browser. The runtime ships with Windows 11; on Windows 10 the installer guides the user.&lt;/p&gt;

&lt;p&gt;So my stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C++ / Win32 host&lt;/strong&gt; — owns the window, system calls, process management, all native work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebView2&lt;/strong&gt; — renders the frontend pages using the system Edge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine.js (~3KB)&lt;/strong&gt; — frontend framework, no virtual DOM, no heavy build chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite multi-entry&lt;/strong&gt; — each feature page (command palette, OCR, translate, download...) is an independent bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is a four-layer sandwich:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend pages (Alpine.js + vanilla JS)
        ↓  via web/lib/zen_api.js (unified wrapper)
WebView2 bridge layer
   BindSync  → synchronous return (most methods)
   BindAsync → async callback (OCR, app-info)
        ↓
C++ NativeApi layer (FileManager / ProcessManager / ...)
        ↓
Win32 system API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend never calls &lt;code&gt;window.Zen&lt;/code&gt; directly. It goes through a &lt;code&gt;ZenAPI&lt;/code&gt; wrapper class that normalizes return values—the C++ side has two JSON paths (hand-rolled strings vs &lt;code&gt;boost::property_tree&lt;/code&gt;) where the same &lt;code&gt;error&lt;/code&gt; field can be number &lt;code&gt;0&lt;/code&gt; or string &lt;code&gt;"0"&lt;/code&gt;. The wrapper does &lt;code&gt;String(res.error) === '0'&lt;/code&gt; to paper over it. (That's documented debt, not a hidden clever trick.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower memory.&lt;/strong&gt; No Chromium process resident. The main process is a thin C++ host plus the system Edge it borrows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster startup.&lt;/strong&gt; No spinning up a fresh Chromium instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller disk.&lt;/strong&gt; The web runtime is shared across the system, not bundled per-app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native when needed.&lt;/strong&gt; File ops, process management, OCR engine calls, screen recording—all C++, no IPC to a JS-land bridge for hot paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Let's be honest about the tradeoffs, because this isn't a free lunch:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows-only.&lt;/strong&gt; WebView2 is Microsoft's. If you want Mac/Linux, you need Electron or a full Qt port. yyzTools targets Windows, so this is fine—but it's the biggest constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hand-written bridge.&lt;/strong&gt; Electron gives you mature IPC for free. With WebView2 you hand-roll the JS↔C++ channel (a "method name → callable" registry, sync/async variants, thread marshaling for async callbacks back to the UI thread). It's not hard, but it's code you own and maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No type system on the boundary.&lt;/strong&gt; The &lt;code&gt;error&lt;/code&gt; field type drift I mentioned isn't caught by TypeScript—it's a runtime contract between C++ and JS. I normalize at the wrapper, but every new native method is a chance to introduce a new type mismatch. A typed IPC (like tRPC over a real protocol) would be safer; I chose simplicity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alpine.js means no React ecosystem.&lt;/strong&gt; Alpine is great for "many simple pages" (which a productivity suite is). But if I wanted a complex single-page app with a rich component library, I'd miss React's ecosystem. The pages here are intentionally simple—search box, settings panel, result grid—so Alpine fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need C++ people.&lt;/strong&gt; A pure-frontend team can't extend the native side. This is a real org constraint. If your team can't staff Win32 expertise, Electron is the pragmatic call, period.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you'd make the same call
&lt;/h2&gt;

&lt;p&gt;I'd pick this again specifically when all four are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The app is resident&lt;/strong&gt; (tray, autostart, always-on) — the memory tax compounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows is the target&lt;/strong&gt; — WebView2 is Windows-only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need native system access&lt;/strong&gt; — file/process/clipboard/OCR, where C++ beats a JS bridge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have C++ capacity&lt;/strong&gt; — someone can own the host and bridge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of those is false, Electron (or Tauri) probably wins. yyzTools happened to clear all four, so the C++/WebView2 path was correct here. It would be wrong for a cross-platform app, or a resident app with no native API needs, or a team of pure-frontend devs.&lt;/p&gt;

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

&lt;p&gt;"Electron vs native" isn't a moral question—it's a fit question. The reflexive "always Electron" is wrong for resident, native-heavy, Windows-targeted tools. The reflexive "never Electron" is wrong for cross-platform or frontend-heavy teams.&lt;/p&gt;

&lt;p&gt;The useful question to ask when you start a desktop project: &lt;strong&gt;is this app resident, and does it need native system access?&lt;/strong&gt; If yes to both and you're on Windows, the C++ + WebView2 path is worth the C++ staffing cost. If no, Electron's defaults will serve you faster.&lt;/p&gt;

&lt;p&gt;yyzTools is the former. Your project might be the latter. Pick on fit, not on what's trendy.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building yyzTools — a free, local-first Windows productivity suite. Website: &lt;a href="https://yyztools.com" rel="noopener noreferrer"&gt;yyztools.com&lt;/a&gt;. I write about the architecture decisions behind it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cpp</category>
      <category>productivity</category>
      <category>software</category>
    </item>
  </channel>
</rss>
