DEV Community

yyzTools
yyzTools

Posted on

You Can Write Desktop Tools in Plain HTML/CSS/JS — Here's the SDK That Lets You

I build yyzTools — 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.* No C++. No build step. No packaging.*


The demo first

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

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

Behind that window:

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
Enter fullscreen mode Exit fullscreen mode

No package.json. No node_modules. No bundler. It's 2026 and I'm shipping an SDK whose sample runs without a toolchain — deliberately, and the reason matters.

How the window gets born: it's declared, not built

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:

{
    "id": "sdk_sample",
    "type": "web",
    "url": "SDK_Sample/index.html",
    "width": 760,
    "height": 540,
    "layoutType": 34,
    "icon": "fas fa-search"
}
Enter fullscreen mode Exit fullscreen mode

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:

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

You write the feature. The host absorbs the decade of Win32 plumbing.

 ## Talking to the system: the ZenAPI bridge

Your webpage gets system powers through a window.Zen bridge, consumed via an official ZenAPI wrapper class:

import ZenAPI from './lib/zen_api.js';

// Full-disk search (async — runs on a background thread)
const res = await ZenAPI.searchFile('invoice ext:pdf', 200);
if (!ZenAPI.isOk(res)) { /* unified error check */ }

// Open the file / reveal in Explorer
await ZenAPI.openFile(fullPath, false);
await ZenAPI.openFileLocation(fullPath);

// Persist your own config (narrow-grained patch, not full rewrite)
await ZenAPI.setConfig({ sdk_sample: { history } });
Enter fullscreen mode Exit fullscreen mode

One design detail worth stealing for anyone building a JS bridge: searchFile is asynchronous, and the native side implements a search slot — a new request displaces 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 overwrite, not queue — the opposite of what server-side intuition tells you.

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 isDir as a 1/0 number 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.

Debugging: just open it in a browser

This is the part that sells frontend developers. Double-click index.html — it opens in your normal browser. There's no window.Zen there, so ZenAPI.call goes down the exception path and returns error: -1, 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.

The honest security section

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

That's a trust-model decision, not an oversight. This SDK targets developers building tools for themselves or their team — 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 .zenmod gains them nothing. Sandboxing buys you nothing here and costs every module author a permissions negotiation they don't need.

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.

Why bother

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.

  • Docs (quick start + full API reference): yyztools.com/sdk.html
  • Sample project download: same page
  • The app: free forever, no accounts, no telemetry, Windows 10/11

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.

Top comments (0)