<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[yyzTools]]></title><description><![CDATA[The official blog of yyzTools — a desktop productivity suite for Windows that packs a command palette, OCR, translation, clipboard history, file preview, and dy]]></description><link>https://yyztools.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 18:01:14 GMT</lastBuildDate><atom:link href="https://yyztools.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Anatomy of a Minimal Desktop Extension SDK: Declarative Hosts, JS Bridges, and Honest Trust Models]]></title><description><![CDATA[Browser extensions, VS Code plugins, uTools scripts — desktop extension systems are usually heavyweight machinery: sandboxes, permission manifests…]]></description><link>https://yyztools.hashnode.dev/the-anatomy-of-a-minimal-desktop-extension-sdk-declarative-hosts-js-bridges-and-honest-trust-models</link><guid isPermaLink="true">https://yyztools.hashnode.dev/the-anatomy-of-a-minimal-desktop-extension-sdk-declarative-hosts-js-bridges-and-honest-trust-models</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Tue, 06 Oct 2026 13:27:56 GMT</pubDate><content:encoded><![CDATA[<p><em>Browser extensions, VS Code plugins, uTools scripts — desktop extension systems are usually heavyweight machinery: sandboxes, permission manifests, dedicated APIs, review pipelines. But there's a whole class of software where that machinery is overkill — tools built for users who want to extend things for themselves. This post dissects one such design, the module SDK of</em> <a href="https://yyztools.com"><em>yyzTools</em></a> <em>(my free Windows productivity suite), as a case study in the minimal end of the spectrum. Even if you never use the tool, the four design decisions in here are reusable.</em></p>
<hr />
<h2>The problem shape determines the extension shape</h2>
<p>Extension systems get over-engineered when teams copy the design of one trust model into another. The key variable is: <strong>who writes extensions, and who runs them?</strong></p>
<table>
<thead>
<tr>
<th>Trust model</th>
<th>Extension author</th>
<th>Extension runner</th>
<th>Required machinery</th>
</tr>
</thead>
<tbody><tr>
<td>Public marketplace</td>
<td>Anyone</td>
<td>Anyone</td>
<td>Sandbox, permissions, review</td>
</tr>
<tr>
<td>Curated platform</td>
<td>Vetted devs</td>
<td>Anyone</td>
<td>Review, signing</td>
</tr>
<tr>
<td><strong>Personal/team tools</strong></td>
<td>The user themselves</td>
<td>Same person / their team</td>
<td>Almost none</td>
</tr>
</tbody></table>
<p>yyzTools' SDK targets the third row: a developer wants a small tool — "quick-open the ticket system", "calculate quotes" — for themselves or their team. The deployment unit is "copy a folder to your own machine". That context frees the design from nearly all the security machinery, and what remains can be genuinely minimal.</p>
<h2>Piece one: the declarative host</h2>
<p>The scariest part of desktop development for a web developer isn't logic — it's windows. Message loops, DPI, taskbar, hotkeys. The SDK's move is to reduce the entire host to a data file:</p>
<pre><code class="language-json">{
    "id": "my_tool",
    "type": "web",
    "name": { "en": "My Tool", "zh_cn": "我的工具" },
    "icon": "fas fa-wrench",
    "url": "MyTool/index.html",
    "width": 760,
    "height": 540,
    "layoutType": 34
}
</code></pre>
<p>The host application scans module files at startup and creates a native window with an embedded WebView for each <code>type: "web"</code> entry. Window size, icon, the multilingual name, load path — all declared, none coded. (That's the real sample file trimmed, not an idealized sketch — the shipped sample carries all twelve language names.)</p>
<p>What makes this more than a config file trick is that <strong>the module inherits the host's integration surface</strong>: command palette search (localized across all 12 UI languages), global hotkey binding, dock mounting, theme variables (<code>--zen-*</code> CSS custom properties injected by the host). The extension author writes a feature; the host absorbs the decade of Win32 plumbing.</p>
<p>The generalizable principle: <em>when your extension format can express "what the host should do for me" declaratively, integration cost drops from per-extension work to per-host work.</em></p>
<h2>Piece two: the JS bridge with a disciplined wrapper</h2>
<p>The webpage talks to the system through a <code>window.Zen</code> bridge, but — critically — the official guidance is that <strong>nobody calls the bridge raw</strong>. Everything goes through a shipped <code>ZenAPI</code> wrapper class.</p>
<p>Why the hard rule? Because native bridges have type semantics that leak — and because those semantics <em>change as the host matures</em>. The case study is instructive in both directions. Early on, the C++ side had two JSON serialization paths (hand-rolled strings and property trees) whose error and boolean conventions disagreed, and the wrapper's <code>isOk()</code> was the one place normalizing them. Later, the host unified serialization into a typed tree — error codes became real numbers, booleans became real booleans — and because every module consumed the bridge exclusively through the wrapper, that migration touched <strong>one file's semantics instead of every extension ever written</strong>. (One deliberate exception survives, documented rather than hidden: configuration values stay strings, because the front end has always written them that way.)</p>
<p>The lesson for anyone designing a JS bridge: <strong>the wrapper layer is where your serialization model gets paid down — once, centrally — instead of being paid forever by every extension author.</strong> Skimp on it and a type-system cleanup becomes an ecosystem-wide breaking change.</p>
<p>Another bridge detail worth stealing: async interfaces implement a <strong>slot mechanism</strong> — a new request displaces the in-flight one instead of queueing. For interactive search, only the latest request is meaningful; queueing would make the background thread execute a series of immediately-obsolete full-index scans. In human-interface contexts, the correct concurrency semantic for bursts is <em>overwrite</em>, not <em>queue</em>.</p>
<h2>Piece three: designed-in degradation</h2>
<p>Here's the detail I most want you to take away. The sample module's page, opened directly in a browser (no host, no <code>window.Zen</code>), <strong>still works</strong> — layout, styles, debounce, rendering. The wrapper detects the missing bridge and takes the exception path, returning <code>error: -1</code>.</p>
<p>This isn't an accident; it's a designed degradation boundary. The consequence: UI iteration happens in a normal browser with normal devtools, at browser speed. Only native-integration testing requires the host. Compare this to extension systems where the only debug target is the host itself, and the value becomes obvious.</p>
<p><em>Design rule: every bridge-dependent page should have a defined behavior in a bridge-less environment, and that behavior should be "run everything that doesn't need the bridge".</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/2bdb6f13-c9c1-4b1c-a90c-08bd9253b13e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/393ab27a-aed9-4197-b094-277abc453695.png" alt="" style="display:block;margin:0 auto" />

<h2>Piece four: the honest trust model</h2>
<p>The SDK's native API opens the host's full system capability to modules — file deletion, process creation, window control — with <strong>no permission prompts, no sandbox, no undo</strong>.</p>
<p>For the personal/team trust model, this is correct rather than reckless: if an attacker can write to your modules directory, they can already drop an executable; a <code>.zenmod</code> disguise adds nothing. And every sandbox layer you don't need is a permission negotiation you'd impose on every module author, in a context where the author and runner are the same person.</p>
<p>The design debt is acknowledged in the docs: a security notice states plainly that module authors must validate inputs and treat irreversible operations with care. And the boundary is explicit — if the project ever builds a public module store, the trust model changes and this design must be rebuilt. <strong>Match security to the trust model, not to a checklist.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/a86a032d-fca7-42e1-9be7-65fb4e2f16ca.png" alt="" style="display:block;margin:0 auto" />

<h2>The whole system in one table</h2>
<table>
<thead>
<tr>
<th>Design decision</th>
<th>Mechanism</th>
<th>Reusable principle</th>
</tr>
</thead>
<tbody><tr>
<td>Window creation</td>
<td>Declarative host (JSON)</td>
<td>Let extensions declare what the host should provide</td>
</tr>
<tr>
<td>System access</td>
<td>JS bridge + mandated wrapper</td>
<td>Pay down type inconsistencies once, centrally</td>
</tr>
<tr>
<td>Async semantics</td>
<td>Displace, don't queue</td>
<td>Interactive bursts mean "latest wins"</td>
</tr>
<tr>
<td>No-host debugging</td>
<td>Bridge-less degradation path</td>
<td>Define behavior outside the host</td>
</tr>
<tr>
<td>No sandbox</td>
<td>Trust model: author = runner</td>
<td>Security follows trust, not fashion</td>
</tr>
</tbody></table>
<h2>If you want to poke at it</h2>
<p>All of this is live: the docs (a quick start plus the full API reference, in English and Chinese — other UI languages fall back to English) and a build-free sample project — a full-disk file search tool, which happens to exercise the suite's new homegrown MFT engine — are at <a href="https://yyztools.com/sdk.html">yyztools.com/sdk.html</a>. The sample is four vanilla-JS files and reads in about an hour, including the debounce/out-of-order-discard handling that most search UIs get wrong.</p>
<p>If you're designing an extension system for a tool whose users are its developers, this end of the spectrum deserves a seat at the table next to the heavyweight sandboxes.</p>
]]></content:encoded></item><item><title><![CDATA[331 Windows Tweaks and an Undo Button]]></title><description><![CDATA[Why the new yyzTools System Optimizer treats registry cleanup as a database migration problem. Part of the 1.0.8 release notes, told from the engine room…]]></description><link>https://yyztools.hashnode.dev/331-windows-tweaks-and-an-undo-button</link><guid isPermaLink="true">https://yyztools.hashnode.dev/331-windows-tweaks-and-an-undo-button</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Mon, 05 Oct 2026 14:58:11 GMT</pubDate><content:encoded><![CDATA[<p><em>Why the new</em> <a href="https://yyztools.com"><em>yyzTools</em></a> <em>System Optimizer treats registry cleanup as a database migration problem. Part of the 1.0.8 release notes, told from the engine room.</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/6bce4654-a0d3-4356-ba0b-a2ed8a195fa8.png" alt="" style="display:block;margin:0 auto" />

