@nativedesktop/native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/include/nd.h ADDED
@@ -0,0 +1,157 @@
1
+ /* include/nd.h — the ONLY public surface of libnd. Hand-written; kept in
2
+ lockstep with src/abi.zig by compile-time @sizeOf asserts. Canonical copy
3
+ lives in include/; packages/native/include/ carries a machine-synced copy
4
+ (scripts/sync-native-headers.sh, cmp-checked in CI) — edit include/ only. */
5
+ #ifndef ND_H
6
+ #define ND_H
7
+ #include <stdint.h>
8
+ #include <stdbool.h>
9
+ #include "nd_plugin.h"
10
+
11
+ typedef void* nd_widget; /* opaque backend handle (GtkWidget* / NSView*) */
12
+ typedef struct nd_context nd_context; /* opaque core instance */
13
+
14
+ /* Geometry in logical window-top-left space (matches getTree contract). */
15
+ typedef struct { int32_t x, y, w, h; } nd_rect;
16
+
17
+ /* The backend vtable: the embedder fills these; the core calls up through them.
18
+ `props_json` / `arg_json` are NUL-terminated UTF-8 JSON (M6a-D2). All calls
19
+ arrive on the embedder's UI thread (the core marshals). */
20
+ typedef struct nd_backend {
21
+ /* structural + prop ops (mirror the Zig seam) */
22
+ nd_widget (*create)(nd_context*, const char* kind, const char* props_json);
23
+ void (*apply_props)(nd_context*, nd_widget, const char* kind, const char* props_json);
24
+ void (*append_child)(nd_context*, nd_widget parent, const char* parent_kind,
25
+ nd_widget child, const char* attached_json);
26
+ void (*insert_before)(nd_context*, nd_widget parent, const char* parent_kind,
27
+ nd_widget child, nd_widget before /* nullable */,
28
+ const char* attached_json);
29
+ void (*remove_child)(nd_context*, nd_widget parent, const char* parent_kind, nd_widget child);
30
+ void (*set_text)(nd_context*, nd_widget, const char* text);
31
+ void (*set_visible)(nd_context*, nd_widget, bool visible);
32
+ void (*apply_style)(nd_context*, nd_widget, uint32_t node_id, const char* style_json);
33
+ void (*connect_events)(nd_context*, nd_widget, const char* kind, uint32_t node_id);
34
+ bool (*has_parent)(nd_context*, nd_widget);
35
+ void (*unparent)(nd_context*, nd_widget);
36
+ nd_widget (*get_window)(nd_context*);
37
+
38
+ /* embedder UI-thread marshal + host chrome (M6a Task 3): the core's
39
+ commit-apply/child-exit/overlay paint call up through these instead of
40
+ importing glib/gio directly. GTK fills marshal_async with
41
+ g_main_context_invoke_full; the Mac shell fills it with
42
+ dispatch_async_f. `show_overlay("")` (empty message) is the clear
43
+ sentinel — the core calls it that way from its dev-mode Restart/respawn
44
+ path instead of a dedicated clear-overlay vtable field. */
45
+ void (*marshal_async)(nd_context*, void (*fn)(void*), void* data);
46
+ void (*show_overlay)(nd_context*, const char* message);
47
+
48
+ /* automation backend half (M6a-D3) */
49
+ bool (*node_visible)(nd_context*, nd_widget);
50
+ bool (*node_bounds)(nd_context*, nd_widget, nd_rect* out); /* false = no bounds */
51
+ bool (*snapshot)(nd_context*, const char* png_path); /* in-process render */
52
+ /* action: "click"|"setValue"|"type"|"scroll"; arg_json carries params.
53
+ returns 0 ok, or a negative JSON-RPC-style code; err_json_out (nullable,
54
+ caller frees via nd_free) gets a data object on failure. */
55
+ int32_t (*semantic_action)(nd_context*, nd_widget, uint32_t node_id,
56
+ const char* action, const char* arg_json,
57
+ char** result_json_out, char** err_json_out);
58
+
59
+ /* app -> widget imperative command (M14, widgetCommand NDP frame): `command`
60
+ is one of the widget's schema-declared commands (widgets.json commands[]),
61
+ `arg_json` its JSON argument (or "null"). Arrives on the UI thread. */
62
+ void (*widget_command)(nd_context*, nd_widget, const char* kind,
63
+ const char* command, const char* arg_json);
64
+
65
+ /* multi-window reconstruction (per-window generalization of get_window):
66
+ given a Window node's stored handle, return the handle to REBIND to when a
67
+ crash/dev-Restart respawn re-creates that window, so the core reuses the
68
+ surviving OS window instead of opening a duplicate. GTK returns the handle
69
+ unchanged (its gtk.Window handle is stable); the AppKit shell resolves it
70
+ to the window's CURRENT content view (which differs once a SplitView took
71
+ over as contentViewController). */
72
+ nd_widget (*resolve_window)(nd_context*, nd_widget);
73
+
74
+ /* widget-preserving cross-window move (drag a tab between windows): relocate
75
+ an EXISTING live native widget from `old_parent` to `new_parent` WITHOUT
76
+ destroying it, so a <webview>'s loaded page / scroll / JS state survives —
77
+ a remove+create would reload it. Reuses each backend's ordinary remove +
78
+ append/insert per parent kind, so the target attaches correctly whatever
79
+ the parent (Box slot, Window, ...). `before` (nullable) positions the child;
80
+ NULL appends. `old_parent` may be NULL (the child is a detached pool node
81
+ not yet shown in any window). The handle stays alive across the unparent
82
+ (GTK g_object_ref brackets the move; the AppKit shell relies on the core's
83
+ create-time retain). Arrives on the UI thread. Appended (append-only
84
+ vtable) — bumps @sizeOf(NdBackend) to 21 words in src/abi.zig. */
85
+ void (*reparent_child)(nd_context*, nd_widget child,
86
+ nd_widget old_parent /* nullable */, const char* old_parent_kind,
87
+ nd_widget new_parent, const char* new_parent_kind,
88
+ nd_widget before /* nullable */, const char* attached_json);
89
+
90
+ /* app -> host system capability request (systemRequest NDP frame). `method`
91
+ is a dotted capability (e.g. "dialog.openFile"), `params_json` its JSON
92
+ argument. Fire-and-forget: the backend delivers the (possibly async)
93
+ result later via nd_system_response. Arrives on the UI thread. Appended
94
+ (append-only vtable) — bumps @sizeOf(NdBackend) to 22 words in src/abi.zig. */
95
+ void (*system_request)(nd_context*, uint32_t id, const char* method, const char* params_json);
96
+
97
+ /* drop the backend's per-node ownership reference when the core forgets a
98
+ node id (tree remove / generation GC / clearAppNodes). GTK: g_object_unref
99
+ of the create-time ref_sink; the Mac shell: Unmanaged.release of the
100
+ create-time retain. The widget object stays alive while native parents
101
+ still reference it. Arrives on the UI thread. Appended (append-only
102
+ vtable) — bumps @sizeOf(NdBackend) to 23 words in src/abi.zig. */
103
+ void (*release_node)(nd_context*, nd_widget);
104
+ } nd_backend;
105
+
106
+ /* lifecycle */
107
+ nd_context* nd_init(void); /* create core, spawn nothing yet */
108
+ void nd_register_backend(nd_context*, const nd_backend*); /* store the vtable */
109
+ /* Name the active widget backend ("gtk" | "appkit"). The core echoes it in the
110
+ NDP helloAck so the Bun child's Platform.backend can branch on the renderer
111
+ (which the OS alone can't reveal — GTK runs on macOS too). Call before
112
+ nd_start_runtime; absent = "unknown". */
113
+ void nd_set_backend_name(nd_context*, const char* name);
114
+ int32_t nd_start_runtime(nd_context*); /* open NDP socket, spawn bun child */
115
+ int32_t nd_start_automation(nd_context*); /* open automation socket + thread */
116
+ void nd_shutdown(nd_context*); /* stop runtime; destroy views/plugins */
117
+ /* Capability ACL (D12): install a per-window grants manifest (NUL-terminated
118
+ JSON; see docs). Absent = safe default (core UI ops granted, plugin ops
119
+ denied). Call before nd_start_runtime. */
120
+ void nd_set_acl(nd_context*, const char* grants_json);
121
+ /* Load a native nd_plugin_v1 shared library (opt-in). Returns 0 ok, negative
122
+ on ABI mismatch / missing entry / capability-denied. */
123
+ int32_t nd_load_plugin(nd_context*, const char* path);
124
+ /* Load every plugin listed in ND_PLUGIN_PATHS (colon-separated; legacy
125
+ single-path ND_PLUGIN_PATH as fallback), skipping empty segments. Prints
126
+ one "ND_PLUGIN_LOAD_FAILED path=... rc=..." diagnostic per failed path. */
127
+ void nd_load_plugins_from_env(nd_context*);
128
+
129
+ /* Host-side native-view bridge (plugin ABI v2): the generic <nativeView>
130
+ widget's create/apply/command/destroy route through these into the loaded
131
+ plugin's nd_view_impl registered under `view_kind`. The GTK backend calls the
132
+ Zig fns in src/plugin.zig directly; the AppKit/Swift shell calls these C
133
+ wrappers. create returns NULL when no module registered `view_kind` (the
134
+ backend then renders an empty placeholder). */
135
+ nd_widget nd_plugin_view_create(nd_context*, const char* view_kind, const char* props_json);
136
+ void nd_plugin_view_apply_props(nd_context*, const char* view_kind, nd_widget, const char* props_json);
137
+ void nd_plugin_view_connect(nd_context*, const char* view_kind, nd_widget, uint32_t node_id);
138
+ void nd_plugin_view_command(nd_context*, const char* view_kind, nd_widget, const char* command, const char* arg_json);
139
+ void nd_plugin_view_destroy(nd_context*, const char* view_kind, nd_widget);
140
+
141
+ void nd_free(void* p); /* free a core-allocated string */
142
+
143
+ /* embedder -> core: a native event happened (button clicked, text changed, …).
144
+ `payload_json` is a NUL-terminated JSON object or "{}". */
145
+ void nd_emit_event(nd_context*, uint32_t node_id, const char* name, const char* payload_json);
146
+
147
+ /* backend -> core: the (possibly async) result of an earlier system_request,
148
+ correlated by `id`. ok=true splices `json` verbatim into the systemResponse
149
+ `result`; ok=false carries `json` as the error message string. */
150
+ void nd_system_response(nd_context*, uint32_t id, bool ok, const char* json);
151
+ /* backend -> core: an app-level event not tied to a widget node (app
152
+ activation, OS open-url/open-file launch, notification click, file drop,
153
+ capability event stream). `channel` names the stream; `data_json` is its
154
+ NUL-terminated JSON payload. */
155
+ void nd_system_event(nd_context*, const char* channel, const char* data_json);
156
+
157
+ #endif /* ND_H */
@@ -0,0 +1,71 @@
1
+ /* include/nd_plugin.h — the nd_plugin_v1 native-plugin ABI (spec §9, D12).
2
+ A native plugin is a C-ABI shared library exporting nd_plugin_entry(), which
3
+ returns a pointer to a statically-lived nd_plugin_v1. Capability strings are
4
+ checked against the NDP-dispatch ACL; a command runs only if its plugin's
5
+ declared permission is granted for the window. Opt-in: nothing loads a
6
+ plugin unless the embedder calls nd_load_plugin (include/nd.h). Canonical
7
+ copy lives in include/; packages/native/include/ carries a machine-synced
8
+ copy (scripts/sync-native-headers.sh, cmp-checked in CI) — edit include/
9
+ only. */
10
+ #ifndef ND_PLUGIN_H
11
+ #define ND_PLUGIN_H
12
+ #include <stdint.h>
13
+ #include <stddef.h>
14
+
15
+ /* Append-only ABI history:
16
+ v2 adds register_view + nd_view_impl so a plugin can register its own native
17
+ widget. v3 appends connect + emit_event so that widget can emit events for a
18
+ retained node. v1/v2 plugins keep loading unchanged; consumers must only read
19
+ fields available in the plugin's declared abi_version. */
20
+ #define ND_PLUGIN_ABI_VERSION 3
21
+
22
+ typedef struct nd_plugin_registry nd_plugin_registry;
23
+
24
+ /* A plugin command handler: receives the arg JSON (NUL-terminated) and writes
25
+ a malloc'd result JSON to *result_out (freed by the core via nd_free).
26
+ Returns 0 ok, negative JSON-RPC-style code on error. */
27
+ typedef int32_t (*nd_command_fn)(const char* arg_json, char** result_out);
28
+
29
+ /* A native-view factory a plugin registers under a `view_kind` string. The
30
+ handle each fn returns/takes is the backend-native widget as an opaque
31
+ pointer (GtkWidget* on GTK, NSView* on AppKit) — the core never dereferences
32
+ it; it flows through append_child/apply_style/unparent by parent kind alone.
33
+ `props_json`/`arg_json` are NUL-terminated JSON. Native views are inherently
34
+ backend-specific: a module ships a GTK impl and/or an AppKit impl. */
35
+ typedef struct nd_view_impl {
36
+ void* (*create)(const char* props_json);
37
+ void (*apply_props)(void* view, const char* props_json);
38
+ void (*command)(void* view, const char* command, const char* arg_json);
39
+ void (*destroy)(void* view);
40
+ /* v3: called after creation once the core node identity is known. */
41
+ void (*connect)(void* view, uint32_t node_id);
42
+ } nd_view_impl;
43
+
44
+ /* Host callbacks a plugin uses from init(). register_command associates a
45
+ command name (reachable over NDP as {"type":"pluginCommand","name",...})
46
+ with a handler; the core namespaces it as plugin:<plugin-name>.<command>.
47
+ register_view (v2, appended) associates a `view_kind` string with a native-
48
+ view factory the generic <nativeView> widget routes to. emit_event is v3 and
49
+ becomes valid only after connect supplies a node id. The core copies the
50
+ nd_view_impl by value, so it may live on the plugin's stack in init(). */
51
+ struct nd_plugin_registry {
52
+ void* host; /* opaque core handle; pass back to the callbacks */
53
+ void (*register_command)(nd_plugin_registry*, const char* command, nd_command_fn);
54
+ void (*register_view)(nd_plugin_registry*, const char* view_kind, const nd_view_impl*);
55
+ /* v3: emit a generic event for a connected node through the core. */
56
+ void (*emit_event)(nd_plugin_registry*, uint32_t node_id, const char* name,
57
+ const char* payload_json);
58
+ };
59
+
60
+ typedef struct nd_plugin_v1 {
61
+ uint32_t abi_version; /* 1, 2, or ND_PLUGIN_ABI_VERSION; old layouts remain valid */
62
+ const char* name; /* plugin identity, e.g. "hello" */
63
+ const char* const* capabilities; /* NULL-terminated permission strings, e.g. {"plugin:hello.greet", NULL} */
64
+ int32_t (*init)(nd_plugin_registry*); /* register commands/widgets; 0 ok */
65
+ void (*deinit)(void);
66
+ } nd_plugin_v1;
67
+
68
+ /* Every plugin shared library exports this symbol. */
69
+ typedef const nd_plugin_v1* (*nd_plugin_entry_fn)(void);
70
+
71
+ #endif /* ND_PLUGIN_H */
@@ -0,0 +1,24 @@
1
+ #ifndef ND_NATIVE_GTK_H
2
+ #define ND_NATIVE_GTK_H
3
+
4
+ #include <gtk/gtk.h>
5
+ #include "../include/nd_plugin.h"
6
+
7
+ /* Small C convenience layer for app-owned GTK plugins. The app still exports
8
+ nd_plugin_entry and owns every widget/state allocation. */
9
+ typedef struct nd_gtk_view_state {
10
+ nd_plugin_registry* registry;
11
+ uint32_t node_id;
12
+ } nd_gtk_view_state;
13
+
14
+ static inline void nd_gtk_connect_state(nd_gtk_view_state* state, nd_plugin_registry* registry, uint32_t node_id) {
15
+ state->registry = registry;
16
+ state->node_id = node_id;
17
+ }
18
+
19
+ static inline void nd_gtk_emit(nd_gtk_view_state* state, const char* name, const char* payload_json) {
20
+ if (state && state->registry && state->registry->emit_event)
21
+ state->registry->emit_event(state->registry, state->node_id, name, payload_json ? payload_json : "{}");
22
+ }
23
+
24
+ #endif
@@ -0,0 +1,103 @@
1
+ import AppKit
2
+ import SwiftUI
3
+ import CNdPlugin
4
+
5
+ /// Protocol implemented by app-owned AppKit views hosted by NativeDesktop.
6
+ public protocol NativeDesktopView: AnyObject {
7
+ func apply(propsJSON: String)
8
+ func connect(nodeID: UInt32, emit: @escaping (_ name: String, _ payloadJSON: String) -> Void)
9
+ func command(_ name: String, argJSON: String)
10
+ func destroy()
11
+ }
12
+
13
+ public extension NativeDesktopView {
14
+ func connect(nodeID: UInt32, emit: @escaping (String, String) -> Void) {}
15
+ func command(_ name: String, argJSON: String) {}
16
+ func destroy() {}
17
+ }
18
+
19
+ /// C-ABI glue for plugins written against NativeDesktopView. nd_view_impl
20
+ /// callbacks carry no context pointer, so registerView supports one view kind
21
+ /// per plugin; hand-roll an nd_view_impl to register more.
22
+ public enum NativeDesktopPlugin {
23
+ private static var registry: UnsafeMutablePointer<nd_plugin_registry>?
24
+ private static var factory: ((String) -> NSView & NativeDesktopView)?
25
+
26
+ /// Allocates an nd_plugin_v1 with the stable lifetime the ABI requires;
27
+ /// return the result from @_cdecl("nd_plugin_entry").
28
+ public static func descriptor(
29
+ name: String,
30
+ capabilities: [String] = [],
31
+ initialize: @escaping @convention(c) (UnsafeMutablePointer<nd_plugin_registry>?) -> Int32,
32
+ deinitialize: @escaping @convention(c) () -> Void = {}
33
+ ) -> UnsafePointer<nd_plugin_v1> {
34
+ let caps = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: capabilities.count + 1)
35
+ for (index, capability) in capabilities.enumerated() { caps[index] = UnsafePointer(strdup(capability)) }
36
+ caps[capabilities.count] = nil
37
+ let plugin = UnsafeMutablePointer<nd_plugin_v1>.allocate(capacity: 1)
38
+ plugin.initialize(to: nd_plugin_v1(abi_version: UInt32(ND_PLUGIN_ABI_VERSION), name: UnsafePointer(strdup(name)), capabilities: UnsafePointer(caps), init: initialize, deinit: deinitialize))
39
+ return UnsafePointer(plugin)
40
+ }
41
+
42
+ /// Call from the descriptor's initialize callback.
43
+ public static func registerView(
44
+ _ registry: UnsafeMutablePointer<nd_plugin_registry>,
45
+ kind: String,
46
+ factory: @escaping (String) -> NSView & NativeDesktopView
47
+ ) {
48
+ self.registry = registry
49
+ self.factory = factory
50
+ var impl = nd_view_impl(
51
+ create: { props in
52
+ guard let view = NativeDesktopPlugin.factory?(NativeDesktopPlugin.string(props)) else { return nil }
53
+ return Unmanaged.passRetained(view as NSView).toOpaque()
54
+ },
55
+ apply_props: { raw, props in
56
+ NativeDesktopPlugin.view(raw)?.apply(propsJSON: NativeDesktopPlugin.string(props))
57
+ },
58
+ command: { raw, name, arg in
59
+ guard let name else { return }
60
+ NativeDesktopPlugin.view(raw)?.command(String(cString: name), argJSON: NativeDesktopPlugin.string(arg))
61
+ },
62
+ destroy: { raw in
63
+ guard let raw else { return }
64
+ NativeDesktopPlugin.view(raw)?.destroy()
65
+ Unmanaged<NSView>.fromOpaque(raw).release()
66
+ },
67
+ connect: { raw, nodeID in
68
+ NativeDesktopPlugin.view(raw)?.connect(nodeID: nodeID) { name, payload in
69
+ guard let registry = NativeDesktopPlugin.registry, let emit = registry.pointee.emit_event else { return }
70
+ name.withCString { n in payload.withCString { p in emit(registry, nodeID, n, p) } }
71
+ }
72
+ }
73
+ )
74
+ registry.pointee.register_view(registry, kind, &impl)
75
+ }
76
+
77
+ private static func view(_ raw: UnsafeMutableRawPointer?) -> (NSView & NativeDesktopView)? {
78
+ raw.flatMap { Unmanaged<NSView>.fromOpaque($0).takeUnretainedValue() as? NSView & NativeDesktopView }
79
+ }
80
+
81
+ private static func string(_ pointer: UnsafePointer<CChar>?) -> String {
82
+ pointer.map { String(cString: $0) } ?? "{}"
83
+ }
84
+ }
85
+
86
+ /// AppKit host for a SwiftUI component. Updating `rootView` preserves the
87
+ /// ordinary NSView identity expected by NativeDesktop's retained tree.
88
+ public final class NativeDesktopSwiftUIView<Content: View>: NSHostingView<Content>, NativeDesktopView {
89
+ private let updateRoot: (String) -> Content
90
+
91
+ public init(initialPropsJSON: String, content: @escaping (String) -> Content) {
92
+ self.updateRoot = content
93
+ super.init(rootView: content(initialPropsJSON))
94
+ }
95
+
96
+ @available(*, unavailable)
97
+ required init(rootView: Content) { fatalError("use init(initialPropsJSON:content:)") }
98
+
99
+ @available(*, unavailable)
100
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
101
+
102
+ public func apply(propsJSON: String) { rootView = updateRoot(propsJSON) }
103
+ }
@@ -0,0 +1,4 @@
1
+ module CNdPlugin {
2
+ header "../include/nd_plugin.h"
3
+ export *
4
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@nativedesktop/native",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/FormalSnake/NativeDesktop#readme",
7
+ "bugs": "https://github.com/FormalSnake/NativeDesktop/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/FormalSnake/NativeDesktop.git",
11
+ "directory": "packages/native"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "files": ["include", "macos", "linux"],
17
+ "exports": {
18
+ "./include/nd.h": "./include/nd.h",
19
+ "./include/nd_plugin.h": "./include/nd_plugin.h",
20
+ "./macos": "./macos/NativeDesktopNative.swift",
21
+ "./linux": "./linux/nd_native_gtk.h"
22
+ }
23
+ }