* Anchor the context-menu fallback surface at the click point - Thread the secondary click's pointer location through the fallback request into Ui.finalize, so the synthesized surface opens at the click like a native menu instead of the target row's bottom-left corner - Give WidgetAnchor a point mode: a zero-size anchor rect at an explicit window-space point, reusing the existing flip-above and window-clamp edge rules - Cover the point mode's placement, bottom-edge flip, and horizontal clamp at the geometry level, and the click-point mount end to end on a wide row Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Present native Windows context menus through TrackPopupMenu - Mirror the tray popup discipline for the runtime's declared items: presentation defers to a fresh loop turn, TPM_RETURNCMD | TPM_NONOTIFY with SetForegroundWindow and the WM_NULL post, logical view-local request coordinates inverted through the presenting HWND's DPI scale and ClientToScreen - Selection and dismissal emit the same journaled context_menu_action payload the macOS host produces (token echo, item id, 0 for dismissal), so session replay stays shape-identical across platforms - Flip .context_menus to the tray's system-engine gate and unit-test the pure seam parts (item translation with separators and disabled flags, the action-event mapping); the TrackPopupMenu modal loop itself is only exercisable on a live Windows session Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Present native Linux context menus through GtkPopoverMenu - Build a sectioned GMenu from the declared items (separators split sections, disabled items ride action enablement) wired to a per-invocation GSimpleActionGroup inserted on the presenting view widget, pointed at the click with a 1x1 rect in the widget's logical coordinates - the same space the inbound pointer path reports in - Emit exactly one journaled context_menu_action per request (selection from the item action, dismissal from a one-turn-later teardown idle after the popover closes), the same token-echo payload the macOS and Windows hosts produce; refs stay balanced with weak pointers covering widgets that die while the menu is up - Flip .context_menus to the system-engine gate, keep the new code outside the WebKitGTK stub fences, and update the docs, skill, and README claims that named native context menus macOS-only Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Gate context-menu resolution on per-request tokens - Mint a fresh correlation token per presented (or automation-armed) request and check it before clearing the pending request, so a superseded menu's late dismissal can never resolve or clear its successor - even when both target the same widget. - Give each GTK popover menu a per-invocation action-group namespace: the deferred teardown of a superseded menu now removes only its own group instead of the one the replacement just inserted on the same parent. - Windows and macOS need no gate by construction (TrackPopupMenu blocks the loop thread and emits inline from a moved-out request; the macOS presentation block captures its token as a local while popUpMenuPositioningItem blocks the main queue) - noted at both fix sites. * Add the native context-menus changelog fragment - One feature fragment covering the Windows and Linux presenters, the .context_menus capability on both system-engine hosts, and the fallback surface's click-point anchoring. * Cancel pending GTK context-menu teardown idles at host destroy - Track every queued teardown idle on the host, linked through the menu states themselves: the popover's closed handler pushes the receipt the moment it exists, and native_sdk_context_menu_free unlinks on every exit path - A superseded menu's deferred teardown captures its state and the host raw while host->context_menu tracks only the current menu, so destroy now removes each pending source and frees its captured state inline instead of letting the idle fire into a freed host - The weak pointers keep covering widgets dying before the menu state; this covers the host dying, and cancelling deliberately drops the superseded request's owed dismissal since the runtime gating on that token is torn down with the host * Re-point the notes context-menu token assertions at the recorded token - Context-menu correlation tokens are minted per request and opaque, so the tests assert the null platform recorded a nonzero token instead of expecting the widget id * Escape mnemonic ampersands in Windows context-menu and tray labels - AppendMenuW treats & as a mnemonic marker, so an authored label like "R&D" rendered with the ampersand eaten and an accidental mnemonic armed; labels now cross the ABI with & doubled - The escape lives in the shared translation helper covering both the app context-menu path and the tray path, whose labels are app-supplied too; a label the pool cannot hold passes through raw rather than truncated - Pin the escaped output and the pool-exhaustion passthrough in the item-translation unit tests * Escape mnemonic underscores in GTK context-menu labels - GtkPopoverMenu treats _ in an item label as a mnemonic marker, so an authored label like "Save_As" rendered with the underscore eaten and an accidental mnemonic armed; labels now cross the ABI with _ doubled - The escape lives in the context-menu translation helper (the GTK host strndup-copies every label before returning, so the caller stack pool is safe); a label the pool cannot hold passes through raw rather than truncated, and the menu bar keeps _ untouched as its intentional GTK mnemonic convention - Pin the escaped output, the underscore-free pointer passthrough, and the pool-exhaustion passthrough in the item-translation unit tests * Map GDK pointer buttons explicitly to the runtime's ordering - GDK numbers secondary=3 and middle=2; the old subtract-one sent right clicks to the runtime as middle (never opening the menu) and middle clicks as secondary (opening it); press and release now share an explicit GDK-to-runtime switch, and the interactive-move stash keeps raw GDK numbering for gdk_toplevel_begin_move - The Linux canvas smoke now owns its Xvfb display and drives a real xdotool right-click through a task row's declared context menu, asserting the selection's Msg dispatch ('1 done') in the snapshot - ui-inbox task rows declare an honest one-item menu (Toggle done) so the smoke exercises an app-declared menu end to end * Resolve context-menu selections from a present-time snapshot - GTK popovers are asynchronous: a rebuild while the menu is open (timers or effects reordering conditional items) could redirect the visible selection through the live tree's handler table - The runtime now emits canvas_widget_context_menu_shown after a native present; UiApp snapshots the shown items' dispatch Msgs keyed by the request token, and a token-matching selection resolves from that snapshot on every platform - never the rebuilt tree - The automation verb and the fallback surface keep live-tree resolution: both validate against the tree they show * Bump the automation protocol to v8 for per-request menu tokens - Recorded context_menu_action tokens changed from widget ids to per-request generations; a v7 journal's selections would be silently swallowed by the token gate instead of refused - The version handshake now turns that into the loud preamble mismatch, and the changelog fragment states the break deliberately * Deep-copy context-menu snapshot slice payloads at present time - The present-time selection snapshot copied MsgT by value, so a menu held open across two rebuilds dispatched slice payloads pointing into reset build-arena storage - Snapshot Msgs now deep-copy every reachable slice into a token-scoped arena, released on resolve, supersession, and teardown - Regression test presents a menu over an arena-derived payload, rebuilds twice with sentinel bytes, and asserts the dispatched Msg carries the bytes the user saw * Drive the smoke's context-menu popover by pointer under Xvfb - Xvfb has no window manager, so the GTK popover never receives keyboard focus and xdotool Down/Return died on the canvas beneath it; the menu's shown event fired but no selection ever dispatched - Locate the popover's override-redirect X window by diffing the root's children across the right-click (topmost new viewable window) and click its center — the row declares exactly one arrowless item, so the center is the item - Failure diagnostics now dump the X window list alongside the snapshot and app log; the workflow installs x11-utils for xwininfo * Sharpen the snapshot copy: const slices only, exact types, loud OOM - Mutable slice payloads pass by reference (update may write through app-owned storage; a copy would swallow the writes), while const slices copy with their declared alignment and sentinel and error unions recurse into successful payloads - An out-of-memory copy now logs which item it disarmed instead of failing silently - Direct unit test pins the copy semantics per shape; the integration test drives the error-union arm through the two-rebuild race * Refuse un-copyable context-menu payload shapes with teaching errors - A mutable slice's aliasing is unknowable to the deferred snapshot (arena bytes must be copied, model-owned storage must not be), so it is a compile-time teaching error instead of a silent guess either way - Slice-bearing fixed arrays are refused too: an array's length says nothing about which elements are initialized, so walking a count-plus-buffer payload would interpret its undefined tail as slices; slice-free arrays and untagged unions still pass by value as plain bytes - typeCanReachSlice gates the refusals so only genuinely un-copyable shapes are rejected; the unit test now covers the scalar count-plus-buffer shape with an undefined tail * Scope the snapshot copy to const slices with a hop budget and a loud fallback - The copier now walks only the blessed arena payload shape (const slices through structs, tagged unions, optionals, and error unions); mutable slices, fixed arrays, untagged unions, and non-slice pointers pass through under the deferred-Msg rule that their storage outlives the menu, so no payload shape is a compile error - A slice-hop budget bounds the walk, turning cyclic or absurdly deep payload graphs into a handled error instead of unbounded recursion - A failed copy (out of memory or over budget) keeps the visible item live: it dispatches the uncopied value with a warning naming the item, never a silent no-op * Pin the presenting build's arena while its context menu is open - Replace the snapshot payload copier outright: while a presented menu's token-keyed snapshot is armed, the arena generation that built the presented tree is exempt from the rebuild reset, so selection dispatches the ORIGINAL Msg value - same bytes and same pointer identity as the fallback surface and the automation verb, with no payload-shape restrictions, no copy allocation after present, and no recursive walk - A new canvas_widget_context_menu_dismissed runtime notice releases the pin (and disarms the snapshot) when the menu closes without a selection; selection and supersession release it as before, and growth while a menu is open is bounded by its open span - The regression test now also asserts pointer identity end to end and the pin's release on selection and dismissal * Key the menu pin by window identity and double-buffer rebuilds under it - The pin names its canvas by stable window identity, never slot index: removing a window swap-moves another slot into its place, and an index-keyed pin would protect the wrong arena; the pin-owning window's teardown now releases the snapshot and pin before its arenas deinit - While pinned, every rebuild of that canvas routes through the partner arena on its normal reset cadence (the consecutive-build pattern the clearance retry already runs), so memory under an open menu holds at two trees regardless of rebuild count - ANY superseding presentation or dispatch releases the previous app-menu request - a default edit/copy menu and the automation verb's direct dispatch now send the dismissed notice, not just app-over-app presentations - Tests: pin follows its window across slot compaction (bytes and address), window removal releases, cross-kind and automation supersession release, and capacity stays flat across 14 rebuilds under an open menu * Name the superseded menu's view in its dismissal and commit the successor first - The pending request now carries a bounded copy of its view label, so the superseded-menu dismissal notice names the canvas it was presented on instead of an empty label (raw apps tracking per-canvas menu state need the correlation, and the view may already be gone) - The replacement pending commits before the fallible dismissed-notice dispatch: if an app handler errors on the notice, the runtime's expected token still matches the menu the platform accepted, so the successor stays selectable - Runtime-level test pins both: the notice carries the old token and the real view label, and the successor menu resolves after the supersession * Keep the automation menu verb's synthetic selection ahead of a failing dismissal notice - The widget-context-menu verb captures the superseded-menu dismissal notice's error and re-raises it only after the synthetic selection dispatches: unlike a presented menu, whose outcome the platform delivers later regardless, that dispatch is the armed request's only outcome, so no error path may leave a pending token with no presented menu and no delivered outcome - The notice keeps its place in the event order (the old menu's dismissal before the successor's outcome), and its error still surfaces after the runtime's bookkeeping settles - Test pins the supersession under an erroring dismissal handler: the selection still dispatches, the error still propagates, and no pending request survives * Keep menu state sound when rebuilds fail or handlers close views - A rebuild routed into the live tree's arena under an open menu's pin now drops the tree reference when the pass fails after its reset: handlers go quiet until the next successful rebuild instead of dangling into reset, partially rewritten storage, and the pinned snapshot still resolves the presented payload - The superseded-menu dismissal notice runs arbitrary app code that can close views and compact their indices, so the shown event and the automation verb's synthetic selection now name their view from the committed request's own copy (showMenu returns it) instead of re-reading a cached index that a compaction can point at another view or another window's canvas - Tests pin all three seams: the failed over-budget rebuild under a pin drops then restores the tree with the presented menu intact, and both supersession paths keep naming the presenting view after the dismissal handler closes a different one * Restore a dropped tree on menu resolution and refuse a superseded automation verb - A menu resolution that dispatches no Msg (dismissal, out-of-range or unmapped-item swallow) now restores a live tree the pinned-rebuild guard dropped: no Msg-driven rebuild is coming, and without a handler table every event silently no-ops until an unrelated resize or effect rebuilds - The widget-context-menu verb now checks its freshly armed token survived the dismissal notice: a handler that synchronously presents a superseding menu replaces the pending request, so the verb refuses with ContextMenuSuperseded instead of reporting success while the token gate swallows its synthetic action - Tests pin both: a dismissal after the failed over-budget rebuild rebuilds the tree once the model fits again, and the superseded verb errors by name while the handler's successor menu stays resolvable * Announce nothing for a presentation superseded during its own notice - showMenu now reports a three-way outcome: shown, refused, or superseded — the dismissal notice's app code can synchronously present a successor menu that replaces the freshly committed request, and a late shown event for it would overwrite the successor's snapshot with a token the action gate no longer accepts, stranding the stale pin - A superseded presentation announces nothing and never falls back to the anchored surface, which would mount a second menu under the successor's native one - Test pins the chain: menu A superseded by B whose notice presents C — three presentations, two announcements, the last carrying C's token, and C resolves normally * Release the menu pin before its selection dispatches and refuse a closed-view verb - A snapshot selection's Msg is stored by value and its pinned-arena payloads are consumed by update itself, so the pin now releases before the dispatch: the rebuild routes into the partner arena naturally, and a Msg whose update breaks a build budget fails the rebuild without resetting the live arena — input keeps working on the previous tree and the app's controls can recover the model - The widget-context-menu verb revalidates its target view after the dismissal notice: a handler that closed it leaves a request that can never resolve, so the verb disarms it and refuses with ContextMenuViewClosed instead of reporting success while the action dispatch silently drops the selection - Tests pin both: the poison selection leaves the tree live and a real click on the recovery control rebuilds in budget, and the closed-view verb errors by name with no orphaned token * Disarm a presentation whose view died mid-notice and restore the tree before menu resolution - showMenu now rechecks the presenting view after the dismissal notice: a handler that closed it leaves a request whose action can never deliver (or never arrive), so the request disarms with a view_closed outcome — never announced, never the anchored fallback on a dead view - Arming a shown snapshot and resolving a snapshot-less selection both restore a live tree the pinned-rebuild guard dropped: a menu presented while the model was unbuildable still resolves once the model recovers, instead of falling through a null tree and dispatching nothing - Tests pin both: the closed-view presentation stays silent, its token inert, a fresh present arms cleanly — and the snapshot-less selection dispatches the restored build's payload --------- Co-authored-by: startewho <898009+startewho@users.noreply.github.com>
9.4 KiB
Native SDK
Native SDK is the complete toolkit for building native desktop applications.
Native SDK exists because expressive UI and native performance should not be competing goals. Developers often choose web-based runtimes because they offer freedom, speed and control over the product experience. But that freedom often comes with a heavy runtime. Native SDK keeps the expressive authoring model and replaces the runtime with native rendering.
Views are declarative markup in .native files, logic is plain TypeScript compiled to native code at build time — or Zig, first-class by choice — and Native SDK's own engine draws every pixel into real OS windows. No browser, no WebView, no JS runtime in the binary: Zig is how everything works, TypeScript and Native markup are how apps are authored.
|
|
Soundboard, Notes, and Calculator from examples/ — every pixel drawn by the Native SDK engine, captured through its deterministic reference renderer. The images follow your color scheme.
Quick start
Install the CLI:
npm install -g @native-sdk/cli
Create and run an app:
native init my_app
cd my_app
native dev
A native window opens with a working counter. The whole app is three files of truth — view, logic, manifest — and no build config. The view is src/app.native, a markup file that binds values and dispatches messages (the counter row at its heart):
<row gap="8" main="center" cross="center" grow="1">
<button variant="secondary" on-press="decrement">-</button>
<text>{count}</text>
<button variant="primary" on-press="increment">+</button>
</row>
All logic lives in src/core.ts: a Model interface, a Msg union, and one pure update function — the only place state changes, plain TypeScript compiled to native code at build time:
export function update(model: Model, msg: Msg): Model {
switch (msg.kind) {
case "increment":
return { ...model, count: model.count + 1 };
case "decrement":
return { ...model, count: model.count - 1 };
case "reset":
return { ...model, count: 0 };
}
}
Prefer Zig for the core? native init my_app --template zig-core scaffolds the same app with src/main.zig — same loop, same runtime, first-class by choice.
Edit src/app.native while native dev runs and the window updates in place, keeping your state. native dev --core runs the TypeScript core under node for instant logic checks, native check validates the core and every view in milliseconds without building, and native build produces an optimized release binary.
Read the full guide at native-sdk.dev/quick-start.
What you get
Beautiful by default — Great software should not start from a blank slate. The built-in component catalog — buttons, tabs, text fields, dialogs, charts, virtual lists, and more — ships with considered typography, spacing, and color, so the app native init scaffolds already looks intentional the first time its window opens.
Customizable by design — Your app should have its own identity, not ours. Styling is design tokens end to end: color, radius, and typography resolve by name, re-resolve live when the theme changes, and can be replaced wholesale — examples/soundboard and examples/deck are the same music player separated only by tokens and a chrome pass.
Native from the start — Every interface is rendered without a browser or WebView. The engine draws into real OS windows while scroll physics, menus, dialogs, the tray, and text input stay with the operating system, and markup compiles into the executable at build time, so a release build carries no parser or interpreter — the scaffolded counter app builds to a single binary a few megabytes small.
Predictable state — State changes should be explicit, inspectable and easy to reason about. Events produce messages, messages update state, and state renders the interface; markup can bind and dispatch but never mutate. The loop is so deterministic that native automate record journals a session and replay reproduces it headlessly, verified frame by frame against state fingerprints.
Simple authoring — Interfaces should be easy to read, easy to write and easy to generate. Views are elements, flex layout, {bindings}, and expressions like selected="{f == filter}", and native check validates every view against your app's actual Model and Msg — bindings, iterables, message tags — in milliseconds, with file:line:column errors that teach.
AI is part of the workflow — Native SDK is designed for a world where humans and AI agents build software together. Every app embeds an automation server, so any agent can read accessibility snapshots, drive widgets, assert on live state, and take deterministic screenshots of the running window; accessibility findings are machine-checked in native check; and the CLI ships the agent skills that teach all of it (native skills list).
Examples
The apps pictured above live in examples/, most as zero-config projects — app.zon plus src/, no build files — run straight from their directory with native dev.
| Example | What it shows |
|---|---|
calculator |
A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. |
notes |
Persistence through the effects channel: debounced writes, restore on boot, dialogs, search. |
soundboard |
Album grid with decoded cover art, context menus, timers, and a custom theme. |
deck |
The soundboard player rebuilt as a dense hardware chassis: two windows, same widgets, different tokens. |
feed |
A 100,000-row list, virtualized with runtime-owned scrolling. |
The full catalog in examples/README.md also covers guarded OS capabilities, GPU surfaces, WebView composition, web-frontend shells, and the iOS/Android embed hosts.
Platforms
macOS is the primary development platform and carries the deepest support: Metal presentation, OS scroll physics, native context menus, app menus, tray, and dialogs. Linux runs the full showcase through the deterministic software renderer in real windows, with pointer, keyboard, scroll, native context menus, IME composition, and HiDPI; Windows runs on a Win32 host with native context menus and IME composition and is exercised in CI, including real input injection. Mobile support is experimental: iOS is simulator-proven through the embed library and Android cross-compiles with the full embed ABI, but APIs and tooling on both are still evolving — desktop is the mature surface. WebView surfaces coexist on every desktop platform. The platform support matrix documents exactly what each host supports today.
Documentation
The full documentation is at native-sdk.dev.
- Quick Start — install to a running, tested app
- Philosophy — the six principles behind the toolkit
- App Model — the model/message/update loop, wiring, and hot reload
- TypeScript Cores — the app-core subset, effects, subscriptions, and the node dev loop
- Native UI — every element, attribute, and pattern in the markup
- Components — the component catalog
- State & Data Flow — derive-don't-store, bindings, and text editing
- Testing — full-loop UI tests, headless on any machine
- Automation — snapshots, widget driving, record/replay, screenshots
- Capabilities — guarded OS services: notifications, clipboard, dialogs, credentials
- Packaging — from binary to distributable app
- Platform Support — what each host supports today
Contributing
Native SDK is pre-1.0: APIs still move, and the toolkit is evolving quickly. Bug reports and focused pull requests are welcome — for larger changes, open an issue first so the design can be discussed. See CONTRIBUTING.md for the development setup and local checks.