<p>Windows tweaks are the dark matter of PC utility software. Every "optimize your PC" tool ships hundreds of them — disable telemetry, clean the right-click menu, kill startup delays — and they all share one property: nobody can audit hundreds of registry edits, so nobody does. You click "Fix All", cross your fingers, and hope the tool's authors were more careful than the average Regedit forum post.</p>
<p>We shipped a System Optimizer in 1.0.8 and gave it 331 tweaks across eight categories. The interesting part is not the number. It's the constraint we imposed on ourselves: <strong>every single change must be reversible, from a snapshot, without trusting the tool's own memory of what it did.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/c3fce5ce-aaa8-4b8c-b98e-0bf75fae9d1c.png" alt="" style="display:block;margin:0 auto" />

<p>Every rule carries a <code>Safe</code> or <code>Caution</code> badge and declares up front whether it needs admin rights or a reboot; the detail view shows the registry key with its current value next to the optimized one, so "what exactly will this change?" is answered before you tick the box.</p>
<h2>The rule model</h2>
<p>Each tweak is a rule with one of five operation types:</p>
<ul>
<li><p><code>reg</code> — write a registry value</p>
</li>
<li><p><code>keydel</code> — delete a registry key</p>
</li>
<li><p><code>svc</code> — change a Windows service</p>
</li>
<li><p><code>task</code> — change a scheduled task</p>
</li>
<li><p><code>cmd</code> — run a command</p>
</li>
</ul>
<p>The categories map to how people actually want to tune a machine: 73 system rules, 66 privacy, 57 Explorer, 56 shell (this is where right-click-menu cleanup lives — those CLSID entries that accumulate from every app you've ever installed), 36 performance, 20 security, 14 gaming, 9 accessibility.</p>
<p>A health check scans all 331 and reports which ones are not in the "optimized" state. You fix what you want, individually or in bulk. Nothing runs on a schedule behind your back.</p>
<h2>Snapshots, not memories</h2>
<p>Here's the part worth stealing for your own tool, whatever it edits.</p>
<p>Before applying a fix, the optimizer reads the current value and writes it to a local backup folder (<code>%LOCALAPPDATA%\yyzTools\Backup\</code>). Rollback restores from that snapshot. The critical detail: <strong>if the original value cannot be read, the fix refuses to run.</strong> A rule that can't snapshot can't apply. That one check eliminates the entire class of "I changed something and now I can't remember what" failures, because you literally cannot make an unrecorded change.</p>
<p>The nastiest operation type is <code>keydel</code> — deleting a registry key. Deleting is the one operation that can't be reconstructed from a single value, so it's doubly constrained:</p>
<ol>
<li><p>It's only allowed on shell-extension keys (the context-menu handlers — a bounded, well-understood namespace).</p>
</li>
<li><p>Before deleting anything, the whole key is dumped to the backup as a snapshot, so rollback re-creates it rather than guessing at it.</p>
</li>
</ol>
<p>There are three protection lists — generated at build time and enforced again at run time — that mark keys which are off-limits no matter what a rule says. And because some rules genuinely need elevation (service changes, HKLM writes), the tool re-launches itself with UAC only for the operations that require it, instead of running elevated from the start.</p>
<h2>The rollback flow</h2>
<p>The whole lifecycle:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/4cfdef74-6d55-4816-a070-089a8aa0ff94.png" alt="" style="display:block;margin:0 auto" />

<p>In practice, rollback gets used exactly once per user — usually right after they discover that disabling every animation and service made Windows feel like a spreadsheet instead of making it faster. That's fine. An optimizer people can experiment with is more useful than an optimizer people are afraid of.</p>
<h2>The uncomfortable economics of tweaks</h2>
<p>One honest note, because the "PC optimizer" category has earned its skepticism: most tweaks don't make a modern machine measurably faster. A registry tweak that trims a menu animation saves milliseconds. The real value of a tweaks tool is closer to <em>configuration</em> — turning off telemetry you didn't consent to, removing context-menu entries from apps you uninstalled years ago, stopping OEM services you'll never use. That's why the health check reports state, not "score". There's no optimization percentage, no big green number that goes up. You see the list, you decide, every change is snapshotted.</p>
<p>That's the product stance in one sentence: <strong>a tweaks tool shouldn't promise speed; it should promise that nothing it does is a one-way door.</strong></p>
<hr />
<p>The System Optimizer ships in <a href="https://yyztools.com">yyzTools</a> 1.0.8 alongside a Lightning File Scan &amp; Clean, a Lightning Disk Space Analyzer, and a Lightning Memory Cleaner — free, 12 languages, everything local. If you've written tooling that edits system state, I'd genuinely like to hear what your rollback story looks like; the comment section is open.</p>
]]></content:encoded></item><item><title><![CDATA[How Live Wallpapers Render Beneath Your Desktop Icons on Windows]]></title><description><![CDATA[yyzTools includes a live-wallpaper engine — web pages and videos rendered under the desktop icon layer. Playing the content is the easy part; the entire…]]></description><link>https://yyztools.hashnode.dev/how-live-wallpapers-render-beneath-your-desktop-icons-on-windows</link><guid isPermaLink="true">https://yyztools.hashnode.dev/how-live-wallpapers-render-beneath-your-desktop-icons-on-windows</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Sun, 04 Oct 2026 13:47:06 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://yyztools.com"><em>yyzTools</em></a> <em>includes a live-wallpaper engine — web pages and videos rendered under the desktop icon layer. Playing the content is the easy part; the entire hard problem of a wallpaper engine is the window tree. This post is the deep dive.</em></p>
<p>The naive version of "live wallpaper" — a borderless fullscreen window playing a video — fails three ways: your icons vanish behind it, clicks hit the video instead of the icons, and the desktop right-click menu dies. Everything interesting about a wallpaper engine is solving for <strong>"pixels behind, input through."</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/61599daf-939e-4842-95e6-8528eeeaf3f6.png" alt="" style="display:block;margin:0 auto" />

