@gjsify/node-gi 0.36.0 → 0.38.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/gi.js CHANGED
@@ -1,58 +1,41 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // @gjsify/node-gi/gi — the L1 GJS-compatibility layer over the native engine.
2
+ // @gjsify/node-gi/gi — the L1 GJS-compatibility layer over the native engine: it turns
3
+ // the engine primitives into a GJS-shaped namespace object, so `requireGi('Gio','2.0')`
4
+ // gives the same surface as `import Gio from 'gi://Gio?version=2.0'` under GJS. This is
5
+ // the seam the bundler's --app node target rewrites `gi://Ns?version=X` onto.
3
6
  //
4
- // Turns the low-level primitives (requireNamespace / findInfo / callFunction /
5
- // newObject / callMethod / get|setProperty / connect|emit|disconnectSignal) into
6
- // a GJS-shaped namespace object: `requireGi('Gio', '2.0')` returns an object
7
- // where `Gio.SomeFunction(...)` calls a namespace function, `new Gio.SomeClass({
8
- // prop: value })` constructs a GObject, and the instance exposes `.method(...)`,
9
- // `.prop` (property get/set) and `.connect()/.emit()/.disconnect()` — the same
10
- // surface `import Gio from 'gi://Gio?version=2.0'` gives under GJS.
11
- //
12
- // This is the seam the bundler's --app node target rewrites `gi://Ns?version=X`
13
- // onto. Reference: GJS's gi module (gjs/modules/esm/gi.js) for the require shape;
14
- // node-gtk (romgrk, MIT) for the binding lineage. Hand-authored JS (the package
15
- // ships no build step). Milestone 1 surfaces functions + GObject classes; enums,
16
- // flags, constants, structs/boxed, interface static methods and camelCase
17
- // aliases land in subsequent drops.
7
+ // Reference: gjs/modules/esm/gi.js for the require shape; node-gtk (romgrk, MIT) for the
8
+ // binding lineage. Hand-authored JS — the package ships no build step.
18
9
  import * as native from './index.js';
19
- // setImmediate is a Node global but NOT a Deno global (Deno requires the explicit
20
- // node:timers import; Bun/Node re-export it there too), so import it for the
21
- // macrotask-deferred runAsync to work on all three runtimes.
22
- //
23
- // setInterval/clearInterval come from here for a second, load-bearing reason:
24
- // the RUNTIME's timers, never `globalThis.setInterval`. A GJS source running
25
- // through the bridge routinely replaces the global timers with GLib-backed ones
26
- // (`@gjsify/node-globals/register` swaps them for `GLib.timeout_add` wrappers) —
27
- // driving the GLib pump off a GLib timeout is a deadlock: the timer that is
28
- // supposed to dispatch GLib sources is itself a GLib source nobody dispatches.
10
+ // From node:timers, not the globals, for two reasons: setImmediate is a Node/Bun global
11
+ // but Deno only exposes it here; and a GJS source running through the bridge routinely
12
+ // replaces the global timers with GLib-backed ones (`@gjsify/node-globals/register`
13
+ // swaps them for `GLib.timeout_add`), which deadlocks the pump — the timer meant to
14
+ // dispatch GLib sources would itself be a GLib source nobody dispatches.
29
15
  import { setImmediate, setInterval, clearInterval } from 'node:timers';
30
16
  // Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
31
- // polyfill, whose EventEmitter never emits the runtime's 'beforeExit'. The
32
- // loop-liveness hooks below must attach to the real one or they silently never fire.
17
+ // polyfill, whose EventEmitter never emits the runtime's 'beforeExit', so the
18
+ // loop-liveness hooks below would silently never fire.
33
19
  import runtimeProcess from 'node:process';
34
- // The GJS Gio DBus surface (Gio.DBus.session/system, own_name/watch_name,
35
- // Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject) — ported to the L1 model.
36
20
  import { createGioDBus } from './overrides/gio-dbus.js';
37
21
 
38
- // Symbol carrying the raw native GObject handle on a wrapped instance, so it can
39
- // be unwrapped again when passed back into the engine as a GI argument.
22
+ // The raw native GObject handle on a wrapped instance, so it can be unwrapped again
23
+ // when passed back into the engine as a GI argument.
40
24
  const HANDLE = Symbol('nodeGiHandle');
41
25
 
42
- // Symbol carrying the user-class prototype (a registerClass subclass) on a wrapped
43
- // instance. Stored on the target rather than captured in the proxy closure so a
44
- // later wrap with a userProto can UPGRADE an already-cached generic wrapper in
45
- // place (preserving identity + expandos) — see wrapInstance.
26
+ // The user-class prototype (a registerClass subclass) on a wrapped instance. On the
27
+ // target rather than in the proxy closure, so a later wrap carrying a userProto can
28
+ // UPGRADE an already-cached generic wrapper in place, preserving identity + expandos.
46
29
  const USER_PROTO = Symbol('nodeGiUserProto');
47
30
 
48
- // JS-builtin / interop names that must NEVER be treated as a GI method or
49
- // property — otherwise awaiting, printing or inspecting a wrapper would call
50
- // into GObject (e.g. a stray `then` would make a wrapper look thenable).
31
+ // Names that must NEVER be treated as a GI method or property — otherwise awaiting,
32
+ // printing or inspecting a wrapper would call into GObject (a stray `then` alone would
33
+ // make every wrapper look thenable).
51
34
  const RESERVED = new Set(['then', 'toString', 'valueOf', 'constructor', 'inspect']);
