@gjsify/node-gi 0.13.0 → 0.20.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/README.md +818 -5
- package/binding.gyp +55 -19
- package/cairo.d.ts +187 -0
- package/cairo.js +570 -0
- package/gettext.d.ts +65 -0
- package/gettext.js +128 -0
- package/gi.d.ts +178 -0
- package/gi.js +2018 -23
- package/globals.d.ts +7 -33
- package/globals.js +40 -51
- package/gtk-runtime.d.ts +17 -0
- package/gtk-runtime.js +245 -0
- package/index.d.ts +306 -12
- package/index.js +447 -14
- package/overrides/_signals.js +167 -0
- package/overrides/gio-dbus.js +749 -0
- package/overrides/mainloop.js +60 -0
- package/package.json +45 -4
- package/src/addon.cc +98 -2074
- package/src/cairo.cc +926 -0
- package/src/calls.cc +1425 -0
- package/src/class.cc +1170 -0
- package/src/common.h +624 -0
- package/src/loop.cc +766 -0
- package/src/marshal.cc +1738 -0
- package/src/object.cc +814 -0
- package/src/private.cc +351 -0
- package/src/repo.cc +297 -0
- package/src/signals.cc +535 -0
- package/src/template.cc +116 -0
- package/src/toggle.cc +718 -0
- package/src/variant.cc +587 -0
- package/system.d.ts +49 -0
- package/system.js +116 -0
package/gi.js
CHANGED
|
@@ -16,11 +16,24 @@
|
|
|
16
16
|
// flags, constants, structs/boxed, interface static methods and camelCase
|
|
17
17
|
// aliases land in subsequent drops.
|
|
18
18
|
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
|
+
import { setImmediate } from 'node:timers';
|
|
23
|
+
// The GJS Gio DBus surface (Gio.DBus.session/system, own_name/watch_name,
|
|
24
|
+
// Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject) — ported to the L1 model.
|
|
25
|
+
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
19
26
|
|
|
20
27
|
// Symbol carrying the raw native GObject handle on a wrapped instance, so it can
|
|
21
28
|
// be unwrapped again when passed back into the engine as a GI argument.
|
|
22
29
|
const HANDLE = Symbol('nodeGiHandle');
|
|
23
30
|
|
|
31
|
+
// Symbol carrying the user-class prototype (a registerClass subclass) on a wrapped
|
|
32
|
+
// instance. Stored on the target rather than captured in the proxy closure so a
|
|
33
|
+
// later wrap with a userProto can UPGRADE an already-cached generic wrapper in
|
|
34
|
+
// place (preserving identity + expandos) — see wrapInstance.
|
|
35
|
+
const USER_PROTO = Symbol('nodeGiUserProto');
|
|
36
|
+
|
|
24
37
|
// JS-builtin / interop names that must NEVER be treated as a GI method or
|
|
25
38
|
// property — otherwise awaiting, printing or inspecting a wrapper would call
|
|
26
39
|
// into GObject (e.g. a stray `then` would make a wrapper look thenable).
|
|
@@ -37,10 +50,34 @@ function toKebab(name) {
|
|
|
37
50
|
return name.replace(/([A-Z])/g, '-$1').replace(/_/g, '-').toLowerCase();
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
// Per-user-function shim cache so repeated passes of the same callback marshal to
|
|
54
|
+
// the SAME native-facing function (stable identity, one ffi/GClosure wrapper per
|
|
55
|
+
// user fn per call site pattern).
|
|
56
|
+
const callbackShims = new WeakMap();
|
|
57
|
+
|
|
58
|
+
// A JS function crossing INTO the engine (a GI callback arg, or a JS fn
|
|
59
|
+
// marshalled as a GClosure IN-arg) is wrapped so that when C invokes it:
|
|
60
|
+
// • each native arg is marshalled through wrapReturn — a GObject handle becomes
|
|
61
|
+
// the cached chainable instance, a GVariant a GLib.Variant wrapper, a GParamSpec
|
|
62
|
+
// a ParamSpec wrapper — matching what GJS hands a callback;
|
|
63
|
+
// • the JS return is unwrapped back to its native handle (a GLib.Variant
|
|
64
|
+
// wrapper returned from e.g. a DBus get-property closure must reach the
|
|
65
|
+
// engine as the raw variant handle).
|
|
66
|
+
// Primitives and plain objects pass through both directions untouched.
|
|
67
|
+
function wrapCallbackFn(fn) {
|
|
68
|
+
let shim = callbackShims.get(fn);
|
|
69
|
+
if (shim === undefined) {
|
|
70
|
+
shim = (...args) => unwrapArg(fn(...args.map(wrapReturn)));
|
|
71
|
+
callbackShims.set(fn, shim);
|
|
72
|
+
}
|
|
73
|
+
return shim;
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
function unwrapArg(value) {
|
|
41
77
|
if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
|
|
42
78
|
return value[HANDLE];
|
|
43
79
|
}
|
|
80
|
+
if (typeof value === 'function') return wrapCallbackFn(value);
|
|
44
81
|
return value;
|
|
45
82
|
}
|
|
46
83
|
|
|
@@ -48,66 +85,1125 @@ function unwrapArgs(args) {
|
|
|
48
85
|
return args.map(unwrapArg);
|
|
49
86
|
}
|
|
50
87
|
|
|
88
|
+
// Normalize a construct-property dict: each KEY to the GObject canonical (kebab)
|
|
89
|
+
// property name and each VALUE unwrapped to its native handle. GJS accepts a
|
|
90
|
+
// construct-prop key in camelCase, snake_case OR already-dashed form and maps it to
|
|
91
|
+
// the GObject property; the native newObject/constructType layer below looks each
|
|
92
|
+
// key up against the GParamSpec by its canonical dashed name, so `{maximumSize:400}`
|
|
93
|
+
// would otherwise miss `maximum-size`. Reuse `toKebab` — the SAME normalization the
|
|
94
|
+
// property getter/setter accessor path already applies (lines below: `x.maximumSize`
|
|
95
|
+
// reads/writes `maximum-size`) — keeping a single source of truth, so construction
|
|
96
|
+
// and accessor agree. Idempotent: an already-dashed (`maximum-size`) or snake_case
|
|
97
|
+
// (`maximum_size`) key passes through unchanged (GObject canonicalizes `_`↔`-`).
|
|
98
|
+
// This is the ONLY caller-facing construct path — newObject for an introspected
|
|
99
|
+
// `new Ns.Class({...})`, and constructType for a registerClass'd subclass plus its
|
|
100
|
+
// `super({...})` chain-up — so all three accept camelCase identically to GJS.
|
|
51
101
|
function unwrapProps(props) {
|
|
52
102
|
const out = {};
|
|
53
|
-
for (const key of Object.keys(props)) out[key] = unwrapArg(props[key]);
|
|
103
|
+
for (const key of Object.keys(props)) out[toKebab(key)] = unwrapArg(props[key]);
|
|
54
104
|
return out;
|
|
55
105
|
}
|
|
56
106
|
|
|
107
|
+
// Symbol carrying an enum object's GError-domain descriptor ({ name, quark }) so
|
|
108
|
+
// GLib.Error.prototype.matches can resolve an error-enum (e.g. Gio.IOErrorEnum)
|
|
109
|
+
// to the domain it represents. Attached by makeEnum when the enum is registered
|
|
110
|
+
// as a GError domain; read by errorDomainOf.
|
|
111
|
+
const ERROR_DOMAIN = Symbol('nodeGiErrorDomain');
|
|
112
|
+
|
|
113
|
+
// ---- GLib.Error (the GError surface, L1) ----
|
|
114
|
+
//
|
|
115
|
+
// A real Error subclass mirroring GJS's GLib.Error: `.domain`, `.code`,
|
|
116
|
+
// `.message` plus `.matches(domain, code)` (the g_error_matches semantics — true
|
|
117
|
+
// when the error's domain AND code both match). The engine throws an instance of
|
|
118
|
+
// this on a failed sync GI invoke (via the builder registered below), and
|
|
119
|
+
// `requireGi('GLib').Error` is this constructor, so a caught error is
|
|
120
|
+
// `instanceof GLib.Error` exactly as under GJS.
|
|
121
|
+
//
|
|
122
|
+
// Divergence from GJS (documented): GJS's `.domain` is the numeric GQuark; here
|
|
123
|
+
// `.domain` is the quark NAME string (more useful in a Node context, and the task
|
|
124
|
+
// permits either). The numeric quark is kept on `.domainQuark`. `matches()`
|
|
125
|
+
// accepts the domain as an error-enum object (Gio.IOErrorEnum), a name string, or
|
|
126
|
+
// a numeric quark, so all the idiomatic call shapes work.
|
|
127
|
+
|
|
128
|
+
// Resolve a `matches` domain argument to a { name?, quark? } descriptor.
|
|
129
|
+
function errorDomainOf(domain) {
|
|
130
|
+
if (domain === null || domain === undefined) return null;
|
|
131
|
+
if (typeof domain === 'object' && domain[ERROR_DOMAIN] !== undefined) return domain[ERROR_DOMAIN];
|
|
132
|
+
if (typeof domain === 'string') return { name: domain };
|
|
133
|
+
if (typeof domain === 'number') return { quark: domain };
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class GLibError extends Error {
|
|
138
|
+
// GJS-shaped public constructor: `new GLib.Error(domain, code, message)` where
|
|
139
|
+
// `domain` may be an error-enum object, a quark name string, or a numeric quark.
|
|
140
|
+
constructor(domain, code, message) {
|
|
141
|
+
super(typeof message === 'string' ? message : '');
|
|
142
|
+
this.name = 'GLib.Error';
|
|
143
|
+
const d = errorDomainOf(domain);
|
|
144
|
+
this.domain = d !== null && d.name !== undefined ? d.name : typeof domain === 'string' ? domain : undefined;
|
|
145
|
+
this.domainQuark = d !== null && d.quark !== undefined ? d.quark : typeof domain === 'number' ? domain : undefined;
|
|
146
|
+
this.code = code;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// g_error_matches: true when the error's domain + code both match. Accepts the
|
|
150
|
+
// domain as an error-enum object / name string / numeric quark (errorDomainOf).
|
|
151
|
+
matches(domain, code) {
|
|
152
|
+
const d = errorDomainOf(domain);
|
|
153
|
+
if (d === null) return false;
|
|
154
|
+
const domainMatch =
|
|
155
|
+
(d.name !== undefined && d.name === this.domain) ||
|
|
156
|
+
(d.quark !== undefined && d.quark === this.domainQuark);
|
|
157
|
+
return domainMatch && code === this.code;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
toString() {
|
|
161
|
+
return `GLib.Error ${this.domain}: ${this.message}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The engine calls this on a failed GI invoke with the GError's authoritative
|
|
166
|
+
// fields (quark name + numeric quark + code + message) to build the thrown error.
|
|
167
|
+
function buildGError(domainName, domainQuark, code, message) {
|
|
168
|
+
const error = new GLibError(domainName, code, message);
|
|
169
|
+
error.domain = domainName;
|
|
170
|
+
error.domainQuark = domainQuark;
|
|
171
|
+
return error;
|
|
172
|
+
}
|
|
173
|
+
native.setErrorBuilder(buildGError);
|
|
174
|
+
|
|
175
|
+
// Symbol stamping a makeClass prototype with its { namespace, typeName }, so
|
|
176
|
+
// Gio._promisify can record WHICH introspected class a registration belongs to
|
|
177
|
+
// (introspected instances have no live prototype chain to resolve by). Read in
|
|
178
|
+
// _promisify; matched against the instance's GType via native.isInstanceOf.
|
|
179
|
+
const CLASS_INFO = Symbol('nodeGiClassInfo');
|
|
180
|
+
|
|
181
|
+
// Promisified async methods, keyed by GI method name (snake_case) → an array of
|
|
182
|
+
// registrations `{ namespace?, typeName?, wrapper }`. Introspected-class instances
|
|
183
|
+
// resolve methods dynamically (no prototype chain), so the instance proxy consults
|
|
184
|
+
// this registry. Usually one registration per name (fast path); when two classes
|
|
185
|
+
// promisify the SAME method name, the instance proxy picks the registration whose
|
|
186
|
+
// class the instance is-a (native.isInstanceOf). See _promisify / wrapInstance.
|
|
187
|
+
const promisifiedMethods = new Map();
|
|
188
|
+
|
|
57
189
|
// Object-typed return values become chainable instance proxies; boxed/struct
|
|
58
190
|
// handles (e.g. GMainLoop) become method-routing proxies; everything else
|
|
59
191
|
// (primitives, strings, null) passes through.
|
|
60
192
|
function wrapReturn(value) {
|
|
61
193
|
if (native.isGObjectHandle(value)) return wrapInstance(value);
|
|
194
|
+
// A GLib.Variant is ALSO a boxed handle (by tag), so it must be checked first
|
|
195
|
+
// to give it the Variant ergonomics rather than a plain method-routing proxy.
|
|
196
|
+
if (native.isVariantHandle(value)) return wrapVariant(value);
|
|
197
|
+
// A GParamSpec is a distinct GObject fundamental (tagged separately from boxed),
|
|
198
|
+
// surfaced with the GObject.ParamSpec ergonomics (name/value_type/get_name()/…).
|
|
199
|
+
if (native.isParamSpecHandle(value)) return wrapParamSpec(value);
|
|
200
|
+
// A non-GObject GObject-fundamental (e.g. a GskRenderNode from Gtk.Snapshot.to_node):
|
|
201
|
+
// an opaque, round-trippable pass-through handle (see wrapFundamental).
|
|
202
|
+
if (native.isFundamentalHandle(value)) return wrapFundamental(value);
|
|
62
203
|
if (native.isBoxedHandle(value)) return wrapBoxed(value);
|
|
204
|
+
// A multi-value OUT tuple (return value + OUT params) — or a container OUT — is a
|
|
205
|
+
// plain JS Array whose ELEMENTS may themselves be GObject/boxed/variant handles
|
|
206
|
+
// (e.g. GLib.Regex.match → [matched, GLib.MatchInfo]; an array-of-objects OUT).
|
|
207
|
+
// Recurse so a handle sitting inside the tuple is wrapped into a usable proxy,
|
|
208
|
+
// not left as a raw External. A Node Buffer (byte array) is NOT Array.isArray, so
|
|
209
|
+
// it passes through untouched; primitives/strings map to themselves.
|
|
210
|
+
if (Array.isArray(value)) return value.map(wrapReturn);
|
|
63
211
|
return value;
|
|
64
212
|
}
|
|
65
213
|
|
|
66
|
-
// Wrap a
|
|
67
|
-
//
|
|
68
|
-
//
|
|
214
|
+
// Wrap a user's signal callback so each native signal argument is passed through
|
|
215
|
+
// {@link wrapReturn} — a GObject arg becomes a chainable instance, a GVariant
|
|
216
|
+
// arg (e.g. the value on Gio.SimpleAction::change-state) becomes a GLib.Variant
|
|
217
|
+
// wrapper, primitives/plain objects pass through. GJS parity: the engine prepends
|
|
218
|
+
// the EMITTER (the object the signal fired on) as the first arg, so a positional
|
|
219
|
+
// handler `(obj, …params) => …` binds correctly; the emitter is itself a GObject
|
|
220
|
+
// arg, so wrapReturn turns it into the cached, toggle-ref-canonical instance. For
|
|
221
|
+
// `notify::x` the args are (object, pspec).
|
|
222
|
+
function wrapSignalCallback(cb) {
|
|
223
|
+
return (...args) => cb(...args.map(wrapReturn));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Resolve a Gtk.Template `<signal handler="…">` handler NAME to a dispatcher that
|
|
227
|
+
// calls the instance's bound JS method, for the native template-callback scope
|
|
228
|
+
// (engine: NodeGiScopeCreateClosure via set_template_scope). Mirrors GJS's
|
|
229
|
+
// `_createClosure` (refs/gjs/modules/core/overrides/Gtk.js).
|
|
230
|
+
//
|
|
231
|
+
// LAZY by necessity: the engine resolves template signals during the C-side
|
|
232
|
+
// constructType (init_template → create_closure), which runs BEFORE this layer
|
|
233
|
+
// attaches the user-class prototype to the instance proxy. So the dispatcher
|
|
234
|
+
// defers the method lookup to FIRE time, by which point the instance proxy is the
|
|
235
|
+
// full, userProto-carrying, toggle-ref-canonical L1 wrapper. `this` inside the
|
|
236
|
+
// handler is therefore the widget — the SAME cached proxy the user constructed
|
|
237
|
+
// (wrapInstance is cached by the canonical handle). Native signal args are wrapped
|
|
238
|
+
// (wrapReturn) into chainable instances, exactly like wrapSignalCallback. GJS
|
|
239
|
+
// parity: the engine prepends the emitter at param 0, so a template handler
|
|
240
|
+
// `on_click(button)` receives the emitter first, then the signal's own params. A
|
|
241
|
+
// handler name with no matching user-prototype method throws a clear error when
|
|
242
|
+
// the signal fires.
|
|
243
|
+
function resolveTemplateCallback(handle, handlerName) {
|
|
244
|
+
return (...args) => {
|
|
245
|
+
const proxy = wrapInstance(handle);
|
|
246
|
+
const userProto = proxy[USER_PROTO];
|
|
247
|
+
const desc = userProto !== undefined ? findProtoDescriptor(userProto, handlerName) : undefined;
|
|
248
|
+
if (desc === undefined || typeof desc.value !== 'function') {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Gtk.Template: signal handler '${handlerName}' is not defined on ${proxy.constructor?.name ?? 'the instance'}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return desc.value.apply(proxy, args.map(wrapReturn));
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
native.setTemplateCallbackResolver(resolveTemplateCallback);
|
|
257
|
+
|
|
258
|
+
// Wrap a non-GObject GObject-fundamental (e.g. a GskRenderNode from
|
|
259
|
+
// Gtk.Snapshot.to_node, a GdkEvent) as an OPAQUE, round-trippable handle. The
|
|
260
|
+
// native tagged External already carries the raw pointer (its Data) + drops the
|
|
261
|
+
// held ref on GC via the type's introspected unref func, so all L1 has to do is
|
|
262
|
+
// expose it under HANDLE — then unwrapArg feeds it straight back into a
|
|
263
|
+
// fundamental-typed IN arg (e.g. Gsk.Renderer.render_texture(node, …)), where the
|
|
264
|
+
// OBJECT External branch reads the pointer. Method dispatch on fundamentals is a
|
|
265
|
+
// documented follow-up; today they are pass-through intermediates (build → hand to
|
|
266
|
+
// a consumer → drop), which is what the GSK screenshot/render path needs.
|
|
267
|
+
function wrapFundamental(handle) {
|
|
268
|
+
return { [HANDLE]: handle, [Symbol.toStringTag]: 'GIFundamental' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Wrap a boxed/struct/union handle so its methods are callable GJS-style
|
|
272
|
+
// (`mainLoop.run()`, snake_case or camelCase) AND its FIELDS are readable/writable
|
|
273
|
+
// as plain properties (`simpleStruct.long_`, `union.long_ = 5`). Resolution rule
|
|
274
|
+
// (VERIFIED against gjs 1.88 find_unique_js_field_name): a name that is BOTH a
|
|
275
|
+
// method and a field resolves to the METHOD — gjs renames the colliding field to
|
|
276
|
+
// `_name`. So the native boxedMemberKind checks methods first: 1 = method (a
|
|
277
|
+
// callable dispatcher), 2 = field (read via getBoxedField / write via
|
|
278
|
+
// setBoxedField), 0 = neither (fall back to a method dispatcher, so an unknown
|
|
279
|
+
// name still throws a clear "no method" at call time — the pre-fields behaviour).
|
|
69
280
|
function wrapBoxed(handle) {
|
|
70
281
|
const target = { [HANDLE]: handle };
|
|
282
|
+
const methodDispatch = (prop) => (...args) =>
|
|
283
|
+
wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
|
|
71
284
|
return new Proxy(target, {
|
|
72
285
|
get(t, prop) {
|
|
73
286
|
if (prop === HANDLE) return handle;
|
|
74
287
|
if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
|
|
288
|
+
// A field set through the set trap below is stored as an expando on the
|
|
289
|
+
// target (gjs allows writing a boxed field that GI declares unwritable to a
|
|
290
|
+
// JS expando); surface it back before consulting introspection.
|
|
291
|
+
if (Object.hasOwn(t, prop)) return t[prop];
|
|
292
|
+
// GBytes convenience: `.toArray()` → the raw bytes as a Uint8Array (Node
|
|
293
|
+
// Buffer). GBytes has no introspected `to_array`; gjs adds it to
|
|
294
|
+
// GLib.Bytes.prototype (imports._byteArrayNative.fromGBytes), so mirror it
|
|
295
|
+
// via the introspected get_data(). Gated on the boxed type so it never
|
|
296
|
+
// shadows a same-named method on some other struct.
|
|
297
|
+
if (prop === 'toArray' && native.boxedTypeName(handle) === 'GBytes') {
|
|
298
|
+
return () => wrapReturn(native.callBoxedMethod(handle, 'get_data', []));
|
|
299
|
+
}
|
|
300
|
+
const snake = camelToSnake(prop);
|
|
301
|
+
// boxedMemberKind: 0 = not a member (type info resolved) → `undefined`, so
|
|
302
|
+
// `typeof boxed.noSuchName === 'undefined'` matches gjs (a fabricated
|
|
303
|
+
// dispatcher would read `'function'` and break JS duck-typing such as
|
|
304
|
+
// `if (typeof value.toArray === 'function')`); 1 = method; 2 = field; 3 =
|
|
305
|
+
// undecidable (unregistered struct, no static info) → keep the dispatcher
|
|
306
|
+
// fallback so a genuine method still resolves + throws a clear error at call.
|
|
307
|
+
const kind = native.boxedMemberKind(handle, snake);
|
|
308
|
+
if (kind === 2) return wrapReturn(native.getBoxedField(handle, snake));
|
|
309
|
+
if (kind === 0) return undefined;
|
|
310
|
+
return methodDispatch(prop);
|
|
311
|
+
},
|
|
312
|
+
set(t, prop, value) {
|
|
313
|
+
if (prop === HANDLE || typeof prop !== 'string' || RESERVED.has(prop)) {
|
|
314
|
+
return Reflect.set(t, prop, value);
|
|
315
|
+
}
|
|
316
|
+
const snake = camelToSnake(prop);
|
|
317
|
+
if (native.boxedMemberKind(handle, snake) === 2) {
|
|
318
|
+
native.setBoxedField(handle, snake, unwrapArg(value));
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return Reflect.set(t, prop, value);
|
|
322
|
+
},
|
|
323
|
+
has(t, prop) {
|
|
324
|
+
if (prop === HANDLE || prop in t) return true;
|
|
325
|
+
if (typeof prop !== 'string') return false;
|
|
326
|
+
return native.boxedMemberKind(handle, camelToSnake(prop)) !== 0;
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Wrap a GParamSpec handle (phase 2.7a) with the GJS-shaped GObject.ParamSpec
|
|
332
|
+
// surface: `.name`/`.nick`/`.blurb`/`.flags`/`.value_type`/`.owner_type`/
|
|
333
|
+
// `.default_value` getters + the `get_name()`/`get_nick()`/`get_blurb()`/
|
|
334
|
+
// `get_default_value()` methods. This is what a `notify` handler's second arg and
|
|
335
|
+
// a GParamSpec-typed value now surface as (fixing the old `{name, valueType}`
|
|
336
|
+
// plain-object shape). Carries [HANDLE] so it can round-trip back into the engine
|
|
337
|
+
// and so `instanceof GObject.ParamSpec` recognises it (Symbol.hasInstance below).
|
|
338
|
+
// value_type/owner_type are native GType handles (like gjs's GType objects); the
|
|
339
|
+
// GTypes' `.name` reads their type name via the introspected surface.
|
|
340
|
+
function wrapParamSpec(handle) {
|
|
341
|
+
const prop = (which) => native.paramSpecProp(handle, which);
|
|
342
|
+
const pspec = {
|
|
343
|
+
[HANDLE]: handle,
|
|
344
|
+
get name() {
|
|
345
|
+
return prop('name');
|
|
346
|
+
},
|
|
347
|
+
get nick() {
|
|
348
|
+
return prop('nick');
|
|
349
|
+
},
|
|
350
|
+
get blurb() {
|
|
351
|
+
return prop('blurb');
|
|
352
|
+
},
|
|
353
|
+
get flags() {
|
|
354
|
+
return prop('flags');
|
|
355
|
+
},
|
|
356
|
+
get value_type() {
|
|
357
|
+
return prop('valueType');
|
|
358
|
+
},
|
|
359
|
+
get owner_type() {
|
|
360
|
+
return prop('ownerType');
|
|
361
|
+
},
|
|
362
|
+
get default_value() {
|
|
363
|
+
return wrapReturn(prop('defaultValue'));
|
|
364
|
+
},
|
|
365
|
+
get_name: () => prop('name'),
|
|
366
|
+
get_nick: () => prop('nick'),
|
|
367
|
+
get_blurb: () => prop('blurb'),
|
|
368
|
+
get_default_value: () => wrapReturn(prop('defaultValue')),
|
|
369
|
+
};
|
|
370
|
+
return pspec;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---- GLib.Variant ergonomics (the GJS GLib.Variant override, L1) ----
|
|
374
|
+
//
|
|
375
|
+
// Mirrors gjs/modules/core/overrides/GLib.js: `new GLib.Variant(sig, value)`
|
|
376
|
+
// recursively packs (native variantNew), and the wrapper exposes `.unpack()`,
|
|
377
|
+
// `.deepUnpack()`/`.deep_unpack`, `.recursiveUnpack()`, `.get_type_string()` and
|
|
378
|
+
// the GJS toString, plus routing of any other GVariant method (n_children,
|
|
379
|
+
// get_child_value, print, …) through the boxed-method engine. The deep-vs-
|
|
380
|
+
// recursive distinction is honoured in the native unpacker (variantUnpack):
|
|
381
|
+
// unpack() → variantUnpack(h, false, false): children stay Variants
|
|
382
|
+
// deepUnpack() → variantUnpack(h, true, false): one level; nested `v`
|
|
383
|
+
// values (e.g. an a{sv} value) STAY Variants
|
|
384
|
+
// recursiveUnpack() → variantUnpack(h, true, true): fully plain JS, no
|
|
385
|
+
// Variants (discards `v` type info)
|
|
386
|
+
|
|
387
|
+
// Re-wrap the result of variantUnpack: any GLib.Variant handle that the native
|
|
388
|
+
// unpacker left in place (an `a{sv}` value under deepUnpack, every child under
|
|
389
|
+
// unpack(), …) becomes a GLib.Variant wrapper; arrays/plain dicts are walked.
|
|
390
|
+
function wrapVariantResult(value) {
|
|
391
|
+
if (value === null || typeof value !== 'object') return value;
|
|
392
|
+
if (native.isVariantHandle(value)) return wrapVariant(value);
|
|
393
|
+
if (value instanceof Uint8Array) return value; // `ay` bytes
|
|
394
|
+
if (Array.isArray(value)) return value.map(wrapVariantResult);
|
|
395
|
+
const out = {};
|
|
396
|
+
for (const key of Object.keys(value)) out[key] = wrapVariantResult(value[key]);
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Wrap a boxed GLib.Variant handle with the GJS-shaped Variant surface. Carries
|
|
401
|
+
// [HANDLE] so it round-trips back into the engine as a GVariant IN argument
|
|
402
|
+
// (action.activate(variant), new_stateful state, change_state value, …).
|
|
403
|
+
function wrapVariant(handle) {
|
|
404
|
+
const target = { [HANDLE]: handle };
|
|
405
|
+
const api = {
|
|
406
|
+
unpack: () => wrapVariantResult(native.variantUnpack(handle, false, false)),
|
|
407
|
+
deepUnpack: () => wrapVariantResult(native.variantUnpack(handle, true, false)),
|
|
408
|
+
deep_unpack: () => wrapVariantResult(native.variantUnpack(handle, true, false)),
|
|
409
|
+
recursiveUnpack: () => native.variantUnpack(handle, true, true),
|
|
410
|
+
get_type_string: () => native.variantGetTypeString(handle),
|
|
411
|
+
toString: () => `[object variant of type "${native.variantGetTypeString(handle)}"]`,
|
|
412
|
+
};
|
|
413
|
+
return new Proxy(target, {
|
|
414
|
+
get(t, prop) {
|
|
415
|
+
if (prop === HANDLE) return handle;
|
|
416
|
+
if (typeof prop !== 'string') return t[prop];
|
|
417
|
+
if (Object.hasOwn(api, prop)) return api[prop];
|
|
418
|
+
if (RESERVED.has(prop)) return t[prop];
|
|
419
|
+
// Any other GVariant method (n_children, get_child_value, print, …).
|
|
75
420
|
return (...args) =>
|
|
76
421
|
wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
|
|
77
422
|
},
|
|
78
423
|
has(t, prop) {
|
|
79
|
-
return prop === HANDLE || prop in t;
|
|
424
|
+
return prop === HANDLE || (typeof prop === 'string' && Object.hasOwn(api, prop)) || prop in t;
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Deep-unwrap a pack value so any nested GLib.Variant wrapper (carried at a `v`
|
|
430
|
+
// position, e.g. an a{sv} value) reaches the native packer as its raw handle —
|
|
431
|
+
// the native `v` case reads a raw boxed handle, not an L1 Proxy. Primitives,
|
|
432
|
+
// byte arrays, plain arrays and dict objects are walked; HANDLE-carrying
|
|
433
|
+
// wrappers collapse to their handle.
|
|
434
|
+
function unwrapVariantValue(value) {
|
|
435
|
+
if (value === null || typeof value !== 'object') return value;
|
|
436
|
+
if (value[HANDLE] !== undefined) return value[HANDLE];
|
|
437
|
+
if (value instanceof Uint8Array) return value; // `ay` bytes
|
|
438
|
+
if (Array.isArray(value)) return value.map(unwrapVariantValue);
|
|
439
|
+
const out = {};
|
|
440
|
+
for (const key of Object.keys(value)) out[key] = unwrapVariantValue(value[key]);
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function packVariant(signature, value) {
|
|
445
|
+
return wrapVariant(native.variantNew(signature, unwrapVariantValue(value)));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// `GLib.Variant` as a class-like object: `new GLib.Variant(sig, value)` and the
|
|
449
|
+
// deprecated `GLib.Variant.new(sig, value)` both pack via the native engine.
|
|
450
|
+
function makeVariantClass() {
|
|
451
|
+
const ctor = function Variant(signature, value) {
|
|
452
|
+
return packVariant(signature, value);
|
|
453
|
+
};
|
|
454
|
+
Object.defineProperty(ctor, 'name', { value: 'Variant', configurable: true });
|
|
455
|
+
ctor.$gtypeName = 'GLib.Variant';
|
|
456
|
+
ctor.new = (signature, value) => packVariant(signature, value);
|
|
457
|
+
// `value instanceof GLib.Variant` — a wrapped Variant is a Proxy over a bare
|
|
458
|
+
// `{[HANDLE]}` target (no class prototype), so the default instanceof always
|
|
459
|
+
// returned false. Recognise any wrapper whose [HANDLE] is a native GVariant
|
|
460
|
+
// boxed handle. Real GAction/GSettings/GLib.log_structured code branches on it.
|
|
461
|
+
Object.defineProperty(ctor, Symbol.hasInstance, {
|
|
462
|
+
value(instance) {
|
|
463
|
+
if (instance === null || typeof instance !== 'object') return false;
|
|
464
|
+
const handle = instance[HANDLE];
|
|
465
|
+
return handle !== undefined && native.isVariantHandle(handle);
|
|
80
466
|
},
|
|
467
|
+
configurable: true,
|
|
81
468
|
});
|
|
469
|
+
return new Proxy(ctor, {
|
|
470
|
+
get(t, prop) {
|
|
471
|
+
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
472
|
+
// Other static constructors (e.g. new_from_bytes) route through the engine.
|
|
473
|
+
const giName = camelToSnake(prop);
|
|
474
|
+
return (...args) =>
|
|
475
|
+
wrapReturn(native.callStaticMethod('GLib', 'Variant', giName, unwrapArgs(args)));
|
|
476
|
+
},
|
|
477
|
+
construct(_t, args) {
|
|
478
|
+
return packVariant(args[0], args[1]);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const variantClass = makeVariantClass();
|
|
484
|
+
|
|
485
|
+
// ---- GObject.Value ergonomics (the GJS GObject.Value override, L1) ----
|
|
486
|
+
//
|
|
487
|
+
// gjs exposes `new GObject.Value()` (an empty GValue) + the convenience
|
|
488
|
+
// `new GObject.Value(gtype, value)` (init + a set_* chosen by the value's type),
|
|
489
|
+
// over the introspected GValue struct methods (.init/.set_*/.get_*/.copy/.reset/
|
|
490
|
+
// .unset) + the static type_compatible/type_transformable —
|
|
491
|
+
// refs/gjs/modules/core/overrides/GObject.js gValueConstructorFunc over the real
|
|
492
|
+
// GValue class. node-gi has no g_value_new(), so construction goes through
|
|
493
|
+
// native.newGValue (a zeroed G_TYPE_VALUE boxed handle) and the method surface then
|
|
494
|
+
// routes through wrapBoxed's boxed-method dispatch (verified: init/set_int/get_int
|
|
495
|
+
// resolve as GValue methods).
|
|
496
|
+
|
|
497
|
+
// Fundamental GType name → GValue setter, for the `new GObject.Value(gtype, value)`
|
|
498
|
+
// convenience. Non-primitive types (enum/flags/boxed/object/param/variant) are
|
|
499
|
+
// matched by g_type_is_a below, exactly like gjs's default switch branch.
|
|
500
|
+
const GVALUE_PRIMITIVE_SETTERS = {
|
|
501
|
+
gboolean: 'set_boolean',
|
|
502
|
+
gchar: 'set_schar',
|
|
503
|
+
guchar: 'set_uchar',
|
|
504
|
+
gint: 'set_int',
|
|
505
|
+
guint: 'set_uint',
|
|
506
|
+
glong: 'set_long',
|
|
507
|
+
gulong: 'set_ulong',
|
|
508
|
+
gint64: 'set_int64',
|
|
509
|
+
guint64: 'set_uint64',
|
|
510
|
+
gfloat: 'set_float',
|
|
511
|
+
gdouble: 'set_double',
|
|
512
|
+
gchararray: 'set_string',
|
|
513
|
+
GType: 'set_gtype',
|
|
514
|
+
GVariant: 'set_variant',
|
|
515
|
+
GParam: 'set_param',
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Pick the GValue setter for a (resolved) GType handle, mirroring gjs's constructor
|
|
519
|
+
// switch: an exact fundamental → its setter, else g_type_is_a → flags/enum/boxed/…
|
|
520
|
+
// (introspected GObject.type_name / type_is_a — both verified on node-gi).
|
|
521
|
+
function gvalueSetterFor(gt) {
|
|
522
|
+
const G = requireGi('GObject', '2.0');
|
|
523
|
+
const prim = GVALUE_PRIMITIVE_SETTERS[G.type_name(gt)];
|
|
524
|
+
if (prim !== undefined) return prim;
|
|
525
|
+
if (G.type_is_a(gt, G.TYPE_FLAGS)) return 'set_flags';
|
|
526
|
+
if (G.type_is_a(gt, G.TYPE_ENUM)) return 'set_enum';
|
|
527
|
+
if (G.type_is_a(gt, G.TYPE_VARIANT)) return 'set_variant';
|
|
528
|
+
if (G.type_is_a(gt, G.TYPE_PARAM)) return 'set_param';
|
|
529
|
+
if (G.type_is_a(gt, G.TYPE_BOXED)) return 'set_boxed';
|
|
530
|
+
if (G.type_is_a(gt, G.TYPE_OBJECT)) return 'set_object';
|
|
531
|
+
throw new TypeError(`Invalid type argument '${G.type_name(gt)}' to the GObject.Value constructor`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Build the empty/convenience GValue: a zeroed GValue boxed handle wrapped with the
|
|
535
|
+
// GValue struct methods; the 2-arg form inits it to `gtype` (a class ctor's $gtype
|
|
536
|
+
// or a GType handle) and sets `value` via the matching setter.
|
|
537
|
+
function buildValue(args) {
|
|
538
|
+
const v = wrapBoxed(native.newGValue());
|
|
539
|
+
if (args.length >= 2) {
|
|
540
|
+
const gt = resolveGTypeArg(args[0]);
|
|
541
|
+
// Resolve the setter BEFORE init so an unsupported type throws a clean JS
|
|
542
|
+
// TypeError rather than driving g_value_init with a value-table-less type (a
|
|
543
|
+
// GLib-CRITICAL that would abort under G_DEBUG=fatal-criticals) — same net
|
|
544
|
+
// result as gjs's constructor, minus the noisy failed init.
|
|
545
|
+
const setter = gvalueSetterFor(gt);
|
|
546
|
+
v.init(gt);
|
|
547
|
+
v[setter](args[1]);
|
|
548
|
+
}
|
|
549
|
+
return v;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// `GObject.Value` as a class-like object: `new GObject.Value()` /
|
|
553
|
+
// `new GObject.Value(gtype, value)` construct via buildValue, static methods
|
|
554
|
+
// (type_compatible / type_transformable + camelCase) route through the engine, and
|
|
555
|
+
// `value instanceof GObject.Value` recognises any wrapper over a GValue boxed handle.
|
|
556
|
+
function makeValueClass() {
|
|
557
|
+
const ctor = function Value(...args) {
|
|
558
|
+
return buildValue(args);
|
|
559
|
+
};
|
|
560
|
+
Object.defineProperty(ctor, 'name', { value: 'Value', configurable: true });
|
|
561
|
+
ctor.$gtypeName = 'GObject.Value';
|
|
562
|
+
Object.defineProperty(ctor, Symbol.hasInstance, {
|
|
563
|
+
value(instance) {
|
|
564
|
+
if (instance === null || typeof instance !== 'object') return false;
|
|
565
|
+
const handle = instance[HANDLE];
|
|
566
|
+
return (
|
|
567
|
+
handle !== undefined &&
|
|
568
|
+
native.isBoxedHandle(handle) &&
|
|
569
|
+
native.boxedTypeName(handle) === 'GValue'
|
|
570
|
+
);
|
|
571
|
+
},
|
|
572
|
+
configurable: true,
|
|
573
|
+
});
|
|
574
|
+
return new Proxy(ctor, {
|
|
575
|
+
get(t, prop) {
|
|
576
|
+
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
577
|
+
const giName = camelToSnake(prop);
|
|
578
|
+
return (...args) =>
|
|
579
|
+
wrapReturn(native.callStaticMethod('GObject', 'Value', giName, unwrapArgs(args)));
|
|
580
|
+
},
|
|
581
|
+
construct(_t, args) {
|
|
582
|
+
return buildValue(args);
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const valueClass = makeValueClass();
|
|
588
|
+
|
|
589
|
+
// Overlay the GJS Variant ergonomics on the introspected GLib namespace, leaving
|
|
590
|
+
// every other member resolving from introspection. (Additive, like the GObject
|
|
591
|
+
// overlay; the introspected struct-based `GLib.Variant` is replaced by the
|
|
592
|
+
// ergonomic wrapper class so `new GLib.Variant(...)` + `.deepUnpack()` work.)
|
|
593
|
+
// The GLib names the L1 overlay adds/replaces on top of introspection.
|
|
594
|
+
// log_set_writer_func / log_set_writer_default mirror gjs's GLib.js overrides:
|
|
595
|
+
// gjs routes them through GjsPrivate (a C wrapper) because a GLogWriterFunc can
|
|
596
|
+
// fire on any thread and its GLogField array is not generically introspectable —
|
|
597
|
+
// node-gi's engine ships the same wrapper natively (logSetWriterFunc /
|
|
598
|
+
// logSetWriterDefault, src/private.cc). The JS writer receives
|
|
599
|
+
// (logLevel, fields) where fields is a plain object whose values are Uint8Arrays
|
|
600
|
+
// of the field bytes (or null for empty fields) — byte-for-byte the shape gjs's
|
|
601
|
+
// `{...stringFields.recursiveUnpack()}` produces (verified vs gjs 1.88).
|
|
602
|
+
const GLIB_OVERLAY_NAMES = new Set([
|
|
603
|
+
'log_structured',
|
|
604
|
+
'idle_add_once',
|
|
605
|
+
'timeout_add_once',
|
|
606
|
+
'timeout_add_seconds_once',
|
|
607
|
+
'log_set_writer_func',
|
|
608
|
+
'log_set_writer_default',
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
// GLib.log_structured(domain, level, fields): pack `fields` into an `a{sv}` and hand
|
|
612
|
+
// it to the introspected GLib.log_variant — refs/gjs/modules/core/overrides/GLib.js
|
|
613
|
+
// log_structured. Each field value is `ay` (Uint8Array), `s` (string) or a Variant
|
|
614
|
+
// passed through; anything else is a TypeError, matching gjs.
|
|
615
|
+
function makeLogStructured(baseNs) {
|
|
616
|
+
return (logDomain, logLevel, fields) => {
|
|
617
|
+
const variantFields = {};
|
|
618
|
+
for (const key of Object.keys(fields)) {
|
|
619
|
+
const field = fields[key];
|
|
620
|
+
if (field instanceof Uint8Array) variantFields[key] = variantClass('ay', field);
|
|
621
|
+
else if (typeof field === 'string') variantFields[key] = variantClass('s', field);
|
|
622
|
+
else if (field instanceof variantClass) variantFields[key] = field;
|
|
623
|
+
else {
|
|
624
|
+
throw new TypeError(
|
|
625
|
+
`Unsupported value ${field}, log_structured supports GLib.Variant, Uint8Array, and string values.`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
baseNs.log_variant(logDomain, logLevel, variantClass('a{sv}', variantFields));
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function decorateGLibNamespace(baseNs) {
|
|
634
|
+
// Per-namespace cache of the overlay functions that need the introspected baseNs
|
|
635
|
+
// (log_structured → log_variant; the *_once helpers → idle_add/timeout_add).
|
|
636
|
+
const cache = new Map();
|
|
637
|
+
const overlay = (prop) => {
|
|
638
|
+
if (cache.has(prop)) return cache.get(prop);
|
|
639
|
+
let value;
|
|
640
|
+
if (prop === 'log_structured') value = makeLogStructured(baseNs);
|
|
641
|
+
// The one-shot idle/timeout conveniences (gjs GLib.js): auto-remove the source
|
|
642
|
+
// after the callback runs, so the callback needs no `return SOURCE_REMOVE`.
|
|
643
|
+
else if (prop === 'idle_add_once') {
|
|
644
|
+
value = (priority, callback) =>
|
|
645
|
+
baseNs.idle_add(priority, () => {
|
|
646
|
+
callback();
|
|
647
|
+
return baseNs.SOURCE_REMOVE;
|
|
648
|
+
});
|
|
649
|
+
} else if (prop === 'timeout_add_once') {
|
|
650
|
+
value = (priority, interval, callback) =>
|
|
651
|
+
baseNs.timeout_add(priority, interval, () => {
|
|
652
|
+
callback();
|
|
653
|
+
return baseNs.SOURCE_REMOVE;
|
|
654
|
+
});
|
|
655
|
+
} else if (prop === 'timeout_add_seconds_once') {
|
|
656
|
+
value = (priority, interval, callback) =>
|
|
657
|
+
baseNs.timeout_add_seconds(priority, interval, () => {
|
|
658
|
+
callback();
|
|
659
|
+
return baseNs.SOURCE_REMOVE;
|
|
660
|
+
});
|
|
661
|
+
} else if (prop === 'log_set_writer_func') {
|
|
662
|
+
// The gjs GLib.js shape: a non-function clears the JS writer (logs fall
|
|
663
|
+
// back to the default writer); a function becomes the structured-log
|
|
664
|
+
// writer. NOTE (GLib contract, identical under gjs): the underlying
|
|
665
|
+
// g_log_set_writer_func may only ever be installed ONCE per process — a
|
|
666
|
+
// second install aborts inside GLib itself.
|
|
667
|
+
value = (writerFunc) => {
|
|
668
|
+
if (typeof writerFunc !== 'function') native.logSetWriterFunc(null);
|
|
669
|
+
else native.logSetWriterFunc(writerFunc);
|
|
670
|
+
};
|
|
671
|
+
} else if (prop === 'log_set_writer_default') {
|
|
672
|
+
value = () => native.logSetWriterDefault();
|
|
673
|
+
}
|
|
674
|
+
cache.set(prop, value);
|
|
675
|
+
return value;
|
|
676
|
+
};
|
|
677
|
+
return new Proxy(baseNs, {
|
|
678
|
+
get(t, prop) {
|
|
679
|
+
if (prop === 'Variant') return variantClass;
|
|
680
|
+
// GLib.Error is the L1 GError subclass (the engine throws instances of it,
|
|
681
|
+
// and `new GLib.Error(domain, code, message)` constructs one), shadowing the
|
|
682
|
+
// introspected boxed type so `instanceof GLib.Error` + `.matches()` work.
|
|
683
|
+
if (prop === 'Error') return GLibError;
|
|
684
|
+
if (typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) return overlay(prop);
|
|
685
|
+
return t[prop];
|
|
686
|
+
},
|
|
687
|
+
has(t, prop) {
|
|
688
|
+
return (
|
|
689
|
+
prop === 'Variant' ||
|
|
690
|
+
prop === 'Error' ||
|
|
691
|
+
(typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) ||
|
|
692
|
+
prop in t
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Overlay the GJS Gio runtime statics on the introspected Gio namespace —
|
|
699
|
+
// additively. `_promisify` is the genuinely-new helper (refs/gjs Gio.js); the
|
|
700
|
+
// DBus surface (Gio.DBus, Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject)
|
|
701
|
+
// is built by createGioDBus (overrides/gio-dbus.js) over the introspected Gio +
|
|
702
|
+
// the ergonomic GLib. Every other member keeps resolving from introspection.
|
|
703
|
+
function decorateGioNamespace(baseNs) {
|
|
704
|
+
// Lazily construct the DBus surface: it needs the ergonomic GLib (`new
|
|
705
|
+
// GLib.Variant`), and building it only on first `Gio.DBus*` access keeps a plain
|
|
706
|
+
// `import Gio` (no DBus) from pulling GLib in.
|
|
707
|
+
let dbus;
|
|
708
|
+
const getDBus = () => {
|
|
709
|
+
if (dbus === undefined) {
|
|
710
|
+
dbus = createGioDBus({ Gio: baseNs, GLib: requireGi('GLib', '2.0'), unwrap: unwrapArg, native });
|
|
711
|
+
}
|
|
712
|
+
return dbus;
|
|
713
|
+
};
|
|
714
|
+
return new Proxy(baseNs, {
|
|
715
|
+
get(t, prop) {
|
|
716
|
+
if (prop === '_promisify') return promisify;
|
|
717
|
+
if (prop === 'DBus') return getDBus().DBus;
|
|
718
|
+
if (prop === 'DBusProxy') return getDBus().DBusProxy;
|
|
719
|
+
if (prop === 'DBusExportedObject') return getDBus().DBusExportedObject;
|
|
720
|
+
return t[prop];
|
|
721
|
+
},
|
|
722
|
+
has(t, prop) {
|
|
723
|
+
return (
|
|
724
|
+
prop === '_promisify' ||
|
|
725
|
+
prop === 'DBus' ||
|
|
726
|
+
prop === 'DBusProxy' ||
|
|
727
|
+
prop === 'DBusExportedObject' ||
|
|
728
|
+
prop in t
|
|
729
|
+
);
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Walk a JS prototype chain (excluding Object.prototype) for an own property
|
|
735
|
+
// descriptor of `prop` — used to surface a registerClass subclass's own methods
|
|
736
|
+
// /getters/setters on the GObject wrapper.
|
|
737
|
+
function findProtoDescriptor(proto, prop) {
|
|
738
|
+
for (let p = proto; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
739
|
+
const d = Object.getOwnPropertyDescriptor(p, prop);
|
|
740
|
+
if (d !== undefined) return d;
|
|
741
|
+
}
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Whether `proto` is the SAME as, or a more-derived descendant of, `maybeAncestor`
|
|
746
|
+
// (i.e. `maybeAncestor` lies on `proto`'s prototype chain). Used to decide whether a
|
|
747
|
+
// later wrapInstance userProto should UPGRADE a cached one: on a multi-level chain a
|
|
748
|
+
// vfunc firing during construction wraps the instance with the OVERRIDING ancestor's
|
|
749
|
+
// prototype before the leaf ctor wraps it with the leaf's, so the leaf (descendant)
|
|
750
|
+
// proto must be allowed to supersede the ancestor's — but never the reverse.
|
|
751
|
+
function isProtoSameOrDescendantOf(proto, maybeAncestor) {
|
|
752
|
+
for (let p = proto; p !== null; p = Object.getPrototypeOf(p)) {
|
|
753
|
+
if (p === maybeAncestor) return true;
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// L1 proxy-identity cache (the user-visible half of the toggle-ref bridge). The
|
|
759
|
+
// native engine now returns the CANONICAL External per GObject (same GObject ⇒
|
|
760
|
+
// same handle), so we cache the per-instance Proxy keyed by that handle: the same
|
|
761
|
+
// GObject always yields the same L1 wrapper, so `===` holds at the ergonomic
|
|
762
|
+
// layer and a plain JS field set on a wrapper survives a round-trip + GC (the
|
|
763
|
+
// External is kept alive by the toggle-up root while C owns the object, which in
|
|
764
|
+
// turn keeps this WeakMap entry — and the proxy + its fields — alive).
|
|
765
|
+
const instanceCache = new WeakMap();
|
|
766
|
+
|
|
767
|
+
// ---- signal-handler bookkeeping (JS function → connected handler ids) ----
|
|
768
|
+
//
|
|
769
|
+
// node-gi connects signals through PRIVATE GClosures (a JS callback wrapped in a C
|
|
770
|
+
// closure — see signals.cc), so the GObject cannot map a JS function back to its
|
|
771
|
+
// handler ids the way gjs's signal_handler_find can. gjs reaches this via its own
|
|
772
|
+
// per-instance private-closure registry (Gi.signals_{block,unblock,disconnect}_
|
|
773
|
+
// symbol, refs/gjs/modules/core/overrides/GObject.js); we keep the equivalent map
|
|
774
|
+
// here, populated at connect() time, so GObject.signal_handlers_{block,unblock,
|
|
775
|
+
// disconnect}_by_func can resolve a function to its ids. Keyed by the canonical
|
|
776
|
+
// native handle (a WeakMap, so it is collected with the object). Divergence
|
|
777
|
+
// (documented): disconnecting a handler by a route OTHER than the L1 `.disconnect(id)`
|
|
778
|
+
// / a by-func disconnect (e.g. the introspected GObject.signal_handler_disconnect)
|
|
779
|
+
// leaves a stale id here; a following block/unblock/disconnect-by-func would then act
|
|
780
|
+
// on a dead id (native.disconnectSignal already guards is_connected; block/unblock
|
|
781
|
+
// would g_warning). Normal connect→block/unblock/disconnect flows are exact.
|
|
782
|
+
const signalHandlerIds = new WeakMap(); // handle → Map<jsFunc, Set<handlerId>>
|
|
783
|
+
|
|
784
|
+
function recordSignalHandler(handle, fn, id) {
|
|
785
|
+
let byFn = signalHandlerIds.get(handle);
|
|
786
|
+
if (byFn === undefined) {
|
|
787
|
+
byFn = new Map();
|
|
788
|
+
signalHandlerIds.set(handle, byFn);
|
|
789
|
+
}
|
|
790
|
+
let ids = byFn.get(fn);
|
|
791
|
+
if (ids === undefined) {
|
|
792
|
+
ids = new Set();
|
|
793
|
+
byFn.set(fn, ids);
|
|
794
|
+
}
|
|
795
|
+
ids.add(id);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Drop a handler id from the per-instance registry (on disconnect), pruning an
|
|
799
|
+
// emptied function entry so a later by-func lookup reports zero matches.
|
|
800
|
+
function forgetSignalHandlerId(handle, id) {
|
|
801
|
+
const byFn = signalHandlerIds.get(handle);
|
|
802
|
+
if (byFn === undefined) return;
|
|
803
|
+
for (const [fn, ids] of byFn) {
|
|
804
|
+
if (ids.delete(id) && ids.size === 0) byFn.delete(fn);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// The connected handler ids for a given JS function on an instance (a fresh array;
|
|
809
|
+
// empty when the function was never connected on this instance).
|
|
810
|
+
function handlerIdsForFunc(handle, fn) {
|
|
811
|
+
const ids = signalHandlerIds.get(handle)?.get(fn);
|
|
812
|
+
return ids === undefined ? [] : [...ids];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ---- G3 mutate-in-place registry ----
|
|
816
|
+
//
|
|
817
|
+
// registerClass registers the GType for the GIVEN class and returns that SAME
|
|
818
|
+
// class (GJS-faithful — refs/gjs/modules/core/overrides/GObject.js mutates `klass`
|
|
819
|
+
// and returns it), so both `const X = registerClass(…,C)` and
|
|
820
|
+
// `static { registerClass(…,C) }` (return discarded) leave the same symbol bound
|
|
821
|
+
// to the registered GType. The construction that produces the registered GType is
|
|
822
|
+
// moved to the introspected base ctor (makeClass), keyed on `new.target`: when
|
|
823
|
+
// `new Sub(args)` runs and `Sub` is registered here, the base ctor builds the
|
|
824
|
+
// registered GType via constructType + wraps it, and RETURNS that wrapper. Because
|
|
825
|
+
// a base constructor that returns an object substitutes `this` in every derived
|
|
826
|
+
// ctor (ES `super` semantics), this single routing point makes the whole super()
|
|
827
|
+
// chain construct the right GType, return the canonical toggle-ref wrapper carrying
|
|
828
|
+
// the leaf's prototype, and run every user ctor body against it — exactly how GJS
|
|
829
|
+
// instantiates `new.target`'s `$gtype` rather than the literal base. Keyed by the
|
|
830
|
+
// JS class → { typeHandle, children?, internalChildren? } (children move here from
|
|
831
|
+
// the old throwaway Subclass so init_template'd children are surfaced on the
|
|
832
|
+
// instance). Multi-level registered-of-registered chains (G2) are supported: the
|
|
833
|
+
// registry is also consulted by findParentGType to resolve a REGISTERED parent's
|
|
834
|
+
// GType handle (subclassing via native.registerClassFromGType), and the new.target
|
|
835
|
+
// routing keys on the leaf so construction is correct at any depth.
|
|
836
|
+
const registeredClasses = new Map();
|
|
837
|
+
|
|
838
|
+
// Surface a templated type's bound children on a freshly-constructed instance
|
|
839
|
+
// (GJS convention: public `this.<name>`, internal `this._<name>`, '-' → '_'). The
|
|
840
|
+
// engine already ran init_template during constructType, so getTemplateChild
|
|
841
|
+
// resolves each. Assigned through the proxy (set trap → stored as an instance
|
|
842
|
+
// expando), exactly as the old Subclass did.
|
|
843
|
+
function assignTemplateChildren(instance, handle, reg) {
|
|
844
|
+
if (reg.children !== undefined) {
|
|
845
|
+
for (const childName of reg.children) {
|
|
846
|
+
instance[childName.replace(/-/g, '_')] = wrapReturn(native.getTemplateChild(handle, childName));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (reg.internalChildren !== undefined) {
|
|
850
|
+
for (const childName of reg.internalChildren) {
|
|
851
|
+
instance[`_${childName.replace(/-/g, '_')}`] = wrapReturn(native.getTemplateChild(handle, childName));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ---- Gio.Application.runAsync (L1, GJS-shaped) ----
|
|
857
|
+
//
|
|
858
|
+
// The Node twin of GJS's `Gio.Application.prototype.runAsync`
|
|
859
|
+
// (refs/gjs/modules/core/overrides/Gio.js → `GLib.MainLoop.prototype.runAsync`
|
|
860
|
+
// in GLib.js). GJS returns a Promise and defers the blocking run() via
|
|
861
|
+
// `setMainLoopHook`, so the program's already-queued microtasks/promises settle
|
|
862
|
+
// before the GLib loop takes over the thread, then resolves with run()'s integer
|
|
863
|
+
// exit status when the app quits/exits.
|
|
864
|
+
//
|
|
865
|
+
// On Node there is no main-loop hook — libuv is always the process loop and the
|
|
866
|
+
// libuv↔GLib bridge (addon.cc UvLoopSource, attached to the DEFAULT GMainContext
|
|
867
|
+
// that g_application_run iterates) co-pumps libuv during a blocking run(). The
|
|
868
|
+
// faithful analogue is therefore to defer the blocking run() to a MACROTASK
|
|
869
|
+
// (setImmediate). This:
|
|
870
|
+
// • returns the Promise immediately — runAsync does NOT block the caller the
|
|
871
|
+
// way a bare `app.run()` would,
|
|
872
|
+
// • lets the caller's already-queued microtasks/promises drain first, and
|
|
873
|
+
// • runs run() OUTSIDE the caller's async/await scope, so the node-gtk
|
|
874
|
+
// #442/#121 nested-microtask-checkpoint caveat (see mainloop.test.mjs) does
|
|
875
|
+
// NOT bite: promise continuations queued before runAsync drain normally WHILE
|
|
876
|
+
// the app runs, because the bridge's microtask checkpoint is no longer
|
|
877
|
+
// deferred to an outer await frame.
|
|
878
|
+
//
|
|
879
|
+
// Mirrors GJS exactly in that the `argv` argument is ignored (GJS's runAsync
|
|
880
|
+
// takes no parameters and calls `this.run()` with an empty command-line) — so an
|
|
881
|
+
// `app.runAsync([programInvocationName, ...ARGV])` source behaves identically on
|
|
882
|
+
// gjs and node. Resolves once; run() is invoked once (no double-run / leak).
|
|
883
|
+
//
|
|
884
|
+
// Same shape on all three runtimes: the deferred blocking run() drives the app's
|
|
885
|
+
// GLib loop (activate → the app's own GLib sources → quit → run returns the exit
|
|
886
|
+
// status). The Node-only EXTRA is the uv↔GLib bridge co-pumping Node's own event
|
|
887
|
+
// loop DURING the blocking run, so a Node timer/promise scheduled before runAsync
|
|
888
|
+
// fires while the app runs. Bun/Deno have no such bridge, so their own event loop
|
|
889
|
+
// is paused for the app's lifetime (exactly as GJS, where the GLib loop IS the
|
|
890
|
+
// process loop) — for concurrent runtime-loop + GLib work there, drive GLib with
|
|
891
|
+
// startMainContextPump instead of a blocking run.
|
|
892
|
+
function applicationRunAsync(handle) {
|
|
893
|
+
return new Promise((resolve, reject) => {
|
|
894
|
+
setImmediate(() => {
|
|
895
|
+
try {
|
|
896
|
+
resolve(wrapReturn(native.callMethod(handle, 'run', unwrapArgs([[]]))));
|
|
897
|
+
} catch (error) {
|
|
898
|
+
reject(error);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ---- portable GLib main-context pump (Bun/Deno) ----
|
|
905
|
+
//
|
|
906
|
+
// On Node the uv-in-GLib bridge co-pumps libuv during a blocking GLib loop, so a
|
|
907
|
+
// GLib loop keeps Node's timers/promises/IO alive. Bun/Deno have no usable libuv,
|
|
908
|
+
// so we co-pump the OTHER way: a repeating runtime timer iterates the default GLib
|
|
909
|
+
// main context non-blockingly, letting GIO async callbacks / GLib timeouts / DBus
|
|
910
|
+
// fire while the runtime's own event loop stays in control — GJS's non-blocking
|
|
911
|
+
// main loop, reached from the runtime side. Reference-counted so nested pumps /
|
|
912
|
+
// concurrent runAsync calls share one timer.
|
|
913
|
+
let pumpTimer = null;
|
|
914
|
+
let pumpRefCount = 0;
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
|
|
918
|
+
* No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
|
|
919
|
+
* returns a disposer that decrements the count (clearing the timer at zero).
|
|
920
|
+
* @returns {() => void}
|
|
921
|
+
*/
|
|
922
|
+
export function startMainContextPump() {
|
|
923
|
+
if (native.isNodeRuntime) return () => {};
|
|
924
|
+
pumpRefCount++;
|
|
925
|
+
if (pumpTimer === null) {
|
|
926
|
+
// ~4 ms cadence: low latency without busy-spinning. Each tick drains every
|
|
927
|
+
// currently-ready source (iterateMainContext(false) returns false when none
|
|
928
|
+
// remain, bounding the inner loop), then yields to the runtime loop.
|
|
929
|
+
pumpTimer = setInterval(() => {
|
|
930
|
+
let guard = 0;
|
|
931
|
+
while (native.iterateMainContext(false) && guard++ < 100000) {
|
|
932
|
+
/* drain ready sources */
|
|
933
|
+
}
|
|
934
|
+
}, 4);
|
|
935
|
+
// Don't let the pump alone keep the process alive where the runtime allows it
|
|
936
|
+
// (Node/Bun expose Timeout.unref; Deno's numeric handle has none).
|
|
937
|
+
if (typeof pumpTimer.unref === 'function') pumpTimer.unref();
|
|
938
|
+
}
|
|
939
|
+
let disposed = false;
|
|
940
|
+
return () => {
|
|
941
|
+
if (disposed) return;
|
|
942
|
+
disposed = true;
|
|
943
|
+
stopMainContextPump();
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** Decrement the main-context pump reference count; clears the timer at zero. */
|
|
948
|
+
export function stopMainContextPump() {
|
|
949
|
+
if (native.isNodeRuntime || pumpRefCount === 0) return;
|
|
950
|
+
pumpRefCount--;
|
|
951
|
+
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
952
|
+
clearInterval(pumpTimer);
|
|
953
|
+
pumpTimer = null;
|
|
954
|
+
}
|
|
82
955
|
}
|
|
83
956
|
|
|
84
|
-
|
|
957
|
+
// bind_property_full / BindingGroup.bind_full: the transform functions are driven
|
|
958
|
+
// by the native GjsPrivate-mirror (src/private.cc) — the same architecture gjs
|
|
959
|
+
// uses (GjsPrivate.g_object_bind_property_full / g_binding_group_bind_full),
|
|
960
|
+
// because the transform's `to_value` is a write-back GValue no marshaled GClosure
|
|
961
|
+
// can reach. The JS contract matches gjs byte-for-byte (verified vs gjs 1.88):
|
|
962
|
+
// (binding, sourceValue) => [ok: boolean, targetValue]
|
|
963
|
+
// — sourceValue arrives unpacked, [false, …] leaves the target unchanged, a
|
|
964
|
+
// non-Array return is reported and treated as no-transform. This wrapper wraps
|
|
965
|
+
// the incoming binding/source into chainable L1 values and unwraps the returned
|
|
966
|
+
// target value back to a native handle (object/variant-valued properties).
|
|
967
|
+
function wrapBindingTransform(fn) {
|
|
968
|
+
if (typeof fn !== 'function') return null;
|
|
969
|
+
return (binding, value) => {
|
|
970
|
+
const result = fn(wrapReturn(binding), wrapReturn(value));
|
|
971
|
+
if (!Array.isArray(result)) return result; // native reports + treats as FALSE
|
|
972
|
+
return [result[0], unwrap(result[1])];
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// gjs's GObject.Object.prototype signal-convenience methods + bind_property_full
|
|
977
|
+
// (and BindingGroup.bind_full), resolved by JS-accessor name (snake_case or
|
|
978
|
+
// camelCase). Returns a `(handle, args) => result` applier, or undefined for a
|
|
979
|
+
// name we do not shim. block/unblock route through the introspected
|
|
980
|
+
// single-handler g_signal_handler_* functions (verified to marshal a node-gi
|
|
981
|
+
// GObject IN-arg); stop_emission_by_name through g_signal_stop_emission_by_name.
|
|
982
|
+
function objectPrototypeShim(prop) {
|
|
983
|
+
switch (prop) {
|
|
984
|
+
case 'block_signal_handler':
|
|
985
|
+
case 'blockSignalHandler':
|
|
986
|
+
return (handle, args) =>
|
|
987
|
+
wrapReturn(native.callFunction('GObject', 'signal_handler_block', [handle, args[0]]));
|
|
988
|
+
case 'unblock_signal_handler':
|
|
989
|
+
case 'unblockSignalHandler':
|
|
990
|
+
return (handle, args) =>
|
|
991
|
+
wrapReturn(native.callFunction('GObject', 'signal_handler_unblock', [handle, args[0]]));
|
|
992
|
+
case 'stop_emission_by_name':
|
|
993
|
+
case 'stopEmissionByName':
|
|
994
|
+
return (handle, args) =>
|
|
995
|
+
wrapReturn(native.callFunction('GObject', 'signal_stop_emission_by_name', [handle, args[0]]));
|
|
996
|
+
case 'bind_property_full':
|
|
997
|
+
case 'bindPropertyFull':
|
|
998
|
+
return (handle, args) =>
|
|
999
|
+
wrapReturn(
|
|
1000
|
+
native.bindPropertyFull(
|
|
1001
|
+
handle,
|
|
1002
|
+
args[0],
|
|
1003
|
+
unwrap(args[1]),
|
|
1004
|
+
args[2],
|
|
1005
|
+
args[3],
|
|
1006
|
+
wrapBindingTransform(args[4]),
|
|
1007
|
+
wrapBindingTransform(args[5]),
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
case 'bind_full':
|
|
1011
|
+
case 'bindFull':
|
|
1012
|
+
// GObject.BindingGroup.bind_full (gjs: GjsPrivate.g_binding_group_bind_full).
|
|
1013
|
+
// Gated per instance: on any OTHER class a genuine `bind_full` method keeps
|
|
1014
|
+
// resolving through the normal GI route. NOTE: without this shim the
|
|
1015
|
+
// introspected name would resolve to bind_with_closures (its GIR shadow),
|
|
1016
|
+
// whose write-back GValue contract a marshaled GClosure cannot satisfy.
|
|
1017
|
+
return (handle, args) => {
|
|
1018
|
+
if (!native.isInstanceOf(handle, 'GObject', 'BindingGroup')) {
|
|
1019
|
+
return wrapReturn(native.callMethod(handle, 'bind_full', unwrapArgs(args)));
|
|
1020
|
+
}
|
|
1021
|
+
return wrapReturn(
|
|
1022
|
+
native.bindingGroupBindFull(
|
|
1023
|
+
handle,
|
|
1024
|
+
args[0],
|
|
1025
|
+
unwrap(args[1]),
|
|
1026
|
+
args[2],
|
|
1027
|
+
args[3],
|
|
1028
|
+
wrapBindingTransform(args[4]),
|
|
1029
|
+
wrapBindingTransform(args[5]),
|
|
1030
|
+
),
|
|
1031
|
+
);
|
|
1032
|
+
};
|
|
1033
|
+
default:
|
|
1034
|
+
return undefined;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Per-GType method feature-detection cache backing the wrapper's GJS-parity
|
|
1039
|
+
// `get` trap: `native.hasMethod` walks the full introspection chain and method
|
|
1040
|
+
// GETs happen on every `obj.method(...)` call, so presence is memoized per
|
|
1041
|
+
// concrete GType name (stable for the process lifetime — typelibs don't gain
|
|
1042
|
+
// methods at runtime).
|
|
1043
|
+
const methodPresenceCache = new Map(); // typeName -> Map<methodName, boolean>
|
|
1044
|
+
|
|
1045
|
+
function instanceHasMethod(handle, name) {
|
|
1046
|
+
const typeName = native.getTypeName(handle);
|
|
1047
|
+
let perType = methodPresenceCache.get(typeName);
|
|
1048
|
+
if (perType === undefined) {
|
|
1049
|
+
perType = new Map();
|
|
1050
|
+
methodPresenceCache.set(typeName, perType);
|
|
1051
|
+
}
|
|
1052
|
+
let present = perType.get(name);
|
|
1053
|
+
if (present === undefined) {
|
|
1054
|
+
present = native.hasMethod(handle, name);
|
|
1055
|
+
perType.set(name, present);
|
|
1056
|
+
}
|
|
1057
|
+
return present;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Wrap a live GObject handle as a GJS-shaped instance. When `userProto` is given
|
|
1061
|
+
// (a registerClass subclass's prototype) the wrapper resolves the user class's
|
|
1062
|
+
// own prototype members FIRST — so `inst.myMethod()` runs the JS method with the
|
|
1063
|
+
// wrapper as `this` — then falls back to GObject property get/set and GI method
|
|
1064
|
+
// routing. `.connect()/.emit()/.disconnect()` work in both modes.
|
|
1065
|
+
function wrapInstance(handle, userProto) {
|
|
1066
|
+
const cached = instanceCache.get(handle);
|
|
1067
|
+
if (cached !== undefined) {
|
|
1068
|
+
// UPGRADE a cached wrapper's user prototype when this wrap supplies a MORE
|
|
1069
|
+
// DERIVED one: an undefined cache (a subclass instance first seen generically —
|
|
1070
|
+
// returned from store.get_item / a signal sender / resurrection), OR a cached
|
|
1071
|
+
// ANCESTOR prototype (a multi-level chain where a vfunc fired during constructType
|
|
1072
|
+
// wrapped the instance with the OVERRIDING ancestor's prototype, before the leaf
|
|
1073
|
+
// ctor's wrapInstance(handle, leaf.prototype) runs — the leaf proto must win so
|
|
1074
|
+
// the leaf's OWN methods resolve). Never DOWNGRADE: a more-derived cached proto is
|
|
1075
|
+
// kept. The userProto lives on the target (read via the proxy here), so the
|
|
1076
|
+
// upgrade is in place — identity (===) and any expando fields are preserved.
|
|
1077
|
+
if (userProto !== undefined && userProto !== cached[USER_PROTO]) {
|
|
1078
|
+
const current = cached[USER_PROTO];
|
|
1079
|
+
if (current === undefined || isProtoSameOrDescendantOf(userProto, current)) {
|
|
1080
|
+
cached[USER_PROTO] = userProto;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return cached;
|
|
1084
|
+
}
|
|
85
1085
|
const target = { [HANDLE]: handle };
|
|
86
|
-
|
|
1086
|
+
if (userProto !== undefined) target[USER_PROTO] = userProto;
|
|
1087
|
+
const proxy = new Proxy(target, {
|
|
87
1088
|
get(t, prop) {
|
|
88
1089
|
if (prop === HANDLE) return handle;
|
|
89
1090
|
if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
|
|
90
1091
|
switch (prop) {
|
|
1092
|
+
case '$typeName':
|
|
1093
|
+
// The instance's concrete RUNTIME GType name —
|
|
1094
|
+
// g_type_name(G_OBJECT_TYPE(obj)) via native.getTypeName. node-gi hands
|
|
1095
|
+
// back a GENERIC wrapper for a returned handle (it does not downcast to
|
|
1096
|
+
// the runtime GType), so `constructor.$gtype` is the STATIC declared
|
|
1097
|
+
// type; this getter reads the TRUE runtime type. It is the portable seam
|
|
1098
|
+
// @gjsify/devtools' widget-tree DumpTree uses (GJS instead reads the
|
|
1099
|
+
// concrete type off its already-downcast `constructor.$gtype.name`).
|
|
1100
|
+
// Distinct from the CLASS-level `$gtypeName`, which is the DECLARED
|
|
1101
|
+
// namespaced string (e.g. 'Gtk.Widget'); this is the raw runtime GType
|
|
1102
|
+
// name (e.g. 'GtkWidget', 'AdwBin', 'FireworksWindow').
|
|
1103
|
+
return native.getTypeName(handle);
|
|
91
1104
|
case 'connect':
|
|
92
|
-
|
|
1105
|
+
// Record the (user fn → id) mapping so signal_handlers_*_by_func can
|
|
1106
|
+
// resolve the private-closure handler back to its ids (see the registry).
|
|
1107
|
+
return (signal, cb) => {
|
|
1108
|
+
const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
|
|
1109
|
+
recordSignalHandler(handle, cb, id);
|
|
1110
|
+
return id;
|
|
1111
|
+
};
|
|
93
1112
|
case 'connect_after':
|
|
94
|
-
return (signal, cb) =>
|
|
1113
|
+
return (signal, cb) => {
|
|
1114
|
+
const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
|
|
1115
|
+
recordSignalHandler(handle, cb, id);
|
|
1116
|
+
return id;
|
|
1117
|
+
};
|
|
95
1118
|
case 'emit':
|
|
96
1119
|
return (signal, ...args) =>
|
|
97
1120
|
wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
|
|
98
1121
|
case 'disconnect':
|
|
99
|
-
return (id) =>
|
|
1122
|
+
return (id) => {
|
|
1123
|
+
forgetSignalHandlerId(handle, id);
|
|
1124
|
+
return native.disconnectSignal(handle, id);
|
|
1125
|
+
};
|
|
100
1126
|
default:
|
|
101
1127
|
break;
|
|
102
1128
|
}
|
|
1129
|
+
const up = t[USER_PROTO];
|
|
1130
|
+
if (up !== undefined) {
|
|
1131
|
+
const desc = findProtoDescriptor(up, prop);
|
|
1132
|
+
if (desc !== undefined) {
|
|
1133
|
+
if (typeof desc.value === 'function') return (...args) => desc.value.apply(proxy, args);
|
|
1134
|
+
if (typeof desc.get === 'function') return desc.get.call(proxy);
|
|
1135
|
+
return desc.value;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Gio.Application.runAsync — the GJS override (refs/gjs Gio.js + GLib.js):
|
|
1139
|
+
// a Promise-returning run() that does NOT block the Node microtask/event
|
|
1140
|
+
// loop (see applicationRunAsync). Gated to GApplication instances
|
|
1141
|
+
// (g_type_is_a, so Gtk/Adw.Application and registerClass subclasses qualify);
|
|
1142
|
+
// placed AFTER the userProto lookup so a subclass may still override it, and
|
|
1143
|
+
// before the GI-method fallback since there is no introspected `run_async`.
|
|
1144
|
+
if (prop === 'runAsync' && native.isInstanceOf(handle, 'Gio', 'Application')) {
|
|
1145
|
+
return () => applicationRunAsync(handle);
|
|
1146
|
+
}
|
|
1147
|
+
// gjs's GObject.Object.prototype signal conveniences (block_signal_handler /
|
|
1148
|
+
// unblock_signal_handler / stop_emission_by_name) + bind_property_full and
|
|
1149
|
+
// BindingGroup.bind_full (refs/gjs GObject.js). These are NOT introspected instance
|
|
1150
|
+
// methods — they are gjs shims over the namespace-level g_signal_* functions —
|
|
1151
|
+
// so route them explicitly before property / GI-method resolution.
|
|
1152
|
+
const shim = objectPrototypeShim(prop);
|
|
1153
|
+
if (shim !== undefined) return (...args) => shim(handle, args);
|
|
103
1154
|
const propName = toKebab(prop);
|
|
104
1155
|
if (native.hasProperty(handle, propName)) {
|
|
105
1156
|
return wrapReturn(native.getProperty(handle, propName));
|
|
106
1157
|
}
|
|
107
|
-
|
|
1158
|
+
// Surface a plain JS field previously written on THIS wrapper. With the
|
|
1159
|
+
// toggle-ref bridge the wrapper is now CANONICAL (one proxy per GObject,
|
|
1160
|
+
// cached by the canonical native handle), so a plain field IS shared across
|
|
1161
|
+
// the vfunc<->instance boundary and survives a round-trip + GC while C owns
|
|
1162
|
+
// the object — a vfunc's `this` resolves to the same cached proxy as
|
|
1163
|
+
// construct, and `store.get_item(x)` returns the same proxy a setter wrote
|
|
1164
|
+
// to. Own-property only (set via the `set` trap), so an introspected GI
|
|
1165
|
+
// method of the same name is never shadowed unless the user explicitly
|
|
1166
|
+
// assigned an expando. (GObject PROPERTIES remain the right choice for state
|
|
1167
|
+
// that must also be visible to C / other language bindings.)
|
|
1168
|
+
if (Object.prototype.hasOwnProperty.call(t, prop)) return t[prop];
|
|
1169
|
+
// LAST resort before treating an unknown name as a GI method: an INHERITED
|
|
1170
|
+
// member (Object.prototype.hasOwnProperty / isPrototypeOf / … — RESERVED
|
|
1171
|
+
// already covers toString/valueOf/etc.) must resolve to the real function,
|
|
1172
|
+
// not a GI callMethod thunk that would throw on `inst.hasOwnProperty('x')`.
|
|
1173
|
+
if (prop in t) return t[prop];
|
|
1174
|
+
// A Gio._promisify'd async method (registerClass subclasses pick it up via
|
|
1175
|
+
// their userProto above; introspected instances have no prototype chain, so
|
|
1176
|
+
// they resolve it here from the registry, per-class). Bound to this instance.
|
|
1177
|
+
const promisified = resolvePromisified(handle, camelToSnake(prop));
|
|
1178
|
+
if (promisified !== undefined) return (...args) => promisified.apply(proxy, args);
|
|
1179
|
+
// GJS parity: an UNKNOWN member is `undefined`, never a throw-on-call
|
|
1180
|
+
// thunk — real consumers feature-detect optional native methods
|
|
1181
|
+
// (`typeof gl.clearBufferfv === 'function'` gates `@gjsify/webgl`'s
|
|
1182
|
+
// clearBuffer emulation; Excalibur's RenderTarget.blitToScreen was the
|
|
1183
|
+
// exposing call on node-gi: the old unconditional thunk made the
|
|
1184
|
+
// detection lie, then threw mid-frame). Presence is resolved via the
|
|
1185
|
+
// SAME native walk callMethod uses (literal-first, snake alias second)
|
|
1186
|
+
// and memoized per concrete GType — method GETs happen on every call.
|
|
1187
|
+
// The LITERAL accessor name is passed through — the engine resolves it
|
|
1188
|
+
// verbatim first (GJS fidelity: GIR names are exposed as-is, incl. Vala
|
|
1189
|
+
// camelCase like Gwebgl's `getString`) and only falls back to the
|
|
1190
|
+
// snake_case alias. Converting here would destroy a literal camelCase
|
|
1191
|
+
// GI name.
|
|
1192
|
+
if (instanceHasMethod(handle, prop)) {
|
|
1193
|
+
return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
|
|
1194
|
+
}
|
|
1195
|
+
return undefined;
|
|
108
1196
|
},
|
|
109
1197
|
set(t, prop, value) {
|
|
110
1198
|
if (typeof prop === 'string') {
|
|
1199
|
+
const up = t[USER_PROTO];
|
|
1200
|
+
if (up !== undefined) {
|
|
1201
|
+
const desc = findProtoDescriptor(up, prop);
|
|
1202
|
+
if (desc !== undefined && typeof desc.set === 'function') {
|
|
1203
|
+
desc.set.call(proxy, value);
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
111
1207
|
const propName = toKebab(prop);
|
|
112
1208
|
if (native.hasProperty(handle, propName)) {
|
|
113
1209
|
native.setProperty(handle, propName, unwrapArg(value));
|
|
@@ -118,50 +1214,237 @@ function wrapInstance(handle) {
|
|
|
118
1214
|
return true;
|
|
119
1215
|
},
|
|
120
1216
|
has(t, prop) {
|
|
121
|
-
|
|
1217
|
+
if (prop === HANDLE || prop in t) return true;
|
|
1218
|
+
if (typeof prop === 'string') {
|
|
1219
|
+
// A subclass's own prototype member (method/getter) and any GObject
|
|
1220
|
+
// property of the instance both count as `in` the wrapper. (Introspected
|
|
1221
|
+
// GI methods are resolved dynamically and are intentionally not reported.)
|
|
1222
|
+
const up = t[USER_PROTO];
|
|
1223
|
+
if (up !== undefined && findProtoDescriptor(up, prop) !== undefined) return true;
|
|
1224
|
+
if (native.hasProperty(handle, toKebab(prop))) return true;
|
|
1225
|
+
}
|
|
1226
|
+
return false;
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
instanceCache.set(handle, proxy);
|
|
1230
|
+
return proxy;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Shared object installed at the BASE of every introspected class's prototype
|
|
1234
|
+
// chain so `super.vfunc_<name>(...)` inside a registerClass override resolves to a
|
|
1235
|
+
// chain-up thunk. A registered subclass extends an introspected base (e.g.
|
|
1236
|
+
// `class X extends GObject.Object`), so `super.vfunc_x` looks up `vfunc_x` on the
|
|
1237
|
+
// base class's prototype; with this Proxy beneath it, any `vfunc_*` access yields
|
|
1238
|
+
// a thunk that invokes the captured parent (C) vfunc via native.callParentVfunc,
|
|
1239
|
+
// with `this` (the canonical wrapper instance) as the GObject. Mirrors GJS, where
|
|
1240
|
+
// the introspected parent's `vfunc_x` is a thunk into the actual C vtable entry.
|
|
1241
|
+
// Non-`vfunc_` lookups fall through to Object.prototype (so the base prototype
|
|
1242
|
+
// keeps its normal object behaviour).
|
|
1243
|
+
const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
|
|
1244
|
+
get(target, prop, receiver) {
|
|
1245
|
+
if (typeof prop === 'string' && prop.startsWith('vfunc_')) {
|
|
1246
|
+
const vfuncName = prop.slice('vfunc_'.length);
|
|
1247
|
+
return function chainUpToParentVfunc(...args) {
|
|
1248
|
+
const handle = this !== null && this !== undefined ? this[HANDLE] : undefined;
|
|
1249
|
+
if (handle === undefined) {
|
|
1250
|
+
throw new TypeError(`super.${prop}(): chain-up requires a node-gi instance as \`this\``);
|
|
1251
|
+
}
|
|
1252
|
+
return wrapReturn(native.callParentVfunc(handle, vfuncName, unwrapArgs(args)));
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
return Reflect.get(target, prop, receiver);
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// Surface a lazy `$gtype` getter on an introspected class/struct ctor: the real
|
|
1260
|
+
// GObject GType (a node-gi GType handle), resolved on first read via the engine
|
|
1261
|
+
// and then cached as a value. GJS exposes `SomeClass.$gtype`; the storybook reads
|
|
1262
|
+
// `Adw.Clamp.$gtype` + passes `StoryWidget.$gtype` to GObject.type_ensure. Lazy
|
|
1263
|
+
// (defineProperty getter) so it costs nothing until accessed. `null` (unknown type)
|
|
1264
|
+
// is also cached. The makeClass/makeStruct Proxy forwards `'$gtype' in t` → the
|
|
1265
|
+
// target getter (it returns `t[prop]` when `prop in t`).
|
|
1266
|
+
function defineLazyGType(ctor, namespace, typeName) {
|
|
1267
|
+
Object.defineProperty(ctor, '$gtype', {
|
|
1268
|
+
configurable: true,
|
|
1269
|
+
enumerable: false,
|
|
1270
|
+
get() {
|
|
1271
|
+
const gt = native.getGType(namespace, typeName);
|
|
1272
|
+
Object.defineProperty(ctor, '$gtype', {
|
|
1273
|
+
value: gt,
|
|
1274
|
+
configurable: true,
|
|
1275
|
+
enumerable: false,
|
|
1276
|
+
writable: false,
|
|
1277
|
+
});
|
|
1278
|
+
return gt;
|
|
122
1279
|
},
|
|
123
1280
|
});
|
|
124
1281
|
}
|
|
125
1282
|
|
|
126
1283
|
function makeClass(namespace, typeName) {
|
|
127
1284
|
const ctor = function ctor(props) {
|
|
1285
|
+
// G3/G2 construction routing. `new.target` is the leaf class the `new` was
|
|
1286
|
+
// applied to and is preserved through every super() hop, so when a registerClass'd
|
|
1287
|
+
// subclass is being constructed (directly or up a super() chain — at ANY depth,
|
|
1288
|
+
// since the leaf's registered GType already IS the full multi-level type) we build
|
|
1289
|
+
// the REGISTERED GType and return its canonical toggle-ref wrapper (carrying the
|
|
1290
|
+
// leaf's prototype as USER_PROTO) — which then substitutes `this` in every derived
|
|
1291
|
+
// ctor body. `new.target === undefined` (a call without `new`) or `=== proxy` (a
|
|
1292
|
+
// direct `new Ns.Class(...)` on the introspected class itself) falls through to the
|
|
1293
|
+
// unchanged introspected construction.
|
|
1294
|
+
const nt = new.target;
|
|
1295
|
+
if (nt !== undefined && nt !== proxy) {
|
|
1296
|
+
const reg = registeredClasses.get(nt);
|
|
1297
|
+
if (reg !== undefined) {
|
|
1298
|
+
const handle = native.constructType(reg.typeHandle, props ? unwrapProps(props) : {});
|
|
1299
|
+
const instance = wrapInstance(handle, nt.prototype);
|
|
1300
|
+
assignTemplateChildren(instance, handle, reg);
|
|
1301
|
+
return instance;
|
|
1302
|
+
}
|
|
1303
|
+
// `nt` is a subclass of this introspected GObject class but was NEVER passed to
|
|
1304
|
+
// GObject.registerClass(): silently building the introspected BASE type would
|
|
1305
|
+
// produce the wrong GType (missing the subclass's props/signals/vfuncs). Throw
|
|
1306
|
+
// the GJS-shaped error instead of constructing a misleading instance — the L1
|
|
1307
|
+
// analogue of GJS's "are you using GObject.registerClass()?" construct guard
|
|
1308
|
+
// (refs/gjs/gi/object.cpp).
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`Object ${nt.name || '(anonymous)'} is not registered with GObject.registerClass()`,
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
128
1313
|
const handle = native.newObject(namespace, typeName, props ? unwrapProps(props) : {});
|
|
129
1314
|
return wrapInstance(handle);
|
|
130
1315
|
};
|
|
131
1316
|
Object.defineProperty(ctor, 'name', { value: typeName, configurable: true });
|
|
132
1317
|
ctor.$gtypeName = `${namespace}.${typeName}`;
|
|
1318
|
+
defineLazyGType(ctor, namespace, typeName);
|
|
1319
|
+
// Put the vfunc chain-up Proxy beneath this base class's prototype so a
|
|
1320
|
+
// registerClass subclass's `super.vfunc_<name>(...)` resolves (see above).
|
|
1321
|
+
Object.setPrototypeOf(ctor.prototype, vfuncChainProto);
|
|
1322
|
+
// Stamp the prototype with its class identity so Gio._promisify(Cls.prototype, …)
|
|
1323
|
+
// can record which class a registration belongs to (non-enumerable).
|
|
1324
|
+
Object.defineProperty(ctor.prototype, CLASS_INFO, {
|
|
1325
|
+
value: { namespace, typeName },
|
|
1326
|
+
enumerable: false,
|
|
1327
|
+
configurable: true,
|
|
1328
|
+
});
|
|
1329
|
+
// `inst instanceof Ns.Class` — GJS parity for the WHOLE GObject hierarchy, not
|
|
1330
|
+
// just the leaf class. An instance is a Proxy over a bare `{[HANDLE]}` (no live JS
|
|
1331
|
+
// prototype chain linking Adw.ApplicationWindow → Gtk.ApplicationWindow → …), so
|
|
1332
|
+
// the default instanceof (a prototype-chain walk) reported `false` for every base
|
|
1333
|
+
// class. Resolve it by the GObject type system instead: `native.isInstanceOf`
|
|
1334
|
+
// (g_type_is_a) recognises subclasses AND implemented interfaces, exactly like GJS.
|
|
1335
|
+
// Guarded by `isGObjectHandle` first — a bare/boxed/variant handle (or any non-node-gi
|
|
1336
|
+
// value) is NOT a GObject, so it must be `false`, never reach `isInstanceOf` (which
|
|
1337
|
+
// throws on a non-GObject handle). The makeClass Proxy `get` trap forwards this
|
|
1338
|
+
// symbol to the target (`typeof prop !== 'string'` → `t[prop]`), so `instanceof`
|
|
1339
|
+
// finds it on the proxy. Overlay-built classes (GLib.Variant / GObject.Value /
|
|
1340
|
+
// GObject.ParamSpec) bypass makeClass and keep their own dedicated hasInstance.
|
|
1341
|
+
Object.defineProperty(ctor, Symbol.hasInstance, {
|
|
1342
|
+
configurable: true,
|
|
1343
|
+
value(instance) {
|
|
1344
|
+
if (instance === null || typeof instance !== 'object') return false;
|
|
1345
|
+
const handle = instance[HANDLE];
|
|
1346
|
+
if (handle === undefined || !native.isGObjectHandle(handle)) return false;
|
|
1347
|
+
// The introspected class ITSELF → resolve by name: g_type_is_a recognises every
|
|
1348
|
+
// subclass AND implemented interface, matching GJS exactly.
|
|
1349
|
+
if (this === proxy) return native.isInstanceOf(handle, namespace, typeName);
|
|
1350
|
+
// A registerClass'd subclass INHERITS this method (it `extends` the proxy) but
|
|
1351
|
+
// must match ITS OWN registered GType, not the shared introspected base — else a
|
|
1352
|
+
// sibling subclass or a bare-base instance would wrongly test true. Resolve the
|
|
1353
|
+
// instance's runtime GType against the subclass's own `$gtype` through the GObject
|
|
1354
|
+
// type system (a live JS prototype chain the subclass instance lacks — the L1
|
|
1355
|
+
// wrapper carries the user prototype as a symbol, not as [[Prototype]]).
|
|
1356
|
+
const subGType = this.$gtype;
|
|
1357
|
+
if (subGType !== undefined && subGType !== null) {
|
|
1358
|
+
const G = requireGi('GObject', '2.0');
|
|
1359
|
+
return G.type_is_a(G.type_from_name(native.getTypeName(handle)), subGType);
|
|
1360
|
+
}
|
|
1361
|
+
// A raw (unregistered) subclass / unrelated ctor: ordinary prototype-chain.
|
|
1362
|
+
return Function.prototype[Symbol.hasInstance].call(this, instance);
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
133
1365
|
// Expose constructor/static methods lazily: Ns.Class.new(...) /
|
|
134
1366
|
// Ns.Class.new_for_path(...) (and the camelCase aliases). `new Ns.Class({...})`
|
|
135
1367
|
// still goes through the target's [[Construct]] (the default Proxy behaviour).
|
|
136
|
-
|
|
137
|
-
get(t, prop) {
|
|
1368
|
+
const proxy = new Proxy(ctor, {
|
|
1369
|
+
get(t, prop, receiver) {
|
|
1370
|
+
// #667: a raw (UNregistered) subclass that `extends` this introspected class
|
|
1371
|
+
// must NOT inherit-read the parent's GType. A registered subclass has its OWN
|
|
1372
|
+
// `$gtype` (set in place by registerClass, so it shadows + never reaches this
|
|
1373
|
+
// trap); a raw subclass is not a GType, so an inherited read (receiver is the
|
|
1374
|
+
// subclass, not this proxy) resolves to undefined. A direct read on the
|
|
1375
|
+
// introspected class itself (receiver === proxy) resolves the real lazy GType.
|
|
1376
|
+
if (prop === '$gtype' && receiver !== proxy) return undefined;
|
|
138
1377
|
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
1378
|
+
// GObject.Object.new(gtype, props) / .new_with_properties(gtype, names,
|
|
1379
|
+
// values): gjs's GObject.js statics that construct a GObject from a runtime
|
|
1380
|
+
// GType. Not introspected (g_object_new is variadic), so route them to the
|
|
1381
|
+
// by-GType constructor (native.constructType accepts any GType handle).
|
|
1382
|
+
if (namespace === 'GObject' && typeName === 'Object' &&
|
|
1383
|
+
(prop === 'new' || prop === 'new_with_properties' || prop === 'newWithProperties')) {
|
|
1384
|
+
return objectStaticConstructor(prop);
|
|
1385
|
+
}
|
|
139
1386
|
const giName = camelToSnake(prop);
|
|
140
1387
|
return (...args) =>
|
|
141
1388
|
wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
|
|
142
1389
|
},
|
|
143
1390
|
});
|
|
1391
|
+
return proxy;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// The GObject.Object.new / new_with_properties statics (gjs GObject.js). `new`
|
|
1395
|
+
// resolves the GType (a class ctor's $gtype or a GType handle) and constructs it via
|
|
1396
|
+
// constructType (which accepts an arbitrary GType, returning the canonical wrapper —
|
|
1397
|
+
// the same routing `new Ns.Class({...})` uses); `new_with_properties` zips the two
|
|
1398
|
+
// arrays into a prop dict and delegates. Mirrors GJS's `GObject.Object.new`, which
|
|
1399
|
+
// looks up the constructor for `gtype` and `new`s it with the prop dict.
|
|
1400
|
+
function objectStaticConstructor(prop) {
|
|
1401
|
+
const objectNew = (gtype, props = {}) => {
|
|
1402
|
+
const gt = resolveGTypeArg(gtype);
|
|
1403
|
+
if (gt === undefined || gt === null) {
|
|
1404
|
+
throw new TypeError('GObject.Object.new: a GType (class or Class.$gtype) is required');
|
|
1405
|
+
}
|
|
1406
|
+
return wrapInstance(native.constructType(gt, props ? unwrapProps(props) : {}));
|
|
1407
|
+
};
|
|
1408
|
+
if (prop === 'new') return objectNew;
|
|
1409
|
+
return (gtype, names, values) => {
|
|
1410
|
+
if (!Array.isArray(names) || !Array.isArray(values) || names.length !== values.length) {
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
'GObject.Object.new_with_properties takes two equal-length arrays (names, values)',
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
const props = {};
|
|
1416
|
+
for (let i = 0; i < names.length; i++) props[names[i]] = values[i];
|
|
1417
|
+
return objectNew(gtype, props);
|
|
1418
|
+
};
|
|
144
1419
|
}
|
|
145
1420
|
|
|
146
1421
|
// Surface a boxed/struct type (e.g. GLib.MainLoop) as a class-like object whose
|
|
147
1422
|
// static/constructor methods route through the engine: `GLib.MainLoop.new(...)`
|
|
148
|
-
// and the camelCase alias
|
|
149
|
-
// constructor
|
|
1423
|
+
// and the camelCase alias. `new GLib.MainLoop(...)` routes through the native
|
|
1424
|
+
// constructStruct — the `new` constructor when the struct has one, else (GJS
|
|
1425
|
+
// gi/boxed.cpp parity) a ZERO-INITIALIZED instance for zero args
|
|
1426
|
+
// (`new Graphene.Rect()`, `new Gdk.RGBA()` — the devtools screenshot chain).
|
|
1427
|
+
// Returned boxed instances are wrapped by {@link wrapBoxed}.
|
|
150
1428
|
function makeStruct(namespace, typeName) {
|
|
151
1429
|
const base = function () {};
|
|
152
1430
|
Object.defineProperty(base, 'name', { value: typeName, configurable: true });
|
|
153
1431
|
base.$gtypeName = `${namespace}.${typeName}`;
|
|
154
|
-
|
|
155
|
-
|
|
1432
|
+
defineLazyGType(base, namespace, typeName);
|
|
1433
|
+
const proxy = new Proxy(base, {
|
|
1434
|
+
get(t, prop, receiver) {
|
|
1435
|
+
// #667: guard inherited `$gtype` reads (see makeClass) — a raw subclass of a
|
|
1436
|
+
// boxed/struct type does not inherit the parent's GType.
|
|
1437
|
+
if (prop === '$gtype' && receiver !== proxy) return undefined;
|
|
156
1438
|
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
157
1439
|
const giName = camelToSnake(prop);
|
|
158
1440
|
return (...args) =>
|
|
159
1441
|
wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
|
|
160
1442
|
},
|
|
161
1443
|
construct(_t, args) {
|
|
162
|
-
return wrapReturn(native.
|
|
1444
|
+
return wrapReturn(native.constructStruct(namespace, typeName, unwrapArgs(args)));
|
|
163
1445
|
},
|
|
164
1446
|
});
|
|
1447
|
+
return proxy;
|
|
165
1448
|
}
|
|
166
1449
|
|
|
167
1450
|
// Build a frozen enum/flags object keyed GJS-style: member names UPPER_CASED
|
|
@@ -172,9 +1455,690 @@ function makeEnum(namespace, typeName) {
|
|
|
172
1455
|
for (const key of Object.keys(raw)) {
|
|
173
1456
|
out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
|
|
174
1457
|
}
|
|
1458
|
+
// If this enum is registered as a GError domain (Gio.IOErrorEnum, …), attach
|
|
1459
|
+
// its domain descriptor (non-enumerable) so GLib.Error.matches(enum, code) can
|
|
1460
|
+
// resolve the enum to its domain. A plain enum gets nothing (getErrorDomain → null).
|
|
1461
|
+
const domain = native.getErrorDomain(namespace, typeName);
|
|
1462
|
+
if (domain !== null) {
|
|
1463
|
+
Object.defineProperty(out, ERROR_DOMAIN, { value: domain, enumerable: false });
|
|
1464
|
+
}
|
|
175
1465
|
return Object.freeze(out);
|
|
176
1466
|
}
|
|
177
1467
|
|
|
1468
|
+
// ---- Gio._promisify (L1, GJS-shaped) ----
|
|
1469
|
+
//
|
|
1470
|
+
// The Node twin of GJS's Gio._promisify (refs/gjs/modules/core/overrides/Gio.js).
|
|
1471
|
+
// Wraps an async method so calling it WITHOUT a trailing GAsyncReadyCallback
|
|
1472
|
+
// returns a Promise: it invokes the underlying async method passing a callback
|
|
1473
|
+
// that runs `finishName` and resolves with its return (the OUT-param tuple), or
|
|
1474
|
+
// rejects with the GLib.Error the finish call throws. Called WITH a trailing
|
|
1475
|
+
// callback it behaves as the plain async method (no Promise).
|
|
1476
|
+
//
|
|
1477
|
+
// Builds on: OUT/INOUT params (finish returns its results via out-params), the
|
|
1478
|
+
// libuv↔GLib mainloop bridge (the async completion fires on the loop) and GI
|
|
1479
|
+
// callbacks (the GAsyncReadyCallback). The async op holds a ref on the source
|
|
1480
|
+
// object and the GAsyncReadyCallback wrapper holds a strong ref to the JS callback
|
|
1481
|
+
// (which captures the instance proxy), so the wrapper + its GObject stay alive
|
|
1482
|
+
// until completion — instance identity, toggle-ref #656.
|
|
1483
|
+
|
|
1484
|
+
// _async / _begin → _finish; otherwise <name>_finish (GJS's convention).
|
|
1485
|
+
function deriveFinishName(asyncName) {
|
|
1486
|
+
if (asyncName.endsWith('_begin') || asyncName.endsWith('_async')) {
|
|
1487
|
+
return `${asyncName.slice(0, -5)}finish`;
|
|
1488
|
+
}
|
|
1489
|
+
return `${asyncName}_finish`;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Build the promisified wrapper for `asyncName`. Uses `this` (the instance proxy)
|
|
1493
|
+
// so it works both bound from the registry and as a prototype method.
|
|
1494
|
+
function makePromisified(asyncName, finishName) {
|
|
1495
|
+
const giAsync = camelToSnake(asyncName);
|
|
1496
|
+
return function promisified(...args) {
|
|
1497
|
+
const handle = this[HANDLE];
|
|
1498
|
+
// A trailing callback → plain async method (the caller drives the callback).
|
|
1499
|
+
// Wrap it so its (source, result) args are marshalled to wrapped instances —
|
|
1500
|
+
// matching the Promise path and GJS, so `source.finish(result)` works.
|
|
1501
|
+
if (args.length > 0 && typeof args[args.length - 1] === 'function') {
|
|
1502
|
+
const userCb = args[args.length - 1];
|
|
1503
|
+
const forwarded = [...args.slice(0, -1), wrapSignalCallback(userCb)];
|
|
1504
|
+
return wrapReturn(native.callMethod(handle, giAsync, unwrapArgs(forwarded)));
|
|
1505
|
+
}
|
|
1506
|
+
const self = this;
|
|
1507
|
+
return new Promise((resolve, reject) => {
|
|
1508
|
+
// wrapSignalCallback marshals each native callback arg (source, res) through
|
|
1509
|
+
// wrapReturn → wrapped instances, so both `source[finishName](res)` and the
|
|
1510
|
+
// captured-instance fallback round-trip cleanly.
|
|
1511
|
+
const onReady = wrapSignalCallback((source, res) => {
|
|
1512
|
+
try {
|
|
1513
|
+
const finisher =
|
|
1514
|
+
source !== null && source !== undefined && typeof source[finishName] === 'function'
|
|
1515
|
+
? source[finishName].bind(source)
|
|
1516
|
+
: self[finishName].bind(self);
|
|
1517
|
+
const value = finisher(res);
|
|
1518
|
+
// GJS's _promisify drops a leading `true` ok-flag so the dominant
|
|
1519
|
+
// finish shape (load_contents_finish, query_info_finish,
|
|
1520
|
+
// communicate_utf8_finish, … → [true, …]) resolves to just the
|
|
1521
|
+
// payload — keeping one source byte-identical on gjs + node.
|
|
1522
|
+
if (Array.isArray(value) && value.length > 1 && value[0] === true) {
|
|
1523
|
+
value.shift();
|
|
1524
|
+
}
|
|
1525
|
+
resolve(value);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
reject(error);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
native.callMethod(handle, giAsync, unwrapArgs([...args, onReady]));
|
|
1531
|
+
});
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Gio._promisify(prototype, asyncName, finishName?) — replace an async method so a
|
|
1537
|
+
* call without a trailing callback returns a Promise. `finishName` defaults per
|
|
1538
|
+
* GJS's convention (`_async`/`_begin` → `_finish`, else `<name>_finish`). Works on
|
|
1539
|
+
* any introspected class prototype (e.g. Gio.File.prototype) or a registerClass
|
|
1540
|
+
* subclass prototype.
|
|
1541
|
+
* @param {object} proto e.g. Gio.File.prototype
|
|
1542
|
+
* @param {string} asyncName e.g. "load_contents_async"
|
|
1543
|
+
* @param {string} [finishName] e.g. "load_contents_finish"
|
|
1544
|
+
* @returns {void}
|
|
1545
|
+
*/
|
|
1546
|
+
function promisify(proto, asyncName, finishName = undefined) {
|
|
1547
|
+
if (proto === null || typeof proto !== 'object') {
|
|
1548
|
+
throw new TypeError('Gio._promisify: prototype must be an object');
|
|
1549
|
+
}
|
|
1550
|
+
if (typeof asyncName !== 'string') {
|
|
1551
|
+
throw new TypeError('Gio._promisify: asyncName must be a string');
|
|
1552
|
+
}
|
|
1553
|
+
const finish = finishName === undefined ? deriveFinishName(asyncName) : finishName;
|
|
1554
|
+
const wrapper = makePromisified(asyncName, finish);
|
|
1555
|
+
// Install on the prototype (so proto[asyncName] IS the promisified method, and a
|
|
1556
|
+
// registerClass subclass — which resolves its userProto — picks it up) AND in
|
|
1557
|
+
// the global registry (introspected instances resolve methods dynamically, with
|
|
1558
|
+
// no live prototype chain to walk).
|
|
1559
|
+
try {
|
|
1560
|
+
proto[asyncName] = wrapper;
|
|
1561
|
+
} catch {
|
|
1562
|
+
// A frozen/sealed prototype — the registry still makes it work.
|
|
1563
|
+
}
|
|
1564
|
+
// Record the registration keyed by the GI method name, tagged with the class it
|
|
1565
|
+
// was registered on (CLASS_INFO from makeClass) so two classes promisifying the
|
|
1566
|
+
// same method name with different finish methods stay disambiguated per-class.
|
|
1567
|
+
const info = proto[CLASS_INFO];
|
|
1568
|
+
const registration = {
|
|
1569
|
+
namespace: info ? info.namespace : undefined,
|
|
1570
|
+
typeName: info ? info.typeName : undefined,
|
|
1571
|
+
wrapper,
|
|
1572
|
+
};
|
|
1573
|
+
const key = camelToSnake(asyncName);
|
|
1574
|
+
const existing = promisifiedMethods.get(key);
|
|
1575
|
+
if (existing === undefined) {
|
|
1576
|
+
promisifiedMethods.set(key, [registration]);
|
|
1577
|
+
} else {
|
|
1578
|
+
// Replace a same-class registration in place; otherwise append (a second class).
|
|
1579
|
+
const i = existing.findIndex(
|
|
1580
|
+
(r) => r.namespace === registration.namespace && r.typeName === registration.typeName,
|
|
1581
|
+
);
|
|
1582
|
+
if (i >= 0) existing[i] = registration;
|
|
1583
|
+
else existing.push(registration);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Resolve the promisified wrapper for `methodName` on an instance handle. The
|
|
1588
|
+
// common case is a single registration (fast path); when several classes
|
|
1589
|
+
// promisified the same method name, pick the one whose class the instance is-a.
|
|
1590
|
+
function resolvePromisified(handle, methodName) {
|
|
1591
|
+
const registrations = promisifiedMethods.get(methodName);
|
|
1592
|
+
if (registrations === undefined) return undefined;
|
|
1593
|
+
if (registrations.length === 1) return registrations[0].wrapper;
|
|
1594
|
+
for (const r of registrations) {
|
|
1595
|
+
if (r.namespace !== undefined && native.isInstanceOf(handle, r.namespace, r.typeName)) {
|
|
1596
|
+
return r.wrapper;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
// No class matched (or a registerClass registration with no CLASS_INFO) — fall
|
|
1600
|
+
// back to the last registration so a single-class misconfig still resolves.
|
|
1601
|
+
return registrations[registrations.length - 1].wrapper;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ---- GObject.registerClass decorator (L1, GJS-shaped) ----
|
|
1605
|
+
//
|
|
1606
|
+
// The Node twin of GJS's `GObject.registerClass(meta, class)`. It accepts both
|
|
1607
|
+
// `registerClass(class)` and `registerClass({ GTypeName?, Properties?, Signals?,
|
|
1608
|
+
// ... }, class)`, registers a GObject subtype via the native engine, registers the
|
|
1609
|
+
// GIVEN class IN PLACE, and returns that SAME class (GJS-faithful — see the G3
|
|
1610
|
+
// registry above). So both `const X = registerClass(…,C)` and the GJS
|
|
1611
|
+
// `static { registerClass(…,C) }` idiom (return discarded) leave the same symbol
|
|
1612
|
+
// bound to the registered GType. `new`-ed instances are usable as GObjects
|
|
1613
|
+
// (property get/set, `.connect/.emit/.disconnect`) AND expose the user class's own
|
|
1614
|
+
// prototype methods (resolved before the GObject routing — see wrapInstance).
|
|
1615
|
+
//
|
|
1616
|
+
// G3 contract (the user JS constructor body now RUNS):
|
|
1617
|
+
// - The construction routing lives in the introspected base ctor (makeClass),
|
|
1618
|
+
// keyed on `new.target`. `new X(args)` runs X's ctor body against the canonical
|
|
1619
|
+
// toggle-ref wrapper, with working `super(args)` semantics (the base ctor
|
|
1620
|
+
// returns the wrapper → it substitutes `this` up the whole chain). This REVERSES
|
|
1621
|
+
// the previous "ctor body is not run" caveat. `vfunc_constructed` still fires
|
|
1622
|
+
// once during construction; a class that inits in BOTH a ctor body and
|
|
1623
|
+
// `vfunc_constructed` will run both (normal GJS behaviour) — do not duplicate.
|
|
1624
|
+
// - Toggle-ref identity (#656): the `this` in the ctor body, the post-construct
|
|
1625
|
+
// wrapper, a vfunc's `this` and a signal-handler's `this` are all the SAME
|
|
1626
|
+
// canonical wrapper (===), and a plain JS field set on it survives a round-trip
|
|
1627
|
+
// + GC while C owns the object. (A GObject PROPERTY is still the right choice for
|
|
1628
|
+
// state that must also be visible to C / other bindings.)
|
|
1629
|
+
// - Instances are Proxies over a native handle, not real `instanceof` instances.
|
|
1630
|
+
// - Ordering caveat: a `vfunc_constructed` that fires DURING constructType sees a
|
|
1631
|
+
// wrapper whose USER_PROTO is attached by the vfunc trampoline (collectVfuncs),
|
|
1632
|
+
// so its own user-proto methods resolve; the post-construct `wrapInstance(handle,
|
|
1633
|
+
// nt.prototype)` then upgrades the same cached wrapper in place (identity kept).
|
|
1634
|
+
// - Multi-level registered subclassing (registering a subclass of a registerClass'd
|
|
1635
|
+
// type, at any depth) IS supported (G2): findParentGType resolves a registered
|
|
1636
|
+
// parent to its runtime GType handle and registerClass subclasses from it via
|
|
1637
|
+
// native.registerClassFromGType. Inherited custom properties, signals and vfuncs
|
|
1638
|
+
// of registered ANCESTORS compose for free through normal GObject inheritance
|
|
1639
|
+
// (g_object_class_find_property / g_signal_lookup / the owner-type-keyed property
|
|
1640
|
+
// store all walk the ancestry). A registered parent must be registered BEFORE the
|
|
1641
|
+
// child's static{} runs — ES module evaluation order guarantees this.
|
|
1642
|
+
|
|
1643
|
+
// GObject.ParamFlags — the GParamFlags bits the native property builder consumes.
|
|
1644
|
+
const ParamFlags = Object.freeze({
|
|
1645
|
+
READABLE: 1,
|
|
1646
|
+
WRITABLE: 2,
|
|
1647
|
+
READWRITE: 3,
|
|
1648
|
+
CONSTRUCT: 4,
|
|
1649
|
+
CONSTRUCT_ONLY: 8,
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
// GObject.SignalFlags — the GSignalFlags bits (native defaults to RUN_LAST).
|
|
1653
|
+
const SignalFlags = Object.freeze({
|
|
1654
|
+
RUN_FIRST: 1,
|
|
1655
|
+
RUN_LAST: 2,
|
|
1656
|
+
RUN_CLEANUP: 4,
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// A ParamSpec factory returns a plain descriptor the decorator maps to the
|
|
1660
|
+
// native PropertySpec. `string`/`boolean` capture (name, nick, blurb, flags,
|
|
1661
|
+
// default); the numeric kinds capture (name, nick, blurb, flags, min, max,
|
|
1662
|
+
// default) — matching GJS's GObject.ParamSpec.* argument order.
|
|
1663
|
+
function paramSpecPlain(type) {
|
|
1664
|
+
return (name, nick, blurb, flags, defaultValue) => ({
|
|
1665
|
+
$paramSpec: true,
|
|
1666
|
+
type,
|
|
1667
|
+
name,
|
|
1668
|
+
nick,
|
|
1669
|
+
blurb,
|
|
1670
|
+
flags,
|
|
1671
|
+
default: defaultValue,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function paramSpecRanged(type) {
|
|
1676
|
+
return (name, nick, blurb, flags, minimum, maximum, defaultValue) => ({
|
|
1677
|
+
$paramSpec: true,
|
|
1678
|
+
type,
|
|
1679
|
+
name,
|
|
1680
|
+
nick,
|
|
1681
|
+
blurb,
|
|
1682
|
+
flags,
|
|
1683
|
+
minimum,
|
|
1684
|
+
maximum,
|
|
1685
|
+
default: defaultValue,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Resolve a GObject.ParamSpec.object/.boxed `gtype` argument to a native GType
|
|
1690
|
+
// handle the engine's BuildParamSpec can consume. The arg is GJS-shaped: a class
|
|
1691
|
+
// ctor (introspected `Adw.Bin` / `GObject.Object`, or a registered class) whose
|
|
1692
|
+
// real `$gtype` is the GType handle (G4), or an already-resolved GType handle.
|
|
1693
|
+
function resolveGTypeArg(gtype) {
|
|
1694
|
+
if (gtype === null || gtype === undefined) return gtype;
|
|
1695
|
+
if (typeof gtype === 'function' || typeof gtype === 'object') {
|
|
1696
|
+
const gt = gtype.$gtype;
|
|
1697
|
+
if (gt !== undefined) return gt;
|
|
1698
|
+
}
|
|
1699
|
+
return gtype; // assume it is already a GType handle
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// object/boxed ParamSpec factories: `(name, nick, blurb, flags, gtype)` — matching
|
|
1703
|
+
// GJS's GObject.ParamSpec.object/.boxed argument order. `gtype` is a class ctor
|
|
1704
|
+
// (its `$gtype` is read) or a GType handle. Produces a real g_param_spec_object /
|
|
1705
|
+
// g_param_spec_boxed via the engine's BuildParamSpec.
|
|
1706
|
+
function paramSpecGTyped(type) {
|
|
1707
|
+
return (name, nick, blurb, flags, gtype) => ({
|
|
1708
|
+
$paramSpec: true,
|
|
1709
|
+
type,
|
|
1710
|
+
name,
|
|
1711
|
+
nick,
|
|
1712
|
+
blurb,
|
|
1713
|
+
flags,
|
|
1714
|
+
gtype: resolveGTypeArg(gtype),
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const ParamSpec = Object.freeze({
|
|
1719
|
+
string: paramSpecPlain('string'),
|
|
1720
|
+
boolean: paramSpecPlain('boolean'),
|
|
1721
|
+
int: paramSpecRanged('int'),
|
|
1722
|
+
uint: paramSpecRanged('uint'),
|
|
1723
|
+
int64: paramSpecRanged('int64'),
|
|
1724
|
+
uint64: paramSpecRanged('uint64'),
|
|
1725
|
+
double: paramSpecRanged('double'),
|
|
1726
|
+
float: paramSpecRanged('float'),
|
|
1727
|
+
object: paramSpecGTyped('object'),
|
|
1728
|
+
boxed: paramSpecGTyped('boxed'),
|
|
1729
|
+
// `wrappedPspec instanceof GObject.ParamSpec` — a wrapped GParamSpec (from a
|
|
1730
|
+
// notify handler / a GParamSpec-typed value) carries [HANDLE], recognised via
|
|
1731
|
+
// the native tag. `instanceof` honours a `[Symbol.hasInstance]` on the RHS even
|
|
1732
|
+
// though ParamSpec is a factory object rather than a constructor.
|
|
1733
|
+
[Symbol.hasInstance](instance) {
|
|
1734
|
+
if (instance === null || typeof instance !== 'object') return false;
|
|
1735
|
+
const handle = instance[HANDLE];
|
|
1736
|
+
return handle !== undefined && native.isParamSpecHandle(handle);
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// Walk up the JS class's prototype chain to the nearest node-gi base and describe
|
|
1741
|
+
// the parent the native engine subclasses from. Two shapes (nearest ancestor wins):
|
|
1742
|
+
// - a REGISTERED ancestor (registerClass'd, present in registeredClasses) →
|
|
1743
|
+
// `{ parentGTypeHandle }` (its runtime GType handle, from #667). The registered
|
|
1744
|
+
// type has no introspection entry, so it must be subclassed from its GType
|
|
1745
|
+
// directly (native.registerClassFromGType) — this is what unlocks multi-level
|
|
1746
|
+
// registered-of-registered subclassing (G2).
|
|
1747
|
+
// - an INTROSPECTED ancestor (a makeClass wrapper carrying a dotted `$gtypeName`
|
|
1748
|
+
// like "Adw.Bin") → `{ parentNamespace, parentType }` (resolved by name).
|
|
1749
|
+
// A registered class carries BOTH a dotless `$gtypeName` AND a registeredClasses
|
|
1750
|
+
// entry, so the registry check MUST come first.
|
|
1751
|
+
function findParentGType(klass) {
|
|
1752
|
+
for (let c = Object.getPrototypeOf(klass); typeof c === 'function'; c = Object.getPrototypeOf(c)) {
|
|
1753
|
+
const reg = registeredClasses.get(c);
|
|
1754
|
+
if (reg !== undefined) {
|
|
1755
|
+
return { parentGTypeHandle: reg.typeHandle };
|
|
1756
|
+
}
|
|
1757
|
+
const gt = c.$gtypeName;
|
|
1758
|
+
if (typeof gt === 'string') {
|
|
1759
|
+
const dot = gt.indexOf('.');
|
|
1760
|
+
if (dot > 0) {
|
|
1761
|
+
return { parentNamespace: gt.slice(0, dot), parentType: gt.slice(dot + 1) };
|
|
1762
|
+
}
|
|
1763
|
+
// A dotless `$gtypeName` with no registeredClasses entry should never occur
|
|
1764
|
+
// (only registered types are dotless, and they are in the registry); be
|
|
1765
|
+
// explicit rather than mis-resolving the namespace split.
|
|
1766
|
+
throw new TypeError(
|
|
1767
|
+
"GObject.registerClass: parent '" + gt + "' has no resolvable GType",
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Map a GObject.ParamSpec.* descriptor (from meta.Properties) to a native
|
|
1775
|
+
// PropertySpec. The property name defaults to the Properties key.
|
|
1776
|
+
function paramSpecToNative(desc, key) {
|
|
1777
|
+
if (desc === null || typeof desc !== 'object') {
|
|
1778
|
+
throw new TypeError(
|
|
1779
|
+
"GObject.registerClass: Properties['" + key + "'] must be a GObject.ParamSpec descriptor",
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
const spec = { name: typeof desc.name === 'string' && desc.name.length > 0 ? desc.name : key };
|
|
1783
|
+
spec.type = desc.type;
|
|
1784
|
+
if (typeof desc.flags === 'number') spec.flags = desc.flags;
|
|
1785
|
+
if (desc.default !== undefined) spec.default = desc.default;
|
|
1786
|
+
if (typeof desc.minimum === 'number') spec.minimum = desc.minimum;
|
|
1787
|
+
if (typeof desc.maximum === 'number') spec.maximum = desc.maximum;
|
|
1788
|
+
// object/boxed ParamSpecs carry a value GType handle (passed straight to the
|
|
1789
|
+
// engine's BuildParamSpec → g_param_spec_object / g_param_spec_boxed).
|
|
1790
|
+
if (desc.gtype !== undefined) spec.gtype = desc.gtype;
|
|
1791
|
+
return spec;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Normalise a signal param/return type to the native type-name vocabulary: a
|
|
1795
|
+
// string passes through; a ParamSpec descriptor contributes its `.type`.
|
|
1796
|
+
function normalizeSignalType(t) {
|
|
1797
|
+
if (typeof t === 'string') return t;
|
|
1798
|
+
if (t !== null && typeof t === 'object' && typeof t.type === 'string') return t.type;
|
|
1799
|
+
return undefined;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Map a meta.Signals entry (`{ param_types?, return_type?, flags? }`, GJS keys,
|
|
1803
|
+
// camelCase also accepted) to a native SignalSpec; the signal name is the key.
|
|
1804
|
+
function signalSpecToNative(spec, key) {
|
|
1805
|
+
const s = spec !== null && typeof spec === 'object' ? spec : {};
|
|
1806
|
+
const out = { name: key };
|
|
1807
|
+
const params = s.param_types !== undefined ? s.param_types : s.paramTypes;
|
|
1808
|
+
if (Array.isArray(params)) {
|
|
1809
|
+
out.paramTypes = params.map(normalizeSignalType).filter((t) => t !== undefined);
|
|
1810
|
+
}
|
|
1811
|
+
const ret = normalizeSignalType(s.return_type !== undefined ? s.return_type : s.returnType);
|
|
1812
|
+
if (ret !== undefined) out.returnType = ret;
|
|
1813
|
+
if (typeof s.flags === 'number') out.flags = s.flags;
|
|
1814
|
+
return out;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Collect this class's OWN newly-declared `vfunc_<name>` methods into the native
|
|
1818
|
+
// vfuncs map (key = name without the `vfunc_` prefix). The native engine invokes
|
|
1819
|
+
// each override with `this` = the raw instance handle; re-wrap it so the user's
|
|
1820
|
+
// vfunc sees a usable class instance (own methods + property routing).
|
|
1821
|
+
// `super.vfunc_<name>(...)` inside the override resolves to a chain-up thunk on the
|
|
1822
|
+
// introspected base class's prototype (vfuncChainProto → native.callParentVfunc):
|
|
1823
|
+
// applying the user fn with a custom `this` keeps `super` bound to its lexical home
|
|
1824
|
+
// object (klass.prototype), so chain-up works through the .apply boundary.
|
|
1825
|
+
//
|
|
1826
|
+
// Multi-level boundary (G2): the walk STOPS at the first REGISTERED ancestor's
|
|
1827
|
+
// prototype. A registered ancestor already installed its own vfunc_* trampolines
|
|
1828
|
+
// into ITS GType's vtable, which this class inherits via the g_type_register_static
|
|
1829
|
+
// class-struct memcpy. Re-collecting them would install a DUPLICATE trampoline on
|
|
1830
|
+
// the leaf's GType whose captured parent IS the ancestor's own trampoline, so the
|
|
1831
|
+
// ancestor's `super.vfunc_*()` (running as the leaf's override) would re-enter
|
|
1832
|
+
// itself forever (stack overflow). Prototypes of UNREGISTERED mixin classes between
|
|
1833
|
+
// the leaf and the nearest registered ancestor ARE still collected — they have no
|
|
1834
|
+
// GType of their own, so their vfuncs only reach the vtable through the leaf.
|
|
1835
|
+
function collectVfuncs(klass) {
|
|
1836
|
+
const vfuncs = {};
|
|
1837
|
+
const seen = new Set();
|
|
1838
|
+
for (let p = klass.prototype; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
1839
|
+
const owner = Object.getOwnPropertyDescriptor(p, 'constructor')?.value;
|
|
1840
|
+
if (owner !== undefined && owner !== klass && registeredClasses.has(owner)) break;
|
|
1841
|
+
for (const key of Object.getOwnPropertyNames(p)) {
|
|
1842
|
+
if (!key.startsWith('vfunc_') || seen.has(key)) continue;
|
|
1843
|
+
const desc = Object.getOwnPropertyDescriptor(p, key);
|
|
1844
|
+
if (desc === undefined || typeof desc.value !== 'function') continue;
|
|
1845
|
+
seen.add(key);
|
|
1846
|
+
const userFn = desc.value;
|
|
1847
|
+
vfuncs[key.slice('vfunc_'.length)] = function (...args) {
|
|
1848
|
+
return userFn.apply(wrapInstance(this, klass.prototype), args);
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return vfuncs;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* GObject.registerClass — register a GObject subclass declared as a JS class.
|
|
1857
|
+
* Supports `registerClass(class)` and `registerClass(meta, class)` where meta is
|
|
1858
|
+
* `{ GTypeName?, Properties?, Signals?, ... }` (GJS allows both). Registers the
|
|
1859
|
+
* GType for the GIVEN class IN PLACE and returns that SAME class (GJS-faithful), so
|
|
1860
|
+
* `static { registerClass(…, X) }` (return discarded) leaves `X` itself bound to
|
|
1861
|
+
* the registered GType. `new X(props)` constructs the registered GType (routed via
|
|
1862
|
+
* the introspected base ctor on `new.target`), runs the user ctor body against the
|
|
1863
|
+
* canonical wrapper, and exposes the user class's own prototype methods + the
|
|
1864
|
+
* GObject property/signal surface. See the block comment above for the G3 contract.
|
|
1865
|
+
* @param {Function|Record<string, unknown>} metaOrClass
|
|
1866
|
+
* @param {Function} [maybeClass]
|
|
1867
|
+
* @returns {Function} the same `klass` (now registered)
|
|
1868
|
+
*/
|
|
1869
|
+
function registerClass(metaOrClass, maybeClass) {
|
|
1870
|
+
let meta;
|
|
1871
|
+
let klass;
|
|
1872
|
+
if (typeof metaOrClass === 'function') {
|
|
1873
|
+
klass = metaOrClass;
|
|
1874
|
+
meta = {};
|
|
1875
|
+
} else {
|
|
1876
|
+
meta = metaOrClass !== null && typeof metaOrClass === 'object' ? metaOrClass : {};
|
|
1877
|
+
klass = maybeClass;
|
|
1878
|
+
}
|
|
1879
|
+
if (typeof klass !== 'function') {
|
|
1880
|
+
throw new TypeError('GObject.registerClass: expected a class to register');
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const parent = findParentGType(klass);
|
|
1884
|
+
if (parent === undefined) {
|
|
1885
|
+
throw new TypeError(
|
|
1886
|
+
'GObject.registerClass: ' +
|
|
1887
|
+
(klass.name || 'the class') +
|
|
1888
|
+
' must extend a node-gi GObject class (e.g. GObject.Object)',
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const properties = [];
|
|
1893
|
+
if (meta.Properties !== null && typeof meta.Properties === 'object') {
|
|
1894
|
+
for (const key of Object.keys(meta.Properties)) {
|
|
1895
|
+
properties.push(paramSpecToNative(meta.Properties[key], key));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const signals = [];
|
|
1900
|
+
if (meta.Signals !== null && typeof meta.Signals === 'object') {
|
|
1901
|
+
for (const key of Object.keys(meta.Signals)) {
|
|
1902
|
+
signals.push(signalSpecToNative(meta.Signals[key], key));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const vfuncs = collectVfuncs(klass);
|
|
1907
|
+
|
|
1908
|
+
const gtypeName =
|
|
1909
|
+
typeof meta.GTypeName === 'string' && meta.GTypeName.length > 0 ? meta.GTypeName : klass.name;
|
|
1910
|
+
if (!gtypeName) {
|
|
1911
|
+
throw new TypeError('GObject.registerClass: a GTypeName is required for an anonymous class');
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Gtk.Widget composite template (GJS-shaped meta: Template / Children /
|
|
1915
|
+
// InternalChildren / CssName). Template is a Uint8Array/Buffer of inline UI-XML,
|
|
1916
|
+
// a "resource:///…" path string, or an inline UI-XML string — passed through to
|
|
1917
|
+
// the engine, which installs it in class_init (set_template[_from_resource] +
|
|
1918
|
+
// bind_template_child_full) and runs gtk_widget_init_template at construction.
|
|
1919
|
+
const options = { properties, signals, vfuncs };
|
|
1920
|
+
if (meta.Template !== undefined && meta.Template !== null) options.template = meta.Template;
|
|
1921
|
+
if (typeof meta.CssName === 'string' && meta.CssName.length > 0) options.cssName = meta.CssName;
|
|
1922
|
+
const children = Array.isArray(meta.Children) ? meta.Children.filter((c) => typeof c === 'string') : [];
|
|
1923
|
+
const internalChildren = Array.isArray(meta.InternalChildren)
|
|
1924
|
+
? meta.InternalChildren.filter((c) => typeof c === 'string')
|
|
1925
|
+
: [];
|
|
1926
|
+
if (children.length > 0) options.children = children;
|
|
1927
|
+
if (internalChildren.length > 0) options.internalChildren = internalChildren;
|
|
1928
|
+
|
|
1929
|
+
// Branch on the parent shape (findParentGType): a REGISTERED parent is subclassed
|
|
1930
|
+
// from its runtime GType handle (it has no introspection entry); an INTROSPECTED
|
|
1931
|
+
// parent is resolved by namespace+type. Construction (the leaf's registered GType
|
|
1932
|
+
// via the makeClass new.target routing) is already multi-level-correct either way.
|
|
1933
|
+
const typeHandle =
|
|
1934
|
+
parent.parentGTypeHandle !== undefined
|
|
1935
|
+
? native.registerClassFromGType(gtypeName, parent.parentGTypeHandle, options)
|
|
1936
|
+
: native.registerClass(gtypeName, parent.parentNamespace, parent.parentType, options);
|
|
1937
|
+
|
|
1938
|
+
// G3: register the GType for the GIVEN class IN PLACE (no throwaway Subclass) and
|
|
1939
|
+
// return that same class. `$gtypeName`/`$gtype` are defined as OWN properties:
|
|
1940
|
+
// `klass` inherits a getter-only `$gtype` accessor from the introspected base ctor
|
|
1941
|
+
// (via the makeClass Proxy), so a plain `klass.$gtype = …` assignment would throw
|
|
1942
|
+
// in strict mode — defineProperty installs an own data property that shadows it.
|
|
1943
|
+
Object.defineProperty(klass, '$gtypeName', {
|
|
1944
|
+
value: gtypeName,
|
|
1945
|
+
configurable: true,
|
|
1946
|
+
writable: true,
|
|
1947
|
+
});
|
|
1948
|
+
// native.registerClass returns the registered GType as a tag-distinct node-gi
|
|
1949
|
+
// GType handle (G4) — surface it as `$gtype` (GObject.type_ensure(X.$gtype),
|
|
1950
|
+
// ParamSpec.object(..., X) all read it). It is the SAME handle constructType
|
|
1951
|
+
// consumes, so registration and `$gtype` share one carrier.
|
|
1952
|
+
Object.defineProperty(klass, '$gtype', {
|
|
1953
|
+
value: typeHandle,
|
|
1954
|
+
configurable: true,
|
|
1955
|
+
enumerable: false,
|
|
1956
|
+
writable: false,
|
|
1957
|
+
});
|
|
1958
|
+
// Record the registration so the introspected base ctor (makeClass) routes
|
|
1959
|
+
// `new X(args)` (where new.target === klass) to constructType(typeHandle, …) +
|
|
1960
|
+
// the canonical wrapper. Template children move here from the old Subclass.
|
|
1961
|
+
registeredClasses.set(klass, {
|
|
1962
|
+
typeHandle,
|
|
1963
|
+
children: children.length > 0 ? children : undefined,
|
|
1964
|
+
internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
|
|
1965
|
+
});
|
|
1966
|
+
return klass;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Merge convenience flag names UNDER an introspected enum: the introspected
|
|
1970
|
+
// members WIN (so every real bit — incl. LAX_VALIDATION / EXPLICIT_NOTIFY /
|
|
1971
|
+
// DETAILED / NO_RECURSE — keeps its true value), and the convenience names only
|
|
1972
|
+
// fill a gap if a platform's typelib happens not to introspect a combined value
|
|
1973
|
+
// (e.g. READWRITE). Returns the convenience table verbatim if introspection
|
|
1974
|
+
// yielded no usable enum.
|
|
1975
|
+
function mergeFlags(introspected, convenience) {
|
|
1976
|
+
if (introspected === null || typeof introspected !== 'object') return convenience;
|
|
1977
|
+
return Object.freeze({ ...convenience, ...introspected });
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// GObject.AccumulatorType — gjs's fake enum for signal accumulators (GObject.js,
|
|
1981
|
+
// "keep in sync with gi/object.c"). Not introspected; a pure convenience table.
|
|
1982
|
+
const AccumulatorType = Object.freeze({ NONE: 0, FIRST_WINS: 1, TRUE_HANDLED: 2 });
|
|
1983
|
+
|
|
1984
|
+
// GObject.signal_handlers_{block,unblock,disconnect}_by_func(instance, fn) — gjs's
|
|
1985
|
+
// GObject.js by-function handler ops. node-gi connects via PRIVATE closures, so the
|
|
1986
|
+
// function is resolved to its recorded handler ids (signalHandlerIds), then each is
|
|
1987
|
+
// blocked/unblocked via the introspected single-handler g_signal_handler_{block,
|
|
1988
|
+
// unblock} (verified to marshal a node-gi GObject IN-arg) or disconnected via
|
|
1989
|
+
// native.disconnectSignal. Returns the count of handlers matched, exactly as gjs's
|
|
1990
|
+
// g_signal_handlers_*_matched-based helpers do.
|
|
1991
|
+
function signalHandlersBlockByFunc(instance, fn) {
|
|
1992
|
+
const handle = unwrapArg(instance);
|
|
1993
|
+
const ids = handlerIdsForFunc(handle, fn);
|
|
1994
|
+
for (const id of ids) native.callFunction('GObject', 'signal_handler_block', [handle, id]);
|
|
1995
|
+
return ids.length;
|
|
1996
|
+
}
|
|
1997
|
+
function signalHandlersUnblockByFunc(instance, fn) {
|
|
1998
|
+
const handle = unwrapArg(instance);
|
|
1999
|
+
const ids = handlerIdsForFunc(handle, fn);
|
|
2000
|
+
for (const id of ids) native.callFunction('GObject', 'signal_handler_unblock', [handle, id]);
|
|
2001
|
+
return ids.length;
|
|
2002
|
+
}
|
|
2003
|
+
function signalHandlersDisconnectByFunc(instance, fn) {
|
|
2004
|
+
const handle = unwrapArg(instance);
|
|
2005
|
+
const ids = handlerIdsForFunc(handle, fn);
|
|
2006
|
+
for (const id of ids) {
|
|
2007
|
+
native.disconnectSignal(handle, id);
|
|
2008
|
+
forgetSignalHandlerId(handle, id);
|
|
2009
|
+
}
|
|
2010
|
+
return ids.length;
|
|
2011
|
+
}
|
|
2012
|
+
// Matches gjs's non-introspectable guard (GObject.js) — there is no func→data map.
|
|
2013
|
+
function signalHandlersDisconnectByData() {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
'GObject.signal_handlers_disconnect_by_data() is not introspectable. Use ' +
|
|
2016
|
+
'GObject.signal_handlers_disconnect_by_func() instead.',
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// GObject.signal_connect / signal_connect_after / signal_emit_by_name — gjs's
|
|
2021
|
+
// GObject.js shims that call the instance's OWN connect/connect_after/emit (a
|
|
2022
|
+
// workaround for classes that shadow those names with their own methods). The
|
|
2023
|
+
// instance is the L1 wrapper, whose .connect records the id in the by-func registry.
|
|
2024
|
+
function signalConnect(object, name, handler) {
|
|
2025
|
+
return object.connect(name, handler);
|
|
2026
|
+
}
|
|
2027
|
+
function signalConnectAfter(object, name, handler) {
|
|
2028
|
+
return object.connect_after(name, handler);
|
|
2029
|
+
}
|
|
2030
|
+
function signalEmitByName(object, ...nameAndArgs) {
|
|
2031
|
+
return object.emit(...nameAndArgs);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Overlay the GJS `GObject` runtime statics on top of the introspected GObject
|
|
2035
|
+
// namespace proxy — ADDITIVELY. `registerClass` + `ParamSpec` + the signal-by-func
|
|
2036
|
+
// helpers + `Value` + `AccumulatorType` are genuinely new (or shadow a broken
|
|
2037
|
+
// introspected member — `Value`, whose struct has no `new()`). `ParamFlags` /
|
|
2038
|
+
// `SignalFlags` are FULL introspected bitfields: returning a hardcoded 5-member
|
|
2039
|
+
// table would make every other member read `undefined` (→ coerces to 0 in `FLAGS |
|
|
2040
|
+
// GObject.ParamFlags.X`, silently dropping the bit), so they resolve to the
|
|
2041
|
+
// introspected enum (convenience names merged underneath as a fallback).
|
|
2042
|
+
// `GObject.Object` etc. keep resolving from introspection (with .new added in
|
|
2043
|
+
// makeClass). Merged enums are cached so identity is stable.
|
|
2044
|
+
const OVERLAY_NAMES = new Set([
|
|
2045
|
+
'registerClass',
|
|
2046
|
+
'ParamSpec',
|
|
2047
|
+
'ParamFlags',
|
|
2048
|
+
'SignalFlags',
|
|
2049
|
+
'Value',
|
|
2050
|
+
'AccumulatorType',
|
|
2051
|
+
'signal_handlers_block_by_func',
|
|
2052
|
+
'signal_handlers_unblock_by_func',
|
|
2053
|
+
'signal_handlers_disconnect_by_func',
|
|
2054
|
+
'signal_handlers_disconnect_by_data',
|
|
2055
|
+
'signal_connect',
|
|
2056
|
+
'signal_connect_after',
|
|
2057
|
+
'signal_emit_by_name',
|
|
2058
|
+
]);
|
|
2059
|
+
|
|
2060
|
+
// The fundamental GObject.TYPE_* constants. GJS defines these in its GObject
|
|
2061
|
+
// override by looking each name up in libgobject AT RUNTIME (not hardcoding the
|
|
2062
|
+
// fundamental ints): refs/gjs/modules/core/overrides/GObject.js `_init` —
|
|
2063
|
+
// `GObject.TYPE_STRING = GObject.type_from_name('gchararray')`, and the
|
|
2064
|
+
// `_makeDummyClass(obj, name, upperName, gtypeName, …)` helper for the numeric/
|
|
2065
|
+
// char/gtype family. We resolve them the SAME way through the introspected
|
|
2066
|
+
// `GObject.type_from_name`, so each constant is the real, process-correct GType —
|
|
2067
|
+
// an OPAQUE GType handle exactly like GJS (`typeof GObject.TYPE_INT === 'object'`,
|
|
2068
|
+
// NOT the number 24; `GObject.type_from_name('gint') === GObject.TYPE_INT`), never
|
|
2069
|
+
// a hardcoded fundamental value. Map: constant name → the registered GType name.
|
|
2070
|
+
// (TYPE_JSOBJECT is intentionally omitted — 'JSObject' is a GJS-internal boxed type
|
|
2071
|
+
// that libgobject does not register on a plain node-gi host, so it has no correct
|
|
2072
|
+
// value here.)
|
|
2073
|
+
const GTYPE_CONSTANT_NAMES = {
|
|
2074
|
+
TYPE_NONE: 'void',
|
|
2075
|
+
TYPE_INTERFACE: 'GInterface',
|
|
2076
|
+
TYPE_CHAR: 'gchar',
|
|
2077
|
+
TYPE_UCHAR: 'guchar',
|
|
2078
|
+
TYPE_BOOLEAN: 'gboolean',
|
|
2079
|
+
TYPE_INT: 'gint',
|
|
2080
|
+
TYPE_UINT: 'guint',
|
|
2081
|
+
TYPE_LONG: 'glong',
|
|
2082
|
+
TYPE_ULONG: 'gulong',
|
|
2083
|
+
TYPE_INT64: 'gint64',
|
|
2084
|
+
TYPE_UINT64: 'guint64',
|
|
2085
|
+
TYPE_ENUM: 'GEnum',
|
|
2086
|
+
TYPE_FLAGS: 'GFlags',
|
|
2087
|
+
TYPE_FLOAT: 'gfloat',
|
|
2088
|
+
TYPE_DOUBLE: 'gdouble',
|
|
2089
|
+
TYPE_STRING: 'gchararray',
|
|
2090
|
+
TYPE_POINTER: 'gpointer',
|
|
2091
|
+
TYPE_BOXED: 'GBoxed',
|
|
2092
|
+
TYPE_PARAM: 'GParam',
|
|
2093
|
+
TYPE_OBJECT: 'GObject',
|
|
2094
|
+
TYPE_VARIANT: 'GVariant',
|
|
2095
|
+
TYPE_GTYPE: 'GType',
|
|
2096
|
+
TYPE_UNICHAR: 'gint',
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
function isGObjectOverlayName(prop) {
|
|
2100
|
+
return (
|
|
2101
|
+
typeof prop === 'string' &&
|
|
2102
|
+
(OVERLAY_NAMES.has(prop) || Object.prototype.hasOwnProperty.call(GTYPE_CONSTANT_NAMES, prop))
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
function decorateGObjectNamespace(baseNs) {
|
|
2107
|
+
const cache = new Map();
|
|
2108
|
+
const resolve = (prop) => {
|
|
2109
|
+
if (cache.has(prop)) return cache.get(prop);
|
|
2110
|
+
let value;
|
|
2111
|
+
if (prop === 'registerClass') value = registerClass;
|
|
2112
|
+
else if (prop === 'ParamSpec') value = ParamSpec;
|
|
2113
|
+
else if (prop === 'ParamFlags') value = mergeFlags(baseNs.ParamFlags, ParamFlags);
|
|
2114
|
+
else if (prop === 'SignalFlags') value = mergeFlags(baseNs.SignalFlags, SignalFlags);
|
|
2115
|
+
else if (prop === 'Value') value = valueClass;
|
|
2116
|
+
else if (prop === 'AccumulatorType') value = AccumulatorType;
|
|
2117
|
+
else if (prop === 'signal_handlers_block_by_func') value = signalHandlersBlockByFunc;
|
|
2118
|
+
else if (prop === 'signal_handlers_unblock_by_func') value = signalHandlersUnblockByFunc;
|
|
2119
|
+
else if (prop === 'signal_handlers_disconnect_by_func') value = signalHandlersDisconnectByFunc;
|
|
2120
|
+
else if (prop === 'signal_handlers_disconnect_by_data') value = signalHandlersDisconnectByData;
|
|
2121
|
+
else if (prop === 'signal_connect') value = signalConnect;
|
|
2122
|
+
else if (prop === 'signal_connect_after') value = signalConnectAfter;
|
|
2123
|
+
else if (prop === 'signal_emit_by_name') value = signalEmitByName;
|
|
2124
|
+
else if (Object.prototype.hasOwnProperty.call(GTYPE_CONSTANT_NAMES, prop)) {
|
|
2125
|
+
// Resolve the real GType from libgobject via the introspected type_from_name
|
|
2126
|
+
// (0 / unregistered → null, matching object.cc's GType marshalling).
|
|
2127
|
+
value = baseNs.type_from_name(GTYPE_CONSTANT_NAMES[prop]);
|
|
2128
|
+
}
|
|
2129
|
+
cache.set(prop, value);
|
|
2130
|
+
return value;
|
|
2131
|
+
};
|
|
2132
|
+
return new Proxy(baseNs, {
|
|
2133
|
+
get(t, prop) {
|
|
2134
|
+
return isGObjectOverlayName(prop) ? resolve(prop) : t[prop];
|
|
2135
|
+
},
|
|
2136
|
+
has(t, prop) {
|
|
2137
|
+
return isGObjectOverlayName(prop) || prop in t;
|
|
2138
|
+
},
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
|
|
178
2142
|
function createNamespace(namespace) {
|
|
179
2143
|
const cache = new Map();
|
|
180
2144
|
return new Proxy(Object.create(null), {
|
|
@@ -228,16 +2192,42 @@ let loopAttached = false;
|
|
|
228
2192
|
*/
|
|
229
2193
|
export function requireGi(namespace, version) {
|
|
230
2194
|
native.requireNamespace(namespace, version);
|
|
231
|
-
// Attach the libuv
|
|
232
|
-
// (GLib.MainLoop.run / Gio.Application.run
|
|
2195
|
+
// Attach the libuv↔GLib integration once. On Node this arms BOTH directions:
|
|
2196
|
+
// the uv-in-GLib bridge (a blocking GLib.MainLoop.run / Gio.Application.run
|
|
2197
|
+
// keeps Node's event loop alive) AND the uv-driven auto-pump (pending GLib
|
|
2198
|
+
// sources — Gio async completions, GLib timeouts/idles, DBus — dispatch from
|
|
2199
|
+
// Node's own loop, so async gi:// code works with NO explicit mainloop). This
|
|
2200
|
+
// is Node-only: Bun/Deno have no usable libuv (Deno exports no libuv symbols;
|
|
2201
|
+
// Bun panics on uv_backend_fd). There, GLib async is co-pumped from the runtime
|
|
2202
|
+
// loop via the portable GLib-iteration pump instead (see startMainContextPump /
|
|
2203
|
+
// runAsync).
|
|
233
2204
|
if (!loopAttached) {
|
|
234
|
-
native.
|
|
2205
|
+
if (native.isNodeRuntime) {
|
|
2206
|
+
native.startMainLoop();
|
|
2207
|
+
// Bootstrap kick for the auto-pump: with ONLY unref'd handles active,
|
|
2208
|
+
// uv_run exits without running the pump's prepare/check phase at all, so
|
|
2209
|
+
// an otherwise-empty loop would never arm GLib's timer/fd wake-ups (a
|
|
2210
|
+
// pending top-level await on a GLib timeout would exit-13 unsettled).
|
|
2211
|
+
// 'beforeExit' fires exactly when the loop runs empty: drain what is
|
|
2212
|
+
// ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
|
|
2213
|
+
// uv timer revives the loop; otherwise the process exits normally.
|
|
2214
|
+
process.on('beforeExit', () => native.pumpKick());
|
|
2215
|
+
}
|
|
235
2216
|
loopAttached = true;
|
|
236
2217
|
}
|
|
237
2218
|
const key = version ? `${namespace}@${version}` : namespace;
|
|
238
2219
|
let ns = namespaceCache.get(key);
|
|
239
2220
|
if (ns === undefined) {
|
|
240
2221
|
ns = createNamespace(namespace);
|
|
2222
|
+
// The GObject namespace also carries the GJS runtime statics (registerClass +
|
|
2223
|
+
// ParamSpec/ParamFlags/SignalFlags) layered over its introspected members.
|
|
2224
|
+
if (namespace === 'GObject') ns = decorateGObjectNamespace(ns);
|
|
2225
|
+
// The GLib namespace carries the GJS-shaped GLib.Variant ergonomics
|
|
2226
|
+
// (new GLib.Variant(sig, value) + deepUnpack/unpack/recursiveUnpack) and the
|
|
2227
|
+
// GLib.Error class.
|
|
2228
|
+
else if (namespace === 'GLib') ns = decorateGLibNamespace(ns);
|
|
2229
|
+
// The Gio namespace carries Gio._promisify (async → Promise).
|
|
2230
|
+
else if (namespace === 'Gio') ns = decorateGioNamespace(ns);
|
|
241
2231
|
namespaceCache.set(key, ns);
|
|
242
2232
|
}
|
|
243
2233
|
return ns;
|
|
@@ -250,7 +2240,12 @@ export function requireGi(namespace, version) {
|
|
|
250
2240
|
* @returns {unknown}
|
|
251
2241
|
*/
|
|
252
2242
|
export function unwrap(value) {
|
|
253
|
-
|
|
2243
|
+
// NOT unwrapArg: the public unwrap only extracts handles — it must never
|
|
2244
|
+
// replace a function with the engine-facing callback shim.
|
|
2245
|
+
if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
|
|
2246
|
+
return value[HANDLE];
|
|
2247
|
+
}
|
|
2248
|
+
return value;
|
|
254
2249
|
}
|
|
255
2250
|
|
|
256
2251
|
export default requireGi;
|