<h2>The desktop is a window tree</h2>
<p><code>Progman</code> ("Program Manager") is the desktop's root window. Under it lives the icon view (a <code>SHELLDLL_DefView</code> hosting a <code>SysListView32</code>), and the static wallpaper is just Progman's background. To render <em>behind</em> the icons, you need a window that sits in the z-order <strong>below the icon view</strong>. The shell doesn't hand you one — unless you ask correctly.</p>
<h2>The 0x052C handshake</h2>
<p>There's an undocumented message — <code>0x052C</code>, often named <code>WM_SHELLPARENTCHANGING</code> — that you can send to <code>Progman</code>. The shell responds by spawning a <code>WorkerW</code> window for you. The resulting tree gives you a slot below the icons that the shell itself maintains:</p>
<pre><code class="language-text">Progman
 ├─ WorkerW                ← you SetParent() your window here
 └─ SHELLDLL_DefView       ← the desktop icons stay in front
     └─ SysListView32
</code></pre>
<p>Your window becomes a child of that WorkerW: visible on the desktop, behind the icons, and outside the icon view's hit-testing. This is the same trick the classic wallpaper engines use. It is unsupported and undocumented — and has worked for over a decade of Windows versions. The usual caveat applies: it's the shell's window tree, not a public API, so you build recovery paths around it (below).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/70055bcf-710c-4723-b333-44ecff1dc134.png" alt="" style="display:block;margin:0 auto" />

