This is the workspace repo (libnativeapi/nativeapi, formerly nativeapi-flutter and nativeapi-workspace) for the libnativeapi project family. Every binding (bindings/dart/, bindings/rust/, bindings/csharp/, bindings/js/, bindings/python/, bindings/go/), the code generator (tools/codegen/), the ./codegen script, the specs and the shared tooling live directly in this repo (the Rust and C# histories were merged in from nativeapi-rust and nativeapi-csharp); only core/ and the leanflutter packages under bindings/dart/ (tray_manager/, screen_retriever/, window_manager/) are git submodules of independent repositories. Work inside a submodule is committed and pushed from that subdirectory; everything else is committed here.
core/ # submodule: nativeapi-core — the C++ core library
bindings/
├── dart/ # the Dart binding: nativeapi/, cnativeapi/, nativeapi_flutter/; tray_manager/, screen_retriever/, window_manager/ (submodules of the leanflutter repos of the same names)
├── rust/ # the Rust binding: crates/{nativeapi,cnativeapi}
├── csharp/ # the C# binding: src/, tests/, NativeAPI.slnx
├── js/ # the JS/TS binding: a Node-API addon (src/) + TypeScript (lib/)
├── python/ # the Python binding: ctypes package (nativeapi/) + native shim (src/)
└── go/ # the Go binding: generated cgo API + native shared library
examples/ # every binding's example apps, prefixed dart_*, flutter_*, rust_*, csharp_*, js_*, python_*
prototype/ # interface prototypes for the examples (Storybook + DazzUI, pnpm), see prototype/docs/architecture.md
pubspec.yaml # pub workspace + melos root: Dart packages and Flutter examples
Cargo.toml # cargo workspace root: Rust crates and examples
tools/codegen/ # in-repo Rust workspace: the code generator
website/ # the project website (TanStack Start on Cloudflare Workers): landing page, docs, API reference
tools/gui/ # GUI tests and demo scenarios for the examples (built on the skills)
codegen # Python entry point orchestrating the generators
.agents/skills/ # agent skills: core API changes, GUI testing, demo recording (see below)
.claude/skills # symlink → ../.agents/skills, so Claude Code discovers the same skills
core— the C++ core library (repo:nativeapi-core). The source of truth for the native API surface (windows, tray icons, menus, displays, keyboard, dialogs, storage, etc.) with per-platform implementations (macOS/Windows/Linux).tools/codegen— three crates:shared(libclang parser, IR, naming),capi(C ABI + umbrella header),bindings(Rust/Dart/C#/JS/Python/Go generators, consuming the IR JSON emitted bycapi). Onlycapidepends on libclang. See tools/codegen/README.md.bindings/*— language bindings wrapping the core library. All live in this repo and build against thecore/submodule directly (Rustbuild.rs, the Dartcnativeapipackage's build hook, the C# native CMake, the JS, Python and Go bindings'CMakeLists.txt). Only a published package carries its own copy of core, incxx_impl/, which the release workflows vendor and never commit. The Rust binding layersnativeapi(safe API) overcnativeapi(FFI). The Python binding has no compiled extension: generatedctypescode (nativeapi/_capi.pyplus one module per header) calls a shared library built from core and a small event loop shim, whichApplication.run_async()pumps from asyncio.
specs/ holds the settled design rules for core/ — layering, the identity/value object
model, the public API style, the platform seam, the event system, managers, and the C ABI.
Start at specs/README.md; read the relevant spec before adding or
reshaping public API in core/src/.
Any diff that touches a public header in core/src/ must pass the checklist at the end of
specs/api-style.md — naming vocabulary, parameter and return types,
failure reporting, platform-availability notes, and the codegen constraints. When existing
headers disagree with each other, follow the spec, not the nearest neighbour: it records
which side of each split is the rule and which is legacy.
There is no separate issue list: each spec carries the open questions and known legacy gaps of its own area inline (an "未决" section, or a "存量缺口" note next to the rule it breaks). When one is resolved, edit the spec text itself.
Always drive the generators through ./codegen at the workspace root:
./codegen— full run: C ABI, then all bindings./codegen capi/./codegen bindings [--lang rust,dart,csharp,js,python,go]./codegen check— read-only verification, non-zero exit when stale (CI mode)./codegen readme— copy the shared README sections (tools/readme/*.md, e.g. Contributing) into core and every binding;checkflags drift,syncruns it. Edit the snippet, never the copies../codegen sync [-m "msg"] [--push]— full downstream propagation, see below
Every bindings run also writes the API reference the website renders, website/content/api/ (one JSON file per header: the doc comments parsed from the headers plus every binding's signature for each symbol). Each generator's reference() builds those signatures with the same helpers that render the binding, keyed by tools/codegen/shared/src/symbols.rs, so the reference cannot drift from the generated code; ./codegen check verifies it and ./codegen sync commits it. Doc text comes only from the doxygen comments in core/src/ — document a symbol there, not in the website.
Generated files start with // AUTO-GENERATED. DO NOT EDIT. (# in Python; a "$comment" key in JSON) — change the C++ headers in core/src/ and regenerate instead of editing outputs. Files without that banner are hand-written and never overwritten. The header list (API_HEADERS) lives in tools/codegen/shared/src/lib.rs.
Go builds require Go 1.22+, cgo and CMake 3.24+; build the shared library with
cmake -S bindings/go -B bindings/go/build && cmake --build bindings/go/build --config Release.
Go examples are standalone modules with a local replace pointing to ../../bindings/go.
The Go generator uses gofmt before writing and checking its output. Public Go getters
omit Get, ToString becomes String, constructor names omit shared overload
parameters, and fallible operations return error while predicates keep bool.
Native singletons are exported values with methods (DisplayManager.All(),
Application.Run()), backed by private stateless types.
Object getters retain nil-for-absence semantics; native failures without detailed
causes wrap ErrOperationFailed. Examples intentionally use dot imports as requested.
A core change ripples to every binding. After editing headers in core, run:
./codegen sync -m "<core commit message>"It regenerates everything, reruns bindgen (Rust raw FFI) and ffigen (Dart raw FFI), then commits core and this repo (Sync with core <sha>: the core pointer plus everything regenerated under bindings/). Add --push to publish in dangling-safe order (core → workspace).
Manual follow-ups sync cannot do (details in tools/codegen/README.md):
- New handle types need an
IdTypeTag<T>entry incore/src/foundation/id_allocator.h(append only; a miss is a compile error, not silent). - Hand-written files in the bindings (exports, re-exports, changelogs, examples) are never touched by the generators. Rust's
pub modlist is generated (modules.rs), and so is Dart's (nativeapi/lib/src/generated.dart) and Python's (nativeapi/__init__.py);nativeapi_flutter's exports are not.
The core-api-change skill walks the whole flow, including what to check before sync commits.
.agents/skills/ holds skills (a SKILL.md plus scripts each) for verifying windowing
work on a real desktop. Read the relevant SKILL.md before doing any of this by hand:
| Skill | Use it to |
|---|---|
core-api-change |
carry a public API change from core/src/*.h through codegen, every binding and the commits in core and this repo — including the pre-flight before ./codegen sync |
flutter-ui-probe |
find where texts/widgets are in a running debug Flutter app (VM service) |
gui-test |
end-to-end test an app: launch, drive with guarded synthetic mouse input (read its safety rules first), assert on real window geometry and state |
remote-hosts |
build and run on another machine over SSH — Windows today, Linux/macOS prepared (SSH session vs. logged-on desktop) |
record-demo |
record a scripted demo of an app and cut it into an X-ready MP4 |
Skills hold only generic harnesses, recorders, and templates. The GUI tests and demo scenarios for this project's examples live in tools/gui/.
coretracksbranch = main. Usemake syncto fast-forward it;make statusto see dirty state everywhere;make bumpto stage its pointer.- The leanflutter packages built on nativeapi (
tray_manager,window_manager,launch_at_startup, …) live in their own repos under github.com/leanflutter and depend on the publishednativeapi. Some ride along as submodules underbindings/dart/<package>(trackingmain):tray_manager,screen_retrieverandwindow_managerso far. They are not members of the root pub workspace, melos ignores them (add the path toignorein the rootpubspec.yaml), and CI does not check them out. To try one against local changes, point adependency_overridesentry in that package atbindings/dart/nativeapi_flutter(andnativeapi,cnativeapi) and never commit the override. - Commit workspace submodule pointer updates only when the combination is compatible (a known-good snapshot).
- Examples live in
examples/<binding>_<name>_example(dart_for plain Dart programs,flutter_,rust_,csharp_,js_,python_,go_), not inside the bindings; a new Dart, Flutter or Rust example must also be listed in the rootpubspec.yaml/Cargo.toml, a C# one inbindings/csharp/NativeAPI.slnx, a JS one in the rootpackage.json. A Python example is a standalone uv project whosepyproject.tomlpointsnativeapiat../../bindings/python(uv run main.pybuilds the wheel). A Go example is a standalone module whosego.modreplacesgithub.com/libnativeapi/nativeapi/bindings/gowith../../bindings/go. Only the pub.dev package examples (bindings/dart/*/example) stay inside their package. - CI is one workflow per binding (
dart-ci.yml,rust-ci.yml,csharp-ci.yml,js-ci.yml,python-ci.yml,go-ci.yml), each running only for changes under itsbindings/<lang>/and its examples (examples/dart_*andexamples/flutter_*for Dart,examples/rust_*,examples/csharp_*,examples/js_*,examples/python_*,examples/go_*). Release tags are per binding:v*publishes the Dart packages (dart-release.yml),rust-v*publishes the crates (rust-release.yml),python-v*publishes the Python package to PyPI (python-release.yml: an sdist carrying core incxx_impl/, and wheels built from it with cibuildwheel; the tag must equalpython-v+ the version inbindings/python/pyproject.toml); never push a barev*tag for anything but the Dart packages. Nothing has been published to PyPI yet: the first release needsnativeapiset up on PyPI with this workflow and thepypienvironment as a trusted publisher. - A release of any package bumps only the last version number (0.5.0 → 0.5.1), unless the user asks for a minor or major bump. Breaking changes do not pick the number: mark them in the CHANGELOG instead.
- The Dart packages
cnativeapi,nativeapiandnativeapi_fluttershare one version: a release bumps all three pubspecs and CHANGELOGs, because pub.dev's automated publishing matches the tagv<version>against each package's own version.cnativeapiandnativeapiare plain Dart (no Flutter dependency; a build hook compiles core);nativeapi_flutteris the Flutter-facing package, holding the widgets, thedart:uiconversions andwindowing.dart, and re-exportingnativeapiminus the names that clash with Flutter's. - Never commit in a submodule while on a detached HEAD — check out
mainfirst (./codegen syncenforces this). - Do not add Co-Authored-By trailers to commits.