Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 998e79afe6 | |||
| dbab55d004 | |||
| 19e7942e8d | |||
| dc0f64c1ea | |||
| a22f2043d1 | |||
| cb6a417965 | |||
| fe41864a15 | |||
| 046ea270a4 | |||
| f6e4d99f09 | |||
| 781b7f9653 | |||
| b25cefe318 | |||
| ef3ba18168 | |||
| 465a163e27 | |||
| a33d579177 | |||
| 393a0ed36e | |||
| e8f9e4ee50 | |||
| 659c893b29 | |||
| 8d0da34e62 | |||
| 1c1fba0c0f | |||
| 86fecf6cee | |||
| 23d0f5908a | |||
| 919d0e6cdf | |||
| 5d7fee8262 | |||
| 5c32accf12 | |||
| d575734635 | |||
| ee63266095 | |||
| e7c161970c | |||
| ddd975e4ea | |||
| 474cb5e364 | |||
| e6ac6ac4fc | |||
| 0ecdc7d2e9 | |||
| e924d7fcac | |||
| 41c4cdc47a | |||
| 31d5b202bc | |||
| 2bc942db46 | |||
| 199b89e06d | |||
| 716eb27c53 | |||
| baef0d96d3 | |||
| f228e861d1 | |||
| 6ec836c491 | |||
| 9ce0370181 |
@@ -20,7 +20,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# TypeScript cores compile through the external core compiler at
|
||||
# build/test time; the compiler and the frontend's toolchain both
|
||||
# arrive with this one install (without it the ts-core suites skip
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# The exact-pinned external core compiler and the frontend's
|
||||
# toolchain, one install (packages/core/package.json is the one
|
||||
# place the pin lives).
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# gpu-components is a TypeScript-core app, so its smoke build needs
|
||||
# the frontend compiler and exact-pinned TypeScript toolchain.
|
||||
- run: npm ci --prefix packages/core
|
||||
@@ -74,6 +74,9 @@ jobs:
|
||||
- run: zig build test-example-mobile-canvas-lib-ios-store
|
||||
- run: zig build test-webview-system-link
|
||||
- run: zig build test-webview-smoke
|
||||
# The zero-config TypeScript runner must load app manifest menus before
|
||||
# automation can select their registered command ids.
|
||||
- run: zig build test-menu-bar-smoke
|
||||
# Signed-package seal pin: an ad-hoc signed package must pass
|
||||
# codesign --verify --strict (macOS runners are the only tier with
|
||||
# codesign; the step skips loudly anywhere else).
|
||||
@@ -181,7 +184,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# The TypeScript core suites compile through the external core
|
||||
# compiler at build/test time; without the install they skip
|
||||
# silently, so CI must provide it.
|
||||
@@ -221,7 +224,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# The TypeScript examples compile through the external core
|
||||
# compiler at build time; the compiler and the frontend toolchain
|
||||
# arrive with this install.
|
||||
@@ -355,7 +358,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# The scaffold default is the TypeScript core; its frontend and
|
||||
# compiler run at build time from this checkout's packages/core.
|
||||
- run: npm ci --prefix packages/core
|
||||
@@ -460,7 +463,7 @@ jobs:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
# The default scaffold is a TypeScript core: its build runs the
|
||||
# @native-sdk/core frontend and the external core compiler from
|
||||
# this checkout's own install.
|
||||
@@ -495,7 +498,7 @@ jobs:
|
||||
app=".zig-cache/scaffold-${frontend}"
|
||||
rm -rf "$app"
|
||||
./zig-out/bin/native init "$app" --frontend "$frontend" --full
|
||||
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon)
|
||||
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.json)
|
||||
# Every scaffold ships a CI workflow; parse it as real YAML.
|
||||
test -s "$app/.github/workflows/ci.yml"
|
||||
python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml"
|
||||
|
||||
@@ -42,6 +42,7 @@ docs/tsconfig.tsbuildinfo
|
||||
|
||||
# Dev-tool dependency trees (installed per package, never committed)
|
||||
packages/core/node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# The CLI-materialized editor copy of @native-sdk/core inside the TS example
|
||||
# apps (node_modules is editor surface, never source — the same entry the
|
||||
|
||||
@@ -6,7 +6,7 @@ Guidance for agents (and humans) working on this repository.
|
||||
|
||||
Native SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.
|
||||
|
||||
- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.zon`. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.
|
||||
- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.
|
||||
- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.
|
||||
- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.
|
||||
- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.
|
||||
@@ -15,7 +15,7 @@ Native SDK itself is implemented in Zig, but Native SDK **apps are authored in T
|
||||
|
||||
```bash
|
||||
zig build test # root engine + runtime suites
|
||||
zig build validate # sample app.zon manifest check
|
||||
zig build validate # framework's legacy app.zon manifest check
|
||||
zig build test-example-<name> # one example's suite (e.g. test-example-notes)
|
||||
scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main)
|
||||
scripts/gate.sh full # everything CI-shaped that runs locally
|
||||
@@ -34,7 +34,8 @@ Do not edit `CHANGELOG.md` as part of regular feature or fix work. The release a
|
||||
## Where things live
|
||||
|
||||
- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.
|
||||
- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`).
|
||||
- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`.
|
||||
- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`).
|
||||
- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.
|
||||
- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).
|
||||
- `tools/` and `scripts/` — dev tooling and the local gate.
|
||||
|
||||
@@ -2,12 +2,148 @@
|
||||
|
||||
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
|
||||
|
||||
## 0.8.4
|
||||
## 0.9.5
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
### New Features
|
||||
|
||||
- **JSON manifests by default**: New TypeScript, Zig, web, full, and ejected apps now scaffold with `app.json`, backed by full parsing, discovery, build conversion, validation, vendoring, a published versioned schema, and seamless `app.zon` fallback for existing projects (#385).
|
||||
- **Registered-image source cropping**: Canvas image options and Native markup can now select atomic source rectangles from registered images for texture-atlas rendering, with schema, compiler, validation, documentation, and sampling-bleed coverage (#390).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Reliable installed TypeScript toolchains**: Core and service builds now resolve ScriptC across nested, hoisted, and global sibling npm layouts, generate the complete SQLite SDK module family, and validate library imports against their actual directories (#389).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Focused schema hosting**: `schema.native-sdk.dev` now serves only the versioned app schema and its current-version alias, redirecting every non-schema route to the main Native SDK site (#388).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### New Features
|
||||
|
||||
- **Model-driven window restore policies**: TypeScript apps can now declare whether each model-driven window restores saved geometry or opens fresh, including center-on-primary placement, with matching defaults, validation, runtime forwarding, tests, and documentation (#381).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Faster, more predictable iterative rebuilds**: Generated core and service ABI artifacts now change only when their contents do, markup and app code compile into independently cached objects, SDK module edits invalidate the right inputs, and rebuild diagnostics expose phase timing, memory use, and cache decisions across platforms (#382).
|
||||
- **Updated TypeScript compiler integration**: ScriptC advances to 0.0.33 with published compile-cache bootstrapping, explicit development and release library profiles, synchronized compiler-surface artifacts, and Node 24 throughout the TypeScript build and CI toolchain (#384).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### New Features
|
||||
|
||||
- **Model-driven TypeScript theme state**: Zero-config TypeScript apps can now derive the built-in pack, color scheme, and accent from committed model state while preserving manifest fallback, live system accessibility settings, deterministic replay, and the existing `themePack` helper (#378).
|
||||
- **Platform-correct line deletion**: Command+Backspace on macOS now deletes to the start of a field or logical textarea line across every editable canvas control, with matching TypeScript text helpers, controlled-state behavior, undo, and replay (#377).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Precise macOS file-drop routing**: AppKit drops now retain labeled canvas and WebView targets with top-left, view-local coordinates, while unlabeled window regions fall back to content coordinates (#374).
|
||||
- **Manifest menus in generated runners**: Zero-config TypeScript and Zig-core apps now load `app.zon` commands, shortcuts, and menus consistently in live and replay runners, including ejected-runner fallbacks (#376).
|
||||
- **Large TypeScript message unions compile reliably**: Generated shims now derive comptime scan quotas from message shape and identifier size, allowing wide unions to compile across persistence, channels, environment routing, and the full external-core pipeline (#375).
|
||||
- **Correct combobox Enter precedence**: A bound `on-submit` now handles Enter before trigger activation, so query submission no longer opens the picker or dispatches the wrong command (#373).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @MohakBajaj
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### New Features
|
||||
|
||||
- **Flash-free accessory startup**: Apps can opt into accessory activation from `app.zon` to launch without a Dock icon or foreground flash, with tray-affordance validation, runtime composition, packaging support, and an updated menu-bar example (#358).
|
||||
- **Logical canvas radio groups**: Nested radios now form accessible single-selection groups with roving focus and consistent keyboard, pointer, handler, and naming semantics (#361).
|
||||
- **Budget-aware photo decoding**: Dynamic encoded images are downsampled across desktop and mobile codecs to fit a configurable registered-pixel budget, with independent source bounds, deterministic replay, and platform-level regression coverage (#366).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Correct anchored surfaces**: Floating and modal surfaces now dismiss without requiring focus, relayout after scroll restoration, resolve against the correct root, and behave consistently across window contexts (#363).
|
||||
- **Reliable autofocus and caret reveal**: Keyboard focus, autofocus, and automation now transactionally reveal offscreen targets while preserving collapsed end-caret selections in text editors (#364).
|
||||
- **Explicit link decoration**: Linked text spans now honor their underline flag while Markdown-generated links retain conventional underlines (#368).
|
||||
- **Stable macOS window geometry**: Fresh windows now distinguish restored, explicit, and default placement, while AppKit and CEF frame events consistently report content geometry without titlebar drift (#369, #370).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Consistent canvas controls and surfaces**: Checkbox and radio labels can contain markup consistently, while actionable states, disabled colors, variant accents, selection geometry, compact layouts, and zero-width strokes now render uniformly across the schema, runtime, accessibility tree, and documentation (#367).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @sepehr-safari
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### New Features
|
||||
|
||||
- **Multi-item macOS menu bars**: Apps can now manage independent, keyed status items with model-driven updates, events, automation, journaling, and regression coverage (#343).
|
||||
- **Complete TypeScript file effects**: Secure, permission-gated effects now support bounded streaming reads, atomic writes, stat, append, and deletion while preserving deterministic record and replay behavior (#339, #350).
|
||||
- **Actionable desktop notifications**: Notification replacement identifiers and actions dispatch through the ordinary command path on macOS, Windows, and Linux (#347).
|
||||
- **Secondary-window lifecycle control**: Window descriptors can declare quit or hide-on-close behavior, preserve hidden-window identity when reopened, and expose the same model-driven window contract to TypeScript apps (#349, #351).
|
||||
- **Mobile TypeScript cores and services**: TypeScript apps with services now compile into iOS and Android library archives, with mobile packaging and device-level runtime coverage (#346).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Safe Linux alert dialogs**: GTK alert dialogs now initialize with a valid empty format string, avoiding a crash from a null constructor argument (#354).
|
||||
- **Correct compiled-core tuple returns**: The SDK now pins the scriptc tuple-normalization fix and verifies bare-model and effect-tuple ABI returns with a compiled-core regression (#356).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Stronger TypeScript core guidance and diagnostics**: Subset rules now distinguish permanent guarantees from deliberately deferred capabilities and point authors to the appropriate service alternative (#345).
|
||||
- **End-to-end services showcase**: The Feed Reader example now demonstrates the full TypeScript service workflow with typed feed parsing, shared data, fixtures, and replay coverage (#352).
|
||||
- **Updated compiler integration**: scriptc advances through 0.0.31 with refreshed generated contracts, compatibility fixtures, and compiler-surface references (#344, #356).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @ElSebas41
|
||||
- @johnlindquist
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Ordinary TypeScript services behind a typed boundary**: Apps can place filesystem, process, JSON, regex, class, and exact-vendored npm work under `src/services/`; Native SDK generates the checked client and codecs, compiles a pinned static service executable, and carries keyed requests, typed streaming, cooperative cancellation, deadlines, supervision, and deterministic replay across the isolated boundary (#317, #321).
|
||||
- **Optional in-process TypeScript services**: Services can use the same boundary through a linked, runtime-localized worker pool with per-key FIFO ordering, parallel independent keys, streaming, timeout and trap isolation, and replay that never starts the carrier; the explicit opt-in now follows the compiler's Windows, Linux, macOS, and cross-target matrix while the isolated child remains the automatic default (#334, #337).
|
||||
- **Engine-owned model persistence**: TypeScript cores can persist committed models through capability-gated, atomically replaced snapshots with generated codecs, debounced writes, backup recovery, explicit restore and migration routes, rollback safety, and journal/replay support (#316).
|
||||
- **SQLite record storage**: TypeScript and Zig apps can use a capability-gated record store for deterministic atomic CRUD effects backed by bundled SQLite across desktop and mobile hosts, with devhost parity and a complete Record Store example (#320).
|
||||
- **Checked relational SQLite**: Append-only migrations, build-validated named SQL, transactions, generated typed commands and live-query subscriptions, replay, and the Relational Notes example make relational SQLite a first-class offline data layer across desktop and mobile (#326).
|
||||
- **Model-driven menu-bar apps**: TypeScript apps can derive status-item labels, icons, tooltips, and rich menus from committed model state, while new macOS effects control hidden startup, fullscreen, Dock visibility, and launch-at-login behavior across both native hosts (#311, #314).
|
||||
- **Platform services for TypeScript cores**: Typed effects now open external URLs, reveal filesystem paths, and format local time through validated macOS, Linux, and Windows backends (#315).
|
||||
- **App-scoped credentials**: TypeScript and Zig cores can store, load, and delete credentials through capability- and permission-gated native providers, with redacted journals, deterministic replay placeholders, and hermetic devhost stores across desktop and mobile (#335).
|
||||
- **Cross-compiled TypeScript cores**: The external core compiler now builds Linux and Windows GNU targets from macOS, Linux, or Windows and macOS targets from macOS, with target-independent contracts and cross-platform end-to-end batteries for Windows and Linux musl (#340).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Working documentation root**: `/docs`, `/docs/`, and the matching Markdown route now resolve to the Introduction instead of ending at a 404 (#338).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Faster retained desktop frames**: Animation pumping and Windows wake scheduling now avoid stalled or redundant work, profiling uses monotonic frame-correlated telemetry, and physical macOS and Windows performance gates protect input latency and frame budgets (#313).
|
||||
- **Measured, compiler-truth service tooling**: A dedicated TypeScript Services reference documents the two-tier model and failure semantics; production-carrier benchmarks measure cold start, latency, and throughput; generated compiler-surface references and manifest diffs keep capability claims mechanically honest; and scriptc advances through 0.0.28 with refreshed contracts and calibration (#325, #327, #328, #329, #333, #336).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @carvalab
|
||||
- @Railly
|
||||
- @camilocbarrera
|
||||
|
||||
## 0.8.4
|
||||
|
||||
### New Features
|
||||
|
||||
- **Streaming fetch responses for TypeScript cores**: `Cmd.fetch` can now deliver line-framed HTTP responses through typed message arms with deterministic terminal errors, loud cancellation, duplicate-key rejection, and bounded line sizes; the rebuilt Chatbot example streams Vercel AI Gateway replies with live model selection and a Stop action (#300).
|
||||
- **Desktop audio capture**: TypeScript cores can start bounded, timestamped microphone or system-output PCM streams on macOS and Windows with explicit lifecycle, permission, drop-count, and replay handling; the new Voice Memo example records, saves, and plays WAV files (#303).
|
||||
- **Customizable macOS DMG packaging**: `native package` now creates polished drag-to-Applications disk images with generated or custom Retina backgrounds, configurable Finder geometry, positioned app and Applications entries, and staged files, directories, or links (#304).
|
||||
@@ -29,8 +165,6 @@ All notable changes to the Native SDK (formerly zero-native) will be documented
|
||||
- @marcusschiesser
|
||||
- @NyxTools-M
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -93,7 +93,7 @@ Read the full guide at [native-sdk.dev/quick-start](https://native-sdk.dev/quick
|
||||
|
||||
## Examples
|
||||
|
||||
The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`. Start with the TypeScript examples when learning the primary authoring path. The `-ts` suffix on `soundboard-ts` and `system-monitor-ts` is historical because those apps are ports kept beside older Zig originals. Chatbot is TypeScript-only and follows the unsuffixed naming used by new apps created with `native init`.
|
||||
The apps pictured above live in [examples/](./examples), most as zero-config projects with a manifest plus `src/` and no build files, run straight from their directory with `native dev`. Many examples predate the current `app.json` default and retain `app.zon`; both formats have the same capabilities. Start with the TypeScript examples when learning the primary authoring path. The `-ts` suffix on `soundboard-ts` and `system-monitor-ts` is historical because those apps are ports kept beside older Zig originals. Chatbot is TypeScript-only and follows the unsuffixed naming used by new apps created with `native init`.
|
||||
|
||||
| Example | What it shows |
|
||||
| --- | --- |
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Native SDK schemas
|
||||
|
||||
Static JSON Schemas published at `schema.native-sdk.dev`.
|
||||
|
||||
- `/app/v1.json` is the stable-major schema URL scaffolded into `app.json`.
|
||||
- `/app.json` is the short-lived current-version alias.
|
||||
|
||||
Only those schema URLs are served from this deployment. Every other path
|
||||
redirects (302) to `https://native-sdk.dev`, so the domain stays a single-
|
||||
purpose home for the manifest rather than hosting arbitrary content.
|
||||
|
||||
Create the Vercel project as `native-schema`, set its root directory to
|
||||
`apps/schema`, leave the framework preset as Other with no build command, and
|
||||
attach `schema.native-sdk.dev`. `vercel.json` sets the output directory to
|
||||
`public`. Add a new versioned file only for a breaking manifest contract;
|
||||
backward-compatible additions update the current major.
|
||||
|
||||
The original `https://native-sdk.dev/schemas/app.schema.json` URL remains a
|
||||
byte-identical compatibility copy owned by the docs deployment.
|
||||
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://schema.native-sdk.dev/app/v1.json",
|
||||
"title": "Native SDK app manifest",
|
||||
"description": "Complete app.json manifest for a Native SDK application. app.zon remains supported as a legacy alternative.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "version"],
|
||||
"properties": {
|
||||
"$schema": { "type": "string", "format": "uri-reference" },
|
||||
"id": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Reverse-DNS application identifier." },
|
||||
"name": { "type": "string", "minLength": 1, "description": "Short machine-readable app name." },
|
||||
"display_name": { "type": "string", "minLength": 1, "description": "Human-readable app name." },
|
||||
"description": { "type": "string", "minLength": 1, "maxLength": 256 },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"icons": { "$ref": "#/$defs/stringArray" },
|
||||
"platforms": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "enum": ["macos", "linux", "windows", "ios", "android", "web"] }
|
||||
},
|
||||
"permissions": { "$ref": "#/$defs/stringArray" },
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"enum": [
|
||||
"native_module", "webview", "js_bridge", "native_views", "gpu_surfaces",
|
||||
"menus", "shortcuts", "tray", "filesystem", "network", "notifications",
|
||||
"dialog", "clipboard", "credentials", "persist", "store", "sqlite",
|
||||
"open_url", "reveal_path", "recent_documents", "file_drops",
|
||||
"app_activation_events", "file_associations", "url_schemes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dock_visible": { "type": "boolean", "default": true },
|
||||
"persist": { "$ref": "#/$defs/persist" },
|
||||
"images": { "$ref": "#/$defs/images" },
|
||||
"service_packages": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/servicePackage" }
|
||||
},
|
||||
"service_carrier": { "enum": ["auto", "in_process", "child"], "default": "auto" },
|
||||
"service_pool_size": { "type": "integer", "minimum": 1, "maximum": 16 },
|
||||
"bridge": { "$ref": "#/$defs/bridge" },
|
||||
"web_engine": { "enum": ["system", "chromium"], "default": "system" },
|
||||
"webview_layer": { "enum": ["auto", "include", "exclude"], "default": "auto" },
|
||||
"core_compiler": { "const": "external", "default": "external" },
|
||||
"theme": { "enum": ["house", "geist"] },
|
||||
"theme_accent": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
|
||||
"cef": { "$ref": "#/$defs/cef" },
|
||||
"frontend": { "$ref": "#/$defs/frontend" },
|
||||
"security": { "$ref": "#/$defs/security" },
|
||||
"assets": { "$ref": "#/$defs/assets" },
|
||||
"windows": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/window" }
|
||||
},
|
||||
"shell": { "$ref": "#/$defs/shell" },
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/command" }
|
||||
},
|
||||
"menus": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/menu" }
|
||||
},
|
||||
"shortcuts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shortcut" }
|
||||
},
|
||||
"file_associations": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fileAssociation" }
|
||||
},
|
||||
"url_schemes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/urlScheme" }
|
||||
},
|
||||
"dmg": { "$ref": "#/$defs/dmg" }
|
||||
},
|
||||
"$defs": {
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"position": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["x", "y"],
|
||||
"properties": {
|
||||
"x": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
||||
"y": { "type": "integer", "minimum": 0, "maximum": 65535 }
|
||||
}
|
||||
},
|
||||
"persist": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "restore"],
|
||||
"properties": {
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"debounce_ms": { "type": "integer", "minimum": 0, "maximum": 60000, "default": 500 },
|
||||
"restore": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ok", "none", "err"],
|
||||
"properties": {
|
||||
"ok": { "type": "string", "minLength": 1 },
|
||||
"none": { "type": "string", "minLength": 1 },
|
||||
"err": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"max_image_pixel_bytes": { "type": "integer", "minimum": 1048576, "maximum": 8388608, "default": 1048576 }
|
||||
}
|
||||
},
|
||||
"servicePackage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "version", "content_hash"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"content_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"permissions": { "$ref": "#/$defs/stringArray" },
|
||||
"origins": { "$ref": "#/$defs/stringArray" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dir": { "type": "string", "default": "third_party/cef/macos" },
|
||||
"auto_install": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dist": { "type": "string", "default": "dist" },
|
||||
"entry": { "type": "string", "default": "index.html" },
|
||||
"spa_fallback": { "type": "boolean", "default": true },
|
||||
"dev": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"command": { "$ref": "#/$defs/stringArray" },
|
||||
"ready_path": { "type": "string", "default": "/" },
|
||||
"timeout_ms": { "type": "integer", "minimum": 1, "maximum": 4294967295, "default": 30000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_origins": { "$ref": "#/$defs/stringArray" },
|
||||
"external_links": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"action": { "enum": ["deny", "open_system_browser"], "default": "deny" },
|
||||
"allowed_urls": { "$ref": "#/$defs/stringArray" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "path"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "minimum": 1 },
|
||||
"path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"windowBase": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": { "type": "string", "default": "main" },
|
||||
"title": { "type": "string" },
|
||||
"width": { "type": "number", "exclusiveMinimum": 0, "default": 720 },
|
||||
"height": { "type": "number", "exclusiveMinimum": 0, "default": 480 },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"resizable": { "type": "boolean", "default": true },
|
||||
"restore_state": { "type": "boolean", "default": true },
|
||||
"titlebar": { "enum": ["standard", "hidden_inset", "hidden_inset_tall", "chromeless"], "default": "standard" },
|
||||
"transparent": { "type": "boolean", "default": false },
|
||||
"always_on_top": { "type": "boolean", "default": false },
|
||||
"click_through": { "type": "boolean", "default": false },
|
||||
"activate_on_show": { "type": "boolean", "default": true },
|
||||
"initially_hidden": { "type": "boolean", "default": false },
|
||||
"allows_fullscreen": { "type": "boolean", "default": true },
|
||||
"min_width": { "type": "number", "minimum": 0, "default": 0 },
|
||||
"min_height": { "type": "number", "minimum": 0, "default": 0 },
|
||||
"close_policy": { "enum": ["quit", "hide"], "default": "quit" }
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"allOf": [{ "$ref": "#/$defs/windowBase" }],
|
||||
"unevaluatedProperties": false
|
||||
},
|
||||
"shell": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"windows": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shellWindow" }
|
||||
},
|
||||
"chrome": { "$ref": "#/$defs/shellChrome" }
|
||||
}
|
||||
},
|
||||
"shellWindow": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/$defs/windowBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"restore_policy": { "enum": ["clamp_to_visible_screen", "center_on_primary"], "default": "clamp_to_visible_screen" },
|
||||
"views": { "type": "array", "items": { "$ref": "#/$defs/shellView" } }
|
||||
}
|
||||
}
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
},
|
||||
"shellChrome": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shellTab" }
|
||||
},
|
||||
"primary_action": { "$ref": "#/$defs/shellTab" }
|
||||
}
|
||||
},
|
||||
"shellTab": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "label"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"label": { "type": "string", "minLength": 1 },
|
||||
"icon": { "type": "string", "default": "" }
|
||||
}
|
||||
},
|
||||
"shellView": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["label", "kind"],
|
||||
"properties": {
|
||||
"label": { "type": "string", "minLength": 1 },
|
||||
"kind": {
|
||||
"enum": [
|
||||
"webview", "toolbar", "titlebar_accessory", "sidebar", "statusbar", "split", "stack",
|
||||
"button", "icon_button", "list_item", "checkbox", "toggle", "segmented_control",
|
||||
"text_field", "search_field", "label", "spacer", "gpu_surface", "progress_indicator"
|
||||
]
|
||||
},
|
||||
"parent": { "type": "string" },
|
||||
"edge": { "enum": ["top", "right", "bottom", "left"] },
|
||||
"axis": { "enum": ["row", "horizontal", "column", "vertical"] },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"width": { "type": "number" },
|
||||
"height": { "type": "number" },
|
||||
"min_width": { "type": "number" },
|
||||
"min_height": { "type": "number" },
|
||||
"max_width": { "type": "number" },
|
||||
"max_height": { "type": "number" },
|
||||
"fill": { "type": "boolean", "default": false },
|
||||
"layer": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647, "default": 0 },
|
||||
"visible": { "type": "boolean", "default": true },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"role": { "type": "string" },
|
||||
"accessibility_label": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"text": { "type": "string" },
|
||||
"command": { "type": "string" },
|
||||
"gpu_backend": { "enum": ["none", "metal", "software"] },
|
||||
"gpu_pixel_format": { "enum": ["none", "bgra8_unorm"] },
|
||||
"gpu_present_mode": { "enum": ["none", "timer"] },
|
||||
"gpu_alpha_mode": { "enum": ["none", "opaque", "premultiplied"] },
|
||||
"gpu_color_space": { "enum": ["none", "srgb", "display_p3"] },
|
||||
"gpu_vsync": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"command": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"title": { "type": "string", "default": "" },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"checked": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title"],
|
||||
"properties": {
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/menuItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"menuItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": { "type": "string", "default": "" },
|
||||
"command": { "type": "string", "default": "" },
|
||||
"key": { "type": "string", "default": "" },
|
||||
"modifiers": { "$ref": "#/$defs/modifiers" },
|
||||
"separator": { "type": "boolean", "default": false },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"checked": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"modifiers": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "enum": ["primary", "command", "control", "option", "alt", "shift"] }
|
||||
},
|
||||
"shortcut": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "key"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"key": { "type": "string", "minLength": 1 },
|
||||
"modifiers": { "$ref": "#/$defs/modifiers" }
|
||||
}
|
||||
},
|
||||
"fileAssociation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"role": { "$ref": "#/$defs/associationRole" },
|
||||
"extensions": { "$ref": "#/$defs/stringArray" },
|
||||
"mime_types": { "$ref": "#/$defs/stringArray" },
|
||||
"icon": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"urlScheme": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["scheme"],
|
||||
"properties": {
|
||||
"scheme": { "type": "string", "minLength": 1 },
|
||||
"role": { "$ref": "#/$defs/associationRole" }
|
||||
}
|
||||
},
|
||||
"associationRole": { "enum": ["viewer", "editor", "shell", "none"], "default": "viewer" },
|
||||
"dmg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"volume_name": { "type": "string" },
|
||||
"background": { "type": "string" },
|
||||
"window_width": { "type": "integer", "minimum": 320, "maximum": 2000, "default": 660 },
|
||||
"window_height": { "type": "integer", "minimum": 240, "maximum": 1400, "default": 400 },
|
||||
"icon_size": { "type": "integer", "minimum": 32, "maximum": 256, "default": 128 },
|
||||
"app_position": { "$ref": "#/$defs/position" },
|
||||
"applications_position": { "$ref": "#/$defs/position" },
|
||||
"applications_link": { "type": "boolean", "default": true },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/dmgItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"dmgItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "position"],
|
||||
"properties": {
|
||||
"kind": { "enum": ["app", "applications", "file", "link"] },
|
||||
"path": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"position": { "$ref": "#/$defs/position" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"outputDirectory": "public",
|
||||
"rewrites": [
|
||||
{ "source": "/app.json", "destination": "/app/v1.json" }
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/((?!app(?:\\.json|/v1\\.json)$).*)",
|
||||
"destination": "https://native-sdk.dev",
|
||||
"statusCode": 302
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/app/v1.json",
|
||||
"headers": [
|
||||
{ "key": "Content-Type", "value": "application/schema+json; charset=utf-8" },
|
||||
{ "key": "Cache-Control", "value": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400" },
|
||||
{ "key": "Access-Control-Allow-Origin", "value": "*" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/app.json",
|
||||
"headers": [
|
||||
{ "key": "Content-Type", "value": "application/schema+json; charset=utf-8" },
|
||||
{ "key": "Cache-Control", "value": "public, max-age=300, s-maxage=300, stale-while-revalidate=86400" },
|
||||
{ "key": "Access-Control-Allow-Origin", "value": "*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
const std = @import("std");
|
||||
const web_engine_tool = @import("src/tooling/web_engine.zig");
|
||||
|
||||
fn repositoryScriptcBin(b: *std.Build) []const u8 {
|
||||
return b.pathFromRoot(if (b.graph.host.result.os.tag == .windows)
|
||||
"packages/core/node_modules/.bin/scriptc.cmd"
|
||||
else
|
||||
"packages/core/node_modules/.bin/scriptc");
|
||||
}
|
||||
|
||||
const PlatformOption = enum {
|
||||
auto,
|
||||
null,
|
||||
@@ -198,11 +205,73 @@ pub fn build(b: *std.Build) void {
|
||||
desktop_mod.addIncludePath(b.path("third_party/sqlite"));
|
||||
desktop_mod.addCSourceFile(.{
|
||||
.file = b.path("third_party/sqlite/sqlite3.c"),
|
||||
.flags = sqliteCompileFlags(),
|
||||
// Mobile -Dtargets (the mobile e2e battery) compile the
|
||||
// amalgamation against the platform SDK/NDK sysroot; desktop
|
||||
// targets keep Zig's ordinary libc discovery.
|
||||
.flags = if (target.result.os.tag == .ios or target.result.abi.isAndroid())
|
||||
@import("build/app.zig").sqliteCFlags(b, target)
|
||||
else
|
||||
sqliteCompileFlags(),
|
||||
});
|
||||
const app_runner_window_placement_mod = module(b, target, optimize, "src/app_runner/window_placement.zig");
|
||||
app_runner_window_placement_mod.addImport("native_sdk", desktop_mod);
|
||||
const app_runner_window_placement_tests = testArtifact(b, app_runner_window_placement_mod);
|
||||
const app_runner_options = b.addOptions();
|
||||
app_runner_options.addOption([]const u8, "platform", "null");
|
||||
app_runner_options.addOption([]const u8, "trace", "off");
|
||||
app_runner_options.addOption([]const u8, "web_engine", "system");
|
||||
app_runner_options.addOption(bool, "debug_overlay", false);
|
||||
app_runner_options.addOption(bool, "automation", false);
|
||||
app_runner_options.addOption(bool, "web_layer", false);
|
||||
const app_runner_mod = module(b, target, optimize, "src/app_runner/root.zig");
|
||||
app_runner_mod.addImport("native_sdk", desktop_mod);
|
||||
app_runner_mod.addImport("build_options", app_runner_options.createModule());
|
||||
app_runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path("tests/app-runner/menu_commands_fixture.zon") }));
|
||||
const app_runner_migrations_mod = module(b, target, optimize, "src/app_runner/no_migrations.zig");
|
||||
app_runner_migrations_mod.addImport("native_sdk", desktop_mod);
|
||||
app_runner_mod.addImport("relational_migrations", app_runner_migrations_mod);
|
||||
const app_runner_tests = testArtifact(b, app_runner_mod);
|
||||
const app_runner_test_run = b.addRunArtifact(app_runner_tests);
|
||||
const app_runner_test_step = b.step("test-app-runner", "Run framework app-runner manifest fallback tests");
|
||||
app_runner_test_step.dependOn(&app_runner_test_run.step);
|
||||
desktop_mod.link_libc = true;
|
||||
if (target.result.os.tag == .macos) {
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot|
|
||||
&.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, b.fmt("-I{s}/usr/include", .{sysroot}) }
|
||||
else
|
||||
&.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" };
|
||||
desktop_mod.addCSourceFile(.{ .file = b.path("src/platform/macos/image_fit_test.m"), .flags = flags });
|
||||
desktop_mod.linkFramework("Foundation", .{});
|
||||
desktop_mod.linkFramework("ImageIO", .{});
|
||||
desktop_mod.linkSystemLibrary("objc", .{});
|
||||
}
|
||||
const desktop_tests = testArtifact(b, desktop_mod);
|
||||
const desktop_test_shards = desktopTestShardArtifacts(b, desktop_mod);
|
||||
// Tier-5 crash battery: a child uses the public streamed sink, signals
|
||||
// after a chunk is durable only in the atomic temporary, then the parent
|
||||
// kills it and verifies the destination still names the old generation.
|
||||
const file_crash_helper_mod = module(b, target, optimize, "src/runtime/file_effect_crash_helper.zig");
|
||||
file_crash_helper_mod.addImport("native_sdk", desktop_mod);
|
||||
const file_crash_helper = b.addExecutable(.{ .name = "file-effect-crash-helper", .root_module = file_crash_helper_mod });
|
||||
const file_crash_tests_mod = module(b, target, optimize, "src/runtime/file_effect_crash_tests.zig");
|
||||
file_crash_tests_mod.addImport("native_sdk", desktop_mod);
|
||||
const file_crash_options = b.addOptions();
|
||||
file_crash_options.addOptionPath("helper_executable", file_crash_helper.getEmittedBin());
|
||||
file_crash_tests_mod.addOptions("file_crash_options", file_crash_options);
|
||||
const file_crash_tests = testArtifact(b, file_crash_tests_mod);
|
||||
const file_crash_run = b.addRunArtifact(file_crash_tests);
|
||||
const file_crash_step = b.step("test-file-effect-crash", "Kill a streamed writer before close and verify atomic destination visibility");
|
||||
file_crash_step.dependOn(&file_crash_run.step);
|
||||
const tier5_tests = filteredTestArtifact(b, desktop_mod, "storage-tier-5-tests", &.{
|
||||
"runtime.effects_file_tests.test",
|
||||
"runtime.file_access.test",
|
||||
"runtime.session_record.test.recorder moves streamed file chunks",
|
||||
"runtime.session_tests.test.a multi-megabyte file stream",
|
||||
});
|
||||
const tier5_run = b.addRunArtifact(tier5_tests);
|
||||
const tier5_step = b.step("test-storage-tier-5", "Run file streaming, bounds, replay/blob, gating, and crash-atomicity batteries");
|
||||
tier5_step.dependOn(&tier5_run.step);
|
||||
tier5_step.dependOn(&file_crash_run.step);
|
||||
|
||||
// SQLite is capability-shed from ordinary app artifacts. Its focused
|
||||
// engine/store suite gets a dedicated module that explicitly compiles the
|
||||
@@ -562,8 +631,11 @@ pub fn build(b: *std.Build) void {
|
||||
test_step.dependOn(&b.addRunArtifact(platform_info_tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(json_tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(app_runner_assets_tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(app_runner_window_placement_tests).step);
|
||||
test_step.dependOn(&app_runner_test_run.step);
|
||||
test_step.dependOn(&b.addRunArtifact(canvas_tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(record_store_tests).step);
|
||||
test_step.dependOn(&file_crash_run.step);
|
||||
for (desktop_test_shards) |shard_tests| {
|
||||
test_step.dependOn(&b.addRunArtifact(shard_tests).step);
|
||||
}
|
||||
@@ -591,7 +663,9 @@ pub fn build(b: *std.Build) void {
|
||||
// no build inputs/outputs to hash, so always run it.
|
||||
scaffold_ide_e2e_run.has_side_effects = true;
|
||||
const ai_chat_e2e_run = b.addRunArtifact(ts_core_artifacts.ai_chat);
|
||||
const feed_reader_e2e_run = b.addRunArtifact(ts_core_artifacts.feed_reader);
|
||||
const services_e2e_run = b.addRunArtifact(ts_core_artifacts.services);
|
||||
ts_services_e2e_step.dependOn(&feed_reader_e2e_run.step);
|
||||
ts_services_e2e_step.dependOn(&services_e2e_run.step);
|
||||
// The same fixture through the in-process carrier (ServicePool over
|
||||
// the linked service archive): parallel keys, per-key FIFO,
|
||||
@@ -618,13 +692,15 @@ pub fn build(b: *std.Build) void {
|
||||
const sidecar_conformance_run = b.addRunArtifact(ts_core_artifacts.sidecar_conformance);
|
||||
const sidecar_conformance_step = b.step("sidecar-conformance", "Validate corewire-generated mirrors over every fixture's frontend-emitted contract (requires node)");
|
||||
sidecar_conformance_step.dependOn(&sidecar_conformance_run.step);
|
||||
// ABI-law suite over a real compiled core: the markup fixture's
|
||||
// archive driven directly through the C ABI (collect invariant,
|
||||
// deterministic re-init, channel envelopes, integer classes).
|
||||
// ABI-law suites over real compiled cores: the broad markup fixture
|
||||
// plus the focused mixed bare-Model/[Model, Cmd] return regression.
|
||||
const abi_laws_run = b.addRunArtifact(ts_core_artifacts.external_core_abi_laws);
|
||||
const abi_laws_step = b.step("test-external-core-abi", "Run the compiled-core ABI-law suite over the markup fixture's archive (requires node and `npm ci` in packages/core)");
|
||||
const mixed_return_abi_run = b.addRunArtifact(ts_core_artifacts.mixed_return_abi_laws);
|
||||
const abi_laws_step = b.step("test-external-core-abi", "Run the compiled-core ABI-law suites, including mixed update returns (requires node and `npm ci` in packages/core)");
|
||||
abi_laws_step.dependOn(&abi_laws_run.step);
|
||||
abi_laws_step.dependOn(&mixed_return_abi_run.step);
|
||||
test_step.dependOn(&abi_laws_run.step);
|
||||
test_step.dependOn(&mixed_return_abi_run.step);
|
||||
// The cross-execution battery staging: the host-fixture and
|
||||
// markup batteries (update/snapshot/effects and the markup view
|
||||
// over genuinely compiled cores) plus, where the target admits
|
||||
@@ -638,6 +714,14 @@ pub fn build(b: *std.Build) void {
|
||||
if (ts_core_artifacts.services_pool) |pool_tests| {
|
||||
cross_e2e_step.dependOn(&b.addInstallArtifact(pool_tests, .{ .dest_dir = e2e_dir }).step);
|
||||
}
|
||||
// The mobile execution battery staging: one static library the
|
||||
// platform toolchain (xcrun clang / NDK clang) links around a tiny
|
||||
// harness for execution on the simulator or emulator. Registered
|
||||
// only for a mobile -Dtarget; scripts/mobile-e2e.sh is the driver.
|
||||
if (ts_core_artifacts.mobile_battery) |battery| {
|
||||
const mobile_e2e_step = b.step("stage-mobile-e2e", "Install the TS mobile e2e battery archive under <prefix>/e2e (pair with -Dtarget=aarch64-ios-simulator or aarch64-linux-android and -p; see scripts/mobile-e2e.sh)");
|
||||
mobile_e2e_step.dependOn(&b.addInstallFileWithDir(battery, .{ .custom = "e2e" }, "libts-mobile-e2e.a").step);
|
||||
}
|
||||
// The corpus contract artifacts an external core toolchain
|
||||
// consumes: per fixture, the frontend-emitted contract sidecar
|
||||
// (after projection), the generated entry module, and the
|
||||
@@ -657,6 +741,7 @@ pub fn build(b: *std.Build) void {
|
||||
ts_core_e2e_step.dependOn(&monitor_e2e_run.step);
|
||||
ts_core_e2e_step.dependOn(&scaffold_ide_e2e_run.step);
|
||||
ts_core_e2e_step.dependOn(&ai_chat_e2e_run.step);
|
||||
ts_core_e2e_step.dependOn(&feed_reader_e2e_run.step);
|
||||
ts_core_e2e_step.dependOn(&services_e2e_run.step);
|
||||
if (services_pool_e2e_run) |run| ts_core_e2e_step.dependOn(&run.step);
|
||||
test_step.dependOn(&host_e2e_run.step);
|
||||
@@ -667,6 +752,7 @@ pub fn build(b: *std.Build) void {
|
||||
test_step.dependOn(&monitor_e2e_run.step);
|
||||
test_step.dependOn(&scaffold_ide_e2e_run.step);
|
||||
test_step.dependOn(&ai_chat_e2e_run.step);
|
||||
test_step.dependOn(&feed_reader_e2e_run.step);
|
||||
test_step.dependOn(&services_e2e_run.step);
|
||||
if (services_pool_e2e_run) |run| test_step.dependOn(&run.step);
|
||||
test_step.dependOn(&sidecar_conformance_run.step);
|
||||
@@ -753,6 +839,43 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "build.zig", .pattern = "if (spec.emit_services) check.addFileInput(b.path(\"packages/core/package.json\"));" },
|
||||
.{ .path = "build/app.zig", .pattern = "if (has_services) check.addFileInput(dep.path(\"packages/core/package.json\"));" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-ts-build-invalidation-boundaries", "Verify generated TypeScript contracts stabilize by content and service implementation inputs cannot dirty the core compiler lane", &.{
|
||||
.{ .path = "build/app.zig", .pattern = "pub fn stabilizeGeneratedFile" },
|
||||
.{ .path = "build/app.zig", .pattern = "const contract = stabilizeGeneratedFile(b, contract_raw, \"core.contract.json\", build_trace);" },
|
||||
.{ .path = "build/app.zig", .pattern = "break :services_contract stabilizeGeneratedFile(b, raw, \"services.contract.json\", build_trace);" },
|
||||
.{ .path = "build/app.zig", .pattern = "addAppCoreTsDirInputs(b, stage_run, appPath(b, app_root, \"src\"));" },
|
||||
.{ .path = "build/app.zig", .pattern = "addStagedCoreSdkInputs(b, dep.builder, stage_run);" },
|
||||
.{ .path = "build/app.zig", .pattern = "for ([_][]const u8{ \"text.ts\", \"events.ts\" }) |source|" },
|
||||
.{ .path = "build.zig", .pattern = "app_build.addStagedCoreSdkInputs(b, b, stage_run);" },
|
||||
.{ .path = "packages/core/scripts/stage_external_core.mjs", .pattern = "for (const sdkFile of [\"text.ts\", \"events.ts\"])" },
|
||||
.{ .path = "build/app.zig", .pattern = "if (std.mem.startsWith(u8, normalized, \"services/\")) continue;" },
|
||||
.{ .path = "tools/corewire/emit_service.zig", .pattern = "native-sdk.services.abi.v3" },
|
||||
.{ .path = "tools/corewire/emit_service.zig", .pattern = "implementation-only fingerprint" },
|
||||
.{ .path = "build/app.zig", .pattern = "link_mod.addObject(markupDataObject(b, target, app_optimize, stage.markup_c));" },
|
||||
.{ .path = "build/app.zig", .pattern = "addPlatformLinkSearchPaths(b, selected_platform, web_engine, cef_dir, link_mod);" },
|
||||
.{ .path = "build/app.zig", .pattern = "mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, \"System/Library/Frameworks\" }) });" },
|
||||
.{ .path = "build/app.zig", .pattern = "b.fmt(\"{s}-app-code\", .{app_options.name})" },
|
||||
.{ .path = "src/app_runner/ts_core_main.zig", .pattern = "extern const native_sdk_app_markup: u8;" },
|
||||
.{ .path = "packages/core/scripts/embed_markup_c.mjs", .pattern = "const unsigned char native_sdk_app_markup[]" },
|
||||
.{ .path = "build/app.zig", .pattern = "node_modules\", \"scriptc\", \"dist\", \"bootstrap.js\"" },
|
||||
.{ .path = "build/app.zig", .pattern = "setEnvironmentVariable(\"SCRIPTC_TIMING\", \"1\")" },
|
||||
.{ .path = "build/app.zig", .pattern = "service_compile.addArg(\"--compiler-package-origin\")" },
|
||||
.{ .path = "build/app.zig", .pattern = "compile.addArg(\"--compiler-package-origin\")" },
|
||||
.{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "publishedScriptcArgv(args[\"compiler-package-origin\"])" },
|
||||
.{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "publishedScriptcArgv(args[\"compiler-package-origin\"])" },
|
||||
.{ .path = "build/app.zig", .pattern = "addFileInput(dep.path(\"packages/core/scripts/compiler_command.mjs\"))" },
|
||||
.{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "compilerArgv(args.compiler)" },
|
||||
.{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "compilerArgv(args.compiler)" },
|
||||
.{ .path = "packages/core/scripts/compiler_command.mjs", .pattern = "npmTarget !== null" },
|
||||
.{ .path = "build.zig", .pattern = "fn repositoryScriptcBin" },
|
||||
.{ .path = "build.zig", .pattern = "packages/core/node_modules/.bin/scriptc.cmd" },
|
||||
.{ .path = "build.zig", .pattern = "compile.addArgs(&.{ \"--compiler\", repositoryScriptcBin(b) });" },
|
||||
.{ .path = "src/tooling/verbs.zig", .pattern = "Zig's full summary reports each named build step's duration" },
|
||||
.{ .path = "src/tooling/verbs.zig", .pattern = "fn rebuildPathIgnored" },
|
||||
.{ .path = "src/tooling/verbs.zig", .pattern = "var walker = try root.walkSelectively(allocator);" },
|
||||
.{ .path = "src/tooling/verbs.zig", .pattern = "if (!rebuildPathIgnored(path)) try walker.enter(io, entry);" },
|
||||
.{ .path = "tools/native-sdk/main.zig", .pattern = "--explain-rebuild" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-scriptc-cross-target-plumbing", "Verify core and service archives share the target-aware ScriptC lane, and every direct Windows archive consumer links its runtime import libraries", &.{
|
||||
.{ .path = "build/app.zig", .pattern = ".linux => !cross or target.result.cpu.arch == .x86_64 or target.result.cpu.arch == .aarch64" },
|
||||
.{ .path = "build/app.zig", .pattern = ".windows => !cross or target.result.abi == .gnu" },
|
||||
@@ -761,10 +884,16 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "build.zig", .pattern = "if (!app_build.linuxGlibcSpellingHitsDefaultFloor(target)) return;" },
|
||||
.{ .path = "build/app.zig", .pattern = "scriptcPlatformTriple(b, target),\n // Keep the core and service archives on the same cross compiler" },
|
||||
.{ .path = "build.zig", .pattern = "app_build.scriptcPlatformTriple(b, target),\n \"--zig-exe\",\n b.graph.zig_exe," },
|
||||
.{ .path = "build.zig", .pattern = "compile.addArgs(&.{ \"--host-platform\", host_platform, \"--target-platform\", app_build.scriptcPlatformTriple(b, target), \"--zig-exe\", b.graph.zig_exe });\n addScriptcGlibcFloorTeaching(b, target, &compile.step);" },
|
||||
.{ .path = "build.zig", .pattern = "app_build.scriptcPlatformTriple(b, target),\n \"--zig-exe\",\n b.graph.zig_exe,\n });\n addScriptcGlibcFloorTeaching(b, target, &compile.step);" },
|
||||
.{ .path = "build.zig", .pattern = "compile.addArgs(&.{ \"--host-platform\", host_platform, \"--target-platform\", app_build.scriptcPlatformTriple(b, target), \"--zig-exe\", b.graph.zig_exe });\n app_build.addScriptcAndroidNdk(b, compile, target);\n addScriptcGlibcFloorTeaching(b, target, &compile.step);" },
|
||||
.{ .path = "build.zig", .pattern = "app_build.scriptcPlatformTriple(b, target),\n \"--zig-exe\",\n b.graph.zig_exe,\n });\n app_build.addScriptcAndroidNdk(b, compile, target);\n addScriptcGlibcFloorTeaching(b, target, &compile.step);" },
|
||||
.{ .path = "build.zig", .pattern = "abi_laws_mod.addObjectFile(markup_fixture.archive);\n addScriptcArchiveSystemLibs(abi_laws_mod, target);" },
|
||||
.{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "SCRIPTC_TARGET: args[\"target-platform\"]" },
|
||||
// Both drivers map the build graph's Zig triple onto the compiler's
|
||||
// own mobile spellings and thread the NDK like --zig-exe.
|
||||
.{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "SCRIPTC_TARGET: scriptcTarget" },
|
||||
.{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "SCRIPTC_TARGET: scriptcTarget" },
|
||||
.{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "ANDROID_NDK_ROOT: args[\"android-ndk\"]" },
|
||||
.{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "ANDROID_NDK_ROOT: args[\"android-ndk\"]" },
|
||||
.{ .path = "build/app.zig", .pattern = ".ios => host.os.tag == .macos and target.result.cpu.arch == .aarch64" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-app-test-entry-analysis", "Verify the managed app test step force-analyzes the entry point (UiApp.create's Model-defaults rule must teach at `native test`, not ambush at `native build`)", &.{
|
||||
.{ .path = "build/app.zig", .pattern = "app_analysis.zig" },
|
||||
@@ -772,6 +901,31 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "build/app.zig", .pattern = "test_step.dependOn(&analysis_obj.step);" },
|
||||
.{ .path = "src/runtime/ui_app.zig", .pattern = "has no default value - give every Model field a default" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-owned-runner-window-placement", "Verify owned example runners preserve explicit origins and distinguish successful state restoration", &.{
|
||||
.{ .path = "examples/hello/src/runner.zig", .pattern = ".initial_placement = if (@hasField(@TypeOf(window), \"x\") or @hasField(@TypeOf(window), \"y\")) .explicit else .default" },
|
||||
.{ .path = "examples/hello/src/runner.zig", .pattern = "window.initial_placement = .restored;" },
|
||||
.{ .path = "examples/hello/src/runner.zig", .pattern = "app_info.main_window.initial_placement = .restored;" },
|
||||
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);" },
|
||||
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);" },
|
||||
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);" },
|
||||
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-macos-window-placement-contracts", "Verify both macOS hosts use the primary display and restore persisted content frames without titlebar growth", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window frameRectForContentRect:restoredContentFrame]" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window frameRectForContentRect:restoredContentFrame]" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:restoredWindowFrame display:NO];" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:restoredWindowFrame display:NO];" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "else if (initialPlacement == 1) {\n // Fresh authored dimensions are content size" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "else if (initialPlacement == 1) {\n // AppKit adds titlebar chrome" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "initWithFrame:window.contentView.bounds" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "initWithFrame:window.contentView.bounds" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-bridge-view-selector-helpers", "Verify injected view helpers accept string selectors", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "viewSelectorPayload(options)" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "viewSelectorPayload(options)" },
|
||||
@@ -1406,6 +1560,13 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkTextNavigationNeedsRawKeyEvent(event)" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSEventModifierFlagCommand | NSEventModifierFlagOption" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-widget-accessibility-hierarchy", "Verify AppKit preserves retained-widget accessibility parentage", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.h", .pattern = "uint64_t parent_id;" },
|
||||
.{ .path = "src/platform/macos/root.zig", .pattern = ".parent_id = node.parent_id orelse 0" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "element.accessibilityParent = parent;" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "parent.accessibilityChildren = [childrenByParentId objectForKey:parentId];" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "return self.widgetAccessibilityRootElements ?: @[];" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-appearance-bridge", "Verify AppKit reports system light and dark appearance changes", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "effectiveAppearance" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityDisplayShouldReduceMotion" },
|
||||
@@ -1415,6 +1576,14 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "src/platform/macos/root.zig", .pattern = ".high_contrast = event.high_contrast != 0" },
|
||||
.{ .path = "src/platform/macos/root.zig", .pattern = ".appearance_changed => state.emit" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-tray-segment-source-selection", "Verify both macOS tray hosts keep segmented selection model-owned", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "@property(nonatomic, assign) NSInteger sourceSelectedSegment;" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "if (options[i].selected != 0) control.sourceSelectedSegment = (NSInteger)i;" },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "for (NSInteger index = 0; index < control.segmentCount; index++) {\n [control setSelected:index == sourceSelected forSegment:index];\n }\n self.trayCallback" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "@property(nonatomic, assign) NSInteger sourceSelectedSegment;" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "if (options[i].selected != 0) control.sourceSelectedSegment = (NSInteger)i;" },
|
||||
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "for (NSInteger index = 0; index < control.segmentCount; index++) {\n [control setSelected:index == sourceSelected forSegment:index];\n }\n self.trayCallback" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-builtin-bridge-policy", "Verify bridge policy docs include guarded dialog commands", &.{
|
||||
.{ .path = "docs/src/app/docs/security/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
.{ .path = "docs/src/app/docs/bridge/builtin-commands/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
@@ -1428,6 +1597,7 @@ pub fn build(b: *std.Build) void {
|
||||
addTestStep(b, "test-diagnostics", "Run diagnostics module tests", diagnostics_tests);
|
||||
addTestStep(b, "test-platform-info", "Run platform info module tests", platform_info_tests);
|
||||
addTestStep(b, "test-json", "Run JSON primitive tests", json_tests);
|
||||
addTestStep(b, "test-app-runner-window-placement", "Run app-runner window placement decision tests", app_runner_window_placement_tests);
|
||||
addTestStep(b, "test-canvas", "Run canvas display list tests", canvas_tests);
|
||||
addTestStep(b, "test-desktop", "Run Native SDK framework tests", desktop_tests);
|
||||
for (desktop_test_shard_specs, desktop_test_shards) |spec, shard_tests| {
|
||||
@@ -1661,6 +1831,7 @@ pub fn build(b: *std.Build) void {
|
||||
"examples/calculator/zig-out/package/test-ios-layout/calculator.xcodeproj/xcshareddata/xcschemes/calculator.xcscheme",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Host/uikit_host.m",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Host/native_sdk_app.h",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Host/apple_image_fit.h",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Host/Info.plist",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/AppIcon.png",
|
||||
"examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/Contents.json",
|
||||
@@ -1761,13 +1932,20 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-mobile-widget-abi", "Verify mobile examples use stable widget ABI lookups", &.{
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_viewport_state_t" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIOGROUP = 21" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_scroll" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" },
|
||||
.{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIOGROUP = 21" },
|
||||
.{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" },
|
||||
.{ .path = "examples/mobile-canvas/ios/native_sdk_app.h", .pattern = "native_sdk_app_set_text_measure" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "native_sdk_app_widget_semantics_by_id" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIO" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "childrenByParentId[node.parentId, default: []].append(element)" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "parent.accessibilityContainerType = .semanticGroup" },
|
||||
.{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "parent.isAccessibilityElement = false" },
|
||||
.{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_widget_semantics_by_id" },
|
||||
.{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeScroll(nativeApp" },
|
||||
.{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "WIDGET_ROLE_RADIOGROUP -> \"android.widget.RadioGroup\"" },
|
||||
.{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeWidgetSemanticsByIdFields" },
|
||||
.{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_widget_semantics_by_id" },
|
||||
.{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_scroll" },
|
||||
@@ -2051,6 +2229,52 @@ pub fn build(b: *std.Build) void {
|
||||
native_shell_smoke_run.step.dependOn(&cli_exe.step);
|
||||
native_shell_smoke_step.dependOn(&native_shell_smoke_run.step);
|
||||
|
||||
const menu_bar_smoke_step = b.step("test-menu-bar-smoke", "Run zero-config TypeScript app-menu automation smoke test");
|
||||
const menu_bar_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" });
|
||||
menu_bar_smoke_build.setCwd(b.path("examples/menu-bar"));
|
||||
const menu_bar_smoke_run = b.addSystemCommand(&.{
|
||||
"sh", "-c",
|
||||
\\set -eu
|
||||
\\cd examples/menu-bar
|
||||
\\app="zig-out/bin/menu-bar"
|
||||
\\cli="$1"
|
||||
\\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac
|
||||
\\automation_dir=".zig-cache/native-sdk-automation"
|
||||
\\mkdir -p "$automation_dir"
|
||||
\\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt
|
||||
\\"$app" > .zig-cache/native-sdk-menu-bar-smoke.log 2>&1 &
|
||||
\\pid=$!
|
||||
\\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-menu-bar-smoke.log) ----" >&2; cat .zig-cache/native-sdk-menu-bar-smoke.log >&2 2>/dev/null || true; fi' EXIT
|
||||
\\ready="$("$cli" automate wait 2>&1)"
|
||||
\\case "$ready" in *"ready=true"*) ;; *) echo "menu-bar automation snapshot was not ready" >&2; exit 1 ;; esac
|
||||
\\before="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
|
||||
\\case "$before" in *'command id="player.next" title="Next Track" enabled=true checked=false'*) ;; *) echo "app.zon command catalog was not loaded by the zero-config runner" >&2; exit 1 ;; esac
|
||||
\\case "$before" in *'app-menu title="Player" items=6'*) ;; *) echo "app.zon Player menu was not loaded by the zero-config runner" >&2; exit 1 ;; esac
|
||||
\\case "$before" in *'app-menu-item label="Next Track" command="player.next" enabled=true checked=false key="n" modifiers=(primary=true,command=false,control=false,option=false,shift=false)'*) ;; *) echo "app.zon player.next menu item was not loaded by the zero-config runner" >&2; exit 1 ;; esac
|
||||
\\case "$before" in
|
||||
\\ *'Ambient Coast'*) expected='Night Drive' ;;
|
||||
\\ *'Night Drive'*) expected='Paper Planes' ;;
|
||||
\\ *'Paper Planes'*) expected='Ambient Coast' ;;
|
||||
\\ *) echo "menu-bar snapshot did not expose the current TypeScript model track" >&2; exit 1 ;;
|
||||
\\esac
|
||||
\\"$cli" automate menu-command player.next >/dev/null 2>&1
|
||||
\\attempts=0
|
||||
\\while [ "$attempts" -lt 50 ]; do
|
||||
\\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
|
||||
\\ case "$snapshot" in *"$expected"*) break ;; esac
|
||||
\\ attempts=$((attempts + 1))
|
||||
\\ sleep 0.1
|
||||
\\done
|
||||
\\case "$snapshot" in *"$expected"*) ;; *) echo "app.zon menu command did not reach the zero-config TypeScript commandMsg mapper" >&2; exit 1 ;; esac
|
||||
\\echo "menu-bar smoke ok"
|
||||
,
|
||||
"sh",
|
||||
});
|
||||
menu_bar_smoke_run.addFileArg(cli_exe.getEmittedBin());
|
||||
menu_bar_smoke_run.step.dependOn(&menu_bar_smoke_build.step);
|
||||
menu_bar_smoke_run.step.dependOn(&cli_exe.step);
|
||||
menu_bar_smoke_step.dependOn(&menu_bar_smoke_run.step);
|
||||
|
||||
const gpu_surface_smoke_step = b.step("test-gpu-surface-smoke", "Run macOS GPU surface automation smoke test");
|
||||
// The GPU smoke apps are managed examples (no build.zig of their own),
|
||||
// so their binaries come from the CLI verb. -Doptimize=Debug keeps the
|
||||
@@ -2541,7 +2765,7 @@ pub fn build(b: *std.Build) void {
|
||||
\\case "$ready_snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "component gallery GPU surface was not ready" >&2; exit 1 ;; esac
|
||||
\\case "$ready_snapshot" in *'view @w1/main kind=webview'*) echo "component gallery created an implicit WebView" >&2; exit 1 ;; *) ;; esac
|
||||
\\"$cli" automate assert 'role=tree name="Components"' 'role=treeitem name="Components".*state=\[expanded\]' 'role=treeitem name="Accordion".*state=\[selected\]' 'role=group name="Details".*state=\[selected,expanded\]' 'name="Accordion details are visible. The model owns this expanded state."'
|
||||
\\"$cli" automate assert 'role=group name="Theme"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"'
|
||||
\\"$cli" automate assert 'role=group name="Theme pack"' 'role=group name="Color scheme"' 'role=group name="Theme accent"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"' 'role=button name="System".*state=\[selected\]' 'role=button name="Pink"' 'role=button name="Teal"'
|
||||
\\"$cli" automate screenshot components-canvas >/dev/null 2>&1
|
||||
\\cp "$automation_dir/screenshot-components-canvas.png" "$automation_dir/screenshot-components-house.png"
|
||||
\\rm -f "$automation_dir/screenshot-components-canvas.png"
|
||||
@@ -2992,6 +3216,10 @@ const TsCoreE2eArtifacts = struct {
|
||||
/// paths, and builds keep working with node_modules deleted.
|
||||
scaffold_ide: *std.Build.Step.Compile,
|
||||
ai_chat: *std.Build.Step.Compile,
|
||||
/// The complete services loop over examples/service-feed-reader: a real
|
||||
/// loopback Cmd.fetch, the generated typed client into a real service
|
||||
/// child, the shipping markup, and record→replay without either.
|
||||
feed_reader: *std.Build.Step.Compile,
|
||||
/// The phase-1 service seam: a real compiled core plus a real plain-scriptc
|
||||
/// service executable driven through the out-of-process carrier.
|
||||
services: *std.Build.Step.Compile,
|
||||
@@ -3001,6 +3229,13 @@ const TsCoreE2eArtifacts = struct {
|
||||
/// cancellation/deadlines, trap isolation, streaming, replay). Null when
|
||||
/// the target cannot carry the archive (cross or non-desktop builds).
|
||||
services_pool: ?*std.Build.Step.Compile,
|
||||
/// The mobile execution battery: the pool fixture's core and service
|
||||
/// archives linked into one static library exporting `nsme_run`, so
|
||||
/// the platform toolchain links a runnable harness for the simulator
|
||||
/// or emulator (scripts/mobile-e2e.sh). Present only for a mobile
|
||||
/// -Dtarget; the Android archive is flattened to plain objects (Zig's
|
||||
/// ELF static-library emission nests archive inputs).
|
||||
mobile_battery: ?std.Build.LazyPath,
|
||||
/// The service-host carrier benchmark: a bytes-echo service compiled
|
||||
/// through the same lane, driven directly through the production
|
||||
/// carrier bindings of BOTH carriers (cold start, round-trip latency,
|
||||
@@ -3016,6 +3251,9 @@ const TsCoreE2eArtifacts = struct {
|
||||
/// (boot fence, collect invariant, deterministic re-init, channel
|
||||
/// envelopes, integer classes).
|
||||
external_core_abi_laws: *std.Build.Step.Compile,
|
||||
/// The mixed-return ABI regression: a real compiled core whose update
|
||||
/// returns both bare Model and [Model, Cmd] through the generated facade.
|
||||
mixed_return_abi_laws: *std.Build.Step.Compile,
|
||||
/// Per-fixture contract artifacts for an external core toolchain:
|
||||
/// the effective contract sidecar and its TypeScript facade/profile
|
||||
/// projections, installed by the stage-core-contracts step.
|
||||
@@ -3049,7 +3287,7 @@ fn tsCoreE2eArtifact(
|
||||
if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER") == null) {
|
||||
b.build_root.handle.access(
|
||||
b.graph.io,
|
||||
"packages/core/node_modules/scriptc/dist/main.js",
|
||||
repositoryScriptcBin(b),
|
||||
.{},
|
||||
) catch return null;
|
||||
}
|
||||
@@ -3110,6 +3348,13 @@ fn tsCoreE2eArtifact(
|
||||
.name = "markup_core",
|
||||
});
|
||||
const markup_fixture_mod = markup_fixture.module;
|
||||
const mixed_return_src = b.addWriteFiles();
|
||||
_ = mixed_return_src.addCopyFile(b.path("tests/ts-core/mixed_return_fixture.ts"), "mixed_return_fixture.ts");
|
||||
const mixed_return_fixture = externalCoreFixtureModule(b, target, optimize, node, corewire_exe, .{
|
||||
.entry = "tests/ts-core/mixed_return_fixture.ts",
|
||||
.src_dir = mixed_return_src.getDirectory(),
|
||||
.name = "mixed_return_core",
|
||||
});
|
||||
|
||||
const e2e_mod = module(b, target, optimize, "tests/ts-core/host_e2e_tests.zig");
|
||||
e2e_mod.addImport("native_sdk", desktop_mod);
|
||||
@@ -3174,6 +3419,8 @@ fn tsCoreE2eArtifact(
|
||||
const monitor_stage = b.addWriteFiles();
|
||||
const monitor_root = monitor_stage.addCopyFile(b.path("tests/ts-core/system_monitor_e2e_tests.zig"), "system_monitor_e2e_tests.zig");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor-ts/src/app.native"), "app.native");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor-ts/src/windows/settings.native"), "settings.native");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor-ts/src/windows/components/sampling.native"), "components/sampling.native");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor/src/fixtures/sysctl.txt"), "fixtures/sysctl.txt");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor/src/fixtures/ps.txt"), "fixtures/ps.txt");
|
||||
_ = monitor_stage.addCopyFile(b.path("examples/system-monitor/src/fixtures/vm_stat.txt"), "fixtures/vm_stat.txt");
|
||||
@@ -3211,6 +3458,42 @@ fn tsCoreE2eArtifact(
|
||||
ai_chat_mod.addImport("native_sdk", desktop_mod);
|
||||
ai_chat_mod.addImport("ts_ai_chat_core", ai_chat_core_mod);
|
||||
|
||||
// The service-feed-reader example's core, service child, and shipping
|
||||
// markup, tested as one app: a real buffered Cmd.fetch against a
|
||||
// loopback fixture, the generated typed client into the real child,
|
||||
// and record→replay with neither the service nor the network present.
|
||||
const feed_reader_fixture = externalCoreFixtureModule(b, target, optimize, node, corewire_exe, .{
|
||||
.entry = "examples/service-feed-reader/src/core.ts",
|
||||
.src_dir = b.path("examples/service-feed-reader/src"),
|
||||
.name = "feed_reader_core",
|
||||
.emit_services = true,
|
||||
});
|
||||
const feed_reader_service = externalServiceFixture(
|
||||
b,
|
||||
target,
|
||||
optimize,
|
||||
node,
|
||||
corewire_exe,
|
||||
b.path("examples/service-feed-reader/src"),
|
||||
feed_reader_fixture.services_contract.?,
|
||||
"feed_reader_services",
|
||||
);
|
||||
const feed_reader_stage = b.addWriteFiles();
|
||||
const feed_reader_root = feed_reader_stage.addCopyFile(b.path("tests/ts-services/feed_reader_e2e_tests.zig"), "feed_reader_e2e_tests.zig");
|
||||
_ = feed_reader_stage.addCopyFile(b.path("examples/service-feed-reader/src/app.native"), "app.native");
|
||||
_ = feed_reader_stage.addCopyFile(b.path("examples/service-feed-reader/fixtures/feed.xml"), "fixture_feed.xml");
|
||||
const feed_reader_mod = b.createModule(.{
|
||||
.root_source_file = feed_reader_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
feed_reader_mod.addImport("native_sdk", desktop_mod);
|
||||
feed_reader_mod.addImport("ts_feed_reader_core", feed_reader_fixture.module);
|
||||
feed_reader_mod.addImport("ts_feed_reader_registry", feed_reader_service.registry);
|
||||
const feed_reader_options = b.addOptions();
|
||||
feed_reader_options.addOptionPath("service_executable", feed_reader_service.executable);
|
||||
feed_reader_mod.addOptions("ts_feed_reader_options", feed_reader_options);
|
||||
|
||||
// Phase-1 TypeScript services, end to end: the frontend emits BOTH
|
||||
// sidecars from one checked two-class program; corewire derives the
|
||||
// registry/host only from the service sidecar; plain scriptc compiles the
|
||||
@@ -3257,6 +3540,46 @@ fn tsCoreE2eArtifact(
|
||||
break :pool pool_mod;
|
||||
} else null;
|
||||
|
||||
// The mobile execution battery: a static library over the same pool
|
||||
// fixture, linked into a runnable harness by the platform toolchain —
|
||||
// the same link pattern embedding apps use for the embed library. The
|
||||
// Android objects are PIC like the embed library's (the harness there
|
||||
// is a bionic executable; a future .so embedding needs it regardless).
|
||||
const battery_target_is_mobile = target.result.os.tag == .ios or target.result.abi.isAndroid();
|
||||
const mobile_battery: ?std.Build.LazyPath = if (battery_target_is_mobile and service_host_fixture.archive != null) battery: {
|
||||
const battery_mod = b.createModule(.{
|
||||
.root_source_file = b.path("tests/ts-services/mobile_e2e_battery.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.pic = if (target.result.abi.isAndroid()) true else null,
|
||||
});
|
||||
battery_mod.addImport("native_sdk", desktop_mod);
|
||||
battery_mod.addImport("ts_services_core", services_fixture.module);
|
||||
battery_mod.addImport("ts_services_registry", service_host_fixture.registry);
|
||||
battery_mod.link_libc = true;
|
||||
battery_mod.addObjectFile(service_host_fixture.archive.?);
|
||||
const battery_lib = b.addLibrary(.{
|
||||
.linkage = .static,
|
||||
.name = "ts-mobile-e2e",
|
||||
.root_module = battery_mod,
|
||||
.use_llvm = @import("build/app.zig").useLlvmWorkaround(target),
|
||||
});
|
||||
if (target.result.abi.isAndroid()) {
|
||||
// Flatten the nested TypeScript archives (Zig's ELF static-lib
|
||||
// emission stores .a inputs as members) so the NDK link consumes
|
||||
// plain objects — the same normalization the app lane applies.
|
||||
const merge = b.addSystemCommand(&.{node});
|
||||
merge.addFileArg(b.path("packages/core/scripts/merge_static_archives.mjs"));
|
||||
merge.addArgs(&.{ "--zig", b.graph.zig_exe, "--format", "gnu" });
|
||||
merge.addArg("--out");
|
||||
const merged = merge.addOutputFileArg("libts-mobile-e2e.a");
|
||||
merge.addArg("--in");
|
||||
merge.addFileArg(battery_lib.getEmittedBin());
|
||||
break :battery merged;
|
||||
}
|
||||
break :battery battery_lib.getEmittedBin();
|
||||
} else null;
|
||||
|
||||
// Service-host carrier benchmark: the same production service lane
|
||||
// (frontend contract -> corewire host/registry -> exact-pinned
|
||||
// plain-scriptc executable) over a bytes-echo operation that returns
|
||||
@@ -3309,6 +3632,9 @@ fn tsCoreE2eArtifact(
|
||||
// against (the same module the generated shims stage).
|
||||
conformance_mod.addImport("corewire_rt", module(b, target, optimize, "tools/corewire/shim_rt.zig"));
|
||||
conformance_mod.addImport("shim_markup_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/markup_fixture.contract.json")));
|
||||
// Compile-cost guard: this generated mirror carries 160 realistically
|
||||
// named Msg arms and must need no quota setting in app or test code.
|
||||
conformance_mod.addImport("shim_wide_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/wide_msg_fixture.contract.json")));
|
||||
// The integer-class fixture: a hand-written sidecar attesting mixed
|
||||
// i64/u64 slot classes, so the suite drives boundary and full-range
|
||||
// integer values through a generated mirror's decode paths.
|
||||
@@ -3369,6 +3695,9 @@ fn tsCoreE2eArtifact(
|
||||
abi_laws_mod.addObjectFile(markup_fixture.archive);
|
||||
addScriptcArchiveSystemLibs(abi_laws_mod, target);
|
||||
|
||||
const mixed_return_abi_mod = module(b, target, optimize, "tests/sidecar/mixed_return_abi_tests.zig");
|
||||
mixed_return_abi_mod.addImport("mixed_return_core", mixed_return_fixture.module);
|
||||
|
||||
return .{
|
||||
.host = filteredTestArtifact(b, e2e_mod, "ts-core-e2e-tests", &.{}),
|
||||
.persist = filteredTestArtifact(b, persist_mod, "ts-persist-e2e-tests", &.{}),
|
||||
@@ -3378,11 +3707,14 @@ fn tsCoreE2eArtifact(
|
||||
.system_monitor = filteredTestArtifact(b, monitor_mod, "ts-system-monitor-e2e-tests", &.{}),
|
||||
.scaffold_ide = filteredTestArtifact(b, scaffold_ide_mod, "ts-scaffold-ide-e2e-tests", &.{}),
|
||||
.ai_chat = filteredTestArtifact(b, ai_chat_mod, "ts-ai-chat-e2e-tests", &.{}),
|
||||
.feed_reader = filteredTestArtifact(b, feed_reader_mod, "ts-feed-reader-e2e-tests", &.{}),
|
||||
.services = filteredTestArtifact(b, services_e2e_mod, "ts-services-e2e-tests", &.{}),
|
||||
.services_pool = if (services_pool_mod) |pool_mod| filteredTestArtifact(b, pool_mod, "ts-services-pool-e2e-tests", &.{}) else null,
|
||||
.mobile_battery = mobile_battery,
|
||||
.service_host_bench = service_bench_exe,
|
||||
.sidecar_conformance = filteredTestArtifact(b, conformance_mod, "sidecar-conformance-tests", &.{}),
|
||||
.external_core_abi_laws = filteredTestArtifact(b, abi_laws_mod, "external-core-abi-tests", &.{}),
|
||||
.mixed_return_abi_laws = filteredTestArtifact(b, mixed_return_abi_mod, "mixed-return-abi-tests", &.{}),
|
||||
.core_contracts = core_contracts.toOwnedSlice(b.allocator) catch @panic("OOM"),
|
||||
};
|
||||
}
|
||||
@@ -3479,27 +3811,36 @@ fn externalServiceFixture(
|
||||
|
||||
const compile = b.addSystemCommand(&.{node});
|
||||
compile.addFileArg(b.path("packages/core/scripts/run_external_service_compiler.mjs"));
|
||||
compile.addFileInput(b.path("packages/core/scripts/compiler_command.mjs"));
|
||||
compile.addArg("--stage");
|
||||
compile.addDirectoryArg(stage_dir);
|
||||
compile.addArg("--manifest");
|
||||
compile.addFileArg(b.path("packages/core/package.json"));
|
||||
compile.addArg("--contract");
|
||||
compile.addFileArg(contract);
|
||||
compile.addArg("--out-exe");
|
||||
const suffix = if (target.result.os.tag == .windows) ".exe" else "";
|
||||
const executable = compile.addOutputFileArg(b.fmt("{s}{s}", .{ name, suffix }));
|
||||
// Mobile targets are archive-only (no child process exists there): the
|
||||
// driver refuses --out-exe, so the child-carrier batteries get a stub
|
||||
// path they never execute for those targets.
|
||||
const mobile_target = target.result.os.tag == .ios or target.result.abi.isAndroid();
|
||||
const executable: std.Build.LazyPath = if (mobile_target)
|
||||
b.addWriteFiles().add(b.fmt("{s}-unavailable-on-mobile", .{name}), "")
|
||||
else exe: {
|
||||
compile.addArg("--out-exe");
|
||||
const suffix = if (target.result.os.tag == .windows) ".exe" else "";
|
||||
break :exe compile.addOutputFileArg(b.fmt("{s}{s}", .{ name, suffix }));
|
||||
};
|
||||
const archive: ?std.Build.LazyPath = if (emit_archive) archive: {
|
||||
compile.addArg("--out-archive");
|
||||
break :archive compile.addOutputFileArg(b.fmt("lib{s}.a", .{name}));
|
||||
} else null;
|
||||
const host_platform = b.fmt("{t}-{t}-{t}", .{ b.graph.host.result.cpu.arch, b.graph.host.result.os.tag, b.graph.host.result.abi });
|
||||
compile.addArgs(&.{ "--host-platform", host_platform, "--target-platform", app_build.scriptcPlatformTriple(b, target), "--zig-exe", b.graph.zig_exe });
|
||||
app_build.addScriptcAndroidNdk(b, compile, target);
|
||||
addScriptcGlibcFloorTeaching(b, target, &compile.step);
|
||||
if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER")) |override| {
|
||||
compile.addArgs(&.{ "--compiler", override });
|
||||
} else {
|
||||
compile.addArg("--compiler-js");
|
||||
compile.addFileArg(b.path("packages/core/node_modules/scriptc/dist/main.js"));
|
||||
compile.addArgs(&.{ "--compiler", repositoryScriptcBin(b) });
|
||||
}
|
||||
|
||||
return .{
|
||||
@@ -3646,12 +3987,14 @@ fn externalCoreFixtureModule(
|
||||
check.addFileArg(b.path("packages/core/src/cli.ts"));
|
||||
check.addFileArg(b.path(spec.entry));
|
||||
check.addArg("--contract");
|
||||
const contract = check.addOutputFileArg("core.contract.json");
|
||||
const contract_raw = check.addOutputFileArg("core.contract.json");
|
||||
const contract = app_build.stabilizeGeneratedFile(b, contract_raw, "core.contract.json", false);
|
||||
check.addArg("--contract-entry");
|
||||
check.addArg(spec.entry);
|
||||
const services_contract: ?std.Build.LazyPath = if (spec.emit_services) services: {
|
||||
check.addArg("--services-contract");
|
||||
break :services check.addOutputFileArg("services.contract.json");
|
||||
const raw = check.addOutputFileArg("services.contract.json");
|
||||
break :services app_build.stabilizeGeneratedFile(b, raw, "services.contract.json", false);
|
||||
} else null;
|
||||
for (spec.service_packages) |package_entry| check.addArgs(&.{ "--service-package", package_entry });
|
||||
const services_client: ?std.Build.LazyPath = if (services_contract) |service_contract| client: {
|
||||
@@ -3659,12 +4002,14 @@ fn externalCoreFixtureModule(
|
||||
service_project.addArg("--services-sidecar");
|
||||
service_project.addFileArg(service_contract);
|
||||
service_project.addArg("--service-client");
|
||||
break :client service_project.addOutputFileArg("services.gen.ts");
|
||||
const client_raw = service_project.addOutputFileArg("services.gen.ts");
|
||||
break :client app_build.stabilizeGeneratedFile(b, client_raw, "services.gen.ts", false);
|
||||
} else null;
|
||||
if (spec.persist_capability) check.addArgs(&.{ "--capability", "persist" });
|
||||
if (spec.store_capability) check.addArgs(&.{ "--capability", "store" });
|
||||
if (spec.relational_capability) check.addArgs(&.{ "--capability", "sqlite" });
|
||||
if (spec.credentials_capability) check.addArgs(&.{ "--capability", "credentials", "--permission", "credentials" });
|
||||
if (std.mem.eql(u8, spec.entry, "tests/ts-core/fixture.ts")) check.addArgs(&.{ "--permission", "filesystem" });
|
||||
tsCoreAddDirInputs(b, check, "packages/core/sdk");
|
||||
tsCoreAddDirInputs(b, check, std.fs.path.dirname(spec.entry) orelse ".");
|
||||
const frontend_sources = [_][]const u8{
|
||||
@@ -3711,6 +4056,8 @@ fn externalCoreFixtureModule(
|
||||
stage_run.addArg("--services-client");
|
||||
stage_run.addFileArg(client);
|
||||
}
|
||||
tsCoreAddCoreDirInputs(b, stage_run, std.fs.path.dirname(spec.entry) orelse ".");
|
||||
app_build.addStagedCoreSdkInputs(b, b, stage_run);
|
||||
stage_run.addArg("--out");
|
||||
const stage_dir = stage_run.addOutputDirectoryArg("stage");
|
||||
|
||||
@@ -3718,6 +4065,7 @@ fn externalCoreFixtureModule(
|
||||
// pin, archive normalized, the co-emitted sidecar captured.
|
||||
const compile = b.addSystemCommand(&.{node});
|
||||
compile.addFileArg(b.path("packages/core/scripts/run_external_core_compiler.mjs"));
|
||||
compile.addFileInput(b.path("packages/core/scripts/compiler_command.mjs"));
|
||||
compile.addArg("--stage");
|
||||
compile.addDirectoryArg(stage_dir);
|
||||
compile.addArgs(&.{ "--name", spec.name });
|
||||
@@ -3737,14 +4085,14 @@ fn externalCoreFixtureModule(
|
||||
"--zig-exe",
|
||||
b.graph.zig_exe,
|
||||
});
|
||||
app_build.addScriptcAndroidNdk(b, compile, target);
|
||||
addScriptcGlibcFloorTeaching(b, target, &compile.step);
|
||||
if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER")) |override| {
|
||||
// The development override: point at any toolchain command; the
|
||||
// driver still refuses a release other than the SDK's pin.
|
||||
compile.addArgs(&.{ "--compiler", override });
|
||||
} else {
|
||||
compile.addArg("--compiler-js");
|
||||
compile.addFileArg(b.path("packages/core/node_modules/scriptc/dist/main.js"));
|
||||
compile.addArgs(&.{ "--compiler", repositoryScriptcBin(b) });
|
||||
}
|
||||
|
||||
// The mirror, generated from the archive's OWN co-emitted contract,
|
||||
@@ -3792,6 +4140,24 @@ fn tsCoreAddDirInputs(b: *std.Build, transpile: *std.Build.Step.Run, dir_path: [
|
||||
}
|
||||
}
|
||||
|
||||
/// The source set stage_external_core.mjs copies: every ordinary `.ts` file,
|
||||
/// excluding the independent services compiler class and declaration files.
|
||||
fn tsCoreAddCoreDirInputs(b: *std.Build, stage: *std.Build.Step.Run, dir_path: []const u8) void {
|
||||
var dir = b.build_root.handle.openDir(b.graph.io, dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close(b.graph.io);
|
||||
var walker = dir.walk(b.allocator) catch return;
|
||||
defer walker.deinit();
|
||||
while (walker.next(b.graph.io) catch null) |entry| {
|
||||
if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".ts") or std.mem.endsWith(u8, entry.basename, ".d.ts")) continue;
|
||||
const normalized = b.dupe(entry.path);
|
||||
for (normalized) |*char| if (char.* == '\\') {
|
||||
char.* = '/';
|
||||
};
|
||||
if (std.mem.startsWith(u8, normalized, "services/")) continue;
|
||||
stage.addFileInput(b.path(b.fmt("{s}/{s}", .{ dir_path, entry.path })));
|
||||
}
|
||||
}
|
||||
|
||||
fn filteredTestArtifact(b: *std.Build, mod: *std.Build.Module, name: []const u8, filters: []const []const u8) *std.Build.Step.Compile {
|
||||
// use_llvm: Zig 0.16.0's self-hosted x86_64 backend miscompiles the
|
||||
// SysV C ABI for f32-heavy signatures (native_sdk_app_viewport); see
|
||||
|
||||
@@ -12,21 +12,16 @@
|
||||
// node's ancestor node_modules walk). Node's own type stripping is never
|
||||
// relied on: it refuses node_modules-resident .ts by design
|
||||
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), and outside node_modules
|
||||
// it only became DEFAULT in node 22.18 — so a checkout target on
|
||||
// 22.15-22.17 would die with ERR_UNKNOWN_FILE_EXTENSION if the hook let it
|
||||
// "fall through". Hooking everything makes the 22.15 floor true for both
|
||||
// layouts. Then the runner imports the requested module with argv
|
||||
// it only became DEFAULT in node 22.18. Hooking everything keeps both layouts
|
||||
// identical. scriptc 0.0.33 requires Node 24 for its published compile-cache
|
||||
// bootstrap, so this shared runner enforces the same floor before importing
|
||||
// any frontend module. Then it imports the requested module with argv
|
||||
// respliced so the target sees its usual shape (its own path at argv[1],
|
||||
// its arguments from argv[2]).
|
||||
//
|
||||
// On node builds without module.registerHooks (pre-22.15, or 23.0-23.4 —
|
||||
// the hook landed in 22.15 and 23.5, so ">=22.15" alone is not the
|
||||
// capability line) NO .ts target can run — node_modules-resident
|
||||
// stripping is refused by design and default stripping outside
|
||||
// node_modules only landed in 22.18, which is above this tier anyway —
|
||||
// so the runner fails fast with one teaching line (upgrade to Node.js
|
||||
// 22.15+, on the 23 line 23.5+) before importing it, instead of
|
||||
// surfacing node's raw extension/stripping error.
|
||||
// A Node 24 build without module.registerHooks is incomplete for this tier,
|
||||
// so the runner gives the same Node 24 teaching instead of surfacing a raw
|
||||
// extension/stripping error.
|
||||
|
||||
import module, { createRequire } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -39,19 +34,21 @@ if (!target) {
|
||||
process.exit(2);
|
||||
}
|
||||
const targetPath = resolve(target);
|
||||
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
||||
if (!Number.isInteger(nodeMajor) || nodeMajor < 24) {
|
||||
console.error(`TypeScript apps need Node.js 24+; you're running ${process.version} - upgrade node and re-run.`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Drop the runner from argv so the target module parses its own argv
|
||||
// exactly as when node runs it directly.
|
||||
process.argv.splice(1, 1);
|
||||
|
||||
if (typeof module.registerHooks !== 'function') {
|
||||
// No load hooks on this node (pre-22.15, or a 23.0-23.4 build). Every
|
||||
// .ts target needs the hook — node_modules-resident stripping is
|
||||
// refused by design, and native default stripping outside node_modules
|
||||
// is 22.18+ — so any .ts target would fail deep inside node with a raw
|
||||
// extension/stripping error. Teach the fix instead.
|
||||
// Every .ts target needs the hook. A supported Node build without it is
|
||||
// incomplete for this tier, so teach the supported installation instead.
|
||||
if (targetPath.endsWith('.ts')) {
|
||||
console.error(
|
||||
`TypeScript apps need Node.js 22.15+ (on the 23 line: 23.5+); you're running ${process.version} - upgrade node and re-run.`,
|
||||
`TypeScript apps need Node.js 24+ with module.registerHooks; you're running ${process.version} - upgrade or reinstall node and re-run.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ next-env.d.ts
|
||||
.next-gate/
|
||||
.next-agent/
|
||||
.next-check/
|
||||
.next-final/
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "pnpm typecheck && pnpm build && node scripts/check-doc-routes.mjs && node scripts/check-code-toggle.mjs && node scripts/check-wasm-preview.mjs"
|
||||
"check": "node scripts/check-app-schema.mjs && pnpm typecheck && pnpm build && node scripts/check-doc-routes.mjs && node scripts/check-code-toggle.mjs && node scripts/check-wasm-preview.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/loader": "^3",
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 1010 B |
|
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://schema.native-sdk.dev/app/v1.json",
|
||||
"title": "Native SDK app manifest",
|
||||
"description": "Complete app.json manifest for a Native SDK application. app.zon remains supported as a legacy alternative.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "version"],
|
||||
"properties": {
|
||||
"$schema": { "type": "string", "format": "uri-reference" },
|
||||
"id": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Reverse-DNS application identifier." },
|
||||
"name": { "type": "string", "minLength": 1, "description": "Short machine-readable app name." },
|
||||
"display_name": { "type": "string", "minLength": 1, "description": "Human-readable app name." },
|
||||
"description": { "type": "string", "minLength": 1, "maxLength": 256 },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"icons": { "$ref": "#/$defs/stringArray" },
|
||||
"platforms": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "enum": ["macos", "linux", "windows", "ios", "android", "web"] }
|
||||
},
|
||||
"permissions": { "$ref": "#/$defs/stringArray" },
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"enum": [
|
||||
"native_module", "webview", "js_bridge", "native_views", "gpu_surfaces",
|
||||
"menus", "shortcuts", "tray", "filesystem", "network", "notifications",
|
||||
"dialog", "clipboard", "credentials", "persist", "store", "sqlite",
|
||||
"open_url", "reveal_path", "recent_documents", "file_drops",
|
||||
"app_activation_events", "file_associations", "url_schemes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dock_visible": { "type": "boolean", "default": true },
|
||||
"persist": { "$ref": "#/$defs/persist" },
|
||||
"images": { "$ref": "#/$defs/images" },
|
||||
"service_packages": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/servicePackage" }
|
||||
},
|
||||
"service_carrier": { "enum": ["auto", "in_process", "child"], "default": "auto" },
|
||||
"service_pool_size": { "type": "integer", "minimum": 1, "maximum": 16 },
|
||||
"bridge": { "$ref": "#/$defs/bridge" },
|
||||
"web_engine": { "enum": ["system", "chromium"], "default": "system" },
|
||||
"webview_layer": { "enum": ["auto", "include", "exclude"], "default": "auto" },
|
||||
"core_compiler": { "const": "external", "default": "external" },
|
||||
"theme": { "enum": ["house", "geist"] },
|
||||
"theme_accent": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
|
||||
"cef": { "$ref": "#/$defs/cef" },
|
||||
"frontend": { "$ref": "#/$defs/frontend" },
|
||||
"security": { "$ref": "#/$defs/security" },
|
||||
"assets": { "$ref": "#/$defs/assets" },
|
||||
"windows": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/window" }
|
||||
},
|
||||
"shell": { "$ref": "#/$defs/shell" },
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/command" }
|
||||
},
|
||||
"menus": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/menu" }
|
||||
},
|
||||
"shortcuts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shortcut" }
|
||||
},
|
||||
"file_associations": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fileAssociation" }
|
||||
},
|
||||
"url_schemes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/urlScheme" }
|
||||
},
|
||||
"dmg": { "$ref": "#/$defs/dmg" }
|
||||
},
|
||||
"$defs": {
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"position": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["x", "y"],
|
||||
"properties": {
|
||||
"x": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
||||
"y": { "type": "integer", "minimum": 0, "maximum": 65535 }
|
||||
}
|
||||
},
|
||||
"persist": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "restore"],
|
||||
"properties": {
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"debounce_ms": { "type": "integer", "minimum": 0, "maximum": 60000, "default": 500 },
|
||||
"restore": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ok", "none", "err"],
|
||||
"properties": {
|
||||
"ok": { "type": "string", "minLength": 1 },
|
||||
"none": { "type": "string", "minLength": 1 },
|
||||
"err": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"max_image_pixel_bytes": { "type": "integer", "minimum": 1048576, "maximum": 8388608, "default": 1048576 }
|
||||
}
|
||||
},
|
||||
"servicePackage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "version", "content_hash"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
||||
"content_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"permissions": { "$ref": "#/$defs/stringArray" },
|
||||
"origins": { "$ref": "#/$defs/stringArray" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dir": { "type": "string", "default": "third_party/cef/macos" },
|
||||
"auto_install": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dist": { "type": "string", "default": "dist" },
|
||||
"entry": { "type": "string", "default": "index.html" },
|
||||
"spa_fallback": { "type": "boolean", "default": true },
|
||||
"dev": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"command": { "$ref": "#/$defs/stringArray" },
|
||||
"ready_path": { "type": "string", "default": "/" },
|
||||
"timeout_ms": { "type": "integer", "minimum": 1, "maximum": 4294967295, "default": 30000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_origins": { "$ref": "#/$defs/stringArray" },
|
||||
"external_links": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"action": { "enum": ["deny", "open_system_browser"], "default": "deny" },
|
||||
"allowed_urls": { "$ref": "#/$defs/stringArray" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "path"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "minimum": 1 },
|
||||
"path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"windowBase": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": { "type": "string", "default": "main" },
|
||||
"title": { "type": "string" },
|
||||
"width": { "type": "number", "exclusiveMinimum": 0, "default": 720 },
|
||||
"height": { "type": "number", "exclusiveMinimum": 0, "default": 480 },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"resizable": { "type": "boolean", "default": true },
|
||||
"restore_state": { "type": "boolean", "default": true },
|
||||
"titlebar": { "enum": ["standard", "hidden_inset", "hidden_inset_tall", "chromeless"], "default": "standard" },
|
||||
"transparent": { "type": "boolean", "default": false },
|
||||
"always_on_top": { "type": "boolean", "default": false },
|
||||
"click_through": { "type": "boolean", "default": false },
|
||||
"activate_on_show": { "type": "boolean", "default": true },
|
||||
"initially_hidden": { "type": "boolean", "default": false },
|
||||
"allows_fullscreen": { "type": "boolean", "default": true },
|
||||
"min_width": { "type": "number", "minimum": 0, "default": 0 },
|
||||
"min_height": { "type": "number", "minimum": 0, "default": 0 },
|
||||
"close_policy": { "enum": ["quit", "hide"], "default": "quit" }
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"allOf": [{ "$ref": "#/$defs/windowBase" }],
|
||||
"unevaluatedProperties": false
|
||||
},
|
||||
"shell": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"windows": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shellWindow" }
|
||||
},
|
||||
"chrome": { "$ref": "#/$defs/shellChrome" }
|
||||
}
|
||||
},
|
||||
"shellWindow": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/$defs/windowBase" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"restore_policy": { "enum": ["clamp_to_visible_screen", "center_on_primary"], "default": "clamp_to_visible_screen" },
|
||||
"views": { "type": "array", "items": { "$ref": "#/$defs/shellView" } }
|
||||
}
|
||||
}
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
},
|
||||
"shellChrome": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/shellTab" }
|
||||
},
|
||||
"primary_action": { "$ref": "#/$defs/shellTab" }
|
||||
}
|
||||
},
|
||||
"shellTab": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "label"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"label": { "type": "string", "minLength": 1 },
|
||||
"icon": { "type": "string", "default": "" }
|
||||
}
|
||||
},
|
||||
"shellView": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["label", "kind"],
|
||||
"properties": {
|
||||
"label": { "type": "string", "minLength": 1 },
|
||||
"kind": {
|
||||
"enum": [
|
||||
"webview", "toolbar", "titlebar_accessory", "sidebar", "statusbar", "split", "stack",
|
||||
"button", "icon_button", "list_item", "checkbox", "toggle", "segmented_control",
|
||||
"text_field", "search_field", "label", "spacer", "gpu_surface", "progress_indicator"
|
||||
]
|
||||
},
|
||||
"parent": { "type": "string" },
|
||||
"edge": { "enum": ["top", "right", "bottom", "left"] },
|
||||
"axis": { "enum": ["row", "horizontal", "column", "vertical"] },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"width": { "type": "number" },
|
||||
"height": { "type": "number" },
|
||||
"min_width": { "type": "number" },
|
||||
"min_height": { "type": "number" },
|
||||
"max_width": { "type": "number" },
|
||||
"max_height": { "type": "number" },
|
||||
"fill": { "type": "boolean", "default": false },
|
||||
"layer": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647, "default": 0 },
|
||||
"visible": { "type": "boolean", "default": true },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"role": { "type": "string" },
|
||||
"accessibility_label": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"text": { "type": "string" },
|
||||
"command": { "type": "string" },
|
||||
"gpu_backend": { "enum": ["none", "metal", "software"] },
|
||||
"gpu_pixel_format": { "enum": ["none", "bgra8_unorm"] },
|
||||
"gpu_present_mode": { "enum": ["none", "timer"] },
|
||||
"gpu_alpha_mode": { "enum": ["none", "opaque", "premultiplied"] },
|
||||
"gpu_color_space": { "enum": ["none", "srgb", "display_p3"] },
|
||||
"gpu_vsync": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"command": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"title": { "type": "string", "default": "" },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"checked": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title"],
|
||||
"properties": {
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/menuItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"menuItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": { "type": "string", "default": "" },
|
||||
"command": { "type": "string", "default": "" },
|
||||
"key": { "type": "string", "default": "" },
|
||||
"modifiers": { "$ref": "#/$defs/modifiers" },
|
||||
"separator": { "type": "boolean", "default": false },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"checked": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"modifiers": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "enum": ["primary", "command", "control", "option", "alt", "shift"] }
|
||||
},
|
||||
"shortcut": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "key"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"key": { "type": "string", "minLength": 1 },
|
||||
"modifiers": { "$ref": "#/$defs/modifiers" }
|
||||
}
|
||||
},
|
||||
"fileAssociation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"role": { "$ref": "#/$defs/associationRole" },
|
||||
"extensions": { "$ref": "#/$defs/stringArray" },
|
||||
"mime_types": { "$ref": "#/$defs/stringArray" },
|
||||
"icon": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"urlScheme": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["scheme"],
|
||||
"properties": {
|
||||
"scheme": { "type": "string", "minLength": 1 },
|
||||
"role": { "$ref": "#/$defs/associationRole" }
|
||||
}
|
||||
},
|
||||
"associationRole": { "enum": ["viewer", "editor", "shell", "none"], "default": "viewer" },
|
||||
"dmg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"volume_name": { "type": "string" },
|
||||
"background": { "type": "string" },
|
||||
"window_width": { "type": "integer", "minimum": 320, "maximum": 2000, "default": 660 },
|
||||
"window_height": { "type": "integer", "minimum": 240, "maximum": 1400, "default": 400 },
|
||||
"icon_size": { "type": "integer", "minimum": 32, "maximum": 256, "default": 128 },
|
||||
"app_position": { "$ref": "#/$defs/position" },
|
||||
"applications_position": { "$ref": "#/$defs/position" },
|
||||
"applications_link": { "type": "boolean", "default": true },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/dmgItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"dmgItem": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "position"],
|
||||
"properties": {
|
||||
"kind": { "enum": ["app", "applications", "file", "link"] },
|
||||
"path": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"position": { "$ref": "#/$defs/position" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const docsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoRoot = path.resolve(docsRoot, "..");
|
||||
const publishedPath = path.join(repoRoot, "apps", "schema", "public", "app", "v1.json");
|
||||
const legacyPath = path.join(docsRoot, "public", "schemas", "app.schema.json");
|
||||
const packagePath = path.join(repoRoot, "packages", "native-sdk", "schemas", "app.schema.json");
|
||||
const deploymentPath = path.join(repoRoot, "apps", "schema", "vercel.json");
|
||||
const publishedBytes = fs.readFileSync(publishedPath);
|
||||
const schema = JSON.parse(publishedBytes.toString("utf8"));
|
||||
const deployment = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
|
||||
|
||||
assert.equal(schema.$id, "https://schema.native-sdk.dev/app/v1.json");
|
||||
assert.equal(schema.$defs.persist.properties.debounce_ms.minimum, 0);
|
||||
assert.equal(schema.$defs.persist.properties.debounce_ms.maximum, 60_000);
|
||||
assert.equal(schema.$defs.frontend.properties.dev.properties.timeout_ms.minimum, 1);
|
||||
assert.equal(schema.$defs.frontend.properties.dev.properties.timeout_ms.maximum, 4_294_967_295);
|
||||
assert.equal(schema.$defs.dmg.properties.window_width.minimum, 320);
|
||||
assert.equal(schema.$defs.dmg.properties.window_width.maximum, 2_000);
|
||||
assert.equal(schema.$defs.dmg.properties.window_height.minimum, 240);
|
||||
assert.equal(schema.$defs.dmg.properties.window_height.maximum, 1_400);
|
||||
assert.equal(schema.$defs.dmg.properties.icon_size.minimum, 32);
|
||||
assert.equal(schema.$defs.dmg.properties.icon_size.maximum, 256);
|
||||
assert.deepEqual(fs.readFileSync(legacyPath), publishedBytes, "legacy docs schema differs from canonical v1");
|
||||
assert.deepEqual(fs.readFileSync(packagePath), publishedBytes, "published and npm-packaged app schemas differ");
|
||||
assert.equal(deployment.outputDirectory, "public");
|
||||
// /app.json aliases the canonical schema; the redirect's negative lookahead
|
||||
// keeps both schema URLs local while every other path returns an actual 302.
|
||||
assert.deepEqual(deployment.rewrites, [{ source: "/app.json", destination: "/app/v1.json" }]);
|
||||
assert.deepEqual(deployment.redirects, [
|
||||
{
|
||||
source: "/((?!app(?:\\.json|/v1\\.json)$).*)",
|
||||
destination: "https://native-sdk.dev",
|
||||
statusCode: 302,
|
||||
},
|
||||
]);
|
||||
const redirectPattern = new RegExp(`^${deployment.redirects[0].source}$`);
|
||||
assert.equal(redirectPattern.test("/app.json"), false);
|
||||
assert.equal(redirectPattern.test("/app/v1.json"), false);
|
||||
assert.equal(redirectPattern.test("/"), true);
|
||||
assert.equal(redirectPattern.test("/docs"), true);
|
||||
assert.equal(redirectPattern.test("/app/v2.json"), true);
|
||||
assert.ok(deployment.headers.some((entry) => entry.source === "/app/v1.json"));
|
||||
assert.ok(deployment.headers.some((entry) => entry.source === "/app.json"));
|
||||
|
||||
console.log("app schema check passed: canonical deployment, runtime bounds, and npm mirror agree");
|
||||
@@ -1,45 +1,52 @@
|
||||
# Config
|
||||
|
||||
The `app.zon` manifest declares app metadata, permissions, security rules, window layout, and packaging inputs. It is read by the CLI and tooling at build, package, and validation time.
|
||||
The `app.json` manifest declares app metadata, permissions, security rules, window layout, and packaging inputs. It is read by the CLI and tooling at build, package, and validation time. New projects use JSON so TypeScript developers get familiar syntax, completion, and inline validation through [`$schema`](https://schema.native-sdk.dev/app/v1.json). Existing `app.zon` manifests remain fully supported and expose the same fields—there is no reduced JSON feature set. The versioned URL stays compatible for the lifetime of the v1 manifest contract; `/app.json` is the current-version alias.
|
||||
|
||||
## Example: native-rendered app
|
||||
|
||||
The manifest `native init` generates — identity, one shell window with a GPU surface view, and the minimal permission set:
|
||||
|
||||
```zig:app.zon
|
||||
.{
|
||||
.id = "dev.native_sdk.my-app",
|
||||
.name = "my-app",
|
||||
.display_name = "My App",
|
||||
.description = "A counter that lives in one native window.",
|
||||
.version = "0.1.0",
|
||||
.icons = .{"assets/icon.png"},
|
||||
.platforms = .{"macos"},
|
||||
.permissions = .{ "view", "command" },
|
||||
.capabilities = .{ "native_views", "gpu_surfaces" },
|
||||
.shell = .{
|
||||
.windows = .{
|
||||
.{
|
||||
.label = "main",
|
||||
.title = "My App",
|
||||
.width = 480,
|
||||
.height = 320,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.views = .{
|
||||
.{ .label = "main-canvas", .kind = "gpu_surface", .fill = true, .role = "Counter canvas", .accessibility_label = "Counter", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
.security = .{
|
||||
.navigation = .{
|
||||
.allowed_origins = .{ "zero://app", "zero://inline" },
|
||||
.external_links = .{ .action = "deny" },
|
||||
},
|
||||
},
|
||||
.web_engine = "system",
|
||||
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
|
||||
```json:app.json
|
||||
{
|
||||
"$schema": "https://schema.native-sdk.dev/app/v1.json",
|
||||
"id": "dev.native_sdk.my-app",
|
||||
"name": "my-app",
|
||||
"display_name": "My App",
|
||||
"description": "A counter that lives in one native window.",
|
||||
"version": "0.1.0",
|
||||
"icons": ["assets/icon.png"],
|
||||
"platforms": ["macos"],
|
||||
"permissions": ["view", "command"],
|
||||
"capabilities": ["native_views", "gpu_surfaces"],
|
||||
"shell": {
|
||||
"windows": [{
|
||||
"label": "main",
|
||||
"title": "My App",
|
||||
"width": 480,
|
||||
"height": 320,
|
||||
"views": [{
|
||||
"label": "main-canvas",
|
||||
"kind": "gpu_surface",
|
||||
"fill": true,
|
||||
"role": "Counter canvas",
|
||||
"accessibility_label": "Counter",
|
||||
"gpu_backend": "metal",
|
||||
"gpu_pixel_format": "bgra8_unorm",
|
||||
"gpu_present_mode": "timer",
|
||||
"gpu_alpha_mode": "opaque",
|
||||
"gpu_color_space": "srgb",
|
||||
"gpu_vsync": true
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"security": {
|
||||
"navigation": {
|
||||
"allowed_origins": ["zero://app", "zero://inline"],
|
||||
"external_links": { "action": "deny" }
|
||||
}
|
||||
},
|
||||
"web_engine": "system",
|
||||
"cef": { "dir": "third_party/cef/macos", "auto_install": false }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -160,10 +167,18 @@ A fuller manifest for an app that also [embeds web content](/docs/frontend) and
|
||||
<td><code>capabilities</code></td>
|
||||
<td>Feature declarations (see <a href="/docs/security">Security</a>). <code>"store"</code> links the engine-owned record store; <code>"sqlite"</code> links relational SQL effects. They share one capability-shed SQLite object but use separate databases; see <a href="/docs/record-store">Record Store</a> and <a href="/docs/sqlite">Relational SQLite</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dock_visible</code></td>
|
||||
<td>Initial macOS Dock and app-switcher presence. Defaults to <code>true</code>. Set <code>false</code> for an Accessory/menu-bar app; the policy is applied before startup windows are created, so no Dock tile flashes. Accessory apps must declare the <code>"tray"</code> capability. Runtime <code>Cmd.setDockPresence</code> can still promote or demote the app later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>persist</code></td>
|
||||
<td>Engine-owned Model snapshot config: monotonic <code>version</code>, optional <code>debounce_ms</code>, and the <code>restore</code> Msg routes (<code>ok</code>/<code>none</code>/<code>err</code>). Requires <code>"persist"</code> in capabilities — see <a href="/docs/persistence">Model Persistence</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>images</code></td>
|
||||
<td>Optional registered-image budget: <code>.images = .{ .max_image_pixel_bytes = 8_388_608 }</code>. The default is 1 MiB and accepted values are 1–8 MiB. Encoded photos decode aspect-preservingly to fit; storage is lazy per used slot, but 16 fully used 8 MiB slots are a declared 128 MiB high-water. See <a href="/docs/dynamic-images">Dynamic Images</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>bridge</code></td>
|
||||
<td>Bridge command policies (see <a href="/docs/bridge">Bridge</a>)</td>
|
||||
@@ -277,7 +292,7 @@ For a scene-first app — a `UiApp` passing its Zig scene (`shell_scene`) to the
|
||||
},
|
||||
```
|
||||
|
||||
Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `initially_hidden` (default false; create the native window ordered out until an explicit show/focus), `allows_fullscreen` (default true; false disables native fullscreen on macOS without disabling ordinary resizing), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same fields are accepted on top-level `windows` entries.
|
||||
Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `initially_hidden` (default false; create the native window ordered out until an explicit show/focus), `allows_fullscreen` (default true; false disables native fullscreen on macOS without disabling ordinary resizing), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `restore_state` defaults to true and controls only whether the state store is consulted: a store hit restores the saved frame, an authored `x` or `y` is explicit placement, and a fresh window with neither is default placement. On macOS the default `clamp_to_visible_screen` policy keeps restored and explicit frames on their matching or nearest display, centers the main fresh window, and cascades fresh secondary windows; `center_on_primary` centers restored and fresh default frames on the primary display. `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same fields are accepted on top-level `windows` entries. `dock_visible` is top-level app policy, not a window field: setting it false removes the Dock/app-switcher presence but does not hide a window; pair it with `initially_hidden = true` when the app should launch behind its status item.
|
||||
|
||||
Overlay presentation is also fixed at create time: `transparent` makes the top-level window alpha-capable, `always_on_top` selects its floating/topmost level, `click_through` passes pointer input to windows underneath, `activate_on_show = false` reveals it without activating the app or taking focus, and `allows_fullscreen = false` removes macOS fullscreen participation while keeping the window resizable. Canvas windows already use present-before-show, so these flags are applied while the window is hidden and its first alpha-correct frame becomes the first visible frame. `initially_hidden = true` is stronger: it suppresses that reveal until `Cmd.showWindow` or explicit focus. Pair `transparent = true` with a non-opaque `gpu_alpha_mode`; `UiApp.WindowDescriptor` makes that canvas-alpha choice and uses an alpha-zero clear automatically. See the <a href="/docs/windows#overlay-windows">overlay window recipe</a>. These fields are accepted on top-level `windows` and `shell.windows`. Runtime `WindowCreateOptions` exposes the same controls but spells the hidden mode `.show = .hidden`; `UiApp.WindowDescriptor` exposes the overlay controls but not the initially-hidden mode.
|
||||
|
||||
@@ -305,11 +320,11 @@ The optional `commands` list declares shared command metadata. The runtime still
|
||||
|
||||
An app can define up to 256 commands. Command ids can be up to 128 bytes and titles can be up to 128 bytes.
|
||||
|
||||
Generated runners load manifest commands into `RuntimeOptions.commands`. Native code can read the active catalog with `runtime.listCommands(...)`, and trusted WebView code can read it with `window.zero.commands.list()` when the built-in command bridge allows it. Use the catalog to keep menus, shortcuts, toolbar controls, tray items, and bridge callers aligned with the same command ids.
|
||||
Generated zero-config TypeScript and Zig-core runners load manifest commands into `RuntimeOptions.commands`; ejected runners use the same fallback. Native code can read the active catalog with `runtime.listCommands(...)`, and trusted WebView code can read it with `window.zero.commands.list()` when the built-in command bridge allows it. Use the catalog to keep menus, shortcuts, toolbar controls, tray items, and bridge callers aligned with the same command ids.
|
||||
|
||||
## `shortcuts`
|
||||
|
||||
The optional `shortcuts` list defines app-level keyboard shortcuts. Generated runners load these automatically:
|
||||
The optional `shortcuts` list defines app-level keyboard shortcuts. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:
|
||||
|
||||
```zig
|
||||
.shortcuts = .{
|
||||
@@ -328,7 +343,7 @@ Chromium builds are currently macOS-only; use the Linux system WebView backend w
|
||||
|
||||
## `menus`
|
||||
|
||||
The optional `menus` list defines native app menus. Generated runners load these automatically:
|
||||
The optional `menus` list defines native app menus. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:
|
||||
|
||||
```zig
|
||||
.menus = .{
|
||||
@@ -420,6 +435,6 @@ The optional `frontend.dev` block configures the managed dev server for `native
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
native validate app.zon
|
||||
native doctor --manifest app.zon --strict
|
||||
native validate app.json
|
||||
native doctor --manifest app.json --strict
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ When the runtime publishes a snapshot, it writes these files to the automation d
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>snapshot.txt</code></td>
|
||||
<td>Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, <code>ready=true/false</code>, and <code>markup_watch=armed|off</code> in the header — whether the markup hot-reload watch is armed (only in builds where the app wired <code>.markup</code> with a <code>watch_path</code> and <code>io</code>, or registered compiled fragments through <code>fragment_watch</code> — i.e. Debug dev builds)</td>
|
||||
<td>Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, the configured command and app-menu catalogs, <code>ready=true/false</code>, and <code>markup_watch=armed|off</code> in the header — whether the markup hot-reload watch is armed (only in builds where the app wired <code>.markup</code> with a <code>watch_path</code> and <code>io</code>, or registered compiled fragments through <code>fragment_watch</code> — i.e. Debug dev builds)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>accessibility.txt</code></td>
|
||||
@@ -146,8 +146,8 @@ The runtime watches the command queue and processes these actions:
|
||||
<td>Dispatch a shortcut command event for the main window</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tray-action <item-id></code></td>
|
||||
<td>Select a status-item dropdown row (ids from the snapshot's <code>tray-item #id</code> lines)</td>
|
||||
<td><code>tray-action <item-id></code> or <code>tray-action <status-item-id> <item-id></code></td>
|
||||
<td>Select a status-item dropdown row. The one-id form targets primary status item <code>#1</code>; multiple-item snapshots print <code>tray #id</code> headers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>focus <view-label></code></td>
|
||||
@@ -261,6 +261,12 @@ Edits refuse rather than guess: a widget authored in Zig, a file that changed on
|
||||
|
||||
`zig build test-writeback-smoke` (macOS) drives the whole loop against the kanban example: query provenance, flip the button label through the verb, assert the repaint, verify the byte-exact diff, and flip it back.
|
||||
|
||||
## Session recording and blob growth
|
||||
|
||||
`NATIVE_SDK_SESSION_RECORD=/path/session.journal` records platform events and effect results; `NATIVE_SDK_SESSION_REPLAY` replays that journal without touching the original network, process, database, or file. Large effect payloads—including every streamed file-read chunk—live content-addressed under the sibling `blobs/` directory, and the journal stores each hash and length. Keep the journal and `blobs/` together when copying a recording.
|
||||
|
||||
The blob store deduplicates identical chunks but is intentionally unbounded today: there is no automatic size quota or garbage collector. Long recordings and repeated large imports can therefore grow the directory substantially. Treat a recording as one disposable artifact, monitor its directory size in long automation runs, and delete the journal plus its sibling `blobs/` directory when it is no longer needed. A future GC must reason over every retained journal before removing an unreferenced content address; Tier 5 does not guess at that retention policy.
|
||||
|
||||
## Custom directory
|
||||
|
||||
Pass a custom path to `automation.Server.init()`:
|
||||
|
||||
@@ -246,7 +246,7 @@ Dialog commands are **always default-deny** and require an explicit `builtin_bri
|
||||
<tr>
|
||||
<td><code>native-sdk.os.showNotification</code></td>
|
||||
<td><code>notifications</code></td>
|
||||
<td>Show a native system notification with a title, optional subtitle, and optional body</td>
|
||||
<td>Show or replace a native notification; optional paired <code>actionLabel</code> / <code>actionCommand</code> fields dispatch an app command</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>native-sdk.os.revealPath</code></td>
|
||||
@@ -266,7 +266,7 @@ Dialog commands are **always default-deny** and require an explicit `builtin_bri
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
OS commands are **always default-deny** and require an explicit `builtin_bridge` policy. `native-sdk.os.openUrl` also checks `security.navigation.external_links`; the URL must match the external link allowlist before the platform service is called. macOS, Linux, and Windows system WebView hosts implement `openUrl`, `revealPath`, notifications, and recent-document commands; macOS Chromium also implements the current OS command set. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented.
|
||||
OS commands are **always default-deny** and require an explicit `builtin_bridge` policy. `native-sdk.os.openUrl` also checks `security.navigation.external_links`; the URL must match the external link allowlist before the platform service is called. An actionable notification additionally requires an allowed `native-sdk.command.invoke` policy and the `command` permission for the same origin, because activating it enters the normal app-command path. macOS, Linux, and Windows system WebView hosts implement `openUrl`, `revealPath`, notifications, and recent-document commands; macOS Chromium also implements the current OS command set. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented.
|
||||
|
||||
## Credential Commands
|
||||
|
||||
@@ -466,9 +466,12 @@ const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
|
||||
|
||||
await window.zero.os.openUrl("https://example.com/docs/start");
|
||||
await window.zero.os.showNotification({
|
||||
id: "build-status",
|
||||
title: "Build finished",
|
||||
subtitle: "native-sdk",
|
||||
body: "All checks passed.",
|
||||
actionLabel: "Open results",
|
||||
actionCommand: "build.open-results",
|
||||
});
|
||||
await window.zero.os.revealPath("/Users/me/Downloads/report.pdf");
|
||||
await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf");
|
||||
|
||||
@@ -173,6 +173,7 @@ OS bridge commands are default-deny. Add explicit `builtin_bridge` entries for e
|
||||
.builtin_bridge = .{
|
||||
.enabled = true,
|
||||
.commands = &.{
|
||||
.{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
|
||||
.{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
|
||||
.{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
|
||||
.{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
|
||||
@@ -193,6 +194,8 @@ OS bridge commands are default-deny. Add explicit `builtin_bridge` entries for e
|
||||
},
|
||||
```
|
||||
|
||||
Because the JavaScript example below supplies a notification action, the app/runtime permission list must also include `command`. Display-only notifications need only the `native-sdk.os.showNotification` policy and `notifications` permission.
|
||||
|
||||
Opening external URLs is also gated by the external-link policy:
|
||||
|
||||
```zig
|
||||
@@ -211,9 +214,12 @@ Opening external URLs is also gated by the external-link policy:
|
||||
```javascript
|
||||
await window.zero.os.openUrl("https://example.com/docs/start");
|
||||
await window.zero.os.showNotification({
|
||||
id: "build-status",
|
||||
title: "Build finished",
|
||||
subtitle: "native-sdk",
|
||||
body: "All checks passed.",
|
||||
actionLabel: "Open results",
|
||||
actionCommand: "build.open-results",
|
||||
});
|
||||
await window.zero.os.revealPath("/Users/me/Downloads/report.pdf");
|
||||
await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf");
|
||||
@@ -254,16 +260,19 @@ Use the `app.zon` app id as `service` when WebView code and a model core need to
|
||||
|
||||
## Model cores
|
||||
|
||||
Model-driven apps request notifications as effects. Delivery is fire-and-forget because OS focus modes and user settings remain authoritative after the host accepts the request; fake-executor tests and session replay do not display notifications.
|
||||
Model-driven apps request notifications as effects. Delivery is fire-and-forget because OS focus modes and user settings remain authoritative after the host accepts the request; fake-executor tests and session replay do not display notifications. An optional nonempty `id` replaces the app's earlier notification with the same id. Supply `actionLabel` and `actionCommand` together to route activation through the same application command path as menus, shortcuts, and bridge command invocation. Action-command delivery is process-scoped: a notification from an earlier execution cannot dispatch its command into a later one. Linux notification shells may still D-Bus-activate an exited app when the notification or its action is clicked, but the stale action is rejected rather than entering the new runtime.
|
||||
|
||||
### TypeScript
|
||||
|
||||
```ts
|
||||
case "build_finished":
|
||||
return [model, Cmd.showNotification({
|
||||
id: asciiBytes("build-status"),
|
||||
title: asciiBytes("Build finished"),
|
||||
subtitle: asciiBytes("native-sdk"),
|
||||
body: asciiBytes("All checks passed."),
|
||||
actionLabel: asciiBytes("Open results"),
|
||||
actionCommand: asciiBytes("build.open-results"),
|
||||
})];
|
||||
```
|
||||
|
||||
@@ -271,9 +280,12 @@ case "build_finished":
|
||||
|
||||
```zig
|
||||
.build_finished => fx.showNotification(.{
|
||||
.id = "build-status",
|
||||
.title = "Build finished",
|
||||
.subtitle = "native-sdk",
|
||||
.body = "All checks passed.",
|
||||
.action_label = "Open results",
|
||||
.action_command = "build.open-results",
|
||||
}),
|
||||
```
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ The `native` CLI provides project scaffolding, markup validation, automation, pa
|
||||
native init [path] [--template <ts-core|zig-core>] [--frontend <native|next|vite|react|svelte|vue>] [--full]
|
||||
```
|
||||
|
||||
Scaffold a new Native SDK project. The default `native` frontend scaffolds a native-rendered markup app with no web frontend and no build files — the CLI owns the build. The default core is TypeScript: `src/core.ts`, `src/app.native`, `app.zon`, and no language flag anywhere else — the build detects which core the tree carries. Omit `path` to scaffold into the current directory.
|
||||
Scaffold a new Native SDK project. The default `native` frontend scaffolds a native-rendered markup app with no web frontend and no build files — the CLI owns the build. The default core is TypeScript: `src/core.ts`, `src/app.native`, `app.json`, and no language flag anywhere else — the build detects which core the tree carries. Omit `path` to scaffold into the current directory.
|
||||
|
||||
<dl>
|
||||
<dt><code>--template</code></dt>
|
||||
@@ -26,7 +26,7 @@ Scaffold a new Native SDK project. The default `native` frontend scaffolds a nat
|
||||
```sh
|
||||
native dev [dir]
|
||||
native dev [dir] --core [--script msgs.ndjson] [--watch]
|
||||
native dev --binary <path> [--manifest app.zon] [--url <url>] [--command "<cmd>"] [--timeout-ms <n>]
|
||||
native dev --binary <path> [--manifest app.json] [--url <url>] [--command "<cmd>"] [--timeout-ms <n>]
|
||||
```
|
||||
|
||||
Build and run the app in the current (or given) app directory — a Debug build by default, printing a one-line completion and naming any failing step. The markup hot-reload watcher and the Debug-only teaching diagnostics are compiled in only in Debug; pass `-Doptimize=...` to override. Apps with a frontend dev config also get the managed dev server — see [Dev Server](/docs/cli/dev).
|
||||
@@ -41,13 +41,13 @@ Build and run the app in the current (or given) app directory — a Debug build
|
||||
<dt><code>--binary</code></dt>
|
||||
<dd>Path to a prebuilt app binary — the legacy prebuilt-shell form: the build step is skipped and only the frontend dev flow runs. Ordinary <code>native dev</code> builds the app itself.</dd>
|
||||
<dt><code>--manifest</code></dt>
|
||||
<dd>Path to <code>app.zon</code> (default: <code>app.zon</code>).</dd>
|
||||
<dd>Path to <code>app.json</code> or <code>app.zon</code> (default: auto-detected, JSON first).</dd>
|
||||
<dt><code>--url</code></dt>
|
||||
<dd>Override the dev server URL from <code>app.zon</code>.</dd>
|
||||
<dd>Override the dev server URL from the app manifest.</dd>
|
||||
<dt><code>--command</code></dt>
|
||||
<dd>Override the dev server command (space-separated).</dd>
|
||||
<dt><code>--timeout-ms</code></dt>
|
||||
<dd>Milliseconds to wait for the dev server (default from <code>app.zon</code>, or 30000).</dd>
|
||||
<dd>Milliseconds to wait for the dev server (default from the app manifest, or 30000).</dd>
|
||||
</dl>
|
||||
|
||||
### `native build`
|
||||
@@ -72,7 +72,7 @@ Run the app's test suite, printing the zig build summary (step/test tally) plus
|
||||
native check [dir] [--strict]
|
||||
```
|
||||
|
||||
Validate the whole tree without building the app. A TypeScript core (`src/core.ts`) runs the subset checker first — real tsc semantics plus the app-core rules, diagnostics verbatim — then every `src/**.native` markup file and `app.zon` are checked as before. With a fresh model contract (`zig-out/model-contract.zon`, refreshed by `native test`) it also checks bindings, iterables, and message tags against your `Model`/`Msg` — for a TypeScript core, against its model contract — and warns on model state no view uses. Without the artifact it degrades to structural checking and says so: "model contract: not yet built - bindings checked structurally only; run `native test` to enable typed checks". Markup accessibility findings are reported per file in full, and a failing `src/*.native` file that no Zig source embeds gets a leftover-file hint.
|
||||
Validate the whole tree without building the app. A TypeScript core (`src/core.ts`) runs the subset checker first — real tsc semantics plus the app-core rules, diagnostics verbatim — then every `src/**.native` markup file and the app manifest are checked as before. With a fresh model contract (`zig-out/model-contract.zon`, refreshed by `native test`) it also checks bindings, iterables, and message tags against your `Model`/`Msg` — for a TypeScript core, against its model contract — and warns on model state no view uses. Without the artifact it degrades to structural checking and says so: "model contract: not yet built - bindings checked structurally only; run `native test` to enable typed checks". Markup accessibility findings are reported per file in full, and a failing `src/*.native` file that no Zig source embeds gets a leftover-file hint.
|
||||
|
||||
<dl>
|
||||
<dt><code>--strict</code></dt>
|
||||
@@ -100,7 +100,7 @@ Write an owned copy of a library composite into `src/components/` (once, never o
|
||||
### `native doctor`
|
||||
|
||||
```sh
|
||||
native doctor [--strict] [--manifest app.zon] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
|
||||
native doctor [--strict] [--manifest app.json] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
|
||||
```
|
||||
|
||||
Check host environment, WebView, manifest, and CEF. See [native doctor](/docs/debugging/doctor) for what each check means.
|
||||
@@ -108,10 +108,10 @@ Check host environment, WebView, manifest, and CEF. See [native doctor](/docs/de
|
||||
### `native validate`
|
||||
|
||||
```sh
|
||||
native validate [app.zon]
|
||||
native validate [app.json|app.zon]
|
||||
```
|
||||
|
||||
Validate `app.zon` against the manifest schema.
|
||||
Validate `app.json` or `app.zon` against the same manifest contract.
|
||||
|
||||
### `native package`
|
||||
|
||||
@@ -119,13 +119,13 @@ Validate `app.zon` against the manifest schema.
|
||||
native package [--target <macos|linux|windows|ios|android>] [flags]
|
||||
```
|
||||
|
||||
Package the app for distribution. The manifest is picked up at `app.zon` and the binary at `zig-out/bin/<name>` automatically; the flags below override.
|
||||
Package the app for distribution. The manifest is picked up at `app.json` (falling back to `app.zon`) and the binary at `zig-out/bin/<name>` automatically; the flags below override.
|
||||
|
||||
<dl>
|
||||
<dt><code>--target</code></dt>
|
||||
<dd>Target platform (<code>macos</code>, <code>linux</code>, <code>windows</code>, <code>ios</code>, <code>android</code>).</dd>
|
||||
<dt><code>--manifest</code></dt>
|
||||
<dd>Path to <code>app.zon</code>.</dd>
|
||||
<dd>Path to <code>app.json</code> or <code>app.zon</code>.</dd>
|
||||
<dt><code>--output</code></dt>
|
||||
<dd>Output path for the package.</dd>
|
||||
<dt><code>--binary</code></dt>
|
||||
@@ -135,11 +135,11 @@ Package the app for distribution. The manifest is picked up at `app.zon` and the
|
||||
<dt><code>--optimize</code></dt>
|
||||
<dd>Optimization level.</dd>
|
||||
<dt><code>--web-engine</code></dt>
|
||||
<dd>Temporarily override <code>app.zon</code> with <code>system</code> or macOS-only <code>chromium</code>.</dd>
|
||||
<dd>Temporarily override the app manifest with <code>system</code> or macOS-only <code>chromium</code>.</dd>
|
||||
<dt><code>--web-layer</code></dt>
|
||||
<dd>Override <code>app.zon</code>'s <code>.webview_layer</code> with <code>auto</code>, <code>include</code>, or <code>exclude</code> — the same precedence as <code>-Dweb-layer</code> in the build graph. <code>zig build package</code> passes the graph's resolved decision here automatically so the package always matches the built executable.</dd>
|
||||
<dd>Override the app manifest's <code>webview_layer</code> with <code>auto</code>, <code>include</code>, or <code>exclude</code> — the same precedence as <code>-Dweb-layer</code> in the build graph. <code>zig build package</code> passes the graph's resolved decision here automatically so the package always matches the built executable.</dd>
|
||||
<dt><code>--cef-dir</code></dt>
|
||||
<dd>Temporarily override the CEF distribution path from <code>app.zon</code>.</dd>
|
||||
<dd>Temporarily override the CEF distribution path from the app manifest.</dd>
|
||||
<dt><code>--cef-auto-install</code></dt>
|
||||
<dd>Temporarily allow prepared CEF installation during Chromium packaging.</dd>
|
||||
<dt><code>--signing</code></dt>
|
||||
@@ -151,7 +151,7 @@ Package the app for distribution. The manifest is picked up at `app.zon` and the
|
||||
<dt><code>--team-id</code></dt>
|
||||
<dd>Apple Developer Team ID.</dd>
|
||||
<dt><code>--archive</code></dt>
|
||||
<dd>Create a distributable archive. On macOS this is a styled DMG with the app, an Applications alias, a generated or custom background, and the Finder layout declared by <code>app.zon</code>.</dd>
|
||||
<dd>Create a distributable archive. On macOS this is a styled DMG with the app, an Applications alias, a generated or custom background, and the Finder layout declared by the app manifest.</dd>
|
||||
</dl>
|
||||
|
||||
### Platform shortcuts
|
||||
@@ -169,7 +169,7 @@ The desktop shortcuts use an explicit `--service-binary` when supplied; otherwis
|
||||
### `native bundle-assets`
|
||||
|
||||
```sh
|
||||
native bundle-assets [app.zon] [assets] [output]
|
||||
native bundle-assets [app.json|app.zon] [assets] [output]
|
||||
```
|
||||
|
||||
Copy frontend assets into the build output.
|
||||
@@ -238,8 +238,8 @@ Interact with the automation server of a running automation-enabled app. See [Au
|
||||
<dd>Dispatch a trackpad pinch gesture at a gpu-surface view (<code>scale</code> is the final multiplicative zoom for the gesture; the anchor point defaults to the view center).</dd>
|
||||
<dt><code>automate shortcut <id></code></dt>
|
||||
<dd>Dispatch a shortcut command event.</dd>
|
||||
<dt><code>automate tray-action <item-id></code></dt>
|
||||
<dd>Select a status-item dropdown row.</dd>
|
||||
<dt><code>automate tray-action <item-id></code> / <code>automate tray-action <status-item-id> <item-id></code></dt>
|
||||
<dd>Select a status-item dropdown row; the one-id shorthand targets primary status item <code>#1</code>.</dd>
|
||||
<dt><code>automate focus <view-label></code></dt>
|
||||
<dd>Focus a native or WebView-backed view.</dd>
|
||||
<dt><code>automate focus-next</code> / <code>automate focus-previous</code></dt>
|
||||
|
||||
@@ -31,6 +31,13 @@ With a registered image the engine clips it to the avatar circle (`cover` fit);
|
||||
ui.avatar(.{ .image = image_id }, "ZN")
|
||||
```
|
||||
|
||||
An avatar can select one region from a registered texture atlas by declaring all four source coordinates in decoded-image pixels. The same crop is `ElementOptions.image_src` in a Zig view:
|
||||
|
||||
```html
|
||||
<avatar image="{atlas_image}" source-x="64" source-y="0"
|
||||
source-width="32" source-height="32" label="Profile picture">ZN</avatar>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
<AttrTable element="avatar" attrs={["text", "image", "label"]} />
|
||||
<AttrTable element="avatar" attrs={["text", "image", "source-x", "source-y", "source-width", "source-height", "label"]} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Checkbox
|
||||
|
||||
A binary value control: the label rides the `text` attribute — checkbox is not a text-bearing element, so text content between the tags is rejected with a teaching error (`label="..."` alone names one for accessibility without a visible label). The model binds `checked`, and `on-toggle` dispatches its Msg — the engine never flips state on its own. For a single choice among options, use [radio](/docs/components/radio); for an on/off setting rendered as a sliding thumb, use [switch](/docs/components/switch).
|
||||
A binary value control. Put its visible label between the tags (the `text` attribute is the equivalent binding-friendly form); `label="..."` alone supplies an accessible name without drawing a label. The model binds `checked`, and `on-toggle` dispatches its Msg — the engine never flips state on its own. For a single choice among options, use [radio](/docs/components/radio); for an on/off setting rendered as a sliding thumb, use [switch](/docs/components/switch).
|
||||
|
||||
<ComponentPreview name="checkbox" alt="Checkboxes rendered by the engine" caption="checked, unchecked, and disabled checkboxes" />
|
||||
|
||||
@@ -12,9 +12,9 @@ A binary value control: the label rides the `text` attribute — checkbox is not
|
||||
|
||||
```html
|
||||
<column gap="12">
|
||||
<checkbox checked="{accepted}" on-toggle="toggle_terms" text="Accept terms and conditions" />
|
||||
<checkbox checked="{reports}" on-toggle="toggle_reports" text="Send usage reports" />
|
||||
<checkbox checked="true" disabled="true" text="Managed by your organization" />
|
||||
<checkbox checked="{accepted}" on-toggle="toggle_terms">Accept terms and conditions</checkbox>
|
||||
<checkbox checked="{reports}" on-toggle="toggle_reports">Send usage reports</checkbox>
|
||||
<checkbox checked="true" disabled="true">Managed by your organization</checkbox>
|
||||
</column>
|
||||
```
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
`combobox` is a trigger-only primitive like [select](/docs/components/select), but the trigger is a text entry with a menu affordance: `on-input` names a Msg variant that receives every edit as a text-input event (`canvas.TextInputEvent` in a Zig core; the `TextInputEvent` union from `@native-sdk/core/text` in a TypeScript core), and the model filters the options as the user types. The options themselves are composed the same way as the select's — an anchored [dropdown-menu](/docs/components/dropdown-menu) of menu-items beside the trigger in a `stack`, rendered under an `if`, with `on-dismiss` clearing the model's open flag when Escape or a click outside closes the surface.
|
||||
|
||||
Enter submits when `on-submit` is bound; otherwise Enter opens the picker. Space and the open-arrow keys (Down/Up) always open it. Once focus moves into the open menu, Enter selects the focused `menu-item` as usual.
|
||||
|
||||
<ComponentPreview name="combobox" alt="A combobox rendered by the engine" caption="a combobox trigger with its search placeholder" />
|
||||
|
||||
## Markup
|
||||
@@ -13,8 +15,8 @@ The model owns the query and the open flag; the `for` source is the model-filter
|
||||
|
||||
```html
|
||||
<stack width="240">
|
||||
<combobox placeholder="Search frameworks" text="{framework_query}" on-input="framework_edited" on-press="open_framework_menu" />
|
||||
<if test="{framework_menu_open}">
|
||||
<combobox placeholder="Search frameworks" text="{frameworkQuery}" on-input="framework_edited" on-submit="commit_framework_query" on-press="open_framework_menu" />
|
||||
<if test="{frameworkMenuOpen}">
|
||||
<dropdown-menu anchor="below" anchor-alignment="stretch" on-dismiss="close_framework_menu">
|
||||
<for each="matchingFrameworks" key="id" as="f">
|
||||
<menu-item on-press="pick_framework:{f.id}">{f.name}</menu-item>
|
||||
@@ -24,6 +26,19 @@ The model owns the query and the open flag; the `for` source is the model-filter
|
||||
</stack>
|
||||
```
|
||||
|
||||
In the primary TypeScript core, the submit arm commits the current model-owned query. The open and submit messages remain separate, so Enter can commit without toggling the picker:
|
||||
|
||||
```ts
|
||||
export type Msg =
|
||||
| { readonly kind: "framework_edited"; readonly edit: TextInputEvent }
|
||||
| { readonly kind: "open_framework_menu" }
|
||||
| { readonly kind: "close_framework_menu" }
|
||||
| { readonly kind: "commit_framework_query" };
|
||||
|
||||
case "commit_framework_query":
|
||||
return { ...model, committedFrameworkQuery: model.frameworkQuery, frameworkMenuOpen: false };
|
||||
```
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_input` takes a comptime message constructor: `Ui.inputMsg(.tag)` builds `Msg{ .tag = edit }` for each `canvas.TextInputEvent`.
|
||||
@@ -34,6 +49,7 @@ ui.stack(.{ .width = 240 }, .{
|
||||
.placeholder = "Search frameworks",
|
||||
.text = model.framework_query,
|
||||
.on_input = Ui.inputMsg(.framework_edited),
|
||||
.on_submit = .commit_framework_query,
|
||||
.on_press = .open_framework_menu,
|
||||
}, .{}),
|
||||
if (model.framework_menu_open) ui.el(.dropdown_menu, .{
|
||||
@@ -56,6 +72,7 @@ ui.stack(.{ .width = 240 }, .{
|
||||
"disabled",
|
||||
"on-press",
|
||||
"on-input",
|
||||
"on-submit",
|
||||
"on-dismiss",
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Dialog
|
||||
|
||||
A modal dialog surface rendered in place: the title comes from the `text` attribute, and visibility is model-owned — wrap the dialog in an `if` on an open flag. `on-dismiss` dispatches when Escape or a click outside dismisses the surface, so `update` clears the flag; the engine hides the surface immediately as an optimistic echo, and the source tree wins on the next rebuild. The title is drawn by the surface chrome and children stack over the full content box, so lead the body column with a fixed-height spacer that clears the title line. For edge-anchored surfaces with the same contract, see [drawer](/docs/components/drawer) and [sheet](/docs/components/sheet).
|
||||
A modal dialog surface centered against the root view, regardless of which container declares it. The title comes from the `text` attribute, and visibility is model-owned — wrap the dialog in an `if` on an open flag. `on-dismiss` dispatches when Escape or a click outside dismisses the surface, so `update` clears the flag; the engine hides the surface immediately as an optimistic echo, and the source tree wins on the next rebuild. The title is drawn by the surface chrome and children stack over the full content box, so lead the body column with a fixed-height spacer that clears the title line. For root-edge surfaces with the same contract, see [drawer](/docs/components/drawer) and [sheet](/docs/components/sheet).
|
||||
|
||||
<ComponentPreview name="dialog" alt="A modal dialog rendered by the engine" caption="title chrome, body content, and a trailing action row" />
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Drawer
|
||||
|
||||
A side-anchored surface with the same contract as [dialog](/docs/components/dialog): rendered in place, title via the `text` attribute, visibility model-owned behind an `if`, and `on-dismiss` dispatched on Escape or click-outside so `update` clears the open flag. As with the dialog, the title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer. For the bottom-edge variant, see [sheet](/docs/components/sheet).
|
||||
A bottom-edge surface with the same contract as [dialog](/docs/components/dialog): it spans the root view width and pins to the root bottom, regardless of which container declares it. The title comes from the `text` attribute, visibility is model-owned behind an `if`, and `on-dismiss` dispatches on Escape or click-outside so `update` clears the open flag. As with the dialog, the title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer. For the right-edge variant, see [sheet](/docs/components/sheet).
|
||||
|
||||
<ComponentPreview name="drawer" alt="A drawer surface rendered by the engine" caption="a side drawer with a title and stacked controls" />
|
||||
<ComponentPreview name="drawer" alt="A drawer surface rendered by the engine" caption="a bottom drawer with a title and stacked controls" />
|
||||
|
||||
## Markup
|
||||
|
||||
@@ -13,7 +13,7 @@ A side-anchored surface with the same contract as [dialog](/docs/components/dial
|
||||
<column padding="24">
|
||||
<button variant="outline" icon="menu" on-press="open_filters">Filters</button>
|
||||
<if test="{filters_open}">
|
||||
<drawer text="Filters" width="260" padding="24" on-dismiss="close_filters">
|
||||
<drawer text="Filters" height="260" padding="24" on-dismiss="close_filters">
|
||||
<column gap="12">
|
||||
<spacer height="34"></spacer>
|
||||
<checkbox checked="{only_unread}" on-toggle="toggle_unread">Only unread</checkbox>
|
||||
@@ -31,7 +31,7 @@ In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically
|
||||
|
||||
```zig
|
||||
// Rendered only while model.filters_open is true; on_dismiss clears it in update.
|
||||
ui.el(.drawer, .{ .text = "Filters", .width = 260, .padding = 24, .on_dismiss = .close_filters }, .{
|
||||
ui.el(.drawer, .{ .text = "Filters", .height = 260, .padding = 24, .on_dismiss = .close_filters }, .{
|
||||
ui.column(.{ .gap = 12 }, .{
|
||||
ui.column(.{ .height = 34 }, .{}), // clears the engine-drawn title line
|
||||
ui.checkbox(.{ .text = "Only unread", .checked = model.only_unread, .on_toggle = .toggle_unread }),
|
||||
|
||||
@@ -43,6 +43,10 @@ pub fn email(model: *const Model) []const u8 {
|
||||
|
||||
</CodeToggle>
|
||||
|
||||
## Editing keys
|
||||
|
||||
The built-in editor follows the platform keymap in `input`, `text-field`, `search-field`, and `combobox`: Backspace/Delete remove one caret unit, Option+Backspace/Delete on macOS (Ctrl+Backspace/Delete elsewhere) remove one word, and Command+Backspace on macOS (with or without Shift) deletes from the caret to the beginning of the field. A non-empty selection always wins and is deleted by itself. The semantic edit still arrives through `on-input`, so `applyTextInputEvent` and `TextBuffer` keep controlled fields synchronized and one undo restores the whole deletion.
|
||||
|
||||
## Search field
|
||||
|
||||
`search-field` renders the search affordance but binds exactly like an input; pair it with a model-filtered list. Whenever the field holds text it also shows a built-in clear affordance — a small x inside its trailing edge — and pressing it (or pressing Escape while focused) clears through the standard text-edit path, so the `on-input` handler receives the clear like any other edit and a model-owned buffer empties with it. No attribute enables or disables this; searchable fields simply carry it. For text entry that opens a menu of suggestions, see [combobox](/docs/components/combobox).
|
||||
|
||||
@@ -4,17 +4,19 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Radio
|
||||
|
||||
The single-choice value control, grouped by a `radio-group` row container. Like [checkbox](/docs/components/checkbox), the label rides the `text` attribute — radio is not a text-bearing element, so text content between the tags is rejected with a teaching error. One model field holds the group's selection: render it with `{a == b}` equalities on each radio's `checked`, and let each radio's `on-toggle` dispatch the Msg that sets the field — the engine never flips state on its own.
|
||||
The single-choice value control, grouped by a `radio-group`. Give the group an accessible `label` that names the shared choice. Like [checkbox](/docs/components/checkbox), each radio takes its visible label as element content; the `text` attribute is the equivalent binding-friendly form. Descendant radios at any nesting depth form one logical group: one Tab stop, arrows wrap through the choices while Home/End move to the edges, focus and selection move together, and selecting one clears the rest. Bind the model's choice through `checked`; an actual selection transition dispatches `on-change` when bound, then falls back to `on-toggle` and `on-press` for compatibility. Activating the already-checked radio has no new `on-change` edge (a legacy fallback handler still receives the activation).
|
||||
|
||||
<ComponentPreview name="radio-group" alt="A radio group rendered by the engine" caption="a radio group with one selected and one disabled option" />
|
||||
|
||||
## Markup
|
||||
|
||||
```html
|
||||
<radio-group gap="12">
|
||||
<radio checked="{density == default}" on-toggle="set_default" text="Default" />
|
||||
<radio checked="{density == comfortable}" on-toggle="set_comfortable" text="Comfortable" />
|
||||
<radio checked="{density == compact}" disabled="true" text="Compact" />
|
||||
<radio-group gap="12" label="Density">
|
||||
<radio checked="{density == default}" on-change="set_default">Default</radio>
|
||||
<row>
|
||||
<radio checked="{density == comfortable}" on-change="set_comfortable">Comfortable</radio>
|
||||
</row>
|
||||
<radio checked="{density == compact}" disabled="true">Compact</radio>
|
||||
</radio-group>
|
||||
```
|
||||
|
||||
@@ -40,9 +42,11 @@ case "set_comfortable":
|
||||
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:
|
||||
|
||||
```zig
|
||||
ui.el(.radio_group, .{ .gap = 12 }, .{
|
||||
ui.el(.radio, .{ .text = "Default", .checked = model.density == .default, .on_toggle = .set_default }, .{}),
|
||||
ui.el(.radio, .{ .text = "Comfortable", .checked = model.density == .comfortable, .on_toggle = .set_comfortable }, .{}),
|
||||
ui.el(.radio_group, .{ .gap = 12, .semantics = .{ .label = "Density" } }, .{
|
||||
ui.el(.radio, .{ .text = "Default", .checked = model.density == .default, .on_change = .set_default }, .{}),
|
||||
ui.row(.{}, .{
|
||||
ui.el(.radio, .{ .text = "Comfortable", .checked = model.density == .comfortable, .on_change = .set_comfortable }, .{}),
|
||||
}),
|
||||
ui.el(.radio, .{ .text = "Compact", .checked = model.density == .compact, .disabled = true }, .{}),
|
||||
})
|
||||
```
|
||||
@@ -54,6 +58,8 @@ ui.el(.radio_group, .{ .gap = 12 }, .{
|
||||
"text",
|
||||
"checked",
|
||||
"disabled",
|
||||
"on-change",
|
||||
"on-toggle",
|
||||
"on-press",
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Sheet
|
||||
|
||||
A bottom-anchored surface with the same contract as [dialog](/docs/components/dialog) and [drawer](/docs/components/drawer): rendered in place, title via the `text` attribute, visibility model-owned behind an `if`, and `on-dismiss` dispatched on Escape or click-outside so `update` clears the open flag. The sheet rises from the bottom edge; size it with `height`. The title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer.
|
||||
A right-edge surface with the same contract as [dialog](/docs/components/dialog) and [drawer](/docs/components/drawer): it spans the root view height and pins to the root right, regardless of which container declares it. Visibility is model-owned behind an `if`, and `on-dismiss` dispatches on Escape or click-outside so `update` clears the open flag. Size the sheet with `width`. The title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer.
|
||||
|
||||
<ComponentPreview name="sheet" alt="A sheet surface rendered by the engine" caption="a bottom sheet with a title, body text, and an action row" />
|
||||
<ComponentPreview name="sheet" alt="A sheet surface rendered by the engine" caption="a right-edge sheet with a title, body text, and an action row" />
|
||||
|
||||
## Markup
|
||||
|
||||
@@ -13,7 +13,7 @@ A bottom-anchored surface with the same contract as [dialog](/docs/components/di
|
||||
<column padding="24">
|
||||
<button variant="outline" icon="external-link" on-press="open_share">Share</button>
|
||||
<if test="{share_open}">
|
||||
<sheet text="Share" height="190" padding="24" on-dismiss="close_share">
|
||||
<sheet text="Share" width="320" padding="24" on-dismiss="close_share">
|
||||
<column gap="12">
|
||||
<spacer height="34"></spacer>
|
||||
<text wrap="true" foreground="text_muted">Anyone with the link can view this board.</text>
|
||||
@@ -32,7 +32,7 @@ A bottom-anchored surface with the same contract as [dialog](/docs/components/di
|
||||
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:
|
||||
|
||||
```zig
|
||||
ui.el(.sheet, .{ .text = "Share", .height = 190, .padding = 24, .on_dismiss = .close_share }, .{
|
||||
ui.el(.sheet, .{ .text = "Share", .width = 320, .padding = 24, .on_dismiss = .close_share }, .{
|
||||
ui.column(.{ .gap = 12 }, .{
|
||||
ui.column(.{ .height = 34 }, .{}), // clears the engine-drawn title line
|
||||
ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "Anyone with the link can view this board."),
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
Multi-line text entry. Like [input](/docs/components/input), `text` and `placeholder` bind from the model and `on-input` names a Msg variant that receives every edit as a text-input event — see [input](/docs/components/input) for the core-side contract in both languages. By default, Enter (and Shift+Enter) inserts a newline; when a textarea carries `on-submit`, submission rides Cmd+Enter on macOS or Ctrl+Enter elsewhere. Chat composers can set `submit-on-enter="true"`: plain Enter then submits, Shift+Enter still inserts a newline, and the primary chord still submits. Give it a definite `width` and `height` (or a `grow`) to size the editing box.
|
||||
|
||||
Textarea editing uses the same platform shortcuts as [input](/docs/components/input#editing-keys). On macOS, Command+Backspace (with or without Shift) deletes from the caret to the beginning of the current hard-newline-delimited line (or deletes the active selection); soft-wrapped visual lines use their logical line start in this first version.
|
||||
|
||||
<ComponentPreview name="textarea" alt="A textarea rendered by the engine" />
|
||||
|
||||
## Markup
|
||||
|
||||
@@ -22,7 +22,7 @@ The `native doctor` command checks your development environment for issues.
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manifest</td>
|
||||
<td><code>app.zon</code> validation (only when <code>--manifest</code> is passed)</td>
|
||||
<td><code>app.json</code> or <code>app.zon</code> validation when a manifest is discovered or passed explicitly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log directory</td>
|
||||
@@ -30,7 +30,7 @@ The `native doctor` command checks your development environment for issues.
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CEF</td>
|
||||
<td>CEF distribution presence when Chromium is selected by <code>app.zon</code> or <code>--web-engine chromium</code></td>
|
||||
<td>CEF distribution presence when Chromium is selected by the app manifest or <code>--web-engine chromium</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signing tools</td>
|
||||
@@ -46,10 +46,10 @@ The `native doctor` command checks your development environment for issues.
|
||||
native doctor
|
||||
|
||||
# Strict mode (exits non-zero on any warning)
|
||||
native doctor --manifest app.zon --strict
|
||||
native doctor --manifest app.json --strict
|
||||
|
||||
# Check CEF setup
|
||||
native doctor --manifest app.zon
|
||||
native doctor --manifest app.json
|
||||
```
|
||||
|
||||
## Flags
|
||||
@@ -68,11 +68,11 @@ native doctor --manifest app.zon
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--manifest</code></td>
|
||||
<td>Path to <code>app.zon</code></td>
|
||||
<td>Path to <code>app.json</code> or <code>app.zon</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--web-engine</code></td>
|
||||
<td>Temporarily override the engine from <code>app.zon</code> with <code>system</code> or <code>chromium</code></td>
|
||||
<td>Temporarily override the engine from the app manifest with <code>system</code> or <code>chromium</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--cef-dir</code></td>
|
||||
|
||||
@@ -93,7 +93,7 @@ Prefer **omitting** `cachePath` for URL sources: when the app wiring configures
|
||||
Exactly one event arm dispatches per load — a five-field record matched by field name: `id`, `state`, `width`, `height`, `status`. `id` echoes the requested ImageId, so two loads in flight at once share one arm and still tell their results apart (that is what the example's `cover: msg.id` reads); `status` is the HTTP status for url loads that performed an exchange, and **0 when none occurred** — local paths and cache hits — so a `loaded` with status 0 is honest signal that the pixels came without a network round trip, never a fabricated 200; `loaded` means the pixels are registered and drawable; everything else names what actually happened:
|
||||
|
||||
- **Source classes** — `not_found` (missing local file, no url), `io_failed` (a local read failure), `connect_failed` / `tls_failed` / `protocol_failed` / `timed_out` (the fetch taxonomy, on the fetch machinery's own timeout), `http_status` (a non-2xx answer, with the status carried through — an error page is not an image, so the body is discarded).
|
||||
- **Decode and registry classes** — the same errors the direct registration API raises: `decode_failed`, `unsupported` (a host without a codec), `too_large`, `registry_full`, and `alloc_failed` (the host refused the memory the registration needed — resource exhaustion, not corrupt bytes: the same source may load once memory frees, so it is never reported as `decode_failed`).
|
||||
- **Decode and registry classes** — `decode_failed`, `unsupported` (a host without a codec), `registry_full`, and `alloc_failed` (the host refused the memory the registration needed — resource exhaustion, not corrupt bytes: the same source may load once memory frees, so it is never reported as `decode_failed`). `too_large` normally means the encoded source exceeded 8 MiB; platform codecs fit decoded pixels to the app's declared target.
|
||||
- **Discipline classes** — `rejected` (an invalid id, no source at all, or a duplicate live id: one load per id at a time, the spawn rule — a load in flight is never replaced implicitly) and `cancelled` (`Cmd.imageCancel(id)` ended the load).
|
||||
|
||||
`Cmd.imageCancel(id)` is the load's cancel — image loads are keyed by their numeric id, so the string-keyed `Cmd.cancel` never touches them. Cancel is **loud**, the spawn discipline: the one terminal still arrives, as the load's own event arm with state `cancelled`, and the id is free for a fresh load once it lands (a slow CDN fetch no longer pins its id against a retry). Aimed at an id with no live load it no-ops — whatever it targeted already delivered its terminal.
|
||||
@@ -104,9 +104,21 @@ A loaded image occupies one of the registry's **16 slots** until you release it,
|
||||
|
||||
Unlike a load, unregister is **synchronous registry surgery, not an effect**: no result Msg follows (registration by `imageLoad` has a terminal because I/O and decode can fail; releasing a slot cannot), and aimed at an id with no registration it no-ops — `imageCancel`'s idle rule. It frees only the **current** registration: a load in flight under the id is untouched, and its terminal still registers the pixels, re-occupying the id. To evict an id whose load is still running, `Cmd.imageCancel(id)` first, then unregister.
|
||||
|
||||
When many small visuals ship together, one slot can hold a texture atlas instead: `<image>` and `<avatar>` accept `source-x`, `source-y`, `source-width`, and `source-height` together, in decoded-image pixel coordinates. Zig views set the equivalent `ElementOptions.image_src` rectangle. Cropped widgets use nearest sampling so adjacent regions cannot bleed into the tile. Every atlas region references the same registered ImageId, so it consumes one registry slot; use the dimensions reported by the load result because decode-to-fit may change the atlas geometry.
|
||||
|
||||
## Limits, honestly
|
||||
|
||||
Decode limits are the registered-image limits, fixed and loud: **16 slots** of **1 MiB decoded pixels** each (512×512 RGBA8 — avatar and cover-art scale, not photo scale), with `Cmd.imageUnregister` releasing a slot when the app is done with an image. The encoded source is bounded at **1.25 MiB** from every source alike, and over-bound sources fail whole with `too_large` — never a silently cropped decode. Loaded pixels live in the existing registered-image storage; there is no separate pool to size. The framework bundles no codecs: bytes decode through CGImageSource on macOS, gdk-pixbuf on GTK, WIC on Windows, and the mobile hosts' embed image service — a host without one answers `unsupported`, never silence.
|
||||
The default is **16 slots** with a **1 MiB decoded-pixel target per slot**. That target is not a refusal: platform codecs decode photo-scale sources down, preserving aspect, until `width × height × 4` fits. A wide 1024×256 image already fits and stays that size; a 640×480 image registers at a smaller geometry, and the result's `width`/`height` report exactly what views draw. The encoded source has its own flat **8 MiB** bound; an over-bound source fails whole with `too_large`, never as truncated bytes.
|
||||
|
||||
Image-centric apps can raise the target in `app.zon`, up to the hard 8 MiB ceiling:
|
||||
|
||||
```zig
|
||||
.images = .{ .max_image_pixel_bytes = 8_388_608 },
|
||||
```
|
||||
|
||||
The value is frozen at startup. Pixel blocks and decode scratch allocate lazily, so declaring a raise costs nothing until an image is used; each used registry slot is a whole-budget block, however, so filling all 16 slots at 8 MiB is a declared **128 MiB** high-water. The source bound stays 8 MiB regardless of this setting.
|
||||
|
||||
This gives three clear tiers: decode-to-fit is the default for feeds, galleries, avatars, and covers; the manifest raise serves wallpaper and image-forward apps that want more display-scale detail; pixel-exact editing, source-resolution zoom, and gigapixel tiling belong in an app-owned `gpu_surface` or [media-surface producer](/docs/media-producers), not the registry. The framework still bundles no codecs: bytes decode through CGImageSource on Apple platforms, gdk-pixbuf on GTK, WIC on Windows, BitmapFactory on Android, and the embed image service — a host without one answers `unsupported`.
|
||||
|
||||
For textures **produced** by your own renderer at video rates — a decoder, a camera, mpv — this is the wrong tool: that is the [media surface](/docs/media-producers)'s dynamic texture channel. `imageLoad` is for images that exist as encoded bytes somewhere and should become long-lived registered pixels.
|
||||
|
||||
@@ -114,7 +126,7 @@ For textures **produced** by your own renderer at video rates — a decoder, a c
|
||||
|
||||
The loaded bytes **are** the effect result, and the session journal treats them that way. When a recorded session performs an image load, the encoded source bytes are written — at effect-result time — into a content-addressed blob store beside the journal (`blobs/<sha256[..16]>` in the session directory), and the journal record carries the hash and length. Loading the same bytes twice stores one blob: content addressing is deduplication — and the deduplicating probe verifies the existing blob's bytes before trusting its name, so a damaged blob is repaired in place on the next same-bytes recording rather than sealing a journal replay would refuse.
|
||||
|
||||
Replay reads the blob, re-runs the same decode and registration with the recorded bytes, and delivers the recorded result — **byte-identical and fully offline**: the original file, the network, and the cache are never consulted, and the fingerprint checkpoints verify the replayed session against the recording frame by frame. A journal whose `blobs/` directory is missing or damaged refuses loudly (the bytes are verified against their address) rather than replaying a different session. The blob record kind is journal format **v7** — older journals are refused at the preamble with the standard re-record teaching, the format's usual honest break.
|
||||
Replay reads the blob, re-runs decode and registration with the recorded bytes, and delivers the recorded result — **byte-identical and fully offline**: the original file, the network, and the cache are never consulted, and the fingerprint checkpoints verify the replayed session against the recording frame by frame. If the current manifest lowers the image budget after recording, the recorded result dimensions still replay verbatim while best-effort presentation re-decodes to the current budget; any resulting screenshot difference is a verification mismatch, not false journal damage. A journal whose `blobs/` directory is missing or damaged refuses loudly (the bytes are verified against their address) rather than replaying a different session. The blob record kind is journal format **v7** — older journals are refused at the preamble with the standard re-record teaching, the format's usual honest break.
|
||||
|
||||
```sh
|
||||
NATIVE_SDK_SESSION_RECORD=session/app.journal native run # records blobs/ beside the journal
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("files");
|
||||
|
||||
export default function FilesLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Files and streaming
|
||||
|
||||
Raw file effects are the escape hatch for files the user owns: imports, exports, recordings, images, CSV, and other blobs. App state belongs in [Model Persistence](/docs/persistence), the [Record Store](/docs/record-store), or [Relational SQLite](/docs/sqlite); secrets belong in credentials.
|
||||
|
||||
Every operation is still an effect. `update` returns a `Cmd`, the host performs I/O after the model commits, and results return as later `Msg` values. Session recording journals those results; streamed chunks live in the content-addressed `blobs/` directory beside the journal so replay never opens the original file.
|
||||
|
||||
The blob directory is deduplicated but currently has no quota or automatic GC; see [Automation: Session recording and blob growth](/docs/automation#session-recording-and-blob-growth).
|
||||
|
||||
## Small whole-file operations
|
||||
|
||||
`Cmd.readFile` and `Cmd.writeFile` remain convenient for payloads up to 1 MiB. An over-bound write is rejected. An over-bound read returns `truncated` rather than passing cut bytes as a successful file. `Cmd.appendFile` appends one payload up to 1 MiB, `Cmd.statFile` reports `{ exists, size, mtimeMs }` without reading the content, and `Cmd.deleteFile` removes one file.
|
||||
|
||||
```ts
|
||||
return [model, Cmd.statFile(path, {
|
||||
ok: "file_stat",
|
||||
err: "file_failed",
|
||||
})];
|
||||
|
||||
return [model, Cmd.deleteFile(path, {
|
||||
ok: "file_deleted",
|
||||
err: "file_failed",
|
||||
})];
|
||||
```
|
||||
|
||||
Deletion is file-only and never recursive: it reports `not_found` when the path is absent and `io_failed` for a directory or another OS refusal. If the final path component is a symlink, deletion removes the link rather than its target. File outcomes are closed and machine-readable: `ok`, `not_found`, `io_failed`, `truncated`, `rejected`, `cancelled`, `sink_missing`, `out_of_order`, and `disk_full`.
|
||||
|
||||
## Streaming reads
|
||||
|
||||
`Cmd.readFileStream` delivers zero or more 256-KiB chunks, followed by exactly one `done` message carrying the total byte count, or one `err` message.
|
||||
|
||||
Read-stream keys follow ordinary file semantics: reissuing a live key silently replaces the old read, and `Cmd.cancel(key)` silently drops it. Stale chunks from the retired generation cannot reach the replacement route.
|
||||
|
||||
```ts
|
||||
return [model, Cmd.readFileStream(path, {
|
||||
key: "import",
|
||||
chunk: "import_chunk",
|
||||
done: "import_done",
|
||||
err: "import_failed",
|
||||
})];
|
||||
```
|
||||
|
||||
Streaming has its own four-slot budget and no total-size ceiling. Each individual chunk remains bounded, so one large import cannot consume the sixteen general spawn/fetch/whole-file slots.
|
||||
|
||||
## Atomic streaming writes
|
||||
|
||||
Open a sink, send one acknowledged chunk at a time, then close it. Close syncs the temporary file and atomically replaces the destination; before a successful close, the old destination stays visible and teardown removes the temporary file.
|
||||
|
||||
```ts
|
||||
Cmd.writeFileStream("export", path, { ok: "sink_open", err: "export_failed" });
|
||||
Cmd.writeFileChunk("export", bytes, { ok: "chunk_written", err: "export_failed" });
|
||||
Cmd.writeFileClose("export", { ok: "export_done", err: "export_failed" });
|
||||
```
|
||||
|
||||
A duplicate live sink rejects. A chunk or close issued before the previous acknowledgment returns `out_of_order`; a chunk or close with no open sink returns `sink_missing`. Chunks may be at most 1 MiB.
|
||||
|
||||
Write sinks use the loud stream discipline: `Cmd.cancel(key)` ends the sink through `err: cancelled` and removes its temporary file. A half-written export is never silently replaced or installed.
|
||||
|
||||
## Filesystem permission
|
||||
|
||||
Raw paths inside this app's resolved `data`, `config`, `cache`, `state`, `logs`, and `temp` directories need no permission. Any path outside those roots requires:
|
||||
|
||||
```zig
|
||||
.permissions = .{ "filesystem" },
|
||||
```
|
||||
|
||||
The runtime is authoritative. Before checking, it resolves the target when it exists, or resolves the deepest existing parent and normalizes the missing suffix. Existing symlinks are followed for authorization, so a symlink inside an app directory that points outside is external and requires the permission. Deletion still unlinks the final symlink itself after that check. `..` cannot escape an allowed root.
|
||||
|
||||
`native check` also reports NS1074 for certainly-external literal paths, but dynamic paths are decided only by the runtime. File pickers therefore require the `filesystem` permission because the selected user file is normally outside app-owned directories.
|
||||
|
||||
Directory enumeration, file watching, chmod/permissions, and per-path grant prompts are not part of this API.
|
||||
@@ -32,4 +32,4 @@ Native SDK is designed for a world where humans and AI agents build software tog
|
||||
|
||||
## Where to go next
|
||||
|
||||
Ready to build? The [Quick Start](/docs/quick-start) takes you from install to a running, tested app, the [CLI](/docs/cli) covers every verb the tooling provides, and [Config](/docs/app-zon) documents the `app.zon` manifest.
|
||||
Ready to build? The [Quick Start](/docs/quick-start) takes you from install to a running, tested app, the [CLI](/docs/cli) covers every verb the tooling provides, and [Config](/docs/app-zon) documents `app.json` and its `app.zon` compatibility format.
|
||||
|
||||
@@ -14,7 +14,7 @@ The Native SDK can configure native app menus from `app.zon` or typed Zig data.
|
||||
},
|
||||
```
|
||||
|
||||
Generated runners load `app.zon` menus automatically. Pass `menus` to `runWithOptions` when an app needs to override the manifest at runtime:
|
||||
Generated zero-config runners load `app.zon` menus automatically for both TypeScript cores and Zig cores. Ejected runners use the same fallback. Pass a non-null `menus` slice to `runWithOptions` when lower-level Zig wiring needs to override the manifest at runtime (an explicit empty slice disables manifest menus):
|
||||
|
||||
```zig
|
||||
const view_items = [_]native_sdk.MenuItem{
|
||||
|
||||
@@ -48,7 +48,7 @@ The runtime owns the rest: install, presentation, resize, typed event dispatch,
|
||||
| `dropdown-menu` | menu surface | vertical; children are `menu-item`s. `anchor="below\|above"` floats it against its parent's frame: a late z-pass above the whole tree, window-clipped (never cropped by a scroll pane), auto-flipping at the window edges, zero flow space — pair with `on-dismiss` so Escape/click-outside close model-side |
|
||||
| `accordion` | accordion | header via the `text` attribute; children show while `selected`, dispatch `on-toggle` |
|
||||
| `alert`, `bubble` | surfaces | `alert` title via the `text` attribute; children stack inside. `bubble` hugs its message up to 80% of the thread (`ghost` exempt; explicit `width` wins) and takes one `<reactions>` child — the reaction pill straddling its bottom edge, one text run, dock via `text-alignment` (default `end`); `text=` on bubble itself is a teaching error (that channel belongs to the pill) |
|
||||
| `dialog`, `drawer`, `sheet` | modal surfaces | rendered in place — title via `text`, wrap in `<if>` to show conditionally |
|
||||
| `dialog`, `drawer`, `sheet` | modal surfaces | root-relative and unaffected by ancestor scroll or clipping: dialogs center in the viewport, drawers span its width and dock to the bottom, and sheets span its height and dock to the right — title via `text`, wrap in `<if>` to show conditionally |
|
||||
| `resizable` | resizable panel | engine-managed drag handle; `width` sets the initial width |
|
||||
| `split` | two-pane splitter | exactly two element children (nest splits for more panes); the engine synthesizes the draggable divider between them. `value` binds the model-owned first-pane fraction, `on-resize` names an f32 Msg variant dispatched with each applied fraction (echo it back through `value`), `min-width` on the panes bounds the drag, `gap` sets the divider band thickness; the focused divider takes Left/Right (Shift for bigger steps) and Home/End. `resize-duration` (milliseconds, split only) animates model-driven value moves — the runtime eases the rendered fraction to the new value one presented frame at a time instead of snapping, and reduced-motion appearances snap automatically; `resize-easing` (`linear`/`standard`/`emphasized`/`spring`) shapes the ramp and needs a nonzero duration beside it |
|
||||
| `tree` | disclosure tree | vertical container whose descendant rows with `role="treeitem"` form one roving keyboard focus set: Up/Down walk visible rows (selection follows focus via `on-change` when bound, otherwise `on-press`), Left collapses or moves to the parent row, Right expands or moves to the first child row, Home/End jump to the edges, Enter/Space activate; nested rows derive hierarchy structurally, while flat `<for>` rows declare their one-based `tree-level`; expandable rows bind `expanded` and `on-toggle`, and the model owns both states |
|
||||
@@ -248,7 +248,7 @@ pub fn draft(model: *const Model) []const u8 {
|
||||
|
||||
On a `list-item`, `on-submit` grows a second home beyond text entry: with a submit handler bound, plain Enter on a ring-focused row dispatches it as the row's PRIMARY action (open the record, play the track — the desktop list convention), while Space keeps the row's select activation (`on-press`). Rows without a submit handler resolve Enter exactly as before — both activation keys select. It is the keyboard mirror of `on-double-press`: bind both to the same Msg and pointer and keyboard users share one primary action (the soundboard's Zig track rows bind `on_press` select, `on_double_press` play, `on_submit` play). How Enter reaches the row at all — and why arrows may not — is [keyboard routing](#keyboard-routing-focus-registers-quiet-list-rows-and-the-app-level-fallback).
|
||||
|
||||
Presses follow one rule: a click lands on the nearest pressable widget under the pointer — plain text, icons, images, badges, and layout containers let it fall through to their closest pressable ancestor, and dragging still selects text. Any element with a bound `on-press`, `on-double-press`, or `on-toggle` is pressable (the handler makes it a hit target), so a pressable row is just `<panel on-press="open:{id}">` — or `<row on-press=...>` — with plain text children: no overlays, no duplicated handlers. Nested pressables resolve to the deepest one (a button inside a pressable row wins); editable text fields, scroll containers, and modal surfaces always claim their own presses. Value/text handlers (`on-change`, `on-submit`, `on-input`) still belong on controls only — the validator, both engines, and the LSP reject them on layout/decoration elements with a teaching error.
|
||||
Presses follow one rule: a click lands on the nearest press-claiming widget under the pointer — plain text, icons, images, badges, and layout containers let it fall through to their closest interactive ancestor, and dragging still selects text. Any element with `on-press`, `on-double-press`, `on-toggle`, `on-hold`, or `on-drag` becomes a hit target and press claimer, so a row can bind `on-press` for clicks or `on-drag` for spatial dragging and child text still resolves to that row without overlays or duplicated handlers. Nested claimers resolve to the deepest one; editable text fields, scroll containers, and modal surfaces always claim their own presses. Value/text handlers (`on-change`, `on-submit`, `on-input`) still belong on controls only — the validator, both engines, and the LSP reject them on layout/decoration elements with a teaching error.
|
||||
|
||||
## Keyboard routing: focus registers, quiet list rows, and the app-level fallback
|
||||
|
||||
@@ -320,7 +320,7 @@ export function dropMsg(drop: FileDropEvent): Msg | null {
|
||||
}
|
||||
```
|
||||
|
||||
The platform event is journaled before either route, so record/replay delivers the identical source, point, and path bytes. A host that cannot resolve the target view leaves `viewLabel` empty and `point` null.
|
||||
The platform event is journaled before either route, so record/replay delivers the identical source, point, and path bytes. The macOS system host reports labeled, view-local points for canvas and WebView drops; an unlabeled window region keeps an empty `viewLabel` and reports a window-content point. A host that cannot resolve either leaves `viewLabel` empty and `point` null.
|
||||
|
||||
## Native scrolling and context menus
|
||||
|
||||
@@ -328,7 +328,7 @@ On macOS, every non-virtualized `scroll` region is driven by an invisible `NSScr
|
||||
|
||||
Scroll regions pin at their content edges by default on every path — no rubber-band bounce; kinetic motion stops cleanly at the boundary. Bouncing is a per-region opt-in: `overscroll="rubber_band"` in markup or `ElementOptions.overscroll = .rubber_band` in Zig views (the native macOS scroller gets elastic edges, the engine physics overscroll under resistance and spring back). The `ScrollPhysics.overscroll` design token flips the app-wide default; per-region values override it, and `overscroll="none"` pins a region regardless of the token.
|
||||
|
||||
Right/ctrl-click presents a real OS context menu at the pointer: `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux. Authors declare ONE menu and the platform decides presentation: hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today) mount the same declared items as an anchored canvas surface at the click point automatically — never two authored menus, never a canvas imitation where the OS menu exists. In markup, the menu is a `<context-menu>` child of the pressable element it answers, holding `menu-item`s (`on-press` required, `disabled` optional) and `separator`s, with `if`/`else`/`for` to swap or repeat items:
|
||||
Right/ctrl-click presents a real OS context menu at the pointer: `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux. Authors declare ONE menu and the platform decides presentation: hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today) mount the same declared items as an anchored canvas surface at the click point automatically — never two authored menus, never a canvas imitation where the OS menu exists. In markup, the menu is a `<context-menu>` child of the interactive element it answers — a hit-target kind or an element made interactive by `on-press`, `on-double-press`, `on-toggle`, `on-hold`, or `on-drag` — holding `menu-item`s (`on-press` required, `disabled` optional) and `separator`s, with `if`/`else`/`for` to swap or repeat items:
|
||||
|
||||
```html
|
||||
<list-item on-press="open:{entry.id}" label="{entry.title}">
|
||||
@@ -437,7 +437,7 @@ case "fetched":
|
||||
|
||||
Response bodies are binary-safe and bounded (256 KiB; longer arrives cut with `truncated = true`), the whole exchange honors a per-fetch timeout (default 30 s), and cancelling a fetch delivers exactly one `cancelled` Msg with nothing after it.
|
||||
|
||||
Files ride the same channel: `fx.writeFile` / `fx.readFile` persist app state — session snapshots, transcripts — without smuggling an `Io` handle from `main` into `update`. Bounded (1 KiB paths, 1 MiB files), key-based, one terminal Msg per operation with an explicit outcome (`ok`, `not_found`, `io_failed`, `truncated` — an over-bound read's own outcome, so a cut JSON snapshot cannot parse as whole — `rejected`, `cancelled`); writes create missing parent directories and replace the file whole:
|
||||
Files ride the same channel without smuggling an `Io` handle from `main` into `update`. Whole-file reads and writes remain bounded at 1 MiB; `appendFile`, `statFile`, and `deleteFile` handle logs and file lifecycle, while the dedicated streaming family carries large imports and atomic exports. External paths require the `filesystem` permission; app-owned directories are exempt after symlink-safe normalization. See [Files & Streaming](/docs/files).
|
||||
|
||||
```zig
|
||||
.save => fx.writeFile(.{
|
||||
@@ -449,24 +449,30 @@ Files ride the same channel: `fx.writeFile` / `fx.readFile` persist app state
|
||||
.saved => |result| model.noteSaved(result.outcome),
|
||||
```
|
||||
|
||||
Desktop notifications use the same bounded platform-services facade but are intentionally fire-and-forget: once the host accepts a request, the OS can still suppress it through Focus / Do Not Disturb or the user's notification settings, so a success Msg would over-promise. Return `Cmd.showNotification` from a TypeScript core or call `fx.showNotification` from a Zig `update_fx` arm; invalid or over-bound fields and unavailable services fail closed. The fake executor and session replay never display one.
|
||||
Desktop notifications use the same bounded platform-services facade but are intentionally fire-and-forget: once the host accepts a request, the OS can still suppress it through Focus / Do Not Disturb or the user's notification settings, so a success Msg would over-promise. A nonempty `id` replaces an earlier notification with that id. Paired `actionLabel` / `actionCommand` fields add an action whose activation returns through the ordinary application command path while the process is running. Return `Cmd.showNotification` from a TypeScript core or call `fx.showNotification` from a Zig `update_fx` arm; invalid or over-bound fields, unpaired action fields, invalid command names, and unavailable services fail closed. The fake executor and session replay never display one; the null test platform records and can activate one deterministically.
|
||||
|
||||
<CodeToggle>
|
||||
|
||||
```ts
|
||||
case "build_finished":
|
||||
return [model, Cmd.showNotification({
|
||||
id: asciiBytes("build-status"),
|
||||
title: asciiBytes("Build finished"),
|
||||
subtitle: asciiBytes("native-sdk"),
|
||||
body: asciiBytes("All checks passed."),
|
||||
actionLabel: asciiBytes("Open results"),
|
||||
actionCommand: asciiBytes("build.open-results"),
|
||||
})];
|
||||
```
|
||||
|
||||
```zig
|
||||
.build_finished => fx.showNotification(.{
|
||||
.id = "build-status",
|
||||
.title = "Build finished",
|
||||
.subtitle = "native-sdk",
|
||||
.body = "All checks passed.",
|
||||
.action_label = "Open results",
|
||||
.action_command = "build.open-results",
|
||||
}),
|
||||
```
|
||||
|
||||
@@ -478,7 +484,9 @@ For timestamps, the facade owns the clocks (Zig 0.16 puts `std.time` behind `std
|
||||
|
||||
## Secondary windows
|
||||
|
||||
Windows are model-declared, exactly like the tray: `Options.windows_fn` returns the window descriptors that should exist right now (presence IS visibility — there is no `visible` flag because the platform window channel is create/focus/close with no hide), and `Options.window_view` builds each declared window's whole canvas tree, keyed by the descriptor's window label. After every dispatched Msg the runtime reconciles: windows the model started declaring are created (a source-less native window wearing one `gpu_surface` view with the descriptor's `canvas_label`, inheriting the main canvas's gpu options), windows it stopped declaring close, and every open window's view rebuilds from the same model — a theme picked in the settings window restyles the main window on the same dispatch.
|
||||
In a default TypeScript app, export `windows(model): readonly WindowDescriptor[]` and put each possible window's markup at `src/windows/<label>.native`; the generated launcher compiles those files, maps descriptor labels to them, and includes shared components they import from nested paths under `src/windows/`. Construct descriptors with `windowDescriptor`. `restorePolicy: "center_on_primary"` centers a fresh descriptor with no `x`/`y` on macOS; Windows and Linux currently keep their native default placement. The default policy is `"clamp_to_visible_screen"`. `closePolicy: "quit"` routes `onCloseCommand` through `commandMsg`; `"hide"` retains the window and dispatches no close command. `titlebar` includes `"chromeless"` for fully skinned windows. `examples/system-monitor-ts` is the complete default-path reference.
|
||||
|
||||
In Zig cores and custom wiring, windows are model-declared exactly like the tray: `Options.windows_fn` returns the window descriptors that should exist right now (presence IS liveness), and `Options.window_view` builds each declared window's whole canvas tree, keyed by the descriptor's window label. After every dispatched Msg the runtime reconciles: windows the model started declaring are created (a source-less native window wearing one `gpu_surface` view with the descriptor's `canvas_label`, inheriting the main canvas's gpu options), windows it stopped declaring close, and every open window's view rebuilds from the same model — a theme picked in the settings window restyles the main window on the same dispatch. There is no `visible` flag: transient visibility is host state changed through `hideWindow`/`showWindow` or a `.hide` close policy; stop declaring a window to really close it and release its retained views.
|
||||
|
||||
```zig
|
||||
fn windows(model: *const Model, scratch: *App.WindowsScratch) []const App.WindowDescriptor {
|
||||
@@ -507,7 +515,7 @@ fn windowView(ui: *App.Ui, model: *const Model, window_label: []const u8) App.Ui
|
||||
// options: .windows_fn = windows, .window_view = windowView,
|
||||
```
|
||||
|
||||
Input from any window dispatches Msgs through that window's own tree with its window identity, and a USER close (the titlebar close button) dispatches the descriptor's `on_close` Msg — the dismissal precedent applied to windows: the window is already gone as an optimistic echo, the model clears its open flag in `update` (or keeps declaring the window and the next rebuild brings it back — source wins). A close the model itself initiated (it stopped declaring the window) never echoes a Msg. At most `UiApp.max_ui_windows` (4) secondary windows may be declared; the excess warns and is ignored. `min_width`/`min_height` declare a content min-size floor the window itself enforces (macOS `contentMinSize`): the user's resize stops at the floor instead of the layout clamping and clipping panes below their declared minimums — the same fields exist on app.zon windows and `ShellWindow`, and the startup window applies its declaration at the host create like `titlebar`. 0 (the default) leaves the axis at the platform's own minimum.
|
||||
Input from any window dispatches Msgs through that window's own tree with its window identity. `WindowDescriptor.close_policy` accepts `.quit` (the default) or `.hide`, with the same behavior and platform safeguards as manifest windows. A USER close under `.quit` dispatches the descriptor's `on_close` Msg — the dismissal precedent applied to windows: the window is already gone as an optimistic echo, the model clears its open flag in `update` (or keeps declaring the window and the next rebuild brings it back — source wins). Under `.hide`, the window stays alive with its views, `on_close` does not fire, and `showWindow(label)` brings it back. A close the model itself initiated (it stopped declaring the window) never echoes a Msg. At most `UiApp.max_ui_windows` (4) secondary windows may be declared; the excess warns and is ignored. `min_width`/`min_height` declare a content min-size floor the window itself enforces (macOS `contentMinSize`): the user's resize stops at the floor instead of the layout clamping and clipping panes below their declared minimums — the same fields exist on app.zon windows and `ShellWindow`, and the startup window applies its declaration at the host create like `titlebar`. 0 (the default) leaves the axis at the platform's own minimum.
|
||||
|
||||
Markup deliberately binds ONE window's content: there is no `window` element in the closed grammar, because windows are app-shell concerns, not view-tree concerns. A markup-authored secondary window is a `canvas.CompiledMarkupView` whose `build` the `window_view` fn calls for the matching label.
|
||||
|
||||
@@ -565,9 +573,27 @@ ui.image(.{ .image = model.chart_image, .width = 120, .height = 80, .semantics =
|
||||
<avatar image="{avatar_image}" label="Octocat">OC</avatar>
|
||||
```
|
||||
|
||||
One registered image can be a texture atlas. `ElementOptions.image_src` is an optional `geometry.RectF` in decoded-image pixel coordinates; Native markup spells the same rectangle with all four source attributes. The widget frame stays the destination, a source rectangle crossing the registered image bounds is clipped, and cropped widgets use nearest sampling so adjacent atlas regions cannot bleed into the tile:
|
||||
|
||||
```zig
|
||||
ui.image(.{
|
||||
.image = model.atlas_image,
|
||||
.image_src = geometry.RectF.init(64, 32, 24, 24),
|
||||
.width = 48,
|
||||
.height = 48,
|
||||
.semantics = .{ .label = "Warning badge" },
|
||||
})
|
||||
```
|
||||
|
||||
```html
|
||||
<image image="{atlas_image}" source-x="64" source-y="32"
|
||||
source-width="24" source-height="24" width="48" height="48"
|
||||
label="Warning badge" />
|
||||
```
|
||||
|
||||
`fx.registerImage(id, width, height, rgba8)` registers already-decoded pixels (the runtime copies them; your buffer is free on return), `fx.registerImageBytes(id, bytes)` decodes through the platform codec first, and `fx.unregisterImage(id)` frees the slot. Re-registering an id replaces its pixels and every view repaints — GPU caches re-upload off the changed content fingerprint, no invalidation calls. For caches, mint fresh ids (effect-key style, monotonically increasing) and unregister the evictee — never re-key different content onto a live id. Outside `UiApp`, the same registry is `Runtime.registerCanvasImage`/`registerCanvasImageBytes`/`unregisterCanvasImage`.
|
||||
|
||||
Capacities are fixed and loud (`canvas_limits`): `max_registered_canvas_images` slots (16) of `max_registered_canvas_image_pixel_bytes` each (1 MiB — 512×512 RGBA8, avatar/icon scale), with `error.ImageRegistryFull`, `error.ImageTooLarge`, `error.ImageDecodeFailed`, and `error.UnsupportedService` (a platform without a codec) never silent. Registered images render everywhere the canvas does: live presentation (GPU packet and software paths), `renderCanvasScreenshot`, and automation screenshots. A draw referencing an id that is not (or no longer) registered skips — a pure view cannot fail presentation with a transient loading state. In tests, the null platform's `image_decode` flag enables a deterministic decoder for the strict PNG subset `canvas.png.writeRgba8` emits, so raw RGBA fixtures exercise the full decode→register→draw path without bundling a codec.
|
||||
Capacities are fixed and loud (`canvas_limits`): 16 slots with a 1 MiB decoded-pixel target by default. Encoded photos decode aspect-preservingly to fit, while raw `fx.registerImage` pixels remain strict. Image-centric apps may raise the startup-frozen target through app.zon `.images.max_image_pixel_bytes`, up to 8 MiB; storage is lazy per used slot, but filling all 16 ceiling-sized slots is a declared 128 MiB high-water. Every encoded entry point, including direct `fx.registerImageBytes`, shares the flat 8 MiB source bound. `error.ImageRegistryFull`, `error.ImageTooLarge` (encoded source, raw pixels, or a codec-contract violation), `error.ImageDecodeFailed`, and `error.UnsupportedService` are never silent. Registered images render everywhere the canvas does: live presentation, screenshots, and automation. A missing id simply draws its fallback. In tests, the null platform's deterministic strict-PNG decoder also pins exact decode-to-fit dimensions and pixels.
|
||||
|
||||
## Rich text: inline spans and markdown
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Packaging
|
||||
|
||||
The Native SDK provides tooling to bundle your app into distributable packages for macOS, Linux, and Windows. A native-rendered app packages as a single binary plus icons, metadata, and whatever lives in your `assets/` directory — no browser runtime. On macOS the asset tree is mirrored into the bundle at its app-relative path (`Contents/Resources/assets/`), and the packaged app resolves relative asset paths (audio files, bundled fonts) against `Contents/Resources`, so a path like `assets/music/track.mp3` names the same file in a dev run and in the installed app. Keep large optional data out of `assets/` when you package, or it ships. The frontend-asset and CEF sections below apply only to apps that [embed web content](/docs/frontend); Chromium packaging is currently supported for macOS and includes the CEF runtime when `.web_engine = "chromium"` and the matching CEF layout is installed.
|
||||
The Native SDK provides tooling to bundle your app into distributable packages for macOS, Linux, and Windows. A native-rendered app packages as a single binary plus icons, metadata, and whatever lives in your `assets/` directory — no browser runtime. On macOS the asset tree is mirrored into the bundle at its app-relative path (`Contents/Resources/assets/`), and the packaged app resolves relative asset paths (audio files, bundled fonts) against `Contents/Resources`, so a path like `assets/music/track.mp3` names the same file in a dev run and in the installed app. Keep large optional data out of `assets/` when you package, or it ships. The frontend-asset and CEF sections below apply only to apps that [embed web content](/docs/frontend); Chromium packaging is currently supported for macOS and includes the CEF runtime when `"web_engine": "chromium"` and the matching CEF layout is installed.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -11,7 +11,7 @@ native build
|
||||
native package --target macos
|
||||
```
|
||||
|
||||
`native package` picks up the manifest at `app.zon` and the binary at `zig-out/bin/<name>` automatically; pass `--manifest`, `--binary`, or the other flags below for more control. Zero-config apps package without ejecting: the two commands above are the complete path from source to distributable, and `native eject` exists only for apps that want to own their build files. Apps that do own their build ([ejected](/docs/cli) or scaffolded with `--full`) also get a `zig build package` step that wires the same thing into the build graph.
|
||||
`native package` picks up `app.json` (falling back to `app.zon`) and the binary at `zig-out/bin/<name>` automatically; pass `--manifest`, `--binary`, or the other flags below for more control. Zero-config apps package without ejecting: the two commands above are the complete path from source to distributable, and `native eject` exists only for apps that want to own their build files. Apps that do own their build ([ejected](/docs/cli) or scaffolded with `--full`) also get a `zig build package` step that wires the same thing into the build graph.
|
||||
|
||||
## Build options
|
||||
|
||||
@@ -36,7 +36,7 @@ The build system exposes options that control platform, web engine, and build fe
|
||||
<tr>
|
||||
<td><code>-Dweb-engine</code></td>
|
||||
<td><code>system</code>, <code>chromium</code></td>
|
||||
<td><code>app.zon</code></td>
|
||||
<td>App manifest</td>
|
||||
<td>Temporary WebView engine override; Chromium is currently macOS-only</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -72,27 +72,27 @@ The build system exposes options that control platform, web engine, and build fe
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## app.zon packaging fields
|
||||
## App manifest packaging fields
|
||||
|
||||
The manifest drives packaging metadata:
|
||||
|
||||
```zig
|
||||
.{
|
||||
.id = "com.example.myapp",
|
||||
.name = "myapp",
|
||||
.display_name = "My App",
|
||||
.version = "1.0.0",
|
||||
.icons = .{"assets/icon.png"},
|
||||
.platforms = .{ "macos", "linux" },
|
||||
.web_engine = "system",
|
||||
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
|
||||
.dmg = .{
|
||||
.background = "assets/dmg-background.png",
|
||||
.window_width = 660,
|
||||
.window_height = 400,
|
||||
.app_position = .{ .x = 166, .y = 182 },
|
||||
.applications_position = .{ .x = 486, .y = 182 },
|
||||
},
|
||||
```json:app.json
|
||||
{
|
||||
"id": "com.example.myapp",
|
||||
"name": "myapp",
|
||||
"display_name": "My App",
|
||||
"version": "1.0.0",
|
||||
"icons": ["assets/icon.png"],
|
||||
"platforms": ["macos", "linux"],
|
||||
"web_engine": "system",
|
||||
"cef": { "dir": "third_party/cef/macos", "auto_install": false },
|
||||
"dmg": {
|
||||
"background": "assets/dmg-background.png",
|
||||
"window_width": 660,
|
||||
"window_height": 400,
|
||||
"app_position": { "x": 166, "y": 182 },
|
||||
"applications_position": { "x": 486, "y": 182 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,7 +141,7 @@ The manifest drives packaging metadata:
|
||||
|
||||
## App icons
|
||||
|
||||
Drop one square image in your project — `assets/icon.png` (1:1, ideally 1024x1024) or `assets/icon.svg` — list it in `.icons`, and packaging generates what each platform needs: a complete `.icns` for macOS (with the platform's rounded-rectangle icon shape and margins applied automatically, so full-bleed artwork looks native in the Dock), a multi-size `.ico` for Windows, and hicolor PNG size sets for Linux, plus asset-catalog and launcher-mipmap images for the mobile host skeletons. Artwork that already has transparent corners is treated as pre-shaped and ships unmasked. For art-directed control, a prebuilt `.icns` (macOS) or `.ico` (Windows) in `.icons` always wins untouched. Everything is generated by the SDK's own rasterizer and encoders — no external tools. `native validate` checks the source up front (square, decodable, large enough) with the same messages packaging prints.
|
||||
Drop one square image in your project — `assets/icon.png` (1:1, ideally 1024x1024) or `assets/icon.svg` — list it in `icons`, and packaging generates what each platform needs: a complete `.icns` for macOS (with the platform's rounded-rectangle icon shape and margins applied automatically, so full-bleed artwork looks native in the Dock), a multi-size `.ico` for Windows, and hicolor PNG size sets for Linux, plus asset-catalog and launcher-mipmap images for the mobile host skeletons. Artwork that already has transparent corners is treated as pre-shaped and ships unmasked. For art-directed control, a prebuilt `.icns` (macOS) or `.ico` (Windows) in `icons` always wins untouched. Everything is generated by the SDK's own rasterizer and encoders — no external tools. `native validate` checks the source up front (square, decodable, large enough) with the same messages packaging prints.
|
||||
|
||||
## macOS
|
||||
|
||||
@@ -151,7 +151,7 @@ Drop one square image in your project — `assets/icon.png` (1:1, ideally 1024x1
|
||||
|
||||
- `Contents/MacOS/<binary>` -- the compiled executable
|
||||
- `Contents/Resources/AppIcon.icns` -- the app icon, generated from your icon source (or your prebuilt `.icns`, copied untouched under its own name)
|
||||
- `Contents/Info.plist` -- generated from `app.zon`
|
||||
- `Contents/Info.plist` -- generated from the app manifest
|
||||
- `Contents/Resources/assets/` -- the app's asset tree, mirrored at its app-relative path so runtime asset paths resolve inside the bundle
|
||||
- `Contents/Resources/dist/` -- frontend assets (if configured; replaces the `assets/` mirror)
|
||||
|
||||
@@ -170,19 +170,19 @@ native package --target macos --archive
|
||||
|
||||
Without any extra configuration, Native creates a 660×400 Finder window with a quiet generated background and arrow, packages matching 1× and 2× representations for crisp Retina rendering, positions the app and an `/Applications` alias on either side, hides the Finder chrome, and compresses the result as a `.dmg`. The package diagnostic prints both the `.app` and `.dmg` paths.
|
||||
|
||||
Customize the presentation in `app.zon`:
|
||||
Customize the presentation in `app.json`:
|
||||
|
||||
```zig
|
||||
.dmg = .{
|
||||
.volume_name = "My App",
|
||||
.background = "assets/dmg-background.png",
|
||||
.window_width = 720,
|
||||
.window_height = 440,
|
||||
.icon_size = 144,
|
||||
.app_position = .{ .x = 180, .y = 210 },
|
||||
.applications_position = .{ .x = 540, .y = 210 },
|
||||
.applications_link = true,
|
||||
},
|
||||
```json:app.json
|
||||
"dmg": {
|
||||
"volume_name": "My App",
|
||||
"background": "assets/dmg-background.png",
|
||||
"window_width": 720,
|
||||
"window_height": 440,
|
||||
"icon_size": 144,
|
||||
"app_position": { "x": 180, "y": 210 },
|
||||
"applications_position": { "x": 540, "y": 210 },
|
||||
"applications_link": true
|
||||
}
|
||||
```
|
||||
|
||||
The window dimensions are the usable background canvas, excluding Finder's title bar. Positions are icon centers measured from the canvas's top-left corner. The background must be a project-relative PNG, JPEG, or TIFF at the configured window size; Finder displays it at its natural size. `native validate` and packaging reject malformed images or dimensions that do not match the configured canvas. For Retina artwork, put a double-sized sibling next to it using the `@2x` convention—for example, `dmg-background.png` at 720×440 and `dmg-background@2x.png` at 1440×880. Native discovers the pair, verifies that the sibling is exactly double-sized, and packages both representations. A prebuilt multi-resolution TIFF also works.
|
||||
@@ -191,19 +191,19 @@ Omitting `background` keeps Native's generated Retina-aware gradient and draws t
|
||||
|
||||
For complete control over which Finder items appear and where they sit, replace the fixed app/Applications pair with `items`:
|
||||
|
||||
```zig
|
||||
.dmg = .{
|
||||
.background = "assets/dmg-background.png",
|
||||
.window_width = 760,
|
||||
.window_height = 480,
|
||||
.icon_size = 112,
|
||||
.items = .{
|
||||
.{ .kind = "app", .position = .{ .x = 150, .y = 180 } },
|
||||
.{ .kind = "applications", .position = .{ .x = 610, .y = 180 } },
|
||||
.{ .kind = "file", .path = "README.pdf", .name = "Read Me.pdf", .position = .{ .x = 250, .y = 370 } },
|
||||
.{ .kind = "link", .path = "/Library/QuickLook", .name = "QuickLook", .position = .{ .x = 510, .y = 370 } },
|
||||
},
|
||||
},
|
||||
```json:app.json
|
||||
"dmg": {
|
||||
"background": "assets/dmg-background.png",
|
||||
"window_width": 760,
|
||||
"window_height": 480,
|
||||
"icon_size": 112,
|
||||
"items": [
|
||||
{ "kind": "app", "position": { "x": 150, "y": 180 } },
|
||||
{ "kind": "applications", "position": { "x": 610, "y": 180 } },
|
||||
{ "kind": "file", "path": "README.pdf", "name": "Read Me.pdf", "position": { "x": 250, "y": 370 } },
|
||||
{ "kind": "link", "path": "/Library/QuickLook", "name": "QuickLook", "position": { "x": 510, "y": 370 } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
An explicit list must contain exactly one `app`; its optional `name` changes only the bundle name shown in the DMG. `applications` creates the `/Applications` alias. `file` copies a project-relative file or directory, with an optional display `name`, while `link` creates a named symbolic link to an absolute path. When `items` is present, it replaces `app_position`, `applications_position`, and `applications_link`. The generated background draws its arrow whenever the list includes both `app` and `applications`.
|
||||
@@ -220,7 +220,7 @@ Linux packaging creates an install tree:
|
||||
- `share/mime/packages/<name>.xml` -- shared MIME metadata when file associations are configured
|
||||
|
||||
```bash
|
||||
native package --target linux --manifest app.zon --binary zig-out/bin/MyApp
|
||||
native package --target linux --manifest app.json --binary zig-out/bin/MyApp
|
||||
```
|
||||
|
||||
Configured file associations and URL schemes are added to the desktop file `MimeType` list. Extension-only associations get generated `application/x-...` MIME types with glob patterns in the shared MIME package.
|
||||
@@ -228,7 +228,7 @@ Configured file associations and URL schemes are added to the desktop file `Mime
|
||||
## Windows
|
||||
|
||||
```bash
|
||||
native package --target windows --manifest app.zon --binary zig-out/bin/MyApp.exe
|
||||
native package --target windows --manifest app.json --binary zig-out/bin/MyApp.exe
|
||||
```
|
||||
|
||||
Windows packaging is in early development. The packager copies the binary and assets into a distributable directory structure and writes a multi-size `app-icon.ico` generated from your icon source (or copies a prebuilt `.ico` from `.icons` untouched). When file associations or URL schemes are configured, the artifact also includes `install/register-file-types.ps1`, which registers the package-local executable under the current user's `HKCU\Software\Classes` registry keys.
|
||||
@@ -245,13 +245,13 @@ zig build bundle-assets
|
||||
|
||||
This copies the configured `dist` directory into the build output. Production packages serve these through `zero://app/`, so paths like `/assets/app.js` work without `file://` URLs.
|
||||
|
||||
### Configure in app.zon
|
||||
### Configure in app.json
|
||||
|
||||
```zig
|
||||
.frontend = .{
|
||||
.dist = "dist",
|
||||
.entry = "index.html",
|
||||
.spa_fallback = true,
|
||||
```json:app.json
|
||||
"frontend": {
|
||||
"dist": "dist",
|
||||
"entry": "index.html",
|
||||
"spa_fallback": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -296,7 +296,7 @@ zig build
|
||||
native package --target macos
|
||||
```
|
||||
|
||||
Set `.web_engine = "chromium"` and `.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }` in `app.zon` for the normal Chromium package path. Use `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, or `--cef-dir` only when you need a one-off override.
|
||||
Set `"web_engine": "chromium"` and `"cef": { "dir": "third_party/cef/macos", "auto_install": false }` in `app.json` for the normal Chromium package path. Use `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, or `--cef-dir` only when you need a one-off override.
|
||||
|
||||
Verify the Chromium macOS package layout locally with:
|
||||
|
||||
@@ -321,8 +321,8 @@ This renders the full icon family (16 through 1024 px with @2x variants) through
|
||||
Check that your manifest and environment are ready for packaging:
|
||||
|
||||
```bash
|
||||
native doctor --manifest app.zon --strict
|
||||
native validate app.zon
|
||||
native doctor --manifest app.json --strict
|
||||
native validate app.json
|
||||
```
|
||||
|
||||
`doctor` checks the host environment, WebView availability, manifest validity, log paths, and optional CEF paths. Add `--strict` to fail on any warning. See [Debugging](/docs/debugging) for details on what `native doctor` checks.
|
||||
|
||||
@@ -78,6 +78,14 @@ macOS, Linux, and Windows run full desktop apps through their own platform hosts
|
||||
<td><Tier tier="none" /></td>
|
||||
<td><Tier tier="none" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System notifications</td>
|
||||
<td><Tier tier="full" note="same-id replacement and a native action button; actions dispatch while the app is running" fn={11} /></td>
|
||||
<td><Tier tier="caveats" note="one in-flight legacy balloon, so every new notification replaces the current one; there is no custom button, and clicking the notification invokes its labeled action" fn={11} /></td>
|
||||
<td><Tier tier="full" note="same-id replacement and a native action button; actions dispatch while the app is running" fn={11} /></td>
|
||||
<td><Tier tier="none" /></td>
|
||||
<td><Tier tier="none" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Menu-bar app lifecycle (<code>close_policy = "hide"</code>)</td>
|
||||
<td><Tier tier="full" note="close hides, the Dock reopen re-shows, tray Open/Quit drive showWindow/quitApp" /></td>
|
||||
@@ -131,6 +139,7 @@ macOS, Linux, and Windows run full desktop apps through their own platform hosts
|
||||
8. `native package` targets all five platforms: macOS gets a `.app` bundle plus a styled drag-to-Applications DMG with `--archive`, Linux an install tree, Windows a distributable directory with a per-user file-type registration script, iOS a complete generated Xcode project — toolkit host sources, Info.plist, asset catalog, shared scheme, and the device-slice embed library — that `xcodebuild archive` builds with zero edits (code signing stays a manual step, like notarization), and Android a complete generated host project whose debug APK assembles with zero edits, directly with the SDK's build tools (store signing keys stay a manual step). Every platform's icons generate from one square source image. See [Packaging](/docs/packaging).
|
||||
9. macOS signing supports `adhoc` and `identity` modes with entitlements; notarization is submitted manually with the platform tools after packaging. No signing tooling exists yet for the other platforms. See [Code Signing](/docs/packaging/signing).
|
||||
10. The automation server is a file-based protocol the runtime serves on every desktop platform: snapshots, assertions, synthetic input, screenshots, record/replay. Engine screenshots render through the deterministic CPU reference renderer on every platform, so they are byte-comparable across hosts. Mobile exposes accessibility snapshots and actions through the embed ABI; the iOS and Android toolkit hosts serve the same file-based protocol inside the app's data container when launched with automation enabled.
|
||||
11. Desktop notification identifiers are app-local replacement keys. macOS and Linux expose the requested action as a native button. The current Windows host uses the in-box `Shell_NotifyIcon` balloon surface, which has no app-defined action button; it includes the label in the notification and treats a notification click as activation. All three deliver action commands only to the process that issued the notification. Linux notification shells may D-Bus-activate an exited app when a persisted notification is clicked, but its process-scoped action token is stale and cannot dispatch a command into the new runtime; macOS and Windows cold-start command activation is not wired.
|
||||
|
||||
## Desktop Hosts
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Native SDK is the complete toolkit for building beautiful native desktop applica
|
||||
## Prerequisites
|
||||
|
||||
- macOS 11 or newer, Linux, or Windows
|
||||
- Node.js 22.15+ (on the 23 line: 23.5+) for the default TypeScript scaffold — the TypeScript frontend (the checker) and the core dev loop run under it at build and dev time, and the external core compiler that builds the checked core to native code ships as an exact-pinned dependency of the CLI; the binary you ship carries no JS runtime. A Zig-core app (`--template zig-core`) needs Node only when it declares relational SQLite, whose schema checker and migration generator run at build time.
|
||||
- Node.js 24+ for the default TypeScript scaffold — the TypeScript frontend (the checker), scriptc compiler, and core dev loop run under it at build and dev time; scriptc 0.0.33 uses Node 24's compile cache to accelerate repeated invocations. The binary you ship carries no JS runtime. A Zig-core app (`--template zig-core`) needs Node only when it declares relational SQLite, whose schema checker and migration generator run at build time.
|
||||
|
||||
## Get the CLI
|
||||
|
||||
@@ -47,8 +47,8 @@ This scaffolds a native-rendered app — and nothing else. There are no build fi
|
||||
<td>The entire UI: elements, layout, bindings, and message dispatch</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>app.zon</code></td>
|
||||
<td>App manifest: identity, window and view declarations, permissions, security policy</td>
|
||||
<td><code>app.json</code></td>
|
||||
<td>App manifest: identity, window and view declarations, permissions, security policy. Its <code>$schema</code> enables editor completion and validation; existing <code>app.zon</code> manifests remain supported.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>assets/icon.png</code></td>
|
||||
@@ -271,13 +271,13 @@ Because the subset is executable TypeScript, the same file runs unmodified under
|
||||
native check
|
||||
```
|
||||
|
||||
`native check` validates the whole tree without building anything: `src/core.ts` runs the subset checker (typecheck plus the app-core rules, with teaching diagnostics that name the rule, the fix, and the reason), then every `.native` file under `src/` and `app.zon`:
|
||||
`native check` validates the whole tree without building anything: `src/core.ts` runs the subset checker (typecheck plus the app-core rules, with teaching diagnostics that name the rule, the fix, and the reason), then every `.native` file under `src/` and `app.json`:
|
||||
|
||||
```
|
||||
model contract: not yet built - bindings and app: icon names checked structurally only; run `native test` to enable typed checks
|
||||
src/app.native: ok
|
||||
info[manifest.valid]: app.zon is valid
|
||||
checked 1 markup file, app.zon and src/core.ts (subset checker clean)
|
||||
info[manifest.valid]: app.json is valid
|
||||
checked 1 markup file, app.json and src/core.ts (subset checker clean)
|
||||
```
|
||||
|
||||
The first line is honest about what a fresh tree can check: once a build has produced the model contract, the markup pass also verifies bindings, iterables, and message tags against the core's `Model`/`Msg`. Markup errors come back with `file:line:column` and a teaching message (`native markup lsp` provides the same diagnostics plus completion and hover in your editor). `native test` runs the app's test suite; the Zig template additionally scaffolds `src/tests.zig` — full-loop UI tests that click buttons through typed dispatch, headless, on any machine. See [Testing](/docs/testing) for the full tiers, including driving the live app from the outside with [automation](/docs/automation).
|
||||
@@ -294,7 +294,7 @@ This produces an optimized binary and tells you where it landed:
|
||||
built zig-out/bin/my-app (ReleaseFast)
|
||||
```
|
||||
|
||||
(The binary name comes from `app.zon`: `native init my_app` sets `.name = "my-app"`.) Where `native dev` runs a Debug build to arm hot reload, `native build` produces an optimized ReleaseFast binary. The TypeScript core compiles to native code inside it — no JS engine, no interpreter. From there, [Packaging](/docs/packaging) turns it into a distributable app bundle with `native package`.
|
||||
(The binary name comes from `app.json`: `native init my_app` sets `"name": "my-app"`.) Where `native dev` runs a Debug build to arm hot reload, `native build` produces an optimized ReleaseFast binary. The TypeScript core compiles to native code inside it — no JS engine, no interpreter. From there, [Packaging](/docs/packaging) turns it into a distributable app bundle with `native package`.
|
||||
|
||||
## Escape hatch: own the build
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Every app declares `permissions` and `capabilities` in `app.zon` — the runtime
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>filesystem</code></td>
|
||||
<td>File system access from bridge commands</td>
|
||||
<td>Raw file effects and bridge commands outside this app's resolved data/config/cache/state/logs/temp roots; symlinks are resolved before the runtime check</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>clipboard</code></td>
|
||||
@@ -199,7 +199,7 @@ The Native SDK provides built-in commands for app command routing (`native-sdk.c
|
||||
|
||||
`js_window_api` exposes the JavaScript command, window, view, WebView, and platform support helpers, but it does not bypass security. Command routing (`native-sdk.command.invoke`) must come from an allowed origin and have the `command` permission when runtime permissions are configured. Generic native view commands (`native-sdk.view.create`, `list`, `update`, `setFrame`, `setVisible`, `focus`, `focusNext`, `focusPrevious`, `close`) require the `view` permission. Platform support queries, window commands (`native-sdk.window.list`, `create`, `focus`, `close`), and WebView commands (`native-sdk.webview.create`, `list`, `setFrame`, `navigate`, `setZoom`, `setLayer`, `close`) require the `window` permission. The legacy `window` grant is still accepted for command and view helpers for compatibility. View and WebView commands can only target the window that sent the bridge message. WebView URLs must also be allowed by `security.navigation.allowed_origins`, and child WebViews receive `window.zero` only when created with `bridge: true`.
|
||||
|
||||
For broader control, use an explicit `builtin_bridge` policy. When you choose this path, list every built-in command your app calls. Dialog commands (`native-sdk.dialog.openFile`, `saveFile`, `showMessage`), OS commands (`native-sdk.os.openUrl`, `showNotification`, `revealPath`, `addRecentDocument`, `clearRecentDocuments`), clipboard commands (`native-sdk.clipboard.readText`, `writeText`, `read`, `write`), and credential commands (`native-sdk.credentials.set`, `get`, `delete`) are **always default-deny** and require an explicit `builtin_bridge` policy with the command listed. `native-sdk.os.openUrl` also requires `security.navigation.external_links` to allow the target URL:
|
||||
For broader control, use an explicit `builtin_bridge` policy. When you choose this path, list every built-in command your app calls. Dialog commands (`native-sdk.dialog.openFile`, `saveFile`, `showMessage`), OS commands (`native-sdk.os.openUrl`, `showNotification`, `revealPath`, `addRecentDocument`, `clearRecentDocuments`), clipboard commands (`native-sdk.clipboard.readText`, `writeText`, `read`, `write`), and credential commands (`native-sdk.credentials.set`, `get`, `delete`) are **always default-deny** and require an explicit `builtin_bridge` policy with the command listed. `native-sdk.os.openUrl` also requires `security.navigation.external_links` to allow the target URL. A `showNotification` request with `actionLabel` / `actionCommand` additionally requires `native-sdk.command.invoke` and its `command` permission for the same origin:
|
||||
|
||||
```zig
|
||||
.builtin_bridge = .{
|
||||
|
||||
@@ -28,21 +28,84 @@ const MyApp = native_sdk.UiApp(Model, Msg);
|
||||
.theme = app_runner.manifestThemePack(),
|
||||
```
|
||||
|
||||
In a zero-config TypeScript app, export a single-model `themePack` helper when the pack itself belongs in live app state. The generated launcher recognizes this helper and wires the stock-token path automatically:
|
||||
In a zero-config TypeScript app, export `themeState(model)` when pack, color scheme, or accent belongs in live app state. The generated launcher recognizes the helper and wires the stock-token path automatically:
|
||||
|
||||
```ts
|
||||
import { type ThemeState } from "@native-sdk/core/events";
|
||||
|
||||
export type ThemePack = "house" | "geist";
|
||||
export type ThemePreference = "system" | "light" | "dark";
|
||||
|
||||
export interface Model {
|
||||
readonly theme: ThemePack;
|
||||
readonly themePreference: ThemePreference;
|
||||
readonly pinkAccent: boolean;
|
||||
}
|
||||
|
||||
export function themePack(model: Model): ThemePack {
|
||||
return model.theme;
|
||||
export function themeState(model: Model): ThemeState {
|
||||
if (model.pinkAccent) {
|
||||
return {
|
||||
pack: model.theme,
|
||||
colorScheme: model.themePreference,
|
||||
accent: "#df2670",
|
||||
};
|
||||
}
|
||||
return { pack: model.theme, colorScheme: model.themePreference };
|
||||
}
|
||||
```
|
||||
|
||||
Change `model.theme` through ordinary messages (for example, from a pair of model-driven `toggle-button`s). The helper is evaluated on every rebuild. It changes only the built-in pack: system light/dark, high contrast, reduced motion, manifest `theme_accent`, and each surface's scale remain live runtime inputs. Without the helper, `app.zon` remains the static pack choice.
|
||||
`ThemeState` has three optional fields:
|
||||
|
||||
```ts
|
||||
export type ThemeState = {
|
||||
readonly pack?: "house" | "geist";
|
||||
readonly colorScheme?: "light" | "dark" | "system";
|
||||
readonly accent?: string; // exactly #rrggbb
|
||||
};
|
||||
```
|
||||
|
||||
Change the model through ordinary messages (for example, model-driven `toggle-button`s). The helper is evaluated after every committed update. Omitted fields inherit the next lower layer; omitted `colorScheme` and `"system"` both follow the OS. A malformed accent is a loud runtime teaching error, never a silent fallback. High contrast and reduced motion remain live OS inputs; high contrast suppresses both manifest and model accent overrides so accessibility wins.
|
||||
|
||||
The stock-theme precedence is:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Axis</th>
|
||||
<th>Highest to lowest precedence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Complete tokens</td>
|
||||
<td><code>tokens_fn</code> → static <code>tokens</code> → stock theme composition below</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pack</td>
|
||||
<td><code>themeState.pack</code> → <code>app.zon theme</code> → <code>house</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Color scheme</td>
|
||||
<td>forced <code>themeState.colorScheme</code> → OS appearance</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Accent</td>
|
||||
<td><code>themeState.accent</code> → <code>app.zon theme_accent</code> → selected pack</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>High contrast / reduced motion</td>
|
||||
<td>OS appearance (high contrast suppresses accent overrides)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Surface scale / text measurement</td>
|
||||
<td>Runtime-stamped last</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
`themeState` deliberately controls canvas design tokens in v1. Native title bars and WebViews still follow the platform's effective appearance; forcing dark canvas content does not call `NSApp.appearance` or impose a scheme on embedded web content.
|
||||
|
||||
The earlier `themePack(model): "house" | "geist"` helper remains supported unchanged for apps that only switch packs. It preserves live OS scheme, manifest accent, high contrast, reduced motion, and surface scale. Export `themePack` or `themeState`, never both; the checker and adapter report that conflict as a teaching error.
|
||||
|
||||
Apps that derive their own tokens select the pack directly — `ThemeOptions.pack` is just another theme axis, exactly as switchable at runtime as the scheme:
|
||||
|
||||
@@ -113,7 +176,7 @@ pub fn brandTokens(scheme: canvas.ColorScheme, contrast: canvas.ColorContrast) c
|
||||
}
|
||||
```
|
||||
|
||||
Hand it to your app via `tokens_fn` (model-owned, follows the system scheme through your model) or `tokens` (fixed). The runtime stamps `pixel_snap.scale` and text measurement after your function runs, so never cache those.
|
||||
Hand it to your app via `tokens_fn` (model-owned, follows the system scheme through your model) or `tokens` (fixed). These complete-token paths take precedence over `themeState`; the runtime stamps `pixel_snap.scale` and text measurement after your function runs, so never cache those.
|
||||
|
||||
## What themes cannot do
|
||||
|
||||
|
||||
@@ -116,6 +116,21 @@ On macOS, `title` renders the tray as a menu-bar extra: a titled `NSStatusItem`
|
||||
<td><code>ShortcutModifiers</code></td>
|
||||
<td><code>.{}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>segmented</code></td>
|
||||
<td><code>?TraySegmentedRow</code></td>
|
||||
<td><code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>metric</code></td>
|
||||
<td><code>?TrayMetricRow</code></td>
|
||||
<td><code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chart</code></td>
|
||||
<td><code>?TrayChartRow</code></td>
|
||||
<td><code>null</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -123,17 +138,22 @@ On macOS, `title` renders the tray as a menu-bar extra: a titled `NSStatusItem`
|
||||
|
||||
Use the runtime methods from app code:
|
||||
|
||||
- `runtime.createStatusItem(id, options)` -- create a status item under a stable non-zero id
|
||||
- `runtime.updateStatusItemShell(id, shell)` -- update icon, tooltip, visibility, and activation/open commands in place
|
||||
- `runtime.updateStatusItemMenu(id, items)` -- update one menu without recreating its status item
|
||||
- `runtime.updateStatusItemPresentation(id, presentation)` -- update one live title and visual presentation
|
||||
- `runtime.removeStatusItem(id)` -- remove only the identified item
|
||||
- `runtime.createTray(options)` -- create or replace the tray icon
|
||||
- `runtime.updateTrayMenu(items)` -- update menu items without recreating the tray
|
||||
- `runtime.updateTrayTitle(title)` -- update only the live tray title
|
||||
- `runtime.updateTrayPresentation(presentation)` -- update the live title and visual presentation
|
||||
- `runtime.removeTray()` -- remove the tray icon
|
||||
|
||||
The lower-level `PlatformServices.createTray`, `updateTrayMenu`, `updateTrayTitle`, `updateTrayPresentation`, and `removeTray` hooks are available for platform adapters. `Runtime` validates tray options before dispatch: non-separator menu items need a label, command-backed items need a unique non-zero `id`, and menus are capped at 32 items.
|
||||
The singular `*Tray` methods are compatibility wrappers for reserved status-item id `1`. The lower-level `PlatformServices` surface exposes the same keyed methods for platform adapters. `Runtime` supports at most eight simultaneous status items and validates every menu independently: non-separator rows need a label, command-backed rows need a unique non-zero row `id`, and each menu is capped at 32 rows.
|
||||
|
||||
## TypeScript: model-derived status items
|
||||
|
||||
In a TypeScript app, export `statusItem(model)` from `src/core.ts`. The generated launcher installs it from the committed boot model and re-runs it after every model update. Presentation and menu are hashed independently, so changing the title, width, tone, icon opacity, or number style does not rebuild the dropdown or recreate the native status item.
|
||||
In a TypeScript app, export `statusItem(model)` from `src/core.ts`. The generated launcher installs it from the committed boot model and re-runs it after every model update. Shell, presentation, and menu are hashed independently, so changing the icon, tooltip, click hooks, title, width, tone, icon opacity, typography, or rows patches only that channel and never recreates the native status item.
|
||||
|
||||
```ts:src/core.ts
|
||||
import { asciiBytes, utf8Bytes } from "@native-sdk/core";
|
||||
@@ -152,6 +172,8 @@ export function statusItem(model: Model): StatusItemState {
|
||||
tone: model.failed ? "critical" : "normal",
|
||||
iconOpacity: model.stale ? 0.5 : 1,
|
||||
monospaced: true,
|
||||
fontSize: 13,
|
||||
fontWeight: "semibold",
|
||||
},
|
||||
items: [
|
||||
{ id: 10, label: model.today, command: asciiBytes(""), separator: false, enabled: false, detail: model.quota, role: "hero", key: asciiBytes(""), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },
|
||||
@@ -162,15 +184,90 @@ export function statusItem(model: Model): StatusItemState {
|
||||
}
|
||||
```
|
||||
|
||||
`iconPath`, `tooltip`, `activationCommand`, `alternateActivationCommand`, and `openCommand` are captured when the item installs. A normal click emits `activationCommand` and opens the menu; an Option-click emits `alternateActivationCommand` without opening it. Every menu open emits `openCommand`, which is useful for an on-demand refresh while the background cadence stays slow. These lifecycle hooks dispatch with `source = .tray`; empty commands disable them.
|
||||
`iconPath`, `tooltip`, `activationCommand`, `alternateActivationCommand`, and `openCommand` update live alongside presentation and rows. A normal click emits `activationCommand` and opens the menu; an Option-click emits `alternateActivationCommand` without opening it. Every menu open emits `openCommand`, which is useful for an on-demand refresh while the background cadence stays slow. These lifecycle hooks dispatch with `source = .tray`; empty commands disable them.
|
||||
|
||||
Rows use the exact `StatusItemMenuItem` record. `role` is `command`, `info`, `header`, `hero`, `agent`, or `context`; capable macOS hosts render the readout roles as native rich content while simpler hosts degrade them to text. `detail` carries secondary readout content, and `key` plus the five explicit `modifiers` fields declares a menu equivalent. Actionable rows need unique non-zero ids; separators conventionally use id 0 and empty byte fields. The menu may contain at most 32 rows. Map every row/click/open command to an ordinary message with `commandMsg(name): Msg | null`; no Zig `status_item_fn` glue is needed.
|
||||
For multiple independent items, export `statusItems(model): readonly StatusItemDescriptor[]` instead. Each descriptor has the same shell, presentation, and row fields plus a stable non-zero `id` and live `visible` flag. Presence creates, absence removes, and changed fields patch only that identifier; menus update without replacing their `NSStatusItem`. Export either `statusItem` or `statusItems`, not both. This is the Vercel-shaped split: one spend indicator can appear or disappear while a separate control-menu item persists.
|
||||
|
||||
Persistent menu-bar composition uses that collection directly: return one descriptor for compact context text, another for a larger live metric, and another icon-only descriptor that owns the dropdown. Each descriptor independently chooses optional `fontSize` (omitted or `0` keeps the platform default), optional `fontWeight` (omitted means `regular`; otherwise `regular`, `medium`, `semibold`, or `bold`), `monospaced`, width, tone, and icon opacity. They remain separate native status items, so each can appear, disappear, or update without recreating its neighbors.
|
||||
|
||||
Rows use the exact `StatusItemMenuItem` record. `role` is `command`, `info`, `header`, `hero`, `agent`, `context`, `segmented`, or `chart`; capable macOS hosts render the readout roles as native rich content while simpler hosts degrade them to text. `detail` carries secondary readout content, and `key` plus the five explicit `modifiers` fields declares a menu equivalent. Actionable rows need unique non-zero ids; separators conventionally use id 0 and empty byte fields. The menu may contain at most 32 rows. Map every row/click/open command to an ordinary message with `commandMsg(name): Msg | null`; no Zig `status_item_fn` glue is needed.
|
||||
|
||||
### Typed rich rows
|
||||
|
||||
A segmented row carries its choices as data rather than encoding them into `label` or `detail`. Each option has its own stable `id`, label, command, selected state, and enabled state. macOS renders the row as `NSSegmentedControl`; selecting a segment emits its option id through the same tray-action → command → `commandMsg` route as an ordinary command row. Other hosts may expose the options as separate command items.
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 0,
|
||||
label: asciiBytes(""),
|
||||
command: asciiBytes(""),
|
||||
separator: false,
|
||||
enabled: true,
|
||||
detail: asciiBytes(""),
|
||||
role: "segmented",
|
||||
key: asciiBytes(""),
|
||||
modifiers: { primary: false, command: false, control: false, option: false, shift: false },
|
||||
segmented: {
|
||||
options: [
|
||||
{ id: 20, label: asciiBytes("Day"), command: asciiBytes("range.day"), selected: model.range === "day", enabled: true },
|
||||
{ id: 21, label: asciiBytes("Week"), command: asciiBytes("range.week"), selected: model.range === "week", enabled: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A typed metric row keeps prominent primary and secondary text inside the dropdown, separate from the persistent menu-bar title:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 0,
|
||||
label: asciiBytes(""),
|
||||
command: asciiBytes(""),
|
||||
separator: false,
|
||||
enabled: false,
|
||||
detail: asciiBytes(""),
|
||||
role: "hero",
|
||||
key: asciiBytes(""),
|
||||
modifiers: { primary: false, command: false, control: false, option: false, shift: false },
|
||||
metric: {
|
||||
primaryText: asciiBytes("2,494 requests"),
|
||||
secondaryText: utf8Bytes("Today · production"),
|
||||
accessibilityLabel: asciiBytes("2,494 requests today in production"),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A chart row carries 1–32 finite values plus an explicit numeric domain, leading caption, trailing summary, and required accessibility label. Every value must fall inside `minValue...maxValue`. The macOS system host draws native AppKit bars in a custom `NSView`; hosts without custom tray rows retain the caption and summary as text.
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 0,
|
||||
label: asciiBytes(""),
|
||||
command: asciiBytes(""),
|
||||
separator: false,
|
||||
enabled: false,
|
||||
detail: asciiBytes(""),
|
||||
role: "chart",
|
||||
key: asciiBytes(""),
|
||||
modifiers: { primary: false, command: false, control: false, option: false, shift: false },
|
||||
chart: {
|
||||
values: model.cpuHistory,
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
leadingCaption: asciiBytes("CPU"),
|
||||
trailingSummary: model.cpuSummary,
|
||||
accessibilityLabel: model.cpuAccessibility,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A segmented row admits at most eight options and at most one selected option. Option ids share the enclosing menu’s command-id namespace, including other segmented rows. The 32-row menu budget is also the fallback budget: a segmented row consumes one fallback row per option, preventing silent truncation on plain-menu hosts.
|
||||
|
||||
Use `utf8Bytes` for titles, labels, tooltips, and details; it preserves characters such as `…`, `·`, and emoji as UTF-8. Use `asciiBytes` for guaranteed-ASCII command names, keys, paths, and empty byte fields. Passing non-ASCII literal/template text to `asciiBytes` is an NS1064 build error.
|
||||
|
||||
## Handling tray actions
|
||||
|
||||
When a user clicks a tray menu item, the runtime dispatches a `CommandEvent` with source `.tray` and the native `tray_item_id`. Prefer command-backed items when the item represents a known app action:
|
||||
When a user clicks a tray menu item, the runtime dispatches a `CommandEvent` with source `.tray`, the native `status_item_id`, and that menu's `tray_item_id`. Row ids only need to be unique within their own menu. Prefer command-backed items when the item represents a known app action:
|
||||
|
||||
```zig
|
||||
try runtime.createTray(.{
|
||||
@@ -227,7 +324,7 @@ macOS (`NSStatusItem`) is the proven host; platforms without a status-bar servic
|
||||
|
||||
## Model-driven title and menu in Zig-core apps
|
||||
|
||||
For a live menu-bar extra — an open-count badge in the title, the latest items in the dropdown — add `UiApp.Options.status_item_fn`. It is consulted on install and after every rebuild, and the runtime re-applies only what actually changed: a presentation-only change patches the live status button (no flicker, no menu rebuild), while a menu change updates the dropdown. The static `status_item` can provide install-time icon, tooltip, activation, alternate-activation, and open commands.
|
||||
For a live menu-bar extra — an open-count badge in the title, the latest items in the dropdown — add `UiApp.Options.status_item_fn`. It is consulted on install and after every rebuild, and the runtime re-applies only what actually changed: shell, presentation, and menu changes patch independently without flicker or native-item recreation. The static `status_item` provides defaults for icon, tooltip, activation, alternate-activation, and open commands; the callback may update those fields live too.
|
||||
|
||||
```zig
|
||||
fn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusItemState {
|
||||
@@ -245,36 +342,53 @@ fn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusIt
|
||||
|
||||
Selections dispatch each item's `command` through `on_command` with source `.tray`, the same shape as window menus. Platforms without a tray-title seam keep the menu updates and log the missing title support once.
|
||||
|
||||
For multiple Zig-core items, use `UiApp.Options.status_items_fn`, returning up to eight `App.StatusItemDescriptor` values from `App.StatusItemsScratch`. Descriptor presence creates/removes by `id`; `visible` hides without removing; shell, presentation, and menu hashes reconcile independently. It is mutually exclusive with the singular `status_item` / `status_item_fn` pair.
|
||||
|
||||
## The menu-bar app lifecycle
|
||||
|
||||
The tray-player pattern (a Spotify-shaped app that lives in the menu bar) is two declarations and two verbs:
|
||||
The default TypeScript + Native markup recipe combines one app policy, two window policies, and two commands:
|
||||
|
||||
1. The window declares `close_policy = "hide"` in app.zon, so the red close button hides it instead of quitting — the app keeps running behind its status item, and on macOS clicking the Dock icon re-shows the hidden window on its own. On Windows this requires the `"tray"` capability (the status item is the only way back to a hidden window there — the build refuses `"hide"` without it, and a tray that fails to install downgrades the first hide-close to a real close with a loud log). The default (`"quit"`) keeps the classic behavior; see [Windows](/docs/windows#close-policy).
|
||||
2. The tray rows map to the window verbs in `update`: "Open" returns `fx.showWindow(label)` (un-hide + activate; it also restores a minimized window) and "Quit" returns `fx.quitApp()` — the real graceful terminate, riding the same shutdown path a last-window close takes.
|
||||
1. Declare the `"tray"` capability and set top-level `dock_visible = false`. macOS selects the Accessory activation policy before creating the startup window, so no Dock tile or cmd+Tab entry flashes. The tray requirement is validated because it becomes the app's route back to hidden windows.
|
||||
2. Set `initially_hidden = true` on the startup window when the app should launch behind the status item. `dock_visible = false` alone removes desktop presence; it does not suppress the window.
|
||||
3. Set `close_policy = "hide"`, so the red close button hides the window instead of quitting. The default `"quit"` keeps classic windowed-app behavior; see [Windows](/docs/windows#close-policy).
|
||||
4. Map tray rows to `Cmd.showWindow("main")` and `Cmd.quitApp()`. Show unhides, orders front, and activates; Quit follows the real graceful shutdown path.
|
||||
|
||||
```zig
|
||||
pub const status_items = [_]native_sdk.TrayMenuItem{
|
||||
.{ .id = 1, .label = "Open Player", .command = "app.open" },
|
||||
.{ .separator = true },
|
||||
.{ .id = 2, .label = "Quit", .command = "app.quit" },
|
||||
};
|
||||
```zig:app.zon
|
||||
.capabilities = .{ "native_views", "gpu_surfaces", "tray" },
|
||||
.dock_visible = false,
|
||||
.shell = .{
|
||||
.windows = .{
|
||||
.{
|
||||
.label = "main",
|
||||
.initially_hidden = true,
|
||||
.close_policy = "hide",
|
||||
// views...
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
pub fn command(name: []const u8) ?Msg {
|
||||
if (std.mem.eql(u8, name, "app.open")) return .open_player;
|
||||
if (std.mem.eql(u8, name, "app.quit")) return .quit;
|
||||
return null;
|
||||
```ts:src/core.ts
|
||||
import { Cmd } from "@native-sdk/core";
|
||||
|
||||
export function commandMsg(name: string): Msg | null {
|
||||
if (name === "app.open") return { kind: "open_player" };
|
||||
if (name === "app.quit") return { kind: "quit" };
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn update(model: *Model, msg: Msg, fx: *Effects) void {
|
||||
switch (msg) {
|
||||
.open_player => fx.showWindow("main"),
|
||||
.quit => fx.quitApp(),
|
||||
// ...
|
||||
}
|
||||
export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
|
||||
switch (msg.kind) {
|
||||
case "open_player":
|
||||
return [model, Cmd.showWindow("main")];
|
||||
case "quit":
|
||||
return [model, Cmd.quitApp()];
|
||||
// other arms...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the TypeScript tier the same verbs are `Cmd.showWindow("main")` and `Cmd.quitApp()`, and the exported `statusItem(model)` helper above supplies the live title and rows. `examples/menu-bar` is the whole zero-Zig loop.
|
||||
`Cmd.setDockPresence(true)` can later promote the same running Accessory app to Regular; passing `false` demotes it again. `examples/menu-bar` is the complete zero-Zig loop with `statusItem(model)` supplying the live title and rows.
|
||||
|
||||
Linux is the honest exception: the toolkit has no status item there yet, so nothing could bring a hidden window back — `close_policy = "hide"` is refused at build/create time with a teaching, and the platform-support matrix states it plainly.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ The question behind "can I use npm?" is almost always one of these five:
|
||||
|
||||
Put ordinary static-tier TypeScript under `src/services/` when the work needs Node built-ins, regexes, JSON, `Map`/`Set`, `Date`, classes, or ambient process authority. Each directly exported, non-default named synchronous function becomes an operation named `<module-basename>.<export>`. Its request and result may be shared, contract-encodable records; the core calls the generated constructor from `@native-sdk/services`, so success and failure still arrive as Msgs and record/replay remains offline.
|
||||
|
||||
This is compiled native code with no JavaScript engine. `native vendor . package@X.Y.Z` resolves an exact package graph once, with lifecycle scripts disabled, into checked-in `src/services/vendor/` sources and hash facts in app.zon. Builds are offline: every byte is verified and scriptc receives only the explicit `--npm-static` package list—never automatic or dynamic fallback. `native check` preserves scriptc's coverage note and refuses anything below 100% static coverage. A five-package scriptc 0.0.28 calibration passed three small source-shipping utilities and refused two (`nanoid` and `micromark`), so package support is intentionally selective. Services run in a lazily started child process by default, with an explicit in-process opt-in where the compiler can localize the target archive; [TypeScript Services](/docs/typescript/services) covers the exact platform/architecture matrix, typed calls, streaming, cancellation, authority, and crash recovery.
|
||||
This is compiled native code with no JavaScript engine. `native vendor . package@X.Y.Z` resolves an exact package graph once, with lifecycle scripts disabled, into checked-in `src/services/vendor/` sources and hash facts in app.zon. Builds are offline: every byte is verified and scriptc receives only the explicit `--npm-static` package list—never automatic or dynamic fallback. `native check` preserves scriptc's coverage note and refuses anything below 100% static coverage. The checked-in five-package calibration passed three small source-shipping utilities and refused two (`nanoid` and `micromark`), so package support is intentionally selective. Services run in a lazily started child process by default, with an explicit in-process opt-in where the compiler can localize the target archive; [TypeScript Services](/docs/typescript/services) covers the exact platform/architecture matrix, typed calls, streaming, cancellation, authority, and crash recovery. [`examples/service-feed-reader`](https://github.com/vercel-labs/native/tree/main/examples/service-feed-reader) is the reference: `Cmd.fetch` downloads a feed, the service parses the bytes into typed records through the generated client, and the recorded loop replays without the service or the network.
|
||||
|
||||
## Calling APIs, AI endpoints included
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# TypeScript Cores
|
||||
|
||||
An app core is the logic tier of a Native SDK app: `Model` (the app state), `Msg` (a discriminated union of everything that can happen), `update(model, msg)` (the one pure transition function), and the pure helpers they call. By default you write it as one TypeScript module — `src/core.ts` — the `@native-sdk/core` frontend checks it, and the external core compiler builds it to native code at build time. No JS engine ships in the binary: the program either passes the subset checker and compiles to native, or you get a teaching error naming the rule, the fix, and the reason.
|
||||
An app core is a Native SDK app's deterministic logic: `Model` (the app state), `Msg` (a discriminated union of everything that can happen), `update(model, msg)` (the one pure transition function), and the pure helpers they call. By default you write it as one TypeScript module — `src/core.ts` — the `@native-sdk/core` frontend checks it, and the external core compiler builds it to native code at build time. No JS engine ships in the binary: the program either passes the subset checker and compiles to native, or you get a teaching error naming the rule, the fix, and the reason.
|
||||
|
||||
This is the two-tier shape of the toolkit: Zig is how everything works — the engine, the runtime, every widget — and TypeScript plus [Native markup](/docs/native-ui) are how applications are authored. A whole app starts as three files and zero Zig: `src/core.ts`, `src/app.native`, and `app.zon`. When ordinary TypeScript work needs filesystem access, JSON, regexes, `Map`, `Date`, classes, or child processes, add modules under `src/services/`; they compile to native code too and answer the core through the same effect→Msg boundary as every other external action. Writing the core in Zig instead ([App Model](/docs/app-model)) is first-class by choice — same loop, same runtime — and extending the toolkit itself (custom widgets and render passes) is always Zig.
|
||||
This is the two-tier shape of the toolkit: Zig is how everything works — the engine, the runtime, every widget — and TypeScript plus [Native markup](/docs/native-ui) are how applications are authored. A whole app starts as three files and zero Zig: `src/core.ts`, `src/app.native`, and `app.json`. Existing `app.zon` manifests remain supported. When ordinary TypeScript work needs filesystem access, JSON, regexes, `Map`, `Date`, classes, or child processes, add modules under `src/services/`; they compile to native code too and answer the core through the same effect→Msg boundary as every other external action. Writing the core in Zig instead ([App Model](/docs/app-model)) is first-class by choice — same loop, same runtime — and extending the toolkit itself (custom widgets and render passes) is always Zig.
|
||||
|
||||
The same `core.ts` is executable TypeScript: it typechecks with stock tsc and runs unmodified under node, which is what makes the fastest dev loop possible:
|
||||
|
||||
@@ -12,7 +12,7 @@ The same `core.ts` is executable TypeScript: it typechecks with stock tsc and ru
|
||||
native dev --core # run the core under node's virtual host: dispatch Msgs as
|
||||
# JSON lines, watch the model + effect transcript
|
||||
native dev # build and run the real app (markup hot reload)
|
||||
native check # subset-check core.ts + validate markup + app.zon
|
||||
native check # subset-check core.ts + validate markup + app.json
|
||||
native build # ReleaseFast binary; native test runs the app's tests
|
||||
```
|
||||
|
||||
@@ -131,6 +131,11 @@ Both regions have fixed, build-time capacities (1 MiB each by default): the fram
|
||||
|
||||
App cores are written in a closed subset of TypeScript, and the subset means one precise thing: TypeScript minus the ecosystem minus the purity violations — never minus basic syntax. Every basic statement, operator, and declaration form compiles: plain interfaces, discriminated unions, `switch` (with `default` arms), every loop shape (`for`, `for...of`, `while`, `do...while`, labels with labeled `break`/`continue`), the full operator and assignment family (`**`, shifts, `+=` through `??=`), const record destructuring, namespace imports, spreads, the array methods (`.map`/`.filter`/`.find`/`.reduce`/`.toSorted`/...), `Math`, template literals — everything with exact JS semantics, pinned so node and native always agree (a machine-checked grammar matrix classifies every production of the language, so nothing is missing by accident). Classes and exceptions compile too: data classes (fields, a constructor, methods, `static` methods and `static readonly` consts, erased `private`/`protected` — `new Task(...)`, `this.count`, `Task.fromRow(...)`, mutation under the same local-ownership rule as arrays) compile to plain structs plus functions, and `throw`/`try`/`catch`/`finally` is deterministic control flow — a thrown kind-tagged subset value unwinds to the nearest catch (several distinct shapes may throw; the checker collects them into the core's thrown union, and `catch (e)` narrows it with plain kind tests, no `as` ceremony), `finally` runs on every path, and an uncaught throw is a defined panic exactly where node would crash. What isn't available is exactly two families: the ecosystem the core cannot carry (npm packages, regexes, `JSON`, Promises, `eval`) and constructs that would break the core's guarantees (class inheritance, `async`/`await` — asynchrony is command data, `Map`/`Set`, module-level `let`, `Date.now()`/`Math.random()` inside `update`, runtime type tests, text as indexable strings — a core's text is bytes). Each has an idiomatic replacement the checker teaches by ID — kind-tagged error shapes narrowed in the catch, time and randomness arrive as message payloads, keyed data is an id-keyed array, and ordinary static-tier work moves behind a `src/services/` request. Immutability is a rule about SHARED data, not a style: mutation is legal on locally-owned arrays — a scratch array your function creates (a literal or a `.slice()` copy) takes `push`/`pop`/`splice`/in-place `sort`, the `xs[xs.length] = v` append, and the rest with exact JS semantics until the value escapes; a `let` reassigned only from fresh copies stays owned, passing into a `readonly T[]` reader parameter borrows instead of escaping, and the checker teaches only at the real boundaries. Generics are ordinary TypeScript too: a module-level generic function, interface, or type alias monomorphizes per call site from tsc's own resolved type arguments — one native function per instantiation. These rules scope to the core class. Files under `src/services/` skip NS1001–NS1064 and are judged by the same pinned compiler's ordinary static tier instead; the class boundary rules NS1065–NS1067 keep the deterministic core and ambient-authority service separate. Where the ecosystem fits has its own page: [Where Packages Go](/docs/typescript/packages).
|
||||
|
||||
Every rule in the catalogue carries a class. A `guarantee` rule protects a core invariant — determinism and replay, fixed shapes, immutability of shared data, the one text representation — and is permanent. A `deferred` rule bans nothing those invariants require; the capability waits on a deliberate easing decision, and its diagnostic says so.
|
||||
|
||||
- `guarantee` — permanent: NS1001 (shared data is immutable), NS1002 (updates are synchronous), NS1005 (update is deterministic), NS1010 (module state lives in the Model), the byte-text rules (NS1004, NS1018, NS1024, NS1060), and every other rule not listed as deferred.
|
||||
- `deferred` — awaiting an easing decision: NS1011 (`Map`/`Set`), NS1019 (fixed arity: parameter defaults, rest, `arguments`, call spreads), NS1040 (regular expressions), NS1042 (generators), NS1044 (`BigInt`/`Symbol`).
|
||||
|
||||
<CodeToggle>
|
||||
|
||||
```ts
|
||||
@@ -279,6 +284,14 @@ The runtime interprets the command after the model commits and dispatches any re
|
||||
<td><code>Cmd.writeFile(path, bytes, { key?, ok, err })</code></td>
|
||||
<td>Write a whole file (parents created, replaced whole); <code>ok</code> carries no payload — a successful write has nothing to report</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Cmd.appendFile</code> / <code>Cmd.statFile</code> / <code>Cmd.deleteFile</code></td>
|
||||
<td>Append one bounded payload, inspect <code>{ exists, size, mtimeMs }</code>, or delete one file with explicit <code>not_found</code> handling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Cmd.readFileStream</code> / <code>Cmd.writeFileStream</code> + <code>writeFileChunk</code>/<code>writeFileClose</code></td>
|
||||
<td>Read 256-KiB chunks without a total-size cliff, or build an atomic export one acknowledged chunk at a time — see <a href="/docs/files">Files & Streaming</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Cmd.fetch(spec, { key?, ok, err })</code></td>
|
||||
<td>A buffered HTTP(S) exchange; <code>ok</code> carries <code>{ status, body }</code> (a 404 is still <code>ok</code> — a delivered response), <code>err</code> the transport reason</td>
|
||||
@@ -292,8 +305,8 @@ The runtime interprets the command after the model commits and dispatches any re
|
||||
<td>System clipboard: write is fire-and-forget, read routes the text bytes back</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Cmd.showNotification({ title, subtitle?, body? })</code></td>
|
||||
<td>Show a desktop notification, fire-and-forget; text fields are bytes and the OS remains authoritative over final delivery</td>
|
||||
<td><code>Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })</code></td>
|
||||
<td>Show or replace a desktop notification, fire-and-forget; paired action fields dispatch through the ordinary app-command path while the process is running</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Cmd.openExternalUrl(url)</code> / <code>Cmd.revealPath(path)</code></td>
|
||||
@@ -362,11 +375,11 @@ The runtime interprets the command after the model commits and dispatches any re
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Result arms are ordinary Msg arms with the shape the effect produces — one `Uint8Array` field for raw host results and errors, a generated service result record, one number field for timer fires and a streaming fetch's terminal status, no fields for `writeFile`'s ok, one number plus one `Uint8Array` field for a buffered fetch's result — and tsc checks the shapes for you. Keys carry ONE in-flight discipline: a keyed engine effect — buffered named engine ops, `Cmd.delay`, a raw `Cmd.request` to an embedder host command — whose key is already in flight replaces the old one (the superseded result is dropped, no message; the debounce shape), and `Cmd.cancel` drops it with no message. Live `Cmd.spawn`, streaming-fetch, and service keys (buffered or streaming) reject a duplicate (`err` gets `rejected`) so two calls can never splice into one result or stream; cancel first if you mean to supersede — cancelling a buffered service request produces no message, while cancelling a live stream routes `cancelled` to `err`. A streaming fetch whose line is cut or dropped also ends with `err: truncated`, never a misleading successful status. Every routed `err` arm receives a machine-readable reason.
|
||||
Result arms are ordinary Msg arms with the shape the effect produces — one `Uint8Array` field for raw host results and errors, a generated service result record, one number field for timer fires and stream totals, no fields for write acknowledgments, and one number plus one `Uint8Array` field for a buffered fetch's result — and tsc checks the shapes for you. Buffered engine effects and streamed file reads replace a live same-key predecessor and cancel silently. Live `Cmd.spawn`, streaming-fetch, streaming-service, and streamed write-sink keys reject duplicates so two producers cannot splice; cancelling those is loud through `err: cancelled`. A streaming fetch whose line is cut or dropped also ends with `err: truncated`, never a misleading successful status. Every routed `err` arm receives a machine-readable reason.
|
||||
|
||||
Platform state stays on the same effect boundary. `Cmd.openExternalUrl(url)` enforces [`security.navigation.external_links`](/docs/security#external-links) before entering the browser; `Cmd.revealPath(path)` uses the desktop file manager. Credential operations take byte `service` and `account` identifiers plus the standard `{ key?, ok, err }` route: set/delete return empty bytes on `ok`, get returns the secret, and a missing item routes `not_found`. `Cmd.formatLocalTime(timestampMs, "date" | "time" | "datetime", route)` returns localized UTF-8 bytes using the current host locale and time zone. That formatting is deliberately a Cmd—not a pure helper—so session recording captures the observed text and replay never re-reads ambient locale or timezone state.
|
||||
|
||||
Durable in-memory state uses `Cmd.persist()`: declare the `persist` capability, configure the boot routes and schema version, then return the command beside the committed model. The engine owns canonical serialization, trailing-edge coalescing, atomic app-data placement, backup recovery, migration, and journal/replay. See [Model Persistence](/docs/persistence) for the complete setup. `Cmd.readFile` and `Cmd.writeFile` remain for user-visible files, exports, and blobs; when using them, request the framework-provided app-data directory through `envMsgs` instead of depending on process cwd.
|
||||
Durable in-memory state uses `Cmd.persist()`: declare the `persist` capability, configure the boot routes and schema version, then return the command beside the committed model. The engine owns canonical serialization, trailing-edge coalescing, atomic app-data placement, backup recovery, migration, and journal/replay. See [Model Persistence](/docs/persistence) for the complete setup. Raw file commands remain for user-visible files, exports, and blobs; [Files & Streaming](/docs/files) covers their bounds, atomic sink protocol, replay, and `filesystem` permission gate.
|
||||
|
||||
Independent byte records use `Cmd.store`: declare the `store` capability, then route set/get/delete/scan/setMany results back to Msg arms. The engine owns the app-data path, SQLite schema, atomic batches, pagination, and replay boundary. See [Record Store](/docs/record-store).
|
||||
|
||||
@@ -420,7 +433,7 @@ export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
|
||||
}
|
||||
```
|
||||
|
||||
Audio input uses the same bounded, wake-driven stream transport without requiring native posting code. `Cmd.audioCaptureStart(key, { source, sampleRate?, channels? }, { event })` captures the microphone or the desktop output mix. The supported canonical rates are 16, 24, and 48 kHz; channels are mono or stereo; the default is 48 kHz mono. Each `data` event carries at most 20 ms of interleaved signed 16-bit little-endian PCM in `pcm`, plus `timestampMs`, `frames`, the delivered format, and drop counters. Microphone and system capture can run concurrently, but only one stream per source is live; starting that source again stops the prior key. `Cmd.audioCaptureStop(key)` quiesces the native callback, drains accepted chunks, then emits one `stopped` terminal. A key remains occupied until that terminal is delivered, so wait for `stopped` before reusing it. Add `"microphone"` and/or `"system_audio"` to `app.zon` permissions so packaged macOS apps receive the required usage descriptions and consent prompts.
|
||||
Audio input uses the same bounded, wake-driven stream transport without requiring native posting code. `Cmd.audioCaptureStart(key, { source, sampleRate?, channels? }, { event })` captures the microphone or the desktop output mix. The supported canonical rates are 16, 24, and 48 kHz; channels are mono or stereo; the default is 48 kHz mono. Each `data` event carries at most 20 ms of interleaved signed 16-bit little-endian PCM in `pcm`, plus `timestampMs`, `frames`, the delivered format, and drop counters. Microphone and system capture can run concurrently, but only one stream per source is live; starting that source again stops the prior key. `Cmd.audioCaptureStop(key)` quiesces the native callback, drains accepted chunks, then emits one `stopped` terminal. A key remains occupied until that terminal is delivered, so wait for `stopped` before reusing it. Add `"microphone"` and/or `"system_audio"` to the `app.json` permissions array so packaged macOS apps receive the required usage descriptions and consent prompts.
|
||||
|
||||
```ts:src/core.ts
|
||||
import { Cmd, type AudioCaptureState, type AudioCaptureSource } from "@native-sdk/core";
|
||||
@@ -437,7 +450,7 @@ export type Msg =
|
||||
|
||||
## Model-derived menu-bar status items
|
||||
|
||||
A `src/core.ts` app can own its complete native menu-bar item without custom Zig wiring. Export `statusItem(model): StatusItemState`; the generated launcher installs its icon, tooltip, click/open commands, presentation, and rows from the boot model, then re-derives the live presentation and menu after committed updates. It patches those two parts independently and never recreates the item just because model state changed.
|
||||
A `src/core.ts` app can own its complete native menu-bar item without custom Zig wiring. Export `statusItem(model): StatusItemState`; the generated launcher installs its icon, tooltip, click/open commands, presentation, and rows from the boot model, then re-derives all of them after committed updates. It patches shell, presentation, and menu independently and never recreates the item just because model state changed.
|
||||
|
||||
```ts:src/core.ts
|
||||
import { asciiBytes, utf8Bytes } from "@native-sdk/core";
|
||||
@@ -450,7 +463,7 @@ export function statusItem(model: Model): StatusItemState {
|
||||
activationCommand: asciiBytes("app.sync"),
|
||||
alternateActivationCommand: asciiBytes(""),
|
||||
openCommand: asciiBytes("app.sync"),
|
||||
presentation: { title: model.syncing ? utf8Bytes("SYNC…") : utf8Bytes("READY"), width: 62, tone: model.failed ? "critical" : "normal", iconOpacity: model.stale ? 0.5 : 1, monospaced: true },
|
||||
presentation: { title: model.syncing ? utf8Bytes("SYNC…") : utf8Bytes("READY"), width: 62, tone: model.failed ? "critical" : "normal", iconOpacity: model.stale ? 0.5 : 1, monospaced: true, fontSize: 13, fontWeight: "semibold" },
|
||||
items: [
|
||||
{ id: 1, label: utf8Bytes("Open"), command: asciiBytes("app.open"), separator: false, enabled: true, detail: asciiBytes(""), role: "command", key: asciiBytes(""), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },
|
||||
{ id: 2, label: utf8Bytes("Sync now…"), command: asciiBytes("app.sync"), separator: false, enabled: !model.syncing, detail: asciiBytes(""), role: "command", key: asciiBytes("r"), modifiers: { primary: true, command: false, control: false, option: false, shift: false } },
|
||||
@@ -459,7 +472,19 @@ export function statusItem(model: Model): StatusItemState {
|
||||
}
|
||||
```
|
||||
|
||||
Import the canonical records and unions from `@native-sdk/core/events`. Presentation includes byte `title`, numeric `width`, `normal | warning | critical` tone, `iconOpacity` in 0…1, and `monospaced`. Rows include id/label/command/separator/enabled plus secondary `detail`, semantic `role`, key equivalent, and all five modifier booleans. Actionable ids are unique and non-zero, and there are at most 32 rows. `commandMsg(name): Msg | null` maps row selection, status-button activation, Option-activation, and menu-open refresh into the ordinary update loop. See [System Tray](/docs/tray) and the zero-Zig `examples/menu-bar` app for the full hide/Open/Quit lifecycle.
|
||||
Import the canonical records and unions from `@native-sdk/core/events`. Presentation includes byte `title`, numeric `width`, tone, `iconOpacity`, `monospaced`, `fontSize`, and `fontWeight`; `statusItems` composes several independently styled persistent menu-bar items. Rows include id/label/command/separator/enabled plus secondary `detail`, semantic `role`, key equivalent, and all five modifier booleans. Actionable ids are unique and non-zero, and there are at most 32 rows. `commandMsg(name): Msg | null` maps row selection, status-button activation, Option-activation, and menu-open refresh into the ordinary update loop. See [System Tray](/docs/tray).
|
||||
|
||||
Export `statusItems(model): readonly StatusItemDescriptor[]` when the app needs several independent items. Each descriptor adds stable non-zero `id` identity and a live `visible` flag to the same shell/presentation/menu record. Adding/removing descriptors creates/removes only those ids; icon, title, tooltip, visibility, activation/open commands, and menu changes patch in place. Export either the singular or collection helper, not both. macOS supports up to eight simultaneous items; every item keeps its own 32-row menu.
|
||||
|
||||
## Model-declared secondary windows
|
||||
|
||||
Export `windows(model): readonly WindowDescriptor[]` to derive the live secondary-window set from model state. Construct entries with `windowDescriptor` from `@native-sdk/core`, import `WindowDescriptor` from `@native-sdk/core/events`, and put each window's markup at `src/windows/<label>.native`. Spell the constructor label as a literal `label: asciiBytes("<label>")`; `native check` and every build reject dynamic labels or a label without that matching root. Window roots can import shared components nested under `src/windows/`; the generated launcher embeds and hot-reloads the complete import closure. Adding/removing descriptors creates/closes only those windows; all open windows rebuild from the same committed model.
|
||||
|
||||
`closePolicy` accepts `"quit"` (the default) or `"hide"`. A `"quit"` user close routes `onCloseCommand` through `commandMsg`, where the app maps it to the Msg that clears its open flag. A `"hide"` close retains the same native window and view and dispatches no close command; `Cmd.showWindow(label)` reveals it. Model-declared secondary windows are desktop-only. See `examples/system-monitor-ts`.
|
||||
|
||||
`restorePolicy` accepts `"clamp_to_visible_screen"` (the default) or `"center_on_primary"`. Model-declared windows do not restore persisted frames. On macOS, `"center_on_primary"` centers a fresh descriptor with no authored `x`/`y`; Windows and Linux currently keep their native default placement.
|
||||
|
||||
`titlebar` accepts `"standard"`, `"hidden_inset"`, `"hidden_inset_tall"`, or `"chromeless"`. Transparent Windows windows require `"chromeless"`; because that removes the system buttons, fully skinned windows must draw working close/minimize controls.
|
||||
|
||||
## Subscriptions are Sub data
|
||||
|
||||
@@ -513,7 +538,7 @@ A markup text control (`<text-field text="{draft}" on-input="draft_edit" />`) ne
|
||||
|
||||
## Splitting a core into modules
|
||||
|
||||
A core that outgrows one file splits into modules under `src/` except `src/services/`: relative imports spelled with their real filenames (`./parsers.ts` — the same file runs under node, whose loader resolves real files), `src/` as the hard boundary (`../` and npm packages are teaching errors), and no runtime cycles (`import type` back-edges are fine and idiomatic — a helper module typically type-imports `Model` from the entry). The core may not import service files, even type-only; shared subset-legal shapes live in an ordinary core-class module which a service may import. Export lists and value re-exports are ordinary module surface: `export { helper, doneCount as remaining }` binds names over existing declarations, and `export { parsePs } from "./parsers.ts"` forwards another module's export by name — what stays out is `export default`, `export =`, and `export * from` (the core's flat namespace resolves by name, so every export names what it binds). `core.ts` stays the entry module and the app's public face: `update`, `initialModel`, `subscriptions`, the wiring channels, `themePack` / `statusItem`, and the exported binding helpers live there (declared and exported under their own names — a rename or re-export cannot bind an entry point), and imported modules hold the machinery they call. The SDK also ships library modules in the same subset — `@native-sdk/core/text` is the byte-splice text engine (caret, selection, IME composition, ASCII case-insensitive compare), and `@native-sdk/core/events` is the canonical event and shell vocabulary (`TextInputEvent` re-exported, `ScrollState`, `FrameEvent`, `KeyEvent`, `PinchPhase`/`PinchEvent`, `ColorScheme`, the chrome records, `AudioState`/`AudioEvent`, and the status-item state/presentation/row/modifier records and unions) so no core re-types it — compiled into your core when imported and absent when not.
|
||||
A core that outgrows one file splits into modules under `src/` except `src/services/`: relative imports spelled with their real filenames (`./parsers.ts` — the same file runs under node, whose loader resolves real files), `src/` as the hard boundary (`../` and npm packages are teaching errors), and no runtime cycles (`import type` back-edges are fine and idiomatic — a helper module typically type-imports `Model` from the entry). The core may not import service files, even type-only; shared subset-legal shapes live in an ordinary core-class module which a service may import. Export lists and value re-exports are ordinary module surface: `export { helper, doneCount as remaining }` binds names over existing declarations, and `export { parsePs } from "./parsers.ts"` forwards another module's export by name — what stays out is `export default`, `export =`, and `export * from` (the core's flat namespace resolves by name, so every export names what it binds). `core.ts` stays the entry module and the app's public face: `update`, `initialModel`, `subscriptions`, the wiring channels, `themeState` / `themePack` / `statusItem` / `statusItems` / `windows`, and the exported binding helpers live there (declared and exported under their own names — a rename or re-export cannot bind an entry point), and imported modules hold the machinery they call. The SDK also ships library modules in the same subset — `@native-sdk/core/text` is the byte-splice text engine (caret, selection, IME composition, ASCII case-insensitive compare), and `@native-sdk/core/events` is the canonical event and shell vocabulary (`TextInputEvent` re-exported, `ScrollState`, `FrameEvent`, `KeyEvent`, `PinchPhase`/`PinchEvent`, `ColorScheme`, `ThemeState`, the chrome records, `AudioState`/`AudioEvent`, status-item records, and `WindowDescriptor`) so no core re-types it — compiled into your core when imported and absent when not.
|
||||
|
||||
<CodeToggle>
|
||||
|
||||
@@ -542,7 +567,7 @@ pub fn parseSample(bytes: []const u8) ?Sample { ... }
|
||||
|
||||
## TypeScript services
|
||||
|
||||
A service operation is a directly exported, non-default named synchronous function under `src/services/`, taking zero or one explicitly typed request and declaring a contract-encodable result. Crossing shapes live in an exported, subset-legal module outside `src/services/` so the core and service import one declaration. The operation name is `<module-basename>.<export>`; `native check` projects its complete type table into `services.contract.json`, checks both classes, and generates the typed core client:
|
||||
The core is the app's deterministic logic — `Model`, `Msg`, `update`; services do the app's imperative work. A service operation is a directly exported, non-default named synchronous function under `src/services/`, taking zero or one explicitly typed request and declaring a contract-encodable result. Crossing shapes live in an exported, subset-legal module outside `src/services/` so the core and service import one declaration. The operation name is `<module-basename>.<export>`; `native check` projects its complete type table into `services.contract.json`, checks both classes, and generates the typed core client:
|
||||
|
||||
```ts:src/core.ts
|
||||
import { feedsParse } from "@native-sdk/services";
|
||||
@@ -566,11 +591,13 @@ The core never receives a synchronous handle. Its update returns a command, the
|
||||
|
||||
`native dev` keeps markup instant — `.native` edits hot-reload into the running window — but a `src/core.ts` edit rebuilds the core through the external core compiler and restarts the app: seconds per rebuild (roughly 3-6s warm), not sub-second. The core loop in the real window is restart-shaped; keep logic iteration under `native dev --core` and rebuild when you want to see it live.
|
||||
|
||||
`native check` runs the subset checker (real tsc semantics plus the app-core rules) over the core class, emits and validates the service contract, runs the pinned compiler's coverage verdict over each independent service root, then validates markup and `app.zon`. Every diagnostic names the rule, the idiomatic rewrite, and the reason — write to them up front and the loop stays fast.
|
||||
`native check` runs the subset checker (real tsc semantics plus the app-core rules) over the core class, emits and validates the service contract, runs the pinned compiler's coverage verdict over each independent service root, then validates markup and the app manifest. Every diagnostic names the rule, the idiomatic rewrite, and the reason — write to them up front and the loop stays fast.
|
||||
|
||||
## Build targets
|
||||
|
||||
Builds compile everything in the app — the core archive, any service executables or in-process archives, and the runner — for one stated target. The default is the build host; `-Dtarget` selects a cross desktop target following the pinned compiler's build matrix: Linux and Windows GNU targets build from any macOS, Linux, or Windows host, and macOS targets build on a macOS host (Apple linking needs the host toolchain's SDK). A Windows MSVC target builds natively on a matching Windows host; cross-Windows builds use the GNU ABI because Zig supplies that target's CRT and system libraries. An explicitly spelled Linux `-gnu` target also states its glibc version — `x86_64-linux-gnu.2.36` or later, or `x86_64-linux-musl` — because the compiled runtime needs glibc 2.36+ (a bare `-gnu` spelling lands on Zig's older default floor and is refused with the same teaching). The executable name and packaging follow the target OS. TypeScript cores remain desktop-only: mobile targets keep Zig and markup cores, and a mobile `-Dtarget` on a TypeScript app teaches the same at configure time.
|
||||
Builds compile everything in the app — the core archive, any service executables or in-process archives, and the runner — for one stated target. The default is the build host; `-Dtarget` selects a cross desktop target following the pinned compiler's build matrix: Linux and Windows GNU targets build from any macOS, Linux, or Windows host, and macOS targets build on a macOS host (Apple linking needs the host toolchain's SDK). A Windows MSVC target builds natively on a matching Windows host; cross-Windows builds use the GNU ABI because Zig supplies that target's CRT and system libraries. An explicitly spelled Linux `-gnu` target also states its glibc version — `x86_64-linux-gnu.2.36` or later, or `x86_64-linux-musl` — because the compiled runtime needs glibc 2.36+ (a bare `-gnu` spelling lands on Zig's older default floor and is refused with the same teaching). The executable name and packaging follow the target OS.
|
||||
|
||||
Mobile targets compile the same core as a static archive merged into the mobile embed library, which `native dev|package --target ios|android` link into the toolkit hosts exactly as they do for Zig cores. The mobile matrix is aarch64 only: `aarch64-ios` and `aarch64-ios-simulator` build on a macOS host against the selected Apple SDK with an iOS 15.0 floor, and `aarch64-linux-android` builds on any desktop host against an installed NDK (`ANDROID_NDK_ROOT`, or the newest `ndk/<version>` under the SDK) with an API 26 floor. Services on mobile run only on the in-process pool — mobile apps cannot spawn a sibling process — so `service_carrier = "auto"` resolves to the pool there and an explicit `"child"` is refused with a teaching; desktop builds of the same app keep the child carrier under `auto`. The vendored npm lane is unchanged. Model persistence (`persist`), boot images, and URL media caching are not wired on mobile yet.
|
||||
|
||||
## Editor support
|
||||
|
||||
@@ -582,7 +609,7 @@ The compiled core is a native static archive, not generated source: `native chec
|
||||
|
||||
## Where the subset ends
|
||||
|
||||
The deterministic core tier owns app state and decisions. The service tier owns imperative application work in ordinary TypeScript: parsing, filesystem transforms, environment inspection, subprocesses, and other ambient operations whose results cross back as messages. The toolkit-extension tier — custom widgets, rasterizer work, new engine-owned effects, and platform integration — remains Zig by design: that layer is the machinery itself, and [Building Components](/docs/building-components) is its guide. Services are not a backdoor storage engine or general FFI surface.
|
||||
The core owns app state and decisions — the app's deterministic logic. Services own imperative application work in ordinary TypeScript: parsing, filesystem transforms, environment inspection, subprocesses, and other ambient operations whose results cross back as messages. The toolkit-extension tier — custom widgets, rasterizer work, new engine-owned effects, and platform integration — remains Zig by design: that layer is the machinery itself, and [Building Components](/docs/building-components) is its guide. Services are not a backdoor storage engine or general FFI surface.
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# TypeScript Services
|
||||
|
||||
Modules under `src/services/` are ordinary TypeScript compiled to native code on the compiler's full static tier: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, `JSON`, `Map`/`Set`, `Date`, and classes, when the pinned compiler supports them. The same pinned compiler builds the deterministic core ([TypeScript Cores](/docs/typescript)) and the service tier; no JavaScript engine ships in either.
|
||||
Modules under `src/services/` are ordinary TypeScript compiled to native code on the compiler's full static tier: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, `JSON`, `Map`/`Set`, `Date`, and classes, when the pinned compiler supports them. The same pinned compiler builds the deterministic core ([TypeScript Cores](/docs/typescript)) and the services; no JavaScript engine ships in either.
|
||||
|
||||
The core calls a service by returning a command from `update`. The typed result returns as an ordinary `Msg`:
|
||||
|
||||
@@ -15,16 +15,16 @@ case "parse":
|
||||
})];
|
||||
```
|
||||
|
||||
Services run on a supervised carrier — as a separate child process by default, or compiled into the app binary on an explicitly selected worker-thread pool — and are desktop-only today (see [Runtime behavior](#runtime-behavior)).
|
||||
Services run on a supervised carrier — on desktop, a separate child process by default or an explicitly selected worker-thread pool compiled into the app binary; on iOS and Android, the in-process pool only (see [Runtime behavior](#runtime-behavior)).
|
||||
|
||||
## The two tiers
|
||||
## The two roles
|
||||
|
||||
Record→replay, headless testing, and [automation](/docs/automation) depend on `update` being a pure function of its inputs. A service reads the real filesystem, clock, and network, so the checker refuses a core import of a service file (NS1065) and the core-to-service edge is always a command. Service results are journaled like every other effect result.
|
||||
The split is by role: the core is the app's deterministic logic — `Model`, `Msg`, `update` — and services do the app's imperative work. Record→replay, headless testing, and [automation](/docs/automation) depend on `update` being a pure function of its inputs. A service reads the real filesystem, clock, and network, so the checker refuses a core import of a service file (NS1065) and the core-to-service edge is always a command. Service results are journaled like every other effect result.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tier</th>
|
||||
<th>Role</th>
|
||||
<th>Owns</th>
|
||||
<th>Language rules</th>
|
||||
</tr>
|
||||
@@ -102,7 +102,7 @@ A service module is any `.ts` file under `src/services/`. Every directly exporte
|
||||
- It declares a contract-encodable result type.
|
||||
- Its name is `<module-basename>.<export>` — `export function parse` in `src/services/feeds.ts` is `feeds.parse`.
|
||||
|
||||
Boundary shapes live in a shared, subset-legal module outside `src/services/`, imported by both tiers:
|
||||
Boundary shapes live in a shared, subset-legal module outside `src/services/`, imported by the core and the service:
|
||||
|
||||
```ts:src/shared.ts
|
||||
export type ParseRequest = {
|
||||
@@ -130,7 +130,7 @@ export function parse(request: ParseRequest): ParseResult {
|
||||
}
|
||||
```
|
||||
|
||||
`native check` projects the complete type table into a contract sidecar (`services.contract.json`), checks both tiers, and generates the typed client the core imports. An operation shaped any other way — `async`, a default export, an unannotated request, a non-encodable result — is a teaching error (NS1067) naming the rewrite.
|
||||
`native check` projects the complete type table into a contract sidecar (`services.contract.json`), checks both classes, and generates the typed client the core imports. An operation shaped any other way — `async`, a default export, an unannotated request, a non-encodable result — is a teaching error (NS1067) naming the rewrite.
|
||||
|
||||
### Boundary types
|
||||
|
||||
@@ -148,7 +148,7 @@ export function parse(request: ParseRequest): ParseResult {
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Uint8Array</code></td>
|
||||
<td>The bytes form both tiers already share</td>
|
||||
<td>The bytes form the core and services already share</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Optionals, readonly slices</td>
|
||||
@@ -318,17 +318,17 @@ Two carriers run the same operations behind the same routes, keys, deadlines, ca
|
||||
<tr>
|
||||
<td><code>child</code></td>
|
||||
<td>A second native executable — <code><app>_services</code> — beside the app binary, packaged with it</td>
|
||||
<td>Unset/<code>auto</code> default; available on every supported desktop build</td>
|
||||
<td>Unset/<code>auto</code> default on desktop; unavailable on mobile</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>in_process</code></td>
|
||||
<td>Compiled into the app binary; a small thread pool, one isolated module instance per thread</td>
|
||||
<td>Explicit opt-in: native Linux, cross-Linux x86_64/aarch64, native Windows x86_64, cross-Windows x86_64 GNU, or macOS built on macOS</td>
|
||||
<td>Explicit opt-in on native Linux, cross-Linux x86_64/aarch64, native Windows x86_64, cross-Windows x86_64 GNU, or macOS built on macOS; unset/<code>auto</code> default on iOS and Android</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
`.service_carrier = "in_process" | "child"` in app.zon (or `-Dservice-carrier`) states the choice; unset/`"auto"` selects the child carrier. `.service_pool_size` (or `-Dservice-pool-size`, 1-16) sets the in-process pool width; the default is min(4, cores).
|
||||
`.service_carrier = "in_process" | "child"` in app.zon (or `-Dservice-carrier`) states the choice. Unset/`"auto"` selects the child carrier on desktop and the in-process pool on iOS/Android, where a child process is unavailable. `.service_pool_size` (or `-Dservice-pool-size`, 1-16) sets the in-process pool width; the default is min(4, cores).
|
||||
|
||||
Shared guarantees:
|
||||
|
||||
@@ -336,7 +336,7 @@ Shared guarantees:
|
||||
- **Verified pairing.** The child's startup handshake checks the protocol version and a fingerprint of the generated operation/type/package registry; the in-process carrier checks the same fingerprint against the linked archive. A mismatch rejects before any operation dispatches.
|
||||
- **Supervision.** Same-key requests run strictly FIFO. The in-process pool runs different keys in parallel across its instances; the child runs everything on one worker. A cancellation or deadline publishes the cooperative token and grants a short grace: an operation that returns inside it keeps its instance (or process) warm. Past the grace, the child is killed and respawns on the next request; the in-process carrier abandons the instance's thread, routes the failure, and adds a fresh instance to the pool. An abandoned dispatch keeps its key reserved until it physically stops, so a same-key replacement cannot overlap its side effects (and can itself expire while waiting). A detected trap poisons only the instance it fired in (`kind: "service_trap"`); other instances keep answering. Every failure produces a routed result: a dead transport `kind: "service_host"`, an expired deadline `kind: "timeout"`.
|
||||
- **Replay.** Terminal results and stream events are journaled like every other effect. Replaying a recorded session parks each request and feeds the recorded result; neither carrier starts anything.
|
||||
- **Scope.** Services are desktop-only today. Child executables follow the pinned compiler's broad matrix: same-platform builds, Linux and Windows GNU targets cross-compiled from a macOS/Linux/Windows host, and macOS targets built on macOS. A Windows MSVC target builds natively on a matching Windows host; cross-Windows uses GNU because Zig supplies that target's CRT and system libraries. In-process archives use the compiler's narrower object-localization matrix: native Linux, cross-Linux x86_64/aarch64, native Windows x86_64, cross-Windows x86_64 GNU, or macOS built on macOS. A pairing outside the relevant matrix fails with a teaching, as does any explicitly spelled Linux `-gnu` target without a glibc version — even when it matches the build host, Zig's target uses its default floor. The service runtime needs glibc 2.36+ or musl, so explicit Linux targets are spelled `x86_64-linux-gnu.2.36` (or later) or `x86_64-linux-musl`. Operations are synchronous.
|
||||
- **Scope.** Child executables are desktop-only and follow the pinned compiler's broad matrix: same-platform builds, Linux and Windows GNU targets cross-compiled from a macOS/Linux/Windows host, and macOS targets built on macOS. A Windows MSVC target builds natively on a matching Windows host; cross-Windows uses GNU because Zig supplies that target's CRT and system libraries. In-process archives use the compiler's narrower object-localization matrix: native Linux, cross-Linux x86_64/aarch64 (`aarch64-linux-android` included, API 26 floor, NDK required), native Windows x86_64, cross-Windows x86_64 GNU, or the Mach-O targets — macOS, `aarch64-ios`, and `aarch64-ios-simulator` (iOS 15.0 floor) — built on macOS. Mobile targets are archive-only: no sibling process exists there, so `service_carrier = "auto"` resolves to the in-process pool and an explicit `"child"` is refused with a teaching. A pairing outside the relevant matrix fails with a teaching, as does any explicitly spelled Linux `-gnu` target without a glibc version — even when it matches the build host, Zig's target uses its default floor. The service runtime needs glibc 2.36+ or musl, so explicit Linux targets are spelled `x86_64-linux-gnu.2.36` (or later) or `x86_64-linux-musl`. Operations are synchronous.
|
||||
|
||||
In-process specifics:
|
||||
|
||||
@@ -380,4 +380,4 @@ Three checker rules enforce the boundary. Each teaches the fix and the reason at
|
||||
|
||||
## Reference
|
||||
|
||||
`examples/service-feed-reader` is the minimal two-tier app: a deterministic core, a service using `node:fs`, regex, `Map`, `Date`, and JSON, and a kind-tagged error path. The machine-precise authoring guide ships as `native skills get ts-services`.
|
||||
[`examples/service-feed-reader`](https://github.com/vercel-labs/native/tree/main/examples/service-feed-reader) is the complete loop as a small app: `Cmd.fetch` downloads a feed, the delivered bytes cross to `feeds.parse` through the generated typed client, the service's regex-and-`Map` parser returns shared `FeedResult` records the markup renders, and malformed input lands on the err arm as kind-tagged JSON. Its end-to-end suite (`tests/ts-services/feed_reader_e2e_tests.zig` in the SDK repo) records the whole loop against a loopback HTTP fixture and replays it byte-identically with the service executable absent. The machine-precise authoring guide ships as `native skills get ts-services`.
|
||||
|
||||
@@ -8,7 +8,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
.title = "My App",
|
||||
.width = 480,
|
||||
.height = 320,
|
||||
.restore_state = false,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
@@ -16,6 +15,8 @@ const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
|
||||
The first scene window adopts the startup window; additional scene windows are created through the window service. Secondary windows can also be created imperatively from Zig — or, in apps that [embed web content](/docs/frontend), from trusted JavaScript.
|
||||
|
||||
Fresh windows use three-way placement. A successful state-store lookup restores the saved frame. Otherwise, an authored `x` or `y` places the window at that origin (constrained to the matching or nearest visible screen on macOS). With neither saved state nor an explicit origin, macOS centers the main window. Under the default `clamp_to_visible_screen` policy, restored frames stay on their matching or nearest display and fresh secondary windows cascade from the active window; `center_on_primary` centers restored and fresh default frames on the primary display. `restore_state = true` is only the persistence opt-in—it no longer makes a first launch look like a restored frame at `(0, 0)`.
|
||||
|
||||
## Overlay windows
|
||||
|
||||
Window creation accepts four overlay controls:
|
||||
@@ -35,7 +36,6 @@ const overlay: native_sdk.ShellWindow = .{
|
||||
.always_on_top = true,
|
||||
.click_through = true,
|
||||
.activate_on_show = false,
|
||||
.restore_state = false,
|
||||
.views = &.{.{
|
||||
.label = "overlay-canvas",
|
||||
.kind = .gpu_surface,
|
||||
@@ -68,6 +68,7 @@ const info = try runtime.createWindow(.{
|
||||
.label = "tools",
|
||||
.title = "Tools",
|
||||
.default_frame = native_sdk.geometry.RectF.init(80, 80, 420, 320),
|
||||
.initial_placement = .explicit,
|
||||
});
|
||||
try runtime.focusWindow(info.id);
|
||||
```
|
||||
@@ -155,7 +156,7 @@ JavaScript creation also accepts `titlebar: "standard" | "hidden_inset" | "hidde
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>WindowRestorePolicy</code></td>
|
||||
<td>How restored frames are placed, such as clamping to the visible screen or centering on the primary display</td>
|
||||
<td>How macOS places restored and fresh default frames: clamp to the matching/nearest visible screen or center on the primary display</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>WindowClosePolicy</code></td>
|
||||
@@ -169,9 +170,13 @@ JavaScript creation also accepts `titlebar: "standard" | "hidden_inset" | "hidde
|
||||
Each window declares what the user's close affordance (the red traffic light, cmd+W, the caption X) does, via `close_policy`:
|
||||
|
||||
- `"quit"` — the default and the classic behavior, unchanged for every existing app: the window really closes, and closing the last one follows the host's exit semantics.
|
||||
- `"hide"` — the menu-bar/tray-app shape: the close affordance hides the window (it stays alive with its views; `WindowState.hidden` flips true on the frame channel, in the session journal, and in the `hidden` field of the window JSON the `window.zero.windows.*` bridge returns — `open` stays true, so `hidden` is what distinguishes a policy-hidden window from a visible unfocused one) and the app keeps running behind its status item. `fx.showWindow(label)` brings it back — a tray "Open" row's natural consequence — and on macOS the Dock-icon reopen re-shows it on its own. Runtime-initiated closes (`fx.closeWindow`, reconcile closes) still really close: the policy governs the USER's affordance, not the app's own decisions.
|
||||
- `"hide"` — the menu-bar/tray-app shape: the close affordance hides the window (it stays alive with its views; `WindowState.hidden` flips true on the frame channel, in the session journal, and in the `hidden` field of the window JSON the `window.zero.windows.*` bridge returns — `open` stays true, so `hidden` is what distinguishes a policy-hidden window from a visible unfocused one) and the app keeps running behind its status item. `Cmd.showWindow(label)` brings it back — a tray "Open" row's natural consequence — and on macOS a visible Dock icon provides a second reopen path. Runtime-initiated closes (`Cmd.closeWindow`, reconcile closes) still really close: the policy governs the USER's affordance, not the app's own decisions.
|
||||
|
||||
`hide` is supported where the host can re-show a hidden window: macOS (`windowShouldClose` orders the window out; the Dock reopen path always exists) and Windows (`WM_CLOSE` hides via `SW_HIDE`; the tray icon is the ONLY re-show affordance, so on Windows `close_policy = "hide"` additionally requires the `"tray"` capability in app.zon — hiding removes the taskbar entry and Windows has no Dock-style reopen, so a declaration without the tray is refused at build time and at window create, exactly like Linux's refusal below; and if the declared tray fails to install at runtime, the first hide-close downgrades to a real close with a loud log rather than stranding an invisible process). Linux has no status item in this toolkit yet, so nothing could bring a hidden window back — the declaration is refused loudly at build time (a compile error for manifest windows) and at window create (`error.UnsupportedWindowClosePolicy`), never a silent no-op. The hidden state is session-transient: it never persists to the window-state store, so every launch starts shown.
|
||||
`UiApp.WindowDescriptor.close_policy` exposes the same enum for model-declared secondary windows. Descriptor presence means the window remains alive: a `.hide` user close retains its slot and views and does not dispatch `on_close`; `showWindow(label)` reveals that same window. Stopping the declaration still performs a real reconcile close.
|
||||
|
||||
TypeScript cores expose the same contract as `windows(model): readonly WindowDescriptor[]`, with camel-case `closePolicy`. Each descriptor uses a literal `label: asciiBytes("<label>")` and the matching `src/windows/<label>.native`; check/build reject dynamic labels and missing roots. Those roots may import components nested under `src/windows/`. `titlebar` includes `"chromeless"`, including the transparent-Windows shape described above. Under `"quit"`, `onCloseCommand` routes through `commandMsg`; under `"hide"`, no close command fires because the window did not close.
|
||||
|
||||
`hide` is supported where the host can re-show a hidden window: macOS (`windowShouldClose` orders the window out) and Windows (`WM_CLOSE` hides via `SW_HIDE`; the tray icon is the ONLY re-show affordance, so on Windows `close_policy = "hide"` additionally requires the `"tray"` capability in app.zon — hiding removes the taskbar entry and Windows has no Dock-style reopen, so a declaration without the tray is refused at build time and at window create, exactly like Linux's refusal below; and if the declared tray fails to install at runtime, the first hide-close downgrades to a real close with a loud log rather than stranding an invisible process). On macOS the Dock reopen path exists only while `dock_visible = true`; an Accessory app has no Dock icon, so its status item must call `Cmd.showWindow(label)` to bring the window back. This is why `dock_visible = false` requires the `"tray"` capability. Linux has no status item in this toolkit yet, so nothing could bring a hidden window back — the declaration is refused loudly at build time (a compile error for manifest windows) and at window create (`error.UnsupportedWindowClosePolicy`), never a silent no-op. The hidden state is session-transient: it never persists to the window-state store, so every launch starts shown unless the manifest explicitly sets `initially_hidden = true`.
|
||||
|
||||
A policy-hidden window is occluded for pacing, exactly like a minimized one: its canvas frame completions drop to the ~1 Hz occluded heartbeat instead of the display grid (a menu-bar app parks hidden for days — full-rate frames there would be pure background CPU burn), and it does not count as a visible display for `.spectrum` emissions. `fx.showWindow` restores full cadence with the window.
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@
|
||||
},
|
||||
{
|
||||
"name": "checkbox",
|
||||
"doc": "Value control; bind checked, dispatch with on-toggle."
|
||||
"doc": "Text-bearing value control; the visible label is text content (or text=), bind checked, dispatch with on-toggle."
|
||||
},
|
||||
{
|
||||
"name": "radio",
|
||||
"doc": "Value control; bind checked or selected, dispatch with on-toggle."
|
||||
"doc": "Text-bearing single-choice value control; the visible label is text content (or text=), bind checked or selected. Selection dispatches on-change when bound, then on-toggle, then on-press for compatibility."
|
||||
},
|
||||
{
|
||||
"name": "toggle",
|
||||
@@ -118,7 +118,7 @@
|
||||
},
|
||||
{
|
||||
"name": "radio-group",
|
||||
"doc": "Row container grouping radio controls; children flow horizontally."
|
||||
"doc": "Logical radiogroup: give it an accessible label; descendant radios at any nesting depth share one Tab stop and selection, arrows wrap, and Home/End move to the scope edges."
|
||||
},
|
||||
{
|
||||
"name": "tabs",
|
||||
@@ -158,15 +158,15 @@
|
||||
},
|
||||
{
|
||||
"name": "dialog",
|
||||
"doc": "Modal dialog surface rendered in place; title via text, wrap in an if to show conditionally."
|
||||
"doc": "Root-relative modal centered in the viewport and unaffected by ancestor scroll or clipping; title via text, wrap in an if to show conditionally."
|
||||
},
|
||||
{
|
||||
"name": "drawer",
|
||||
"doc": "Drawer surface rendered in place; title via text, wrap in an if to show conditionally."
|
||||
"doc": "Root-relative modal spanning the viewport width and docked to its bottom; title via text, wrap in an if to show conditionally."
|
||||
},
|
||||
{
|
||||
"name": "sheet",
|
||||
"doc": "Sheet surface rendered in place; title via text, wrap in an if to show conditionally."
|
||||
"doc": "Root-relative modal spanning the viewport height and docked to its right edge; title via text, wrap in an if to show conditionally."
|
||||
},
|
||||
{
|
||||
"name": "resizable",
|
||||
@@ -746,6 +746,22 @@
|
||||
{
|
||||
"name": "image",
|
||||
"doc": "avatar and image: one {binding} to a u64 ImageId the app registered at runtime (Cmd.imageLoad, fx.loadImage, fx.registerImageBytes); 0 draws nothing (an avatar falls back to its initials). Required on the image leaf."
|
||||
},
|
||||
{
|
||||
"name": "source-x",
|
||||
"doc": "avatar and image: left edge of an optional source crop, in decoded-image pixels. Declare all four source-* attributes together beside image."
|
||||
},
|
||||
{
|
||||
"name": "source-y",
|
||||
"doc": "avatar and image: top edge of an optional source crop, in decoded-image pixels. Declare all four source-* attributes together beside image."
|
||||
},
|
||||
{
|
||||
"name": "source-width",
|
||||
"doc": "avatar and image: width of an optional source crop, in decoded-image pixels. Declare all four source-* attributes together beside image."
|
||||
},
|
||||
{
|
||||
"name": "source-height",
|
||||
"doc": "avatar and image: height of an optional source crop, in decoded-image pixels. Declare all four source-* attributes together beside image."
|
||||
}
|
||||
],
|
||||
"media-surface": [
|
||||
|
||||
@@ -44,6 +44,7 @@ const unprefixedNavSections: NavSection[] = [
|
||||
{ name: "Model Persistence", href: "/persistence" },
|
||||
{ name: "Record Store", href: "/record-store" },
|
||||
{ name: "Relational SQLite", href: "/sqlite" },
|
||||
{ name: "Files & Streaming", href: "/files" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
||||
persistence: "Model Persistence",
|
||||
"record-store": "Record Store",
|
||||
sqlite: "Relational SQLite",
|
||||
files: "Files & Streaming",
|
||||
terminal: "Terminal",
|
||||
state: "State & Data Flow",
|
||||
theming: "Theming",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Decoder over the app-core Cmd/Sub wire format (rt.zig, cmd_format_version
|
||||
//! 4), shared by the ts-track behavioral harnesses. The graders copy this
|
||||
//! 7), shared by the ts-track behavioral harnesses. The graders copy this
|
||||
//! file next to each case's harness so assertions read decoded ops — "a
|
||||
//! fetch with key `feed` targeting this URL", "the delay re-armed" — instead
|
||||
//! of hand-built byte strings, which keeps harnesses lenient about the parts
|
||||
@@ -29,6 +29,13 @@ pub const Op = union(enum) {
|
||||
cancel: struct { key: []const u8 },
|
||||
read_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8 },
|
||||
write_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8, bytes: []const u8 },
|
||||
append_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8, bytes: []const u8 },
|
||||
stat_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8 },
|
||||
delete_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8 },
|
||||
read_file_stream: struct { key: []const u8, chunk_tag: u8, done_tag: u8, err_tag: u8, path: []const u8 },
|
||||
write_file_stream: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8 },
|
||||
write_file_chunk: struct { key: []const u8, ok_tag: u8, err_tag: u8, bytes: []const u8 },
|
||||
write_file_close: struct { key: []const u8, ok_tag: u8, err_tag: u8 },
|
||||
fetch: Fetch,
|
||||
fetch_stream: FetchStream,
|
||||
clip_write: struct { bytes: []const u8 },
|
||||
@@ -55,7 +62,7 @@ pub const Op = union(enum) {
|
||||
pty_write: struct { key: []const u8, bytes: []const u8 },
|
||||
pty_resize: struct { key: []const u8, cols: f64, rows: f64 },
|
||||
pty_kill: struct { key: []const u8 },
|
||||
show_notification: struct { title: []const u8, subtitle: []const u8, body: []const u8 },
|
||||
show_notification: struct { id: []const u8, title: []const u8, subtitle: []const u8, body: []const u8, action_label: []const u8, action_command: []const u8 },
|
||||
audio_capture_start: struct { key: f64, source: u8, sample_rate: u32, channels: u8, event_tag: u8 },
|
||||
audio_capture_stop: struct { key: f64 },
|
||||
|
||||
@@ -406,7 +413,7 @@ pub const CmdIter = struct {
|
||||
const title = longBytes(b, &off);
|
||||
const subtitle = longBytes(b, &off);
|
||||
const body = longBytes(b, &off);
|
||||
break :blk .{ .show_notification = .{ .title = title, .subtitle = subtitle, .body = body } };
|
||||
break :blk .{ .show_notification = .{ .id = "", .title = title, .subtitle = subtitle, .body = body, .action_label = "", .action_command = "" } };
|
||||
},
|
||||
// audio_capture_start [op 0x1E][key f64 LE][source u8]
|
||||
// [sample_rate u32 LE][channels u8][event_tag u8].
|
||||
@@ -473,6 +480,24 @@ pub const CmdIter = struct {
|
||||
.body = body,
|
||||
} };
|
||||
},
|
||||
// actionable_notification [op 0x31][id/title/subtitle/body/
|
||||
// action_label/action_command as u32-length bytes].
|
||||
0x31 => blk: {
|
||||
const id = longBytes(b, &off);
|
||||
const title = longBytes(b, &off);
|
||||
const subtitle = longBytes(b, &off);
|
||||
const body = longBytes(b, &off);
|
||||
const action_label = longBytes(b, &off);
|
||||
const action_command = longBytes(b, &off);
|
||||
break :blk .{ .show_notification = .{
|
||||
.id = id,
|
||||
.title = title,
|
||||
.subtitle = subtitle,
|
||||
.body = body,
|
||||
.action_label = action_label,
|
||||
.action_command = action_command,
|
||||
} };
|
||||
},
|
||||
// window_hide [op 0x21][label_len u8][label].
|
||||
0x21 => blk: {
|
||||
const label = shortBytes(b, &off);
|
||||
@@ -546,6 +571,38 @@ pub const CmdIter = struct {
|
||||
}
|
||||
break :blk .{ .store_set_many = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .scope = scope, .count = count, .entry_bytes = b[entries_start..off] } };
|
||||
},
|
||||
0x2B => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .append_file = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = longBytes(b, &off), .bytes = longBytes(b, &off) } };
|
||||
},
|
||||
0x2C => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .stat_file = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = longBytes(b, &off) } };
|
||||
},
|
||||
0x2D => blk: {
|
||||
const key = shortBytes(b, &off);
|
||||
const chunk_tag = b[off];
|
||||
const done_tag = b[off + 1];
|
||||
const err_tag = b[off + 2];
|
||||
off += 3;
|
||||
break :blk .{ .read_file_stream = .{ .key = key, .chunk_tag = chunk_tag, .done_tag = done_tag, .err_tag = err_tag, .path = longBytes(b, &off) } };
|
||||
},
|
||||
0x2E => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .write_file_stream = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = longBytes(b, &off) } };
|
||||
},
|
||||
0x2F => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .write_file_chunk = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .bytes = longBytes(b, &off) } };
|
||||
},
|
||||
0x30 => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .write_file_close = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err } };
|
||||
},
|
||||
0x32 => blk: {
|
||||
const head = routedHead(b, &off);
|
||||
break :blk .{ .delete_file = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = longBytes(b, &off) } };
|
||||
},
|
||||
else => std.debug.panic("cmdview: unknown op byte 0x{X:0>2} at offset {d}", .{ op, self.off }),
|
||||
};
|
||||
self.off = off;
|
||||
@@ -711,6 +768,23 @@ test "record store command records decode and advance exactly" {
|
||||
try std.testing.expectEqual(@as(?Op, null), iter.next());
|
||||
}
|
||||
|
||||
test "delete_file decodes and advances a batch exactly" {
|
||||
const batch = [_]u8{
|
||||
0x32, 4, 'f', 'i', 'l', 'e', 2, 3,
|
||||
12, 0, 0, 0, 'o', 'b', 's', 'o',
|
||||
'l', 'e', 't', 'e', '.', 'b', 'i', 'n',
|
||||
0x02, 7,
|
||||
};
|
||||
var iter = CmdIter.init(&batch);
|
||||
const deleted = (iter.next() orelse return error.TestUnexpectedResult).delete_file;
|
||||
try std.testing.expectEqualStrings("file", deleted.key);
|
||||
try std.testing.expectEqual(@as(u8, 2), deleted.ok_tag);
|
||||
try std.testing.expectEqual(@as(u8, 3), deleted.err_tag);
|
||||
try std.testing.expectEqualStrings("obsolete.bin", deleted.path);
|
||||
try std.testing.expectEqual(@as(u8, 7), (iter.next() orelse return error.TestUnexpectedResult).now.msg_tag);
|
||||
try std.testing.expectEqual(@as(?Op, null), iter.next());
|
||||
}
|
||||
|
||||
test "the image records decode, alone and inside a batch" {
|
||||
// image_load: [op 0x12][id f64 LE][event_tag][path][url][cache]
|
||||
// [expected f64 LE] — the bytes rt.zig's cmdImageLoad pins (the same
|
||||
|
||||
@@ -17,6 +17,7 @@ TypeScript is the primary app-authoring language. A new `native init my_app` pro
|
||||
| Example | Shows |
|
||||
| --- | --- |
|
||||
| `chatbot` | Multi-module TypeScript core, text editing, streaming `Cmd.fetch`, environment messages, and deterministic replay. |
|
||||
| `service-feed-reader` | The complete services loop: `Cmd.fetch`, a parsing service reached through the generated `@native-sdk/services` client, shared record shapes, and recorded replay without the service. |
|
||||
| `relational-notes` | Append-only SQLite migrations, build-time checked SQL, generated typed transactions and page decoders, FTS5, and live queries. |
|
||||
| `gpu-components` | Isolated interactive Native UI specimens, disclosure trees, anchored menus, and controlled component state. |
|
||||
| `soundboard-ts` | Full music player: audio effects, timers, search, assets, native context menus, and adaptive markup. |
|
||||
|
||||
@@ -24,6 +24,8 @@ enum {
|
||||
NATIVE_SDK_WIDGET_ROLE_SWITCH = 17,
|
||||
NATIVE_SDK_WIDGET_ROLE_SLIDER = 18,
|
||||
NATIVE_SDK_WIDGET_ROLE_PROGRESSBAR = 19,
|
||||
NATIVE_SDK_WIDGET_ROLE_RADIO = 20,
|
||||
NATIVE_SDK_WIDGET_ROLE_RADIOGROUP = 21,
|
||||
};
|
||||
|
||||
enum {
|
||||
|
||||
@@ -248,7 +248,7 @@ class MainActivity : Activity(), SurfaceHolder.Callback {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
info.stateDescription = widgetStateDescription(node)
|
||||
}
|
||||
info.isCheckable = node.role == WIDGET_ROLE_CHECKBOX || node.role == WIDGET_ROLE_SWITCH
|
||||
info.isCheckable = node.role == WIDGET_ROLE_CHECKBOX || node.role == WIDGET_ROLE_RADIO || node.role == WIDGET_ROLE_SWITCH
|
||||
info.isChecked = info.isCheckable && widgetValueSelected(node)
|
||||
info.isClickable = widgetSupportsAnyAction(node, WIDGET_ACTION_PRESS or WIDGET_ACTION_TOGGLE or WIDGET_ACTION_SELECT)
|
||||
info.isEditable = node.role == WIDGET_ROLE_TEXTBOX && (node.flags and WIDGET_FLAG_READ_ONLY) == 0
|
||||
@@ -347,6 +347,8 @@ class MainActivity : Activity(), SurfaceHolder.Callback {
|
||||
WIDGET_ROLE_BUTTON, WIDGET_ROLE_MENUITEM -> "android.widget.Button"
|
||||
WIDGET_ROLE_TEXTBOX -> "android.widget.EditText"
|
||||
WIDGET_ROLE_CHECKBOX -> "android.widget.CheckBox"
|
||||
WIDGET_ROLE_RADIO -> "android.widget.RadioButton"
|
||||
WIDGET_ROLE_RADIOGROUP -> "android.widget.RadioGroup"
|
||||
WIDGET_ROLE_SWITCH -> "android.widget.Switch"
|
||||
WIDGET_ROLE_SLIDER -> "android.widget.SeekBar"
|
||||
WIDGET_ROLE_PROGRESSBAR -> "android.widget.ProgressBar"
|
||||
@@ -784,6 +786,8 @@ class MainActivity : Activity(), SurfaceHolder.Callback {
|
||||
private const val WIDGET_ROLE_SWITCH = 17
|
||||
private const val WIDGET_ROLE_SLIDER = 18
|
||||
private const val WIDGET_ROLE_PROGRESSBAR = 19
|
||||
private const val WIDGET_ROLE_RADIO = 20
|
||||
private const val WIDGET_ROLE_RADIOGROUP = 21
|
||||
private const val WIDGET_FLAG_FOCUSED = 1 shl 0
|
||||
private const val WIDGET_FLAG_SELECTED = 1 shl 3
|
||||
private const val WIDGET_FLAG_DISABLED = 1 shl 4
|
||||
|
||||
@@ -118,6 +118,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
|
||||
.resizable = windowBool(window, "resizable", true),
|
||||
.restore_state = windowBool(window, "restore_state", true),
|
||||
.restore_policy = windowRestorePolicy(window),
|
||||
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
.width = 320,
|
||||
.height = 490,
|
||||
.resizable = false,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.titlebar = "hidden_inset",
|
||||
.views = .{
|
||||
|
||||
@@ -56,7 +56,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
// the in-canvas drag band carries the window (see view.zig), and
|
||||
// app.zon's startup window declares the same style.
|
||||
.resizable = false,
|
||||
.restore_state = false,
|
||||
.titlebar = .hidden_inset,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
@@ -152,7 +151,6 @@ pub fn main(init: std.process.Init) !void {
|
||||
.window_title = "Calculator",
|
||||
.bundle_id = "dev.native_sdk.calculator",
|
||||
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
|
||||
.restore_state = false,
|
||||
.js_window_api = false,
|
||||
.shortcuts = &app_shortcuts,
|
||||
.security = .{
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
.title = "Native SDK Canvas Preview",
|
||||
.width = 960,
|
||||
.height = 640,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.views = .{
|
||||
.{ .label = "preview-canvas", .kind = "gpu_surface", .fill = true, .role = "Canvas chrome", .accessibility_label = "Canvas Preview chrome", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
|
||||
|
||||
@@ -49,7 +49,6 @@ pub const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
.title = "Native SDK Canvas Preview",
|
||||
.width = window_width,
|
||||
.height = window_height,
|
||||
.restore_state = false,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
pub const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
@@ -196,7 +195,6 @@ pub fn main(init: std.process.Init) !void {
|
||||
.window_title = "Native SDK Canvas Preview",
|
||||
.bundle_id = "dev.native_sdk.canvas_preview",
|
||||
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
|
||||
.restore_state = false,
|
||||
.js_window_api = false,
|
||||
.security = .{
|
||||
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app", "https://example.com", "https://native-sdk.dev" } },
|
||||
|
||||
@@ -126,7 +126,7 @@ test "toolbar and status-item commands navigate and reload the webview" {
|
||||
try testing.expectEqual(@as(usize, 1), harness.null_platform.trayCreateCount());
|
||||
try testing.expectEqualStrings("NS", harness.null_platform.lastTrayTitle());
|
||||
try testing.expectEqual(@as(usize, main.status_items.len), harness.null_platform.trayItems().len);
|
||||
try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .tray_action = 3 });
|
||||
try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .tray_action = .{ .item_id = 3 } });
|
||||
try testing.expectEqual(@as(u32, 1), app_state.model.reload_count);
|
||||
try testing.expectEqualStrings(main.docs_url, (try previewWebView(harness)).url);
|
||||
try testing.expectEqual(navigations_after_install + 2, harness.null_platform.webview_navigate_count);
|
||||
|
||||
@@ -8,7 +8,7 @@ This example shows guarded OS capabilities from trusted WebView code:
|
||||
- Clipboard text read and write.
|
||||
- Message dialogs.
|
||||
- Credential set, get, and delete.
|
||||
- File-drop events delivered to Zig and the WebView event bridge.
|
||||
- File-drop events delivered to Zig and the WebView event bridge, plus a real canvas `drop_files` target.
|
||||
- File association and custom URL scheme packaging metadata.
|
||||
- App activation and deactivation events.
|
||||
|
||||
@@ -24,6 +24,8 @@ Run the headless test path:
|
||||
zig build test -Dplatform=null
|
||||
```
|
||||
|
||||
For the macOS host integration check, run the app with the system backend and drag a Finder file onto the right-hand **Drop files here** canvas. The status bar must report `Widget target 2 fired` and the dropped path. Dropping over the left WebView must still report the ordinary app-level drop without a widget target. The guest-VM harness cannot synthesize an AppKit drag session yet, so this is the documented manual receipt for the real host path.
|
||||
|
||||
Run all native-first example tests from the repository root:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"webview",
|
||||
"js_bridge",
|
||||
"native_views",
|
||||
"gpu_surfaces",
|
||||
"open_url",
|
||||
"reveal_path",
|
||||
"recent_documents",
|
||||
@@ -39,11 +40,13 @@
|
||||
.title = "Native SDK Capabilities",
|
||||
.width = 900,
|
||||
.height = 620,
|
||||
.min_width = 770,
|
||||
.restore_policy = "center_on_primary",
|
||||
.views = .{
|
||||
.{ .label = "main", .kind = "webview", .url = "zero://inline", .fill = true },
|
||||
.{ .label = "statusbar", .kind = "statusbar", .edge = "bottom", .height = 34, .role = "Status" },
|
||||
.{ .label = "status-label", .kind = "label", .parent = "statusbar", .x = 14, .y = 8, .width = 640, .height = 18, .text = "Ready." },
|
||||
.{ .label = "drop-canvas", .kind = "gpu_surface", .edge = "right", .width = 250, .min_width = 220, .role = "File drop canvas", .accessibility_label = "File drop target", .gpu_backend = "metal" },
|
||||
.{ .label = "main", .kind = "webview", .url = "zero://inline", .fill = true, .min_width = 520 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,7 +10,10 @@ const manifest_url_schemes = if (@hasField(@TypeOf(app_manifest), "url_schemes")
|
||||
|
||||
const window_width: f32 = 900;
|
||||
const window_height: f32 = 620;
|
||||
const window_min_width: f32 = 770;
|
||||
const statusbar_height: f32 = 34;
|
||||
const drop_canvas_label = "drop-canvas";
|
||||
const drop_target_id: native_sdk.canvas.ObjectId = 2;
|
||||
|
||||
const html =
|
||||
\\<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -54,24 +57,30 @@ const builtin_policies = [_]native_sdk.BridgeCommandPolicy{
|
||||
.{ .name = "native-sdk.credentials.delete", .permissions = &credential_permission, .origins = &bridge_origins },
|
||||
};
|
||||
const shell_views = [_]native_sdk.ShellView{
|
||||
.{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true },
|
||||
.{ .label = "statusbar", .kind = .statusbar, .edge = .bottom, .height = statusbar_height, .layer = 20, .role = "Status" },
|
||||
.{ .label = "status-label", .kind = .label, .parent = "statusbar", .x = 14, .y = 8, .width = 640, .height = 18, .layer = 21, .text = "Ready." },
|
||||
.{ .label = drop_canvas_label, .kind = .gpu_surface, .edge = .right, .width = 250, .min_width = 220, .role = "File drop canvas", .accessibility_label = "File drop target", .gpu_backend = .metal },
|
||||
.{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true, .min_width = 520 },
|
||||
};
|
||||
const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
.label = "main",
|
||||
.title = "Native SDK Capabilities",
|
||||
.width = window_width,
|
||||
.height = window_height,
|
||||
.min_width = window_min_width,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
|
||||
const CapabilitiesApp = struct {
|
||||
drop_count: u32 = 0,
|
||||
widget_drop_count: u32 = 0,
|
||||
activation_count: u32 = 0,
|
||||
deactivation_count: u32 = 0,
|
||||
last_drop_paths: []const []const u8 = &.{},
|
||||
last_drop_target_id: native_sdk.canvas.ObjectId = 0,
|
||||
pending_drop_target_id: ?native_sdk.canvas.ObjectId = null,
|
||||
drop_target_installed: bool = false,
|
||||
|
||||
fn app(self: *@This()) native_sdk.App {
|
||||
return .{
|
||||
@@ -96,9 +105,34 @@ const CapabilitiesApp = struct {
|
||||
self.last_drop_paths = drop.paths;
|
||||
var status_buffer: [160]u8 = undefined;
|
||||
const first_path = if (drop.paths.len > 0) drop.paths[0] else "";
|
||||
const status = try std.fmt.bufPrint(&status_buffer, "Received file drop {d}: {d} file(s): {s}", .{ self.drop_count, drop.paths.len, first_path });
|
||||
const drop_target_id_value = self.pending_drop_target_id;
|
||||
self.pending_drop_target_id = null;
|
||||
const status = if (drop_target_id_value) |target_id|
|
||||
try std.fmt.bufPrint(&status_buffer, "Widget target {d} fired; app drop {d}: {d} file(s): {s}", .{ target_id, self.drop_count, drop.paths.len, first_path })
|
||||
else
|
||||
try std.fmt.bufPrint(&status_buffer, "Received file drop {d}: {d} file(s): {s}", .{ self.drop_count, drop.paths.len, first_path });
|
||||
_ = try runtime.updateView(drop.window_id, "status-label", .{ .text = status });
|
||||
},
|
||||
.canvas_widget_file_drop => |drop| {
|
||||
self.widget_drop_count += 1;
|
||||
if (drop.target) |target| {
|
||||
self.last_drop_target_id = target.id;
|
||||
self.pending_drop_target_id = target.id;
|
||||
} else {
|
||||
self.pending_drop_target_id = null;
|
||||
}
|
||||
},
|
||||
.gpu_surface_frame => |frame| {
|
||||
if (!self.drop_target_installed and std.mem.eql(u8, frame.label, drop_canvas_label)) {
|
||||
try installDropTarget(runtime, frame.window_id, frame.label, frame.size);
|
||||
self.drop_target_installed = true;
|
||||
}
|
||||
},
|
||||
.gpu_surface_resized => |resize| {
|
||||
if (self.drop_target_installed and std.mem.eql(u8, resize.label, drop_canvas_label)) {
|
||||
try installDropTarget(runtime, resize.window_id, resize.label, resize.frame.size());
|
||||
}
|
||||
},
|
||||
.lifecycle => |lifecycle| switch (lifecycle) {
|
||||
.activate => {
|
||||
self.activation_count += 1;
|
||||
@@ -110,11 +144,31 @@ const CapabilitiesApp = struct {
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
.appearance_changed, .command, .shortcut, .timer, .effects_wake, .audio, .video, .gpu_surface_frame, .gpu_surface_resized, .gpu_surface_input, .canvas_widget_pointer, .canvas_widget_keyboard, .canvas_widget_scroll, .canvas_widget_file_drop, .canvas_widget_drag, .canvas_widget_context_menu, .canvas_widget_context_menu_shown, .canvas_widget_context_menu_dismissed, .canvas_widget_context_menu_request, .canvas_widget_dismiss, .canvas_widget_context_press, .canvas_widget_resize, .canvas_widget_change, .window_closed, .automation_provenance => {},
|
||||
.appearance_changed, .command, .shortcut, .timer, .effects_wake, .audio, .video, .gpu_surface_input, .canvas_widget_pointer, .canvas_widget_keyboard, .canvas_widget_scroll, .canvas_widget_drag, .canvas_widget_context_menu, .canvas_widget_context_menu_shown, .canvas_widget_context_menu_dismissed, .canvas_widget_context_menu_request, .canvas_widget_dismiss, .canvas_widget_context_press, .canvas_widget_resize, .canvas_widget_change, .window_closed, .automation_provenance => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn installDropTarget(runtime: *native_sdk.Runtime, window_id: native_sdk.WindowId, label: []const u8, size: native_sdk.geometry.SizeF) !void {
|
||||
const canvas = native_sdk.canvas;
|
||||
const margin: f32 = 24;
|
||||
const target = canvas.Widget{
|
||||
.id = drop_target_id,
|
||||
.kind = .button,
|
||||
.frame = native_sdk.geometry.RectF.init(margin, margin, @max(1, size.width - margin * 2), @max(1, size.height - margin * 2)),
|
||||
.text = "Drop files here",
|
||||
.semantics = .{ .label = "Drop files here", .actions = .{ .drop_files = true } },
|
||||
};
|
||||
var nodes: [2]canvas.WidgetLayoutNode = undefined;
|
||||
const layout = try canvas.layoutWidgetTree(
|
||||
.{ .id = 1, .kind = .panel, .children = &.{target} },
|
||||
native_sdk.geometry.RectF.init(0, 0, size.width, size.height),
|
||||
&nodes,
|
||||
);
|
||||
_ = try runtime.setCanvasWidgetLayout(window_id, label, layout);
|
||||
_ = try runtime.emitCanvasWidgetDisplayList(window_id, label, .{});
|
||||
}
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
var app = CapabilitiesApp{};
|
||||
try runner.runWithOptions(app.app(), .{
|
||||
@@ -139,6 +193,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
test "capabilities bridge gates native services and dispatches file drops" {
|
||||
const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(window_width, window_height) });
|
||||
defer harness.destroy(std.testing.allocator);
|
||||
harness.null_platform.gpu_surfaces = true;
|
||||
harness.runtime.options.builtin_bridge = .{ .enabled = true, .commands = &builtin_policies };
|
||||
harness.runtime.options.security = .{
|
||||
.permissions = &app_permissions,
|
||||
@@ -155,6 +210,30 @@ test "capabilities bridge gates native services and dispatches file drops" {
|
||||
const app = app_state.app();
|
||||
try harness.start(app);
|
||||
|
||||
var views_buffer: [8]native_sdk.ViewInfo = undefined;
|
||||
const views = harness.runtime.listViews(1, &views_buffer);
|
||||
const webview = viewByLabel(views, "main").?;
|
||||
const drop_canvas = viewByLabel(views, drop_canvas_label).?;
|
||||
const statusbar = viewByLabel(views, "statusbar").?;
|
||||
try std.testing.expect(webview.parent == null);
|
||||
try std.testing.expect(drop_canvas.parent == null);
|
||||
try std.testing.expectEqual(native_sdk.geometry.RectF.init(0, 0, 650, window_height - statusbar_height), webview.frame);
|
||||
try std.testing.expectEqual(native_sdk.geometry.RectF.init(650, 0, 250, window_height - statusbar_height), drop_canvas.frame);
|
||||
try std.testing.expectEqual(native_sdk.geometry.RectF.init(0, window_height - statusbar_height, window_width, statusbar_height), statusbar.frame);
|
||||
|
||||
try harness.runtime.dispatchPlatformEvent(app, .{ .gpu_surface_frame = .{
|
||||
.window_id = 1,
|
||||
.label = drop_canvas_label,
|
||||
.size = native_sdk.geometry.SizeF.init(250, window_height - statusbar_height),
|
||||
.frame_index = 1,
|
||||
.nonblank = true,
|
||||
} });
|
||||
try harness.runtime.dispatchPlatformEvent(app, .{ .gpu_surface_resized = .{
|
||||
.window_id = 1,
|
||||
.label = drop_canvas_label,
|
||||
.frame = native_sdk.geometry.RectF.init(0, 0, 350, window_height - statusbar_height),
|
||||
} });
|
||||
|
||||
try dispatchBridge(harness, app, "{\"id\":\"notify\",\"command\":\"native-sdk.os.showNotification\",\"payload\":{\"title\":\"Capabilities\",\"subtitle\":\"native-sdk\",\"body\":\"Done\"}}");
|
||||
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"ok\":true") != null);
|
||||
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.notificationCount());
|
||||
@@ -194,15 +273,37 @@ test "capabilities bridge gates native services and dispatches file drops" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"result\":true") != null);
|
||||
|
||||
const dropped_paths = [_][]const u8{ "/tmp/one\nname.txt", "/tmp/two.txt" };
|
||||
try harness.runtime.dispatchPlatformEvent(app, .{ .files_dropped = .{
|
||||
.window_id = 1,
|
||||
.paths = &dropped_paths,
|
||||
} });
|
||||
try harness.runtime.dispatchPlatformEvent(app, .{
|
||||
.files_dropped = .{
|
||||
.window_id = 1,
|
||||
.view_label = drop_canvas_label,
|
||||
// x=300 is outside the initial 250-point surface and proves the
|
||||
// resize event rebuilt the retained widget hit-test geometry.
|
||||
.point = native_sdk.geometry.PointF.init(300, 40),
|
||||
.paths = &dropped_paths,
|
||||
},
|
||||
});
|
||||
try std.testing.expectEqual(@as(u32, 1), app_state.widget_drop_count);
|
||||
try std.testing.expectEqual(drop_target_id, app_state.last_drop_target_id);
|
||||
try std.testing.expectEqual(@as(u32, 1), app_state.drop_count);
|
||||
try std.testing.expectEqual(@as(usize, 2), app_state.last_drop_paths.len);
|
||||
try std.testing.expectEqualStrings("/tmp/one\nname.txt", app_state.last_drop_paths[0]);
|
||||
try std.testing.expectEqualStrings("/tmp/two.txt", app_state.last_drop_paths[1]);
|
||||
try std.testing.expectEqualStrings("drop:files", harness.null_platform.lastWindowEventName());
|
||||
try std.testing.expect(std.mem.startsWith(u8, nullViewText(harness, "status-label"), "Widget target 2 fired"));
|
||||
try std.testing.expect(app_state.pending_drop_target_id == null);
|
||||
|
||||
const webview_paths = [_][]const u8{"/tmp/webview.txt"};
|
||||
try harness.runtime.dispatchPlatformEvent(app, .{ .files_dropped = .{
|
||||
.window_id = 1,
|
||||
.view_label = "main",
|
||||
.point = native_sdk.geometry.PointF.init(40, 40),
|
||||
.paths = &webview_paths,
|
||||
} });
|
||||
try std.testing.expectEqual(@as(u32, 1), app_state.widget_drop_count);
|
||||
try std.testing.expect(app_state.pending_drop_target_id == null);
|
||||
try std.testing.expectEqual(@as(u32, 2), app_state.drop_count);
|
||||
try std.testing.expect(std.mem.startsWith(u8, nullViewText(harness, "status-label"), "Received file drop 2"));
|
||||
|
||||
try harness.runtime.dispatchPlatformEvent(app, .app_activated);
|
||||
try std.testing.expectEqual(@as(u32, 1), app_state.activation_count);
|
||||
@@ -213,6 +314,9 @@ test "capabilities bridge gates native services and dispatches file drops" {
|
||||
}
|
||||
|
||||
test "capabilities manifest declares package integration metadata" {
|
||||
try std.testing.expectEqual(window_min_width, app_manifest.shell.windows[0].min_width);
|
||||
try std.testing.expectEqual(window_min_width, shell_windows[0].min_width);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), manifest_file_associations.len);
|
||||
try std.testing.expectEqualStrings("Native SDK Capability Document", manifest_file_associations[0].name);
|
||||
try std.testing.expectEqualStrings("viewer", manifest_file_associations[0].role);
|
||||
@@ -231,3 +335,17 @@ fn dispatchBridge(harness: *native_sdk.TestHarness(), app: native_sdk.App, bytes
|
||||
.webview_label = "main",
|
||||
} });
|
||||
}
|
||||
|
||||
fn nullViewText(harness: *native_sdk.TestHarness(), label: []const u8) []const u8 {
|
||||
for (harness.null_platform.views[0..harness.null_platform.view_count]) |view| {
|
||||
if (std.mem.eql(u8, view.label, label)) return view.text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
fn viewByLabel(views: []const native_sdk.ViewInfo, label: []const u8) ?native_sdk.ViewInfo {
|
||||
for (views) |view| {
|
||||
if (std.mem.eql(u8, view.label, label)) return view;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ pub const RunOptions = struct {
|
||||
if (windows.len > 0) {
|
||||
info.main_window = windows[0];
|
||||
info.windows = windows;
|
||||
} else {
|
||||
info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);
|
||||
info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);
|
||||
info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);
|
||||
info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
@@ -138,6 +143,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
|
||||
.resizable = windowBool(window, "resizable", true),
|
||||
.restore_state = windowBool(window, "restore_state", true),
|
||||
.restore_policy = windowRestorePolicy(window),
|
||||
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,6 +177,57 @@ fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy
|
||||
@compileError("unknown app.zon window restore_policy");
|
||||
}
|
||||
|
||||
fn windowHasExplicitOrigin(comptime window: anytype) bool {
|
||||
return @hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y");
|
||||
}
|
||||
|
||||
fn manifestShellStartupFrame(fallback: native_sdk.geometry.RectF) native_sdk.geometry.RectF {
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
|
||||
const shell = app_manifest.shell;
|
||||
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
|
||||
if (comptime shell.windows.len == 0) return fallback;
|
||||
const window = shell.windows[0];
|
||||
return native_sdk.geometry.RectF.init(
|
||||
windowFloatFallback(window, "x", fallback.x),
|
||||
windowFloatFallback(window, "y", fallback.y),
|
||||
windowFloatFallback(window, "width", fallback.width),
|
||||
windowFloatFallback(window, "height", fallback.height),
|
||||
);
|
||||
}
|
||||
|
||||
fn windowFloatFallback(comptime window: anytype, comptime field: []const u8, fallback: f32) f32 {
|
||||
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
fn manifestShellStartupRestoreState(fallback: bool) bool {
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
|
||||
const shell = app_manifest.shell;
|
||||
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
|
||||
if (comptime shell.windows.len == 0) return fallback;
|
||||
const window = shell.windows[0];
|
||||
if (comptime @hasField(@TypeOf(window), "restore_state")) return window.restore_state;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
fn manifestShellStartupRestorePolicy(fallback: native_sdk.WindowRestorePolicy) native_sdk.WindowRestorePolicy {
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
|
||||
const shell = app_manifest.shell;
|
||||
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
|
||||
if (comptime shell.windows.len == 0) return fallback;
|
||||
const window = shell.windows[0];
|
||||
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return fallback;
|
||||
return windowRestorePolicy(window);
|
||||
}
|
||||
|
||||
fn manifestShellStartupInitialPlacement(fallback: native_sdk.WindowInitialPlacement) native_sdk.WindowInitialPlacement {
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
|
||||
const shell = app_manifest.shell;
|
||||
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
|
||||
if (comptime shell.windows.len == 0) return fallback;
|
||||
return if (windowHasExplicitOrigin(shell.windows[0])) .explicit else fallback;
|
||||
}
|
||||
|
||||
fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers {
|
||||
const values = if (@hasField(@TypeOf(shortcut), "modifiers")) shortcut.modifiers else .{};
|
||||
var modifiers: native_sdk.ShortcutModifiers = .{};
|
||||
@@ -408,12 +465,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
|
||||
if (!window.restore_state) continue;
|
||||
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
|
||||
window.default_frame = saved.frame;
|
||||
if (index == 0) app_info.main_window.default_frame = saved.frame;
|
||||
window.initial_placement = .restored;
|
||||
if (index == 0) {
|
||||
app_info.main_window.default_frame = saved.frame;
|
||||
app_info.main_window.initial_placement = .restored;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (app_info.main_window.restore_state) {
|
||||
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
|
||||
app_info.main_window.default_frame = saved.frame;
|
||||
app_info.main_window.initial_placement = .restored;
|
||||
}
|
||||
}
|
||||
return store;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
.title = "Native SDK Channel Monitor",
|
||||
.width = 560,
|
||||
.height = 420,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.views = .{
|
||||
.{ .label = "monitor-canvas", .kind = "gpu_surface", .fill = true, .role = "Channel monitor canvas", .accessibility_label = "Channel monitor", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
|
||||
|
||||
@@ -38,7 +38,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
.title = "Native SDK Channel Monitor",
|
||||
.width = window_width,
|
||||
.height = window_height,
|
||||
.restore_state = false,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
@@ -306,7 +305,6 @@ pub fn main(init: std.process.Init) !void {
|
||||
.window_title = "Native SDK Channel Monitor",
|
||||
.bundle_id = "dev.native_sdk.channel_monitor",
|
||||
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
|
||||
.restore_state = false,
|
||||
.js_window_api = false,
|
||||
.security = .{
|
||||
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } },
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
.height = 640,
|
||||
.min_width = 560,
|
||||
.min_height = 420,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.titlebar = "hidden_inset_tall",
|
||||
.views = .{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.description = "Choose a folder and edit its source files in a native two-pane window.",
|
||||
.version = "0.1.0",
|
||||
.platforms = .{"macos"},
|
||||
.permissions = .{ "view", "command", "dialog" },
|
||||
.permissions = .{ "view", "command", "dialog", "filesystem" },
|
||||
.capabilities = .{ "native_views", "gpu_surfaces" },
|
||||
.shortcuts = .{
|
||||
.{ .id = "save-file", .key = "s", .modifiers = .{ "primary" } },
|
||||
@@ -24,7 +24,6 @@
|
||||
.height = 720,
|
||||
.min_width = 760,
|
||||
.min_height = 480,
|
||||
.restore_state = false,
|
||||
.restore_policy = "center_on_primary",
|
||||
.titlebar = "hidden_inset_tall",
|
||||
.views = .{
|
||||
|
||||