<h2>The 24H2 plot twist</h2>
<p>The WorkerW layout is <em>not</em> stable across Windows builds, and this cost me an evening:</p>
<ul>
<li><p><strong>Windows 11 24H2 and newer:</strong> the WorkerW you want is a <strong>direct child of Progman</strong> — one <code>FindWindowEx</code> call under Progman finds it.</p>
</li>
<li><p><strong>Older builds:</strong> WorkerW windows are <strong>top-level</strong> — you have to enumerate them and take the one whose parent is Progman.</p>
</li>
</ul>
<p>Same goal, different tree shape. Branch on the OS build, or your wallpaper works on your machine and black-screens for your colleagues.</p>
<p>And the spawn is asynchronous: after sending <code>0x052C</code> you go looking for the WorkerW, and if it isn't there yet, you send and look again — a bounded retry loop, because <code>SendMessage</code> returns before the shell has finished rearranging its windows.</p>
<h2>The web part: WebView2 in composition mode</h2>
<p>yyzTools' wallpaper content is a WebView2, so web pages, HTML files, and local videos all work as wallpaper. But WebView2 doesn't hand you a child HWND to <code>SetParent</code>. The composition route: create a desktop window target on your host window (<code>ICompositorDesktopInterop::CreateDesktopWindowTarget</code>), then give the WebView2's composition controller a root visual target (<code>ICoreWebView2CompositionController::put_RootVisualTarget</code>). The browser renders into your visual tree; your window is the host that gets re-parented under the WorkerW.</p>
<h2>Input: click-through by default, interactive on demand</h2>
<p>The host window carries <code>WS_EX_TRANSPARENT</code>, so desktop clicks, rubber-band selection, and the right-click context menu all fall through to the icons. But interactive wallpapers need mouse events. So:</p>
<ul>
<li><p>A <strong>Raw Input</strong> listener receives mouse events globally without installing any hook — faster, and no hook-chain conflicts with other software.</p>
</li>
<li><p>A hit-test decides whether the event actually landed on the desktop — the window under the cursor is the icon layer (or the wallpaper's own window). Events aimed at normal applications are ignored; your wallpaper must never eat a click meant for a fullscreen app.</p>
</li>
<li><p>Desktop events are forwarded into the composition controller (<code>SendMouseInput</code>) with client coordinates, which makes the page interactive.</p>
</li>
</ul>
<h2>Be a good citizen: sleep when covered, die cleanly, restore</h2>
<p>A wallpaper that renders 24/7 is a space heater. The engine watches for coverage — fullscreen apps (the OS notification state reports D3D fullscreen or presentation mode) or the work area fully occluded by normal windows, checked on a coarse grid — and <strong>suspends</strong> the WebView2 whenever nobody can see the desktop, resuming on reveal. "Game mode" is exactly this machinery with a manual switch.</p>
<p>Explorer restarts destroy the WorkerW, and your window gets silently re-parented into limbo. A health check (is my ancestor still the WorkerW?) detects it; the wallpaper process exits on its own, and a watchdog relaunches it and re-embeds it in the fresh tree. On exit, the engine restores the static system wallpaper, so users are never left staring at a black desktop.</p>
<p>Two footnotes from the field. First, only one engine can own the WorkerW slot — embedding politely terminates whatever other processes have parked there (if you've ever run two wallpaper apps at once, you know why). Second, WebView2's renderer process creates its own fullscreen helper window that occasionally sits too high in the z-order and eats clicks; the fix is force-adding <code>WS_EX_TRANSPARENT</code> to it, and both the wallpaper process and the main app carry that patch, belt and suspenders.</p>
<hr />
<p>That's the party-trick module — one of 40+ in yyzTools, free at <a href="https://yyztools.com">yyztools.com</a>. The wallpaper engine ships with web, video, and HTML wallpapers, slideshows, and a countdown widget pinned above it all.</p>
]]></content:encoded></item><item><title><![CDATA[Delta Updates for a 150 MB Desktop App With No App Store]]></title><description><![CDATA[yyzTools is distributed as a plain download — an installer that carries FFmpeg, ImageMagick, Ghostscript, an offline OCR engine with models, a file-index…]]></description><link>https://yyztools.hashnode.dev/delta-updates-for-a-150-mb-desktop-app-with-no-app-store</link><guid isPermaLink="true">https://yyztools.hashnode.dev/delta-updates-for-a-150-mb-desktop-app-with-no-app-store</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Fri, 18 Sep 2026 14:21:53 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://yyztools.com/"><em>yyzTools</em></a> <em>is distributed as a plain download — an installer that carries FFmpeg, ImageMagick, Ghostscript, an offline OCR engine with models, a file-index engine, and 379 command modules. This post is about how its updater avoids re-downloading all of that every time I fix a typo.</em></p>
<h2><strong>The constraint</strong></h2>
<p>No store, no silent channel. Users download from the website or GitHub Releases, sometimes through third-party mirrors that proxy GitHub and cache aggressively. An update that says "re-download the installer" is an update users don't install — and an asset replaced in place is an asset whose cached copies now lie to people.</p>
<h2><strong>Split by what changes</strong></h2>
<p>The install isn't one blob. It's <strong>nine packages</strong>, cut along the lines of <em>what actually changes together</em>:</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Contains</th>
</tr>
</thead>
<tbody><tr>
<td>main</td>
<td>Framework + updater</td>
</tr>
<tr>
<td>yyztools</td>
<td>The executables</td>
</tr>
<tr>
<td>web</td>
<td>Every feature's front-end pages</td>
</tr>
<tr>
<td>modules</td>
<td>The 379 command modules</td>
</tr>
<tr>
<td>tools</td>
<td>Aria2, 7-Zip, OpenSSL, pdfcpu, ImageMagick, Ghostscript</td>
</tr>
<tr>
<td>ffmpeg</td>
<td>Video processing</td>
</tr>
<tr>
<td>rapidocr</td>
<td>OCR engine + models</td>
</tr>
<tr>
<td>everything</td>
<td>File-index engine</td>
</tr>
<tr>
<td>wallpaper</td>
<td>Live-wallpaper assets</td>
</tr>
</tbody></table>
<p>The <code>web</code> package is the hero of this story. UI fixes, copy changes, new feature pages — the most frequent kind of release — touch exactly one small package.</p>
<h2><strong>Two version numbers, two jobs</strong></h2>
<p>Every release has a <strong>release number</strong> — the identity. The GitHub tag, the installer filename, everything user-facing derives from it, and it increments on every release.</p>
<p>Every <strong>package</strong> has its own <strong>content version</strong>, which increments only when that package's contents actually change. Fixed a CSS bug? <code>web</code> bumps; <code>ffmpeg</code> doesn't. The rule that took longest to internalize: <strong>never bump an unchanged package "for the release."</strong> Bumping <code>main</code> because you shipped v1.0.5 re-downloads an identical binary for everyone — the content version is a promise that the bytes changed.</p>
<h2><strong>The manifest resolves each package to its own version</strong></h2>
<p>The update manifest points every package at its <strong>own</strong> versioned URL:</p>
<pre><code class="language-text">web      → /v1.0.5/web.7z
ffmpeg   → /v1.0.2/ffmpeg.7z       ← unchanged since v1.0.2
rapidocr → /v1.0.3/rapidocr.7z
...
</code></pre>
<p>An unchanged package resolves to the <em>identical file</em> it resolved to last release. The updater compares versions, downloads only the minority that moved, and the user's delta is proportional to the change — not to the app's size.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/bfdff7f8-2c04-4264-9412-47bfa5f03ef2.png" alt="Delta update pipeline: each package carries its own content version, the manifest maps packages to versioned URLs, and the updater downloads only what changed into cache-safe immutable release URLs" style="display:block;margin:0 auto" />


