A key-smashing game for kids, built on zero-native (Zig native shell + Next.js web UI). Every key paints a different burst of color and plays a different sound. While playing it runs locked down in kiosk mode so a child can mash the whole keyboard without quitting the game, switching apps, or triggering anything in the operating system.
It ships as a desktop app for macOS and Windows (both lock the keyboard the same way), plus a browser version that plays the same but can't lock the keyboard.
zig build runThe app opens in a small menu window with Start and Quit — and, on macOS, the one-time Accessibility setup needed for full keyboard locking (Windows needs no extra permission). Press Start to enter kiosk mode: the window takes over the whole screen — menu bar and dock on macOS, the taskbar on Windows — and the keyboard is locked. Smash any key, or click and drag with the mouse:
- Letters — glowing letter + colored burst, a musical note per letter
- Numbers — bouncing digit + star burst
- Space — rainbow firework, a boom, and a screen shake
- Enter — expanding rainbow ripple + a little chord
- Arrows — a shooting stream of triangles + a swoop
- Backspace / Delete / Tab / Escape / punctuation — their own shapes and tones
- Modifiers — ⇧ Shift, ⌃ Control, ⌥ Option, ⌘ Command, Caps Lock and Fn each get their own color, symbol, and chime. Pressing them (alone or as part of a chord) still makes something happen instead of doing nothing.
- Clicks & drags — paint colored splashes and trails at the pointer. The cursor is a friendly glowing star you can always see.
While playing there is no quit button and no menu shortcut. The only way back is the four-key chord:
Control + Option + Shift + Q
It returns to the menu (it does not quit the app) — from there a grown-up can press Quit, or Start again. Every other key combination — ⌘Q, ⌘Tab, ⌘Space (Spotlight), ⌘H, ⌘⌥Esc, the screenshot shortcuts, even the power/sleep and media keys — is swallowed before the system ever sees it, and still produces an on-screen effect. When a combo that would normally poke the OS is pressed, the game also briefly flashes a small hint showing the chord.
To swallow every system shortcut — including the ones the window server handles below any normal app (Spotlight, the screenshot keys, the power/sleep chord, media keys) — the game installs a global CGEventTap, which macOS only allows once the app is trusted for Accessibility. (Windows needs no such permission — its low-level keyboard hook works as soon as the app starts.)
The menu is where this is handled. If KeyParty isn't trusted yet, the menu shows a Grant Accessibility Access… button that opens the system prompt and jumps straight to System Settings → Privacy & Security → Accessibility. Turn KeyParty on there and come back — the menu polls for the grant and flips to "Keyboard lock ready" on its own, no restart needed. You can still press Start without it: the game falls back to an in-app monitor that blocks every app/menu shortcut (just not the window-server-level ones) and upgrades to the full tap the moment the grant lands.
The kiosk behavior lives in a patched copy of the macOS native host,
native/appkit_host.m (vendored from zero-native and
wired up in build.zig). The app opens at a small, ordinary titled
window (the menu). The web UI drives the transitions over a tiny one-way bridge
(window.keyparty.{start,quit,requestAccessibility,checkAccessibility}); status
flows back as keyparty:* events on the existing window.zero event bus.
On Start (-enterKiosk) it:
- turns the menu window borderless and resizes it to cover the entire screen,
then raises it to
CGShieldingWindowLevel()so it sits above the menu bar and dock (the level screen savers use), non-movable; - sets
NSApplicationPresentationOptionsto hide the dock and menu bar and to disable process switching (⌘Tab / Mission Control), force-quit (⌘⌥Esc), the power-key session dialog, and app hiding (⌘H); - installs a global CGEventTap at the HID level that swallows every key-down, key-up, and modifier change, plus the system-defined power/sleep/media events, and forwards each one to the web UI so it still produces an effect — nothing a child presses reaches another app or the OS;
- falls back to an in-app key monitor when Accessibility hasn't been granted yet, and auto-upgrades to the tap once it is.
On the Control + Option + Shift + Q chord (-exitKioskToMenu) it reverses all
of that — removes the tap, restores the dock/menu bar, and shrinks the window back
to the centred menu — and emits keyparty:menu so the UI shows the menu again.
The menu's Quit button (-handleKeyPartyControlCommand:) is what shuts the app
down. The menu's Quit / Close / Hide key-equivalents are also stripped as another
layer of defense.
The game UI (the menu, canvas, effects, sounds, pointer splashes, custom cursor,
the on-screen hint) is in frontend/app/page.tsx. It
consumes the native key events the tap forwards, and also handles plain DOM
keys (for development / browser use), calling preventDefault() so the web layer
never scrolls, quick-finds, or moves focus.
Windows uses a patched copy of zero-native's WebView2 host,
native/webview2_host.cpp. The upstream host only
opened a bare window, so this copy also adds the main-window WebView2 (serving the
embedded frontend), the same window.zero / window.keyparty bridge as macOS, and
the kiosk lockdown. The shared web UI is unchanged — it sees the same bridge
events on both platforms.
The Windows build is a single self-contained keyparty.exe: the frontend is
embedded in the executable (a build step packs frontend/out into a byte blob —
see scripts/embed-assets.mjs — which the host serves
from memory over WebResourceRequested), and the WebView2 loader is statically
linked, so there's no WebView2Loader.dll and no resources/ folder to ship.
On Start it:
- makes the menu window borderless + topmost and resizes it to cover the whole monitor, the taskbar included;
- installs a global low-level keyboard hook (
WH_KEYBOARD_LL) that swallows every key — Win, Alt+Tab, Alt+Esc, Alt+F4, Ctrl+Esc, F5/refresh, and the rest — and forwards each one to the web UI as a synthetickeyevent, so it still produces an on-screen effect while never reaching another app or the OS; - detects the grown-up Control + Alt + Shift + Q chord and returns to the menu.
The one shortcut Windows will not let any app intercept is Ctrl + Alt + Del (the Secure Attention Sequence) — blocking it needs the OS Assigned Access / kiosk policy, which is outside the app.
The exe still needs the WebView2 Evergreen runtime installed (it ships with Windows 11 and current Windows 10); only the loader is linked in, not the runtime itself.
Run with the screen takeover and keyboard lock disabled for development:
KEYPARTY_NO_KIOSK=1 zig build runThe menu and Start/Quit still work; pressing Start just plays in the normal window (no full-screen, no keyboard grab), and the Control + Option + Shift + Q chord still returns to the menu — handled by the DOM key path in the web UI.
The zero-native framework is a normal dependency, so install it (and the release tooling) from the repo root, then let the build pull in the frontend deps:
npm install # installs node_modules/zero-native + its CLI, and Changesetszig build dev, zig build run, and zig build package also install the
frontend deps automatically (npm install --prefix frontend).
The build reads the framework from node_modules/zero-native (anchored at the
repo root) by default — override with -Dzero-native-path=/path/to/zero-native
to use a different checkout. The patched native hosts
(native/appkit_host.m,
native/webview2_host.cpp) ship in this repo, so
only the rest of the framework comes from that path.
zig build dev and zig build package shell out to the zero-native CLI, which
npm install puts in node_modules/.bin. Run those via npx zig build … (or
add node_modules/.bin to your PATH) so the CLI resolves — or install it
globally with npm i -g zero-native@0.2.0.
On Windows, the kiosk host (native/webview2_host.cpp)
is compiled with MSVC's cl.exe, not Zig's bundled clang — the WebView2/WRL
(wrl.h) headers are MSVC-flavored and Zig's libc++ can't target the MSVC C++
ABI. The build produces a single self-contained keyparty.exe (the frontend
is embedded and the WebView2 loader is static-linked), so it needs both the
WebView2 SDK headers and the static loader lib. Build from an x64 Native Tools
Command Prompt (which puts cl.exe and the Windows SDK on PATH/%INCLUDE%):
zig build -Dtarget=x86_64-windows-msvc -Doptimize=ReleaseFast ^
-Dwebview2-include="C:\path\to\Microsoft.Web.WebView2\build\native\include" ^
-Dwebview2-lib-dir="C:\path\to\Microsoft.Web.WebView2\build\native\x64"The result is zig-out\bin\keyparty.exe — one file, no DLL, no resources\
folder. cl.exe resolves the Windows SDK, winrt (wrl.h), and STL headers from
%INCLUDE% on its own (-Dwinrt-include=... is accepted but rarely needed).
Omitting -Dwebview2-include makes the host compile to a blank-window stub (it
says so at launch); omitting -Dwebview2-lib-dir fails the link. The embed step
runs Node, so building also (re)builds the frontend. The WebView2 Evergreen
runtime must be installed to run (it is on Windows 11 / current Windows 10).
Don't want to install MSVC? Run the Windows build (test) workflow
(.github/workflows/windows-build.yml)
from the repo's Actions tab — or just push a change under native/,
build.zig, app.zon, or frontend/ and it runs automatically. Download the
keyparty-windows artifact, unzip it, and run KeyParty.exe — it's a
single self-contained file (frontend embedded, loader static-linked). No release
needed.
zig build run # build the frontend + launch the game
zig build dev # frontend dev server + native shell
zig build test # run tests
zig build package # create a distributable .app
zero-native doctor --manifest app.zonDiagnostics:
- Set
ZERO_NATIVE_LOG_DIRto override the log directory during development. - Set
ZERO_NATIVE_LOG_FORMAT=text|jsonlto choose the persistent log format.
Versioning and releases run on Changesets.
- With each change worth shipping, add a changeset and commit it:
npx changeset
- On push to
main, the Release workflow opens (or updates) a "Version Packages" PR. Merging it bumps the version, updatesCHANGELOG.md, syncs that version intoapp.zon,build.zig.zon, andfrontend/package.json(viascripts/sync-version.mjs), tags the release (keyparty@x.y.z), and creates a GitHub Release. - The same workflow then builds the apps on two runners and uploads both to the
release:
- macOS —
KeyParty.dmg(a drag-to-Applications disk image holdingKey Party.app, built fromzig build package). Signed (Developer ID) and notarized when the signing secrets are configured (see Code signing below) — it then launches with no Gatekeeper prompt. Without those secrets it ships unsigned, so first launch needs right-click → Open (or clearing the quarantine flag). - Windows —
KeyParty.exe, a single self-contained file (frontend embedded, WebView2 loader static-linked), viazig buildagainst the WebView2 SDK headers + static loader lib (restored from NuGet). Currently unsigned (signing is scaffolded but inert — see Code signing below), so SmartScreen shows a "More info → Run anyway" prompt on first launch; the WebView2 Evergreen runtime must be present (it is on Windows 11 and current Windows 10).
- macOS —
One-time repo setup:
- Settings → Pages → Source: GitHub Actions (for the web build).
- Settings → Actions → General → Workflow permissions: allow GitHub Actions to create and approve pull requests (so the version PR can be opened).
Signing is wired into the Release workflow as
post-build steps gated on secrets — local zig build always produces
unsigned binaries, and CI signs only when the relevant secrets exist, so the
pipeline keeps working before any certificate is in place.
macOS — Developer ID + notarization. Once the Apple Developer Program membership is active:
-
In Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority…, generate a CSR. In the Apple Developer portal create a Developer ID Application certificate from that CSR, download it, double-click to add it to your keychain, then export it with its private key as a
.p12. -
Create an App Store Connect API key (Users and Access → Integrations → Keys, role Developer) for notarization. Record the Key ID and Issuer ID, and download the
.p8(one chance only). -
Add these repository secrets (Settings → Secrets and variables → Actions):
Secret Value MACOS_CERTIFICATEthe .p12, base64-encoded (base64 -i cert.p12 | pbcopy)MACOS_CERTIFICATE_PWDthe password set when exporting the .p12MACOS_SIGN_IDENTITYe.g. Developer ID Application: Your Name (TEAMID)APPLE_NOTARY_KEYthe .p8, base64-encodedAPPLE_NOTARY_KEY_IDthe API Key ID APPLE_NOTARY_ISSUERthe Issuer ID (a UUID) With all six set, the next release signs
Key Party.appwith the Hardened Runtime (assets/keyparty.entitlements), signs and notarizes the.dmg, and staples the ticket so it opens with no Gatekeeper prompt. Set just the first three to sign without notarizing (Gatekeeper still warns). WKWebView runs JavaScript in the Apple-signed system WebKit process, so the host needs no JIT/unsigned-memory entitlements.
Windows — unsigned for now (scaffolded). The release workflow has an inert
Sign the Windows exe step that activates once a WINDOWS_SIGN_CERT secret is
set and a real signing command is dropped into it. Since June 2023 every
publicly-trusted Windows code-signing certificate must keep its key on hardware
or a cloud service, so the practical CI-friendly routes are Microsoft Trusted
Signing (~$10/mo, cheapest), DigiCert KeyLocker, or SSL.com eSigner —
not a plain .pfx. Until then the exe ships unsigned and SmartScreen shows a
"More info → Run anyway" prompt on first launch.