52
35
 
53
- // GJS accepts both snake_case and camelCase for methods/properties. Map a JS
54
- // accessor to the GI method name (snake_case) and to a GObject property name
55
- // (kebab-case); a name that is already in the target case passes through.
36
+ // GJS accepts both snake_case and camelCase for methods/properties: map a JS accessor
37
+ // to the GI method name (snake_case) and to a GObject property name (kebab-case); a
38
+ // name already in the target case passes through.
56
39
  function camelToSnake(name) {
57
40
  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
58
41
  }
@@ -64,20 +47,16 @@ function toKebab(name) {
64
47
  .toLowerCase();
65
48
  }
66
49
 
67
- // Per-user-function shim cache so repeated passes of the same callback marshal to
68
- // the SAME native-facing function (stable identity, one ffi/GClosure wrapper per
69
- // user fn per call site pattern).
50
+ // Repeated passes of the same callback must marshal to the SAME native-facing function:
51
+ // stable identity, one ffi/GClosure wrapper per user fn.
70
52
  const callbackShims = new WeakMap();
71
53
 
72
- // A JS function crossing INTO the engine (a GI callback arg, or a JS fn
73
- // marshalled as a GClosure IN-arg) is wrapped so that when C invokes it:
74
- // • each native arg is marshalled through wrapReturn — a GObject handle becomes
75
- // the cached chainable instance, a GVariant a GLib.Variant wrapper, a GParamSpec
76
- // a ParamSpec wrapper — matching what GJS hands a callback;
77
- // • the JS return is unwrapped back to its native handle (a GLib.Variant
78
- // wrapper returned from e.g. a DBus get-property closure must reach the
79
- // engine as the raw variant handle).
80
- // Primitives and plain objects pass through both directions untouched.
54
+ // A JS function crossing INTO the engine (a GI callback arg, or a JS fn marshalled as a
55
+ // GClosure IN-arg) is wrapped so C sees GJS's calling convention: each native arg goes
56
+ // through wrapReturn (a GObject handle becomes the cached chainable instance, a GVariant
57
+ // a GLib.Variant wrapper, …) and the JS return is unwrapped back to its native handle —
58
+ // a GLib.Variant wrapper returned from e.g. a DBus get-property closure must reach the
59
+ // engine as the raw variant handle.
81
60
  function wrapCallbackFn(fn) {
82
61
  let shim = callbackShims.get(fn);
83
62
  if (shim === undefined) {
@@ -99,45 +78,30 @@ function unwrapArgs(args) {
99
78
  return args.map(unwrapArg);
100
79
  }
101
80
 
102
- // Normalize a construct-property dict: each KEY to the GObject canonical (kebab)
103
- // property name and each VALUE unwrapped to its native handle. GJS accepts a
104
- // construct-prop key in camelCase, snake_case OR already-dashed form and maps it to
105
- // the GObject property; the native newObject/constructType layer below looks each
106
- // key up against the GParamSpec by its canonical dashed name, so `{maximumSize:400}`
107
- // would otherwise miss `maximum-size`. Reuse `toKebab` — the SAME normalization the
108
- // property getter/setter accessor path already applies (lines below: `x.maximumSize`
109
- // reads/writes `maximum-size`) — keeping a single source of truth, so construction
110
- // and accessor agree. Idempotent: an already-dashed (`maximum-size`) or snake_case
111
- // (`maximum_size`) key passes through unchanged (GObject canonicalizes `_`↔`-`).
112
- // This is the ONLY caller-facing construct path — newObject for an introspected
113
- // `new Ns.Class({...})`, and constructType for a registerClass'd subclass plus its
114
- // `super({...})` chain-up — so all three accept camelCase identically to GJS.
81
+ // Normalize a construct-property dict: KEY to the GObject canonical (kebab) property
82
+ // name, VALUE unwrapped to its native handle. GJS accepts camelCase, snake_case or
83
+ // already-dashed keys, while the native layer looks each key up against the GParamSpec
84
+ // by its canonical dashed name — `{maximumSize:400}` would otherwise miss
85
+ // `maximum-size`. Reusing `toKebab` keeps construction and property accessors on one
86
+ // normalization. Idempotent, since GObject canonicalizes `_`↔`-`.
115
87
  function unwrapProps(props) {
116
88
  const out = {};
117
89
  for (const key of Object.keys(props)) out[toKebab(key)] = unwrapArg(props[key]);
118
90
  return out;
119
91
  }
120
92
 
121
- // Symbol carrying an enum object's GError-domain descriptor ({ name, quark }) so
122
- // GLib.Error.prototype.matches can resolve an error-enum (e.g. Gio.IOErrorEnum)
123
- // to the domain it represents. Attached by makeEnum when the enum is registered
124
- // as a GError domain; read by errorDomainOf.
93
+ // An enum object's GError-domain descriptor ({ name, quark }), so GLib.Error.matches
94
+ // can resolve an error-enum (Gio.IOErrorEnum) to the domain it represents. Attached by
95
+ // makeEnum when the enum is registered as a GError domain.
125
96
  const ERROR_DOMAIN = Symbol('nodeGiErrorDomain');
126
97
 
127
- // ---- GLib.Error (the GError surface, L1) ----
98
+ // GLib.Error, L1: the engine throws one of these on a failed sync GI invoke (via the
99
+ // builder registered below) and `requireGi('GLib').Error` is this constructor, so a
100
+ // caught error is `instanceof GLib.Error` exactly as under GJS.
128
101
  //
129
- // A real Error subclass mirroring GJS's GLib.Error: `.domain`, `.code`,
130
- // `.message` plus `.matches(domain, code)` (the g_error_matches semantics — true
131
- // when the error's domain AND code both match). The engine throws an instance of
132
- // this on a failed sync GI invoke (via the builder registered below), and
133
- // `requireGi('GLib').Error` is this constructor, so a caught error is
134
- // `instanceof GLib.Error` exactly as under GJS.
135
- //
136
- // Divergence from GJS (documented): GJS's `.domain` is the numeric GQuark; here
137
- // `.domain` is the quark NAME string (more useful in a Node context, and the task
138
- // permits either). The numeric quark is kept on `.domainQuark`. `matches()`
139
- // accepts the domain as an error-enum object (Gio.IOErrorEnum), a name string, or
140
- // a numeric quark, so all the idiomatic call shapes work.
102
+ // Deliberate divergence from GJS: `.domain` is the quark NAME string (GJS's is the
103
+ // numeric GQuark), with the numeric quark on `.domainQuark`. `matches()` accepts the
104
+ // domain as an error-enum object, a name string or a numeric quark.
141
105
 
142
106
  // Resolve a `matches` domain argument to a { name?, quark? } descriptor.
143
107
  function errorDomainOf(domain) {
@@ -149,8 +113,8 @@ function errorDomainOf(domain) {
149
113
  }
150
114
 
151
115
  class GLibError extends Error {
152
- // GJS-shaped public constructor: `new GLib.Error(domain, code, message)` where
153
- // `domain` may be an error-enum object, a quark name string, or a numeric quark.
116
+ // `new GLib.Error(domain, code, message)`, where `domain` may be an error-enum
117
+ // object, a quark name string, or a numeric quark.
154
118
  constructor(domain, code, message) {
155
119
  super(typeof message === 'string' ? message : '');
156
120
  this.name = 'GLib.Error';
@@ -161,8 +125,7 @@ class GLibError extends Error {
161
125
  this.code = code;
162
126
  }
163
127
 
164
- // g_error_matches: true when the error's domain + code both match. Accepts the
165
- // domain as an error-enum object / name string / numeric quark (errorDomainOf).
128
+ // g_error_matches: true when the error's domain AND code both match.
166
129
  matches(domain, code) {
167
130
  const d = errorDomainOf(domain);
168
131
  if (d === null) return false;
@@ -176,8 +139,7 @@ class GLibError extends Error {
176
139
  }
177
140
  }
178
141
 
179
- // The engine calls this on a failed GI invoke with the GError's authoritative
180
- // fields (quark name + numeric quark + code + message) to build the thrown error.
142
+ // Called by the engine on a failed GI invoke, with the GError's authoritative fields.
181
143
  function buildGError(domainName, domainQuark, code, message) {
182
144
  const error = new GLibError(domainName, code, message);
183
145
  error.domain = domainName;
@@ -186,74 +148,53 @@ function buildGError(domainName, domainQuark, code, message) {
186
148
  }
187
149
  native.setErrorBuilder(buildGError);
188
150
 
189
- // Symbol stamping a makeClass prototype with its { namespace, typeName }, so
190
- // Gio._promisify can record WHICH introspected class a registration belongs to
191
- // (introspected instances have no live prototype chain to resolve by). Read in
192
- // _promisify; matched against the instance's GType via native.isInstanceOf.
151
+ // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
152
+ // record WHICH introspected class a registration belongs to — introspected instances
153
+ // have no live prototype chain to resolve by, so it is matched against the instance's
154
+ // GType via native.isInstanceOf.
193
155
  const CLASS_INFO = Symbol('nodeGiClassInfo');
194
156
 
195
- // Promisified async methods, keyed by GI method name (snake_case) → an array of
196
- // registrations `{ namespace?, typeName?, wrapper }`. Introspected-class instances
197
- // resolve methods dynamically (no prototype chain), so the instance proxy consults
198
- // this registry. Usually one registration per name (fast path); when two classes
199
- // promisify the SAME method name, the instance proxy picks the registration whose
200
- // class the instance is-a (native.isInstanceOf). See _promisify / wrapInstance.
157
+ // Promisified async methods: GI method name (snake_case) → registrations
158
+ // `{ namespace?, typeName?, wrapper }`. Introspected-class instances resolve methods
159
+ // dynamically (no prototype chain), so the instance proxy consults this registry;
160
+ // when two classes promisify the SAME method name it picks the registration whose
161
+ // class the instance is-a.
201
162
  const promisifiedMethods = new Map();
202
163
 
203
- // Object-typed return values become chainable instance proxies; boxed/struct
204
- // handles (e.g. GMainLoop) become method-routing proxies; everything else
205
- // (primitives, strings, null) passes through.
206
164
  function wrapReturn(value) {
207
165
  if (native.isGObjectHandle(value)) return wrapInstance(value);
208
- // A GLib.Variant is ALSO a boxed handle (by tag), so it must be checked first
209
- // to give it the Variant ergonomics rather than a plain method-routing proxy.
166
+ // A GLib.Variant is ALSO a boxed handle by tag, so it must be checked first to get
167
+ // the Variant ergonomics rather than a plain method-routing proxy.
210
168
  if (native.isVariantHandle(value)) return wrapVariant(value);
211
- // A GParamSpec is a distinct GObject fundamental (tagged separately from boxed),
212
- // surfaced with the GObject.ParamSpec ergonomics (name/value_type/get_name()/…).
169
+ // A GParamSpec is a distinct GObject fundamental, tagged separately from boxed.
213
170
  if (native.isParamSpecHandle(value)) return wrapParamSpec(value);
214
- // A non-GObject GObject-fundamental (e.g. a GskRenderNode from Gtk.Snapshot.to_node):
215
- // an opaque, round-trippable pass-through handle (see wrapFundamental).
216
171
  if (native.isFundamentalHandle(value)) return wrapFundamental(value);
217
172
  if (native.isBoxedHandle(value)) return wrapBoxed(value);
218
- // A multi-value OUT tuple (return value + OUT params) — or a container OUT — is a
219
- // plain JS Array whose ELEMENTS may themselves be GObject/boxed/variant handles
220
- // (e.g. GLib.Regex.match → [matched, GLib.MatchInfo]; an array-of-objects OUT).
221
- // Recurse so a handle sitting inside the tuple is wrapped into a usable proxy,
222
- // not left as a raw External. A Node Buffer (byte array) is NOT Array.isArray, so
223
- // it passes through untouched; primitives/strings map to themselves.
173
+ // A multi-value OUT tuple (return + OUT params), or a container OUT, is a plain
174
+ // Array whose ELEMENTS may themselves be handles (GLib.Regex.match → [matched,
175
+ // GLib.MatchInfo]), so recurse rather than leave a raw External. A Node Buffer is
176
+ // not Array.isArray, so byte arrays pass through untouched.
224
177
  if (Array.isArray(value)) return value.map(wrapReturn);
225
178
  return value;
226
179
  }
227
180
 
228
- // Wrap a user's signal callback so each native signal argument is passed through
229
- // {@link wrapReturn} — a GObject arg becomes a chainable instance, a GVariant
230
- // arg (e.g. the value on Gio.SimpleAction::change-state) becomes a GLib.Variant
231
- // wrapper, primitives/plain objects pass through. GJS parity: the engine prepends
232
- // the EMITTER (the object the signal fired on) as the first arg, so a positional
233
- // handler `(obj, …params) => …` binds correctly; the emitter is itself a GObject
234
- // arg, so wrapReturn turns it into the cached, toggle-ref-canonical instance. For
235
- // `notify::x` the args are (object, pspec).
181
+ // GJS parity: the engine prepends the EMITTER as the first arg, so a positional handler
182
+ // `(obj, …params) => …` binds correctly (for `notify::x` the args are (object, pspec)).
183
+ // The emitter is itself a GObject arg, so wrapReturn turns it into the cached,
184
+ // toggle-ref-canonical instance.
236
185
  function wrapSignalCallback(cb) {
237
186
  return (...args) => cb(...args.map(wrapReturn));
238
187
  }
239
188
 
240
- // Resolve a Gtk.Template `<signal handler="…">` handler NAME to a dispatcher that
241
- // calls the instance's bound JS method, for the native template-callback scope
242
- // (engine: NodeGiScopeCreateClosure via set_template_scope). Mirrors GJS's
243
- // `_createClosure` (refs/gjs/modules/core/overrides/Gtk.js).
189
+ // Resolve a Gtk.Template `<signal handler="…">` handler NAME to a dispatcher calling the
190
+ // instance's bound JS method, for the native template-callback scope. Mirrors GJS's
191
+ // `_createClosure` in its Gtk override.
244
192
  //
245
193
  // LAZY by necessity: the engine resolves template signals during the C-side
246
- // constructType (init_template → create_closure), which runs BEFORE this layer
247
- // attaches the user-class prototype to the instance proxy. So the dispatcher
248
- // defers the method lookup to FIRE time, by which point the instance proxy is the
249
- // full, userProto-carrying, toggle-ref-canonical L1 wrapper. `this` inside the
250
- // handler is therefore the widget — the SAME cached proxy the user constructed
251
- // (wrapInstance is cached by the canonical handle). Native signal args are wrapped
252
- // (wrapReturn) into chainable instances, exactly like wrapSignalCallback. GJS
253
- // parity: the engine prepends the emitter at param 0, so a template handler
254
- // `on_click(button)` receives the emitter first, then the signal's own params. A
255
- // handler name with no matching user-prototype method throws a clear error when
256
- // the signal fires.
194
+ // constructType (init_template → create_closure), which runs BEFORE this layer attaches
195
+ // the user-class prototype to the instance proxy. Deferring the lookup to FIRE time
196
+ // means `this` is the full userProto-carrying wrapper — the SAME cached proxy the user
197
+ // constructed. Emitter at param 0, as in wrapSignalCallback.
257
198
  function resolveTemplateCallback(handle, handlerName) {
258
199
  return (...args) => {
259
200
  const proxy = wrapInstance(handle);
@@ -269,16 +210,14 @@ function resolveTemplateCallback(handle, handlerName) {
269
210
  }
270
211
  native.setTemplateCallbackResolver(resolveTemplateCallback);
271
212
 
272
- // Run a registered class's JS constructor for a GObject the ENGINE instantiated
273
- // from C — a GtkBuilder composite-template InternalChild is the common case. node-gi's
274
- // JS-`new` path runs the ctor via the makeClass super-substitution; a C-created
275
- // instance has no such driver, so NodeGiConstructor (the overridden `constructor`
276
- // vfunc) calls this with the instance's canonical handle + its GType name. We set
277
- // `adoptHandle` and Reflect.construct the class: its `super()` reaches the makeClass
278
- // base ctor, which adopts the handle (no second GObject) and returns the canonical
279
- // wrapper, so the ctor body runs against it (`this._x = …` become expandos on the
280
- // one wrapper GtkBuilder-fetched children later resolve to). GJS-faithful
281
- // (gjs_object_constructor's native-construction branch runs the JS constructor).
213
+ // Run a registered class's JS constructor for a GObject the ENGINE instantiated from C
214
+ // — typically a GtkBuilder composite-template InternalChild, which has no JS-`new` to
215
+ // drive the ctor, so the overridden `constructor` vfunc calls this with the canonical
216
+ // handle + GType name. Setting `adoptHandle` before Reflect.construct makes the
217
+ // makeClass base ctor ADOPT that handle instead of creating a second GObject, so the
218
+ // ctor body's `this._x = …` land as expandos on the one wrapper GtkBuilder-fetched
219
+ // children later resolve to. GJS does the same in gjs_object_constructor's
220
+ // native-construction branch.
282
221
  function runCtorForCObject(handle, gtypeName) {
283
222
  const cls = classesByGType.get(gtypeName);
284
223
  if (cls === undefined) return; // not a node-gi-registered class (shouldn't happen)
@@ -294,28 +233,22 @@ function runCtorForCObject(handle, gtypeName) {
294
233
  }
295
234
  native.setConstructCallback(runCtorForCObject);
296
235
 
297
- // Wrap a non-GObject GObject-fundamental (e.g. a GskRenderNode from
298
- // Gtk.Snapshot.to_node, a GdkEvent) as an OPAQUE, round-trippable handle. The
299
- // native tagged External already carries the raw pointer (its Data) + drops the
300
- // held ref on GC via the type's introspected unref func, so all L1 has to do is
301
- // expose it under HANDLE — then unwrapArg feeds it straight back into a
302
- // fundamental-typed IN arg (e.g. Gsk.Renderer.render_texture(node, …)), where the
303
- // OBJECT External branch reads the pointer. Method dispatch on fundamentals is a
304
- // documented follow-up; today they are pass-through intermediates (build → hand to
305
- // a consumer → drop), which is what the GSK screenshot/render path needs.
236
+ // A non-GObject GObject-fundamental (GskRenderNode, GdkEvent) as an OPAQUE,
237
+ // round-trippable handle: the native tagged External already carries the pointer and
238
+ // drops the held ref on GC via the type's own unref func, so L1 need only expose it
239
+ // under HANDLE for unwrapArg to feed back into a fundamental-typed IN arg. No method
240
+ // dispatch — these are pass-through intermediates (build → hand to a consumer → drop),
241
+ // which is what the GSK screenshot/render path needs.
306
242
  function wrapFundamental(handle) {
307
243
  return { [HANDLE]: handle, [Symbol.toStringTag]: 'GIFundamental' };
308
244
  }
309
245
 
310
- // Wrap a boxed/struct/union handle so its methods are callable GJS-style
311
- // (`mainLoop.run()`, snake_case or camelCase) AND its FIELDS are readable/writable
312
- // as plain properties (`simpleStruct.long_`, `union.long_ = 5`). Resolution rule
313
- // (VERIFIED against gjs 1.88 find_unique_js_field_name): a name that is BOTH a
314
- // method and a field resolves to the METHOD — gjs renames the colliding field to
315
- // `_name`. So the native boxedMemberKind checks methods first: 1 = method (a
316
- // callable dispatcher), 2 = field (read via getBoxedField / write via
317
- // setBoxedField), 0 = neither (fall back to a method dispatcher, so an unknown
318
- // name still throws a clear "no method" at call time — the pre-fields behaviour).
246
+ // A boxed/struct/union handle: methods callable GJS-style (`mainLoop.run()`, snake_case
247
+ // or camelCase) and FIELDS readable/writable as plain properties (`union.long_ = 5`).
248
+ // Resolution rule, verified against gjs 1.88's find_unique_js_field_name: a name that is
249
+ // BOTH resolves to the METHOD, and gjs renames the colliding field to `_name`. Hence
250
+ // boxedMemberKind checks methods first — 1 = method, 2 = field, 0 = neither,
251
+ // 3 = undecidable.
319
252
  function wrapBoxed(handle) {
320
253
  const target = { [HANDLE]: handle };
321
254
  const methodDispatch =
@@ -326,25 +259,22 @@ function wrapBoxed(handle) {
326
259
  get(t, prop) {
327
260
  if (prop === HANDLE) return handle;
328
261
  if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
329
- // A field set through the set trap below is stored as an expando on the
330
- // target (gjs allows writing a boxed field that GI declares unwritable to a
331
- // JS expando); surface it back before consulting introspection.
262
+ // A field written through the set trap below lands as an expando on the
263
+ // target (gjs likewise allows writing a GI-unwritable boxed field to a JS
264
+ // expando); surface it before consulting introspection.
332
265
  if (Object.hasOwn(t, prop)) return t[prop];
333
- // GBytes convenience: `.toArray()` → the raw bytes as a Uint8Array (Node
334
- // Buffer). GBytes has no introspected `to_array`; gjs adds it to
335
- // GLib.Bytes.prototype (imports._byteArrayNative.fromGBytes), so mirror it
336
- // via the introspected get_data(). Gated on the boxed type so it never
337
- // shadows a same-named method on some other struct.
266
+ // GBytes has no introspected `to_array`; gjs adds `toArray` to
267
+ // GLib.Bytes.prototype, so mirror it via the introspected get_data(). Gated
268
+ // on the boxed type so it never shadows a same-named method on another struct.
338
269
  if (prop === 'toArray' && native.boxedTypeName(handle) === 'GBytes') {
339
270
  return () => wrapReturn(native.callBoxedMethod(handle, 'get_data', []));
340
271
  }
341
272
  const snake = camelToSnake(prop);
342
- // boxedMemberKind: 0 = not a member (type info resolved) → `undefined`, so
343
- // `typeof boxed.noSuchName === 'undefined'` matches gjs (a fabricated
344
- // dispatcher would read `'function'` and break JS duck-typing such as
345
- // `if (typeof value.toArray === 'function')`); 1 = method; 2 = field; 3 =
346
- // undecidable (unregistered struct, no static info) → keep the dispatcher
347
- // fallback so a genuine method still resolves + throws a clear error at call.
273
+ // kind 0 (type info resolved, not a member) must read `undefined` to match
274
+ // gjs: a fabricated dispatcher would make `typeof boxed.noSuchName` report
275
+ // 'function' and break duck-typing checks. Kind 3 (unregistered struct, no
276
+ // static info) keeps the dispatcher fallback, so a genuine method still
277
+ // resolves and an unknown one throws a clear error at call time.
348
278
  const kind = native.boxedMemberKind(handle, snake);
349
279
  if (kind === 2) return wrapReturn(native.getBoxedField(handle, snake));
350
280
  if (kind === 0) return undefined;
@@ -369,15 +299,10 @@ function wrapBoxed(handle) {
369
299
  });
370
300
  }
371
301
 
372
- // Wrap a GParamSpec handle (phase 2.7a) with the GJS-shaped GObject.ParamSpec
373
- // surface: `.name`/`.nick`/`.blurb`/`.flags`/`.value_type`/`.owner_type`/
374
- // `.default_value` getters + the `get_name()`/`get_nick()`/`get_blurb()`/
375
- // `get_default_value()` methods. This is what a `notify` handler's second arg and
376
- // a GParamSpec-typed value now surface as (fixing the old `{name, valueType}`
377
- // plain-object shape). Carries [HANDLE] so it can round-trip back into the engine
378
- // and so `instanceof GObject.ParamSpec` recognises it (Symbol.hasInstance below).
379
- // value_type/owner_type are native GType handles (like gjs's GType objects); the
380
- // GTypes' `.name` reads their type name via the introspected surface.
302
+ // The GJS-shaped GObject.ParamSpec surface a `notify` handler's second arg and any
303
+ // GParamSpec-typed value surface as. Carries [HANDLE] so it round-trips back into the
304
+ // engine and `instanceof GObject.ParamSpec` recognises it. value_type/owner_type are
305
+ // native GType handles, like gjs's GType objects.
381
306
  function wrapParamSpec(handle) {
382
307
  const prop = (which) => native.paramSpecProp(handle, which);
383
308
  const pspec = {
@@ -411,23 +336,14 @@ function wrapParamSpec(handle) {
411
336
  return pspec;
412
337
  }
413
338
 
414
- // ---- GLib.Variant ergonomics (the GJS GLib.Variant override, L1) ----
415
- //
416
- // Mirrors gjs/modules/core/overrides/GLib.js: `new GLib.Variant(sig, value)`
417
- // recursively packs (native variantNew), and the wrapper exposes `.unpack()`,
418
- // `.deepUnpack()`/`.deep_unpack`, `.recursiveUnpack()`, `.get_type_string()` and
419
- // the GJS toString, plus routing of any other GVariant method (n_children,
420
- // get_child_value, print, …) through the boxed-method engine. The deep-vs-
421
- // recursive distinction is honoured in the native unpacker (variantUnpack):
422
- // unpack() → variantUnpack(h, false, false): children stay Variants
423
- // deepUnpack() → variantUnpack(h, true, false): one level; nested `v`
424
- // values (e.g. an a{sv} value) STAY Variants
425
- // recursiveUnpack() → variantUnpack(h, true, true): fully plain JS, no
426
- // Variants (discards `v` type info)
427
-
428
- // Re-wrap the result of variantUnpack: any GLib.Variant handle that the native
429
- // unpacker left in place (an `a{sv}` value under deepUnpack, every child under
430
- // unpack(), …) becomes a GLib.Variant wrapper; arrays/plain dicts are walked.
339
+ // GLib.Variant ergonomics, mirroring GJS's GLib override. The three unpack depths the
340
+ // native unpacker implements:
341
+ // unpack() children stay Variants
342
+ // deepUnpack() one level; nested `v` values (an a{sv} value) STAY Variants
343
+ // recursiveUnpack() fully plain JS, discarding the `v` type info
344
+
345
+ // Re-wrap what variantUnpack left in place: any GLib.Variant handle still in the result
346
+ // becomes a wrapper; arrays and plain dicts are walked.
431
347
  function wrapVariantResult(value) {
432
348
  if (value === null || typeof value !== 'object') return value;
433
349
  if (native.isVariantHandle(value)) return wrapVariant(value);
@@ -438,9 +354,8 @@ function wrapVariantResult(value) {
438
354
  return out;
439
355
  }
440
356
 
441
- // Wrap a boxed GLib.Variant handle with the GJS-shaped Variant surface. Carries
442
- // [HANDLE] so it round-trips back into the engine as a GVariant IN argument
443
- // (action.activate(variant), new_stateful state, change_state value, …).
357
+ // Carries [HANDLE] so the wrapper round-trips back into the engine as a GVariant IN
358
+ // argument (action.activate(variant), new_stateful state, change_state value, …).
444
359
  function wrapVariant(handle) {
445
360
  const target = { [HANDLE]: handle };
446
361
  const api = {
@@ -466,11 +381,8 @@ function wrapVariant(handle) {
466
381
  });
467
382
  }
468
383
 
469
- // Deep-unwrap a pack value so any nested GLib.Variant wrapper (carried at a `v`
470
- // position, e.g. an a{sv} value) reaches the native packer as its raw handle —
471
- // the native `v` case reads a raw boxed handle, not an L1 Proxy. Primitives,
472
- // byte arrays, plain arrays and dict objects are walked; HANDLE-carrying
473
- // wrappers collapse to their handle.
384
+ // Deep-unwrap a pack value: the native `v` case reads a raw boxed handle, not an L1
385
+ // Proxy, so a nested GLib.Variant wrapper must collapse to its handle before packing.
474
386
  function unwrapVariantValue(value) {
475
387
  if (value === null || typeof value !== 'object') return value;
476
388
  if (value[HANDLE] !== undefined) return value[HANDLE];
@@ -485,8 +397,6 @@ function packVariant(signature, value) {
485
397
  return wrapVariant(native.variantNew(signature, unwrapVariantValue(value)));
486
398
  }
487
399
 
488
- // `GLib.Variant` as a class-like object: `new GLib.Variant(sig, value)` and the
489
- // deprecated `GLib.Variant.new(sig, value)` both pack via the native engine.
490
400
  function makeVariantClass() {
491
401
  const ctor = function Variant(signature, value) {
492
402
  return packVariant(signature, value);
@@ -494,10 +404,10 @@ function makeVariantClass() {
494
404
  Object.defineProperty(ctor, 'name', { value: 'Variant', configurable: true });
495
405
  ctor.$gtypeName = 'GLib.Variant';
496
406
  ctor.new = (signature, value) => packVariant(signature, value);
497
- // `value instanceof GLib.Variant` — a wrapped Variant is a Proxy over a bare
498
- // `{[HANDLE]}` target (no class prototype), so the default instanceof always
499
- // returned false. Recognise any wrapper whose [HANDLE] is a native GVariant
500
- // boxed handle. Real GAction/GSettings/GLib.log_structured code branches on it.
407
+ // A wrapped Variant is a Proxy over a bare `{[HANDLE]}` target with no class
408
+ // prototype, so the default `value instanceof GLib.Variant` is always false —
409
+ // recognise any wrapper whose [HANDLE] is a native GVariant boxed handle instead.
410
+ // Real GAction/GSettings/GLib.log_structured code branches on it.
501
411
  Object.defineProperty(ctor, Symbol.hasInstance, {
502
412
  value(instance) {
503
413
  if (instance === null || typeof instance !== 'object') return false;
@@ -521,21 +431,14 @@ function makeVariantClass() {
521
431
 
522
432
  const variantClass = makeVariantClass();
523
433
 
524
- // ---- GObject.Value ergonomics (the GJS GObject.Value override, L1) ----
525
- //
526
- // gjs exposes `new GObject.Value()` (an empty GValue) + the convenience
527
- // `new GObject.Value(gtype, value)` (init + a set_* chosen by the value's type),
528
- // over the introspected GValue struct methods (.init/.set_*/.get_*/.copy/.reset/
529
- // .unset) + the static type_compatible/type_transformable —
530
- // refs/gjs/modules/core/overrides/GObject.js gValueConstructorFunc over the real
531
- // GValue class. node-gi has no g_value_new(), so construction goes through
532
- // native.newGValue (a zeroed G_TYPE_VALUE boxed handle) and the method surface then
533
- // routes through wrapBoxed's boxed-method dispatch (verified: init/set_int/get_int
534
- // resolve as GValue methods).
535
-
536
- // Fundamental GType name → GValue setter, for the `new GObject.Value(gtype, value)`
537
- // convenience. Non-primitive types (enum/flags/boxed/object/param/variant) are
538
- // matched by g_type_is_a below, exactly like gjs's default switch branch.
434
+ // GObject.Value ergonomics, mirroring GJS's GObject override: `new GObject.Value()` and
435
+ // the convenience `new GObject.Value(gtype, value)`. There is no g_value_new(), so
436
+ // construction goes through native.newGValue (a zeroed G_TYPE_VALUE boxed handle) and
437
+ // the .init/.set_*/.get_*/.copy/.reset/.unset surface routes through wrapBoxed's
438
+ // boxed-method dispatch.
439
+
440
+ // Fundamental GType name → GValue setter. Non-primitive types (enum/flags/boxed/object/
441
+ // param/variant) are matched by g_type_is_a below, like gjs's default switch branch.
539
442
  const GVALUE_PRIMITIVE_SETTERS = {
540
443
  gboolean: 'set_boolean',
541
444
  gchar: 'set_schar',
@@ -554,9 +457,8 @@ const GVALUE_PRIMITIVE_SETTERS = {
554
457
  GParam: 'set_param',
555
458
  };
556
459
 
557
- // Pick the GValue setter for a (resolved) GType handle, mirroring gjs's constructor
558
- // switch: an exact fundamental → its setter, else g_type_is_a → flags/enum/boxed/…
559
- // (introspected GObject.type_name / type_is_a — both verified on node-gi).
460
+ // Mirrors gjs's constructor switch: an exact fundamental → its setter, else g_type_is_a
461
+ // → flags/enum/boxed/…
560
462
  function gvalueSetterFor(gt) {
561
463
  const G = requireGi('GObject', '2.0');
562
464
  const prim = GVALUE_PRIMITIVE_SETTERS[G.type_name(gt)];
@@ -570,17 +472,13 @@ function gvalueSetterFor(gt) {
570
472
  throw new TypeError(`Invalid type argument '${G.type_name(gt)}' to the GObject.Value constructor`);
571
473
  }
572
474
 
573
- // Build the empty/convenience GValue: a zeroed GValue boxed handle wrapped with the
574
- // GValue struct methods; the 2-arg form inits it to `gtype` (a class ctor's $gtype
575
- // or a GType handle) and sets `value` via the matching setter.
576
475
  function buildValue(args) {
577
476
  const v = wrapBoxed(native.newGValue());
578
477
  if (args.length >= 2) {
579
478
  const gt = resolveGTypeArg(args[0]);
580
479
  // Resolve the setter BEFORE init so an unsupported type throws a clean JS
581
- // TypeError rather than driving g_value_init with a value-table-less type (a
582
- // GLib-CRITICAL that would abort under G_DEBUG=fatal-criticals) — same net
583
- // result as gjs's constructor, minus the noisy failed init.
480
+ // TypeError instead of driving g_value_init with a value-table-less type — a
481
+ // GLib-CRITICAL that aborts under G_DEBUG=fatal-criticals.
584
482
  const setter = gvalueSetterFor(gt);
585
483
  v.init(gt);
586
484
  v[setter](args[1]);
@@ -588,10 +486,8 @@ function buildValue(args) {
588
486
  return v;
589
487
  }
590
488
 
591
- // `GObject.Value` as a class-like object: `new GObject.Value()` /
592
- // `new GObject.Value(gtype, value)` construct via buildValue, static methods
593
- // (type_compatible / type_transformable + camelCase) route through the engine, and
594
- // `value instanceof GObject.Value` recognises any wrapper over a GValue boxed handle.
489
+ // `instanceof GObject.Value` recognises any wrapper over a GValue boxed handle (same
490
+ // prototype-less-Proxy reason as GLib.Variant above).
595
491
  function makeValueClass() {
596
492
  const ctor = function Value(...args) {
597
493
  return buildValue(args);
package/gtk-runtime.d.ts CHANGED
@@ -14,4 +14,12 @@ export function maybePrependGtkRuntimeDllPath(): void;
14
14
  /** Windows: wire the env for a full-windowing bundle's runtime data — GSettings schemas, gdk-pixbuf loaders, icon themes, fontconfig (no-op off win32 / for a display-free bundle). */
15
15
  export function maybeWireGtkWindowingEnv(): void;
16
16
  /** Activate the bundled GTK runtime for the native engine, if one is present. */
17
- export function activateBundledGtkRuntime(native: { prependSearchPath: (p: string) => void }): GtkRuntimeBundle | null;
17
+ export function activateBundledGtkRuntime(native: {
18
+ prependSearchPath: (p: string) => void;
19
+ prependLibraryPath?: (p: string) => void;
20
+ }): GtkRuntimeBundle | null;
21
+
22
+ /** Prepend GI's shared-library search path for the GTK the policy chose. */
23
+ export function activateGiLibraryPath(native: { prependLibraryPath?: (p: string) => void }): string[];
24
+ /** TEST-ONLY: allow the activation to run again. */
25
+ export function resetGiLibraryPathForTests(): void;