<h2><strong>Releases are immutable</strong></h2>
<p>A release, once published, is never overwritten: the uploader checks whether the release already exists and skips. This isn't politeness; it's cache hygiene. Mirror proxies cache by URL. Replace an asset in place and you split the world into users-with-old-bytes and users-with-new-bytes under the same URL — undebuggable. Immutable releases mean every URL in history serves the same bytes forever, so caches can be as aggressive as they like.</p>
<p>The build side leans on the same property: a package whose version directory already exists in the build output is skipped, not re-packed. Packing is effectively content-addressed.</p>
<h2><strong>Two invariants (both learned the hard way)</strong></h2>
<ol>
<li><p><strong>A changed package's version must equal the release number.</strong> Otherwise it gets packed under its own version directory while the uploader looks under the release directory — the package exists in the build, misses the upload, and users receive an update that 404s.</p>
</li>
<li><p><strong>An unchanged package's version must correspond to some past release.</strong> Since it will be fetched from a historical URL, that URL has to exist. A brand-new package aligns its first version with the release that introduces it.</p>
</li>
</ol>
<p>Both are preflight checks now, because "the build succeeded" and "the upload contains everything" are different claims.</p>
<h2><strong>What it feels like</strong></h2>
<p>A typo fix in a settings label: the <code>web</code> package, a few hundred KB. An FFmpeg bump for a new codec: one package. Refreshing OCR models: one package. The full 150 MB only crosses the wire on first install — where it belongs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/f3637cfc-a3ec-4e55-8b9f-33c5ac84ad1b.png" alt="The suite in light theme" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/97527dd4-d2f0-4d2b-9c08-609357c8e7f2.png" alt="The suite in dark theme" style="display:block;margin:0 auto" />


<p>The cost of all this is bookkeeping: a version map, a packer, an uploader, and checks to keep them honest. For an indie-scale app it's the difference between shipping a fix the same day and batching it for weeks because a release "isn't worth a full download."</p>
<hr />
<p>yyzTools is a free download at <a href="http://yyztools.com">yyztools.com</a> — and thanks to the above, keeping it current is cheap.</p>
]]></content:encoded></item><item><title><![CDATA[How "Instant" Full-Disk Search Actually Works on NTFS (MFT, USN, and Columnar Indexes)]]></title><description><![CDATA[Ever wondered how tools like Everything can find any file on a 2 TB drive before you finish typing? It feels like magic, but it's three well-understood…]]></description><link>https://yyztools.hashnode.dev/how-instant-full-disk-search-actually-works-on-ntfs-mft-usn-and-columnar-indexes</link><guid isPermaLink="true">https://yyztools.hashnode.dev/how-instant-full-disk-search-actually-works-on-ntfs-mft-usn-and-columnar-indexes</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Thu, 03 Sep 2026 13:16:08 GMT</pubDate><content:encoded><![CDATA[<p><em>Ever wondered how tools like Everything can find any file on a 2 TB drive before you finish typing? It feels like magic, but it's three well-understood NTFS mechanics stacked together. This post walks through them from first principles, using a real implementation — the 1.0.5 release of my tool <a href="https://yyztools.com">yyzTools</a>, which replaced its Everything dependency with a homegrown engine — as the worked example.</em></p>
<hr />
<h2>The problem with "just search the disk"</h2>
<p>The obvious approach to file search is what <code>FindFirstFile</code>/<code>FindNextFile</code> recursion gives you: walk every directory, look at every file. On a drive with a million-plus files, that's minutes of disk thrashing — and it has to happen <em>per query</em>.</p>
<p>Everything-class search is fast for one reason: <strong>it doesn't search the disk. It searches an in-memory index, and it has clever ways to keep that index built and fresh.</strong> The three mechanisms:</p>
<ol>
<li><strong>$MFT</strong> — how you enumerate the entire volume in one sequential read</li>
<li><strong>The USN journal</strong> — how you track changes after the initial build</li>
<li><strong>A compact in-memory index</strong> — how it all fits in a few MB instead of a few hundred</li>
</ol>
<p>Let's take them one at a time.</p>
<h2>1. The $MFT: NTFS's census book</h2>
<p>NTFS keeps a table called the <strong>Master File Table</strong>. Every file and directory on the volume — every one, including NTFS's own metadata files — has exactly one record in it. Each record is a fixed 1024 bytes and holds the file's names (yes, plural — hard links mean multiple), size, and timestamps.</p>
<p>Two consequences fall out of this design:</p>
<p><strong>Enumerating the whole volume = reading one file sequentially.</strong> $MFT is just a file. Read it start to finish and you've seen every file on the disk — no directory traversal, no permission checks, no per-file syscalls. One pass of sequential IO. That's the "secret" behind sub-second full enumeration, and it's exactly the channel Everything and WizFile use.</p>
<p><strong>You need the file's <em>identity</em>, not its path.</strong> Each record has a File Reference Number (FRN): a 64-bit value that is <code>sequence_number &lt;&lt; 48 | record_index</code>. The sequence number increments when a record is reused — which is why you must keep the full 64 bits. Truncating to 32 bits works right up until NTFS reuses a record and your index confidently conflates a deleted file with a new one.</p>
<p>Fun trap for the implementer: there's a documented API that seems made for this — <code>FSCTL_ENUM_USN_DATA</code>. It hands you FRNs file by file. Tempting, but wrong-shaped for a <em>full</em> build: USN records carry no size or timestamps, so you end up issuing a <code>FSCTL_GET_NTFS_FILE_RECORD</code> call per file to fill in the gaps. In the yyzTools implementation, 1.34 million files took <strong>120 seconds</strong> that way. Direct $MFT scanning does the same job in seconds. The lesson generalizes: <em>when you need bulk data, find the channel where that data is already laid out contiguously.</em></p>
<h2>2. The USN journal: NTFS's append-only changelog</h2>
<p>You can't rebuild your index from scratch every boot. NTFS provides the <strong>Update Sequence Number journal</strong> — an append-only log of filesystem changes (creates, deletes, renames, data writes). Each record has a monotonically increasing USN, and the journal as a whole has an ID.</p>
<p>The standard architecture:</p>
<ol>
<li>Build the full index by scanning $MFT. Record an <strong>anchor</strong>: the journal's ID plus the NextUsn at a known moment.</li>
<li>Periodically read the journal from the anchor forward, applying each change to your index.</li>
<li>Update the anchor.</li>
</ol>
<p>Two failure modes you must design for:</p>
<p><strong>The enumeration-window bug.</strong> Your full scan takes seconds. Files change <em>during</em> the scan. If your anchor is the USN you read <em>after</em> the scan finishes, changes from mid-scan fall in a dead zone: the scan may have missed them (they weren't on disk when it passed), but the journal replay starts <em>after</em> them. They're gone from your index — silently, forever. The fix is counterintuitive but strict: <strong>take the anchor before you start enumerating.</strong> You'll replay a few changes redundantly; that's idempotent and harmless. Missing is not.</p>
<p><strong>Journal death.</strong> The journal is a fixed-size circular buffer — old records get overwritten — and admins can delete and recreate it (the ID changes). So the anchor must store the JournalId, and on mismatch you throw away incremental catch-up and do a full rebuild. Boring, correct, and non-negotiable.</p>
<h2>3. Fitting it in memory: the columnar trick</h2>
<p>Here's the part I find most elegant, because it's not really an <em>algorithm</em> win — it's a <em>layout</em> win.</p>
<p>A million files, each with a name, size, and mtime. The obvious representation is a list of objects: <code>{name: string, size: u64, mtime: i64}</code>. For C++ and JS developers alike, that means per-object overhead: string headers, heap allocations, padding, pointer-chasing on every scan. That layout costs a hundred-plus MB for a 1.34M-file volume.</p>
<p>The alternative is <strong>columnar</strong>: one tight array per attribute.</p>
<pre><code>frns:     [ u64, u64, u64, ... ]
sizes:    [ u64, u64, u64, ... ]
mtimes:   [ u32, u32, u32, ... ]   // Unix seconds: 1970–2106 fits fine
nameOffs: [ u32, u32, u32, ... ]   // offset into the blob
nameLens: [ u32, u32, u32, ... ]
nameBlob: "package.json新建文件夹index.js..."   // interned, deduplicated
</code></pre>
<p>No per-row overhead. Sequential scans touch pure contiguous memory. And the name pool deduplicates — "New folder" and "package.json" each exist once for the whole disk, with every row referencing them by an 8-byte offset+length slice.</p>
<p>Frontend folks will recognize this instantly: it's AoS-to-SoA, the same move as replacing an array of objects with one TypedArray per column in a hot loop.</p>
<p>From there, the compounding tricks:</p>
<ul>
<li><strong>Time collapses to u32</strong> — Unix seconds, domain clamped to 1970–2106. Half the bytes, zero practical loss.</li>
<li><strong>Lowercase names often don't exist.</strong> Case-insensitive search wants folded names, but if a filename is already all-lowercase (most are), a per-row flag bit makes the lowercase view read the original blob. Saves an entire duplicate name pool.</li>
<li><strong>Derived data isn't stored at all.</strong> Need the parent directory? Derivable from a row-number tree at query time, so it's absent from the snapshot.</li>
<li><strong>Persistence via mmap.</strong> Snapshot the columns to a file; on boot, map it and point your column pointers at the mapped pages. Zero-copy, O(1) open — and the <em>query acceleration</em> structures (e.g., an ASCII bigram inverted index, delta-compressed) get materialized into the same snapshot so nothing rebuilds at startup.</li>
</ul>
<p>Net result in the yyzTools engine: the full-disk index holds a few MB of resident memory, versus the hundreds of MB a separate indexing service costs.</p>
<h2>Putting it together</h2>
<table>
<thead>
<tr>
<th>Question</th>
<th>Mechanism</th>
</tr>
</thead>
<tbody><tr>
<td>How do I see every file instantly?</td>
<td>Sequential $MFT scan, one pass of IO</td>
</tr>
<tr>
<td>How do I stay current?</td>
<td>USN journal replay from a <em>pre-scan</em> anchor</td>
</tr>
<tr>
<td>What if the journal dies?</td>
<td>JournalId mismatch → full rebuild</td>
</tr>
<tr>
<td>How does it fit in RAM?</td>
<td>Columnar layout + interned names + derived-not-stored</td>
</tr>
<tr>
<td>How does it start instantly?</td>
<td>mmap'd snapshot with materialized indexes</td>
</tr>
</tbody></table>
<p>None of these pieces is exotic. The craft is in the composition — and in the dozen unglamorous edge cases (hard links, record fixups, mapped-file replacement semantics, page-fault prefetching) that each get to cost you a week if you skip them.</p>
<h2>Try it</h2>
<p>All of this shipped in <a href="https://yyztools.com">yyzTools</a> 1.0.5 — a free, local-first Windows suite where the command palette's <code>f</code> prefix gives you this engine, with Everything-compatible syntax (<code>ext:pdf size:&gt;100mb dm:today</code>). If you're curious about any layer in more depth — the query AST, the delta-overlay merge, or the parallel scan pool — ask in the comments and I'll write the follow-up.</p>
]]></content:encoded></item><item><title><![CDATA[Shipping a Windows App in 12 Languages Without 12× the Cost]]></title><description><![CDATA[yyzTools ships its entire UI in 12 languages — the app, the installer, and the website, all from the same vocabulary. This post is the engineering that…]]></description><link>https://yyztools.hashnode.dev/shipping-a-windows-app-in-12-languages-without-12-the-cost</link><guid isPermaLink="true">https://yyztools.hashnode.dev/shipping-a-windows-app-in-12-languages-without-12-the-cost</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Wed, 02 Sep 2026 14:02:22 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://yyztools.com"><em>yyzTools</em></a> <em>ships its entire UI in 12 languages — the app, the installer, and the website, all from the same vocabulary. This post is the engineering that keeps the language count from multiplying the cost.</em></p>
<p>The list: English, Simplified and Traditional Chinese, Japanese, Korean, French, German, Spanish, Russian, Arabic, Portuguese, Italian. Arabic means right-to-left. Traditional Chinese is not a feature-flag away from Simplified. And the installer, the website, and the app must agree on what things are called — or users conclude the product is sloppy in a language they read better than you do.</p>
<h2>Load only the locale you need</h2>
<p>Every feature page is its own bundle, and locales are lazy: on launch, a page loads <strong>the active locale and one fallback — nothing else</strong>. Each language is a separate chunk, fetched on first switch and cached afterwards. The consequence: the app's startup weight is identical at 2 languages or 12. Adding a language costs its own chunk and never anyone else's bundle.</p>
<h2>A three-tier fallback that makes legacy free</h2>
<p>Module and config text resolves like this:</p>
<pre><code class="language-text">current language → English → whatever string is literally there
</code></pre>
<p>That last tier is the one I'm proudest of, because of what it buys: any entry can still be written as a plain string — <code>{"name": "关于"}</code> — and it just works, in every language, forever. Old data files, quick internal tools, a contributor who speaks one language: none of them ever see a validation error about missing keys. Backward compatibility isn't maintained here; it's structural.</p>
<h2>Store IDs, not words</h2>
<p>My favorite rule in the whole system: <strong>the config file stores IDs; words are derived at runtime.</strong></p>
<p>A dock entry or a hotkey binding records an <code>appId</code> — never the display name, never the icon. Display text comes from i18n at render time. The presence of a <code>name</code> field means exactly one thing: the user typed it themselves, so show it verbatim and stop translating.</p>
<p>The alternative — snapshotting names and icons into config — feels harmless until a user switches language and their dock shows two-week-old English labels next to fresh Japanese ones. IDs are language-independent, so <strong>usage statistics keyed by ID survive a language switch</strong> too: your "most-launched apps" chart doesn't reset when you move countries and change languages.</p>
<p>First-run language is detected from the system UI language and mapped to the nearest supported locale — Simplified vs. Traditional Chinese decided by the system's regional subtype. The installer deliberately writes <strong>no</strong> language preference: the app figures it out on first launch, so a German user who installs through an English-language session still ends up with a German interface.</p>
<h2>Right-to-left without a second stylesheet</h2>
<p>The website fully renders Arabic, and the discipline that makes it cheap is CSS <strong>logical properties</strong> everywhere: <code>margin-inline-end</code>, <code>padding-inline-start</code>, <code>border-inline-start</code>, <code>text-align: start</code>. Direction comes from the document; layout mirrors itself. The site's stylesheet carries exactly one <code>[dir=rtl]</code> special case — a single absolutely-positioned dropdown that couldn't be expressed logically. One. If you find yourself writing a mirrored stylesheet, you've almost certainly missed a logical property.</p>
<h2>The boring 20%: keeping 12 files honest</h2>
<p>Twelve locale files drift. A key renamed in the base, a <code>{{count}}</code> placeholder typo'd in Portuguese, an array that grew in Japanese — any of these ships a broken page in one language while the other eleven look fine and you never happen to open the broken one.</p>
<p>So it's scripted: a check compares every locale against the base on three axes — <strong>key trees identical,</strong> <code>{{placeholders}}</code> <strong>identical per key, array lengths identical</strong> — and it runs in CI, not in hope. The same discipline keeps the three surfaces (app, installer, website) aligned: one language list, defined once, referenced everywhere.</p>
<p>Is it perfectly DRY? No — twelve files must each be fully written; translation work doesn't compress. But the <em>failure modes</em> compress to zero, which is what actually matters at 3 a.m.</p>
<hr />
<p>If you use yyzTools in a language I've mangled, the GitHub tracker linked from <a href="https://yyztools.com">yyztools.com</a> takes corrections gladly — and the download, as always, is free.</p>
]]></content:encoded></item><item><title><![CDATA[Why I Fused 40+ Windows Tools Into One Installer]]></title><description><![CDATA[How 11 separate utilities became one free, local-first Windows app: one command palette, 40+ tools, offline OCR, and no plugins to hunt.]]></description><link>https://yyztools.hashnode.dev/why-i-fused-40-windows-tools-into-one-installer</link><guid isPermaLink="true">https://yyztools.hashnode.dev/why-i-fused-40-windows-tools-into-one-installer</guid><dc:creator><![CDATA[yyzTools]]></dc:creator><pubDate>Mon, 24 Aug 2026 13:55:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8812a1e34645c218b83748/67f72c59-8a62-4af2-b96f-7c9b263ff21b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>I build</em> <a href="https://yyztools.com/"><em>yyzTools</em></a> <em>— a free, local-first Windows productivity toolkit that bundles 40+ tools behind one command palette. This is the opening post of a build log: what the tool is, why it exists, and what I'll be writing about next.</em></p>
<h2><strong>How my system tray used to look</strong></h2>
<p>If you're the designated "computer person" for your friends and family, your Windows setup probably looks like mine did:</p>
<table>
<thead>
<tr>
<th>The job</th>
<th>The tool I used</th>
</tr>
</thead>
<tbody><tr>
<td>App launching</td>
<td>A launcher (PowerToys Run, later Flow Launcher)</td>
</tr>
<tr>
<td>File search</td>
<td>Everything</td>
</tr>
<tr>
<td>Clipboard history</td>
<td>A clipboard manager</td>
</tr>
<tr>
<td>Translation</td>
<td>A browser tab plus an extension</td>
</tr>
<tr>
<td>OCR</td>
<td>An online tool, eventually a local one</td>
</tr>
<tr>
<td>File preview</td>
<td>A QuickLook-style utility</td>
</tr>
<tr>
<td>Screenshots</td>
<td>A screenshot tool</td>
</tr>
<tr>
<td>Batch renaming</td>
<td>Yet another utility</td>
</tr>
<tr>
<td>JSON formatting, regex, Base64</td>
<td>Three permanent browser tabs</td>
</tr>
</tbody></table>
<p>Eleven tools. Eleven sets of shortcuts, settings dialogs, update channels, and privacy policies. And the part that annoyed me most wasn't the install sprawl — it was that <strong>none of them talked to each other</strong>. The clipboard manager can't send a copied screenshot to OCR. The OCR tool can't pipe its output to translation. Every cross-tool workflow was really a human copy-paste pipeline with extra steps.</p>
<p>So I built the thing I wanted to use: one installer, one resident process, one shared data layer — and every tool reachable from a single input box.</p>
<h2><strong>One shortcut, prefixes for everything</strong></h2>
<p><code>Alt+Space</code> opens <strong>CmdPlate</strong>, the hub of yyzTools. Apps, files, processes, windows, bookmarks, and clipboard history all live behind the same input. Type a prefix to switch domains:</p>
<table>
<thead>
<tr>
<th>Prefix</th>
<th>Searches</th>
</tr>
</thead>
<tbody><tr>
<td><em>(none)</em></td>
<td>Apps, system commands, settings, and 379 command modules</td>
</tr>
<tr>
<td><code>f␣</code></td>
<td>Files — full-disk search on the Everything engine</td>
</tr>
<tr>
<td><code>p␣</code></td>
<td>Running processes</td>
</tr>
<tr>
<td><code>w␣</code></td>
<td>Open windows</td>
</tr>
<tr>
<td><code>b␣</code></td>
<td>Browser bookmarks</td>
</tr>
<tr>
<td><code>c␣</code></td>
<td>Clipboard history</td>
</tr>
<tr>
<td><code>&gt;␣</code></td>
<td>Command line</td>
</tr>
<tr>
<td><code>=␣</code></td>
<td>Instant calculation</td>
</tr>
</tbody></table>
<p>The point of prefixes isn't raw keystroke savings — it's that <strong>you never have to remember what a tool is called or where it lives</strong>. The entire muscle memory is one key combo plus one letter.</p>
<p>Beyond the palette: clipboard history that can pin images to the desktop, offline OCR, multi-engine translation, file preview for Word/Excel/PDF/code/archives without Office installed, batch image/video/PDF processing, a screen recorder whose camera follows your cursor, a download manager, regex/Base64/hash/crypto/JSON tools, live wallpaper, and — because why not — a small built-in game for coffee breaks.</p>
<h2><strong>Three rules I code by</strong></h2>
<p><strong>Batteries included.</strong> 40+ modules ship in the installer. No plugin market to browse, no ratings to weigh, no discovering that the good one is paywalled. The cost is honest: a ~600 MB install, because FFmpeg, ImageMagick, Ghostscript, an offline OCR engine with models, and friends all come along. I think that's the right trade — disk is cheap, plugin-hunting is not.</p>
<p><strong>Local-first.</strong> Clipboard history, file search, app-usage stats, and OCR are processed and stored on your machine. Nothing uploads itself. The only network traffic is what you initiate: translation (to whichever service you configure), downloads, and update checks.</p>
<p><strong>Free forever.</strong> No account, no telemetry, no paid tier, no trial countdown. If I ever can't afford to keep the project alive, the honest move is to say so out loud — not to quietly hold features hostage behind a subscription.</p>
<h2><strong>How it's built (the short version)</strong></h2>
<p>The stack is C++ (Win32) hosting WebView2 for the UI: one lightweight HTML page per feature, Alpine.js on the front, a native API bridge in the middle, and third-party engines spawned as subprocesses rather than linked as SDKs. One resident process; specialists like the wallpaper engine start on demand and exit when done. I'll do proper deep-dives in later posts — the interesting parts are all in the trade-offs.</p>
<h2><strong>Where it stands</strong></h2>
<p>As of v1.0.4: 40+ modules, 379 command modules, a 12-language interface (English through Arabic, right-to-left included), both an installer and a portable no-install build, for 64-bit Windows 10 (1809+) and 11.</p>
<h2><strong>What's next on this blog</strong></h2>
<ul>
<li><p>How the command palette answers to both Chinese and English input — pinyin indexing and cross-language matching</p>
</li>
<li><p>Shipping those 12 languages without shipping 12× the bytes</p>
</li>
<li><p>Delta updates: how a nine-package scheme keeps most updates at kilobytes instead of full reinstalls</p>
</li>
<li><p>The party trick: rendering live wallpapers <em>beneath</em> your desktop icons</p>
</li>
</ul>
<p>If any of that sounds interesting, <a href="http://yyztools.com">yyztools.com</a> has the full feature list and the free download — no sign-up. Feedback and bug reports go to the GitHub tracker linked there.</p>
]]></content:encoded></item></channel></rss>