@gjsify/node-gi 0.13.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/LICENSE +26 -0
- package/README.md +220 -0
- package/binding.gyp +34 -0
- package/gi.d.ts +28 -0
- package/gi.js +256 -0
- package/globals.d.ts +66 -0
- package/globals.js +140 -0
- package/index.d.ts +268 -0
- package/index.js +261 -0
- package/package.json +63 -0
- package/src/addon.cc +2110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gjsify contributors
|
|
4
|
+
|
|
5
|
+
Portions of this package are derived from node-gtk
|
|
6
|
+
(https://github.com/romgrk/node-gtk):
|
|
7
|
+
Copyright (c) romgrk and node-gtk contributors.
|
|
8
|
+
node-gtk is licensed under the MIT License.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @gjsify/node-gi
|
|
2
|
+
|
|
3
|
+
**GObject-Introspection runtime for Node.js** — the native engine that lets
|
|
4
|
+
unchanged GJS / GObject-Introspection code run under Node.js, the inverse of
|
|
5
|
+
gjsify's Node/Web/DOM → GJS direction (see the gjsify `AGENTS.md`
|
|
6
|
+
`### Axis 5 active track`).
|
|
7
|
+
|
|
8
|
+
It loads `gi://` namespaces (GLib, GObject, Gio, …) via `libgirepository` and
|
|
9
|
+
exposes them with GJS-compatible semantics, so the same source builds and runs
|
|
10
|
+
on both GJS and Node via `gjsify build --app {gjs,node}`.
|
|
11
|
+
|
|
12
|
+
> **Status: milestone 1 (headless core) — in progress.** The native engine over
|
|
13
|
+
> the modern `girepository-2.0` API now does: resolve the default repository,
|
|
14
|
+
> `require` a namespace, enumerate its infos; call namespace-level functions and
|
|
15
|
+
> **instance methods** (own + implemented-interface methods, up the parent chain)
|
|
16
|
+
> with value marshalling (numbers, booleans, strings, GObjects, enums/flags);
|
|
17
|
+
> construct GObjects and read/write properties (GValue round-trip); connect /
|
|
18
|
+
> emit / disconnect signals (incl. detailed names like `notify::prop`); and
|
|
19
|
+
> **register GObject subclasses** (subtype + construct-by-type, inheriting the
|
|
20
|
+
> parent's properties/methods, plus **custom properties + signals** declared on
|
|
21
|
+
> the subclass). Ownership rides
|
|
22
|
+
> N-API finalizers (no V8-GC reentrancy). On top of the engine, an **L1
|
|
23
|
+
> GJS-compatibility layer** (`@gjsify/node-gi/gi`, `requireGi`) surfaces a
|
|
24
|
+
> GJS-shaped namespace: `new Gio.SimpleAction({ name })`, `action.name` property
|
|
25
|
+
> access, `action.get_name()` methods, `.connect()/.emit()/.disconnect()`, and
|
|
26
|
+
> enums / flags / constants (`Gio.BusType.SESSION`, `GLib.PRIORITY_DEFAULT`);
|
|
27
|
+
> constructor/static methods (`Gio.File.new_for_path(...)`); and both snake_case
|
|
28
|
+
> and camelCase accessors. A **libuv↔GLib mainloop bridge** (`startMainLoop`,
|
|
29
|
+
> auto-attached by `requireGi`) nests Node's libuv loop inside the GLib loop, so a
|
|
30
|
+
> blocking `GLib.MainLoop.run()` keeps Node's timers/I/O alive — including the
|
|
31
|
+
> **boxed/struct slice** that needs (`GLib.MainLoop.new(...)` → a boxed handle →
|
|
32
|
+
> `.run()`/`.quit()`). **JS functions marshal as GI callbacks** via an ffi
|
|
33
|
+
> closure (`GLib.timeout_add`/`idle_add` fire from the loop, the boolean return
|
|
34
|
+
> drives source continuation; the hidden user_data/destroy slots are auto-filled).
|
|
35
|
+
> register GObject subclasses (subtype + construct-by-type, inheriting the
|
|
36
|
+
> parent's properties/methods, plus **custom properties + signals**, plus
|
|
37
|
+
> **vfunc overrides** — a JS function overriding a parent GObject vfunc, hooked
|
|
38
|
+
> into the new type's class vtable). The gjsify `--app node` bundler integration
|
|
39
|
+
> already rewrites `gi://` onto the L1 layer. vfunc **chain-up** to the parent
|
|
40
|
+
> implementation (with the toggle-ref GC bridge) and general struct field access
|
|
41
|
+
> land in subsequent drops — for now a vfunc override fully replaces the inherited
|
|
42
|
+
> implementation.
|
|
43
|
+
|
|
44
|
+
## Provenance
|
|
45
|
+
|
|
46
|
+
Derived from [node-gtk](https://github.com/romgrk/node-gtk) (romgrk and
|
|
47
|
+
contributors, MIT) — vendored and rewritten under MIT (see `LICENSE`). The
|
|
48
|
+
native binding is **retargeted to `girepository-2.0`** (the GIRepository merged
|
|
49
|
+
into GLib ≥ 2.80); the standalone `libgirepository-1.0` node-gtk linked is no
|
|
50
|
+
longer shipped on modern systems. GJS's own `gi/repo.cpp` is the reference for
|
|
51
|
+
the `girepository-2.0` API surface. node-gtk's own examples and tests are **not**
|
|
52
|
+
vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Node.js ≥ 20
|
|
57
|
+
- A C++ toolchain (`g++`/`clang`, `make`), `node-gyp`
|
|
58
|
+
- GLib ≥ 2.80 development headers exposing `girepository-2.0`
|
|
59
|
+
(Fedora: `glib2-devel gobject-introspection-devel gcc-c++`;
|
|
60
|
+
Debian/Ubuntu: `libglib2.0-dev libgirepository-2.0-dev g++`)
|
|
61
|
+
- At runtime, the target libraries' typelibs must be installed (same as `gi://`
|
|
62
|
+
under GJS).
|
|
63
|
+
|
|
64
|
+
## Build & test
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install # builds the native addon via node-gyp (install script)
|
|
68
|
+
npm test # node --test (smoke tests)
|
|
69
|
+
# or rebuild explicitly:
|
|
70
|
+
npm run rebuild
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage (milestone 1)
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import {
|
|
77
|
+
requireNamespace, listInfoNames, callFunction,
|
|
78
|
+
newObject, getProperty, setProperty, callMethod,
|
|
79
|
+
connectSignal, emitSignal,
|
|
80
|
+
} from '@gjsify/node-gi';
|
|
81
|
+
|
|
82
|
+
requireNamespace('GLib', '2.0');
|
|
83
|
+
console.log(listInfoNames('GLib').includes('MainLoop')); // true
|
|
84
|
+
console.log(callFunction('GLib', 'get_host_name')); // namespace function
|
|
85
|
+
|
|
86
|
+
requireNamespace('Gio', '2.0');
|
|
87
|
+
const action = newObject('Gio', 'SimpleAction', { name: 'greet', enabled: true });
|
|
88
|
+
console.log(getProperty(action, 'name')); // 'greet' (GValue round-trip)
|
|
89
|
+
console.log(callMethod(action, 'get_name')); // 'greet' (interface method)
|
|
90
|
+
callMethod(action, 'set_enabled', [false]); // method with an IN argument
|
|
91
|
+
|
|
92
|
+
const c = newObject('Gio', 'Cancellable', {});
|
|
93
|
+
connectSignal(c, 'cancelled', () => console.log('cancelled'));
|
|
94
|
+
emitSignal(c, 'cancelled');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Register a GObject subclass and construct it (inherited properties + methods):
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { registerClass, constructType, callMethod } from '@gjsify/node-gi';
|
|
101
|
+
|
|
102
|
+
const MyAction = registerClass('MyAction', 'Gio', 'SimpleAction');
|
|
103
|
+
const a = constructType(MyAction, { name: 'greet', enabled: true });
|
|
104
|
+
console.log(callMethod(a, 'get_name')); // 'greet' (inherited GAction method)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Declare custom properties and signals on the subclass:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { registerClass, constructType, getProperty, setProperty,
|
|
111
|
+
connectSignal, emitSignal } from '@gjsify/node-gi';
|
|
112
|
+
|
|
113
|
+
const Counter = registerClass('Counter', 'GObject', 'Object', {
|
|
114
|
+
properties: [{ name: 'count', type: 'int', default: 0, minimum: 0, maximum: 100 }],
|
|
115
|
+
signals: [{ name: 'changed', paramTypes: ['int'] }],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const c = constructType(Counter, { count: 1 });
|
|
119
|
+
connectSignal(c, 'notify::count', (pspec) => console.log('changed:', pspec.name));
|
|
120
|
+
connectSignal(c, 'changed', (n) => console.log('count is now', n));
|
|
121
|
+
setProperty(c, 'count', 5); // fires notify::count
|
|
122
|
+
emitSignal(c, 'changed', [5]);
|
|
123
|
+
console.log(getProperty(c, 'count')); // 5
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Override a parent GObject vfunc with a JS function (the override runs as a method
|
|
127
|
+
on the instance — `this` is the GObject handle). Chain-up to the parent vfunc
|
|
128
|
+
lands in a later drop, so an override fully replaces the inherited implementation:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { registerClass, constructType, getProperty } from '@gjsify/node-gi';
|
|
132
|
+
|
|
133
|
+
const Greeter = registerClass('Greeter', 'Gio', 'SimpleAction', {
|
|
134
|
+
vfuncs: {
|
|
135
|
+
// GObject's `constructed` vfunc — runs once, after construct properties are
|
|
136
|
+
// set. `name` is a CONSTRUCT_ONLY property already available on `this`.
|
|
137
|
+
constructed() {
|
|
138
|
+
console.log('constructed:', getProperty(this, 'name'));
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
constructType(Greeter, { name: 'greet' }); // logs "constructed: greet"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### L1 — GJS-shaped surface (`@gjsify/node-gi/gi`)
|
|
147
|
+
|
|
148
|
+
The ergonomic layer the gjsify `--app node` build rewrites `gi://` imports onto.
|
|
149
|
+
This is the same code you would write under GJS:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { requireGi } from '@gjsify/node-gi/gi';
|
|
153
|
+
|
|
154
|
+
const GLib = requireGi('GLib', '2.0');
|
|
155
|
+
console.log(GLib.get_host_name());
|
|
156
|
+
|
|
157
|
+
const Gio = requireGi('Gio', '2.0');
|
|
158
|
+
const action = new Gio.SimpleAction({ name: 'greet', enabled: true });
|
|
159
|
+
console.log(action.name); // 'greet' (property accessor)
|
|
160
|
+
console.log(action.get_name()); // 'greet' (method)
|
|
161
|
+
action.enabled = false; // property set → set_property
|
|
162
|
+
|
|
163
|
+
const c = new Gio.Cancellable();
|
|
164
|
+
c.connect('cancelled', () => console.log('cancelled'));
|
|
165
|
+
c.cancel(); // fires the signal
|
|
166
|
+
|
|
167
|
+
// enums, flags and constants (GJS-style UPPER_CASE members)
|
|
168
|
+
console.log(GLib.PRIORITY_DEFAULT); // 0
|
|
169
|
+
console.log(Gio.BusType.SESSION); // 2
|
|
170
|
+
console.log(Gio.ApplicationFlags.HANDLES_OPEN); // 4
|
|
171
|
+
|
|
172
|
+
// constructor/static methods + camelCase aliases
|
|
173
|
+
const file = Gio.File.new_for_path('/usr/bin/gjs');
|
|
174
|
+
console.log(file.get_path()); // '/usr/bin/gjs'
|
|
175
|
+
console.log(file.getBasename()); // 'gjs' (camelCase alias)
|
|
176
|
+
|
|
177
|
+
// mainloop: a blocking GLib loop, with Node's libuv kept alive underneath
|
|
178
|
+
const loop = GLib.MainLoop.new(null, false);
|
|
179
|
+
setTimeout(() => loop.quit(), 100); // a libuv timer that fires during run()
|
|
180
|
+
loop.run(); // blocks like under GJS; returns on quit()
|
|
181
|
+
|
|
182
|
+
// GI callbacks: a JS function passed where a GI callback is expected. The
|
|
183
|
+
// GLib source fires from the loop; returning false (G_SOURCE_REMOVE) stops it.
|
|
184
|
+
const ticker = GLib.MainLoop.new(null, false);
|
|
185
|
+
let n = 0;
|
|
186
|
+
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
|
|
187
|
+
if (++n >= 3) { ticker.quit(); return false; }
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
ticker.run();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The mainloop bridge (`startMainLoop`) is auto-attached the first time `requireGi`
|
|
194
|
+
loads a namespace, so `GLib.MainLoop.run()` / `Gio.Application.run()` block as
|
|
195
|
+
they do under GJS while Node's timers, I/O and signal handlers keep running.
|
|
196
|
+
|
|
197
|
+
### GJS ambient globals (`@gjsify/node-gi/globals`)
|
|
198
|
+
|
|
199
|
+
GJS source relies on globals that exist implicitly under `gjs` — `print`,
|
|
200
|
+
`printerr`, `log`, `logError`, `ARGV`, and the legacy `imports` object. Importing
|
|
201
|
+
`@gjsify/node-gi/globals` (a side effect) installs Node-backed equivalents that
|
|
202
|
+
route through the same backend:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
import '@gjsify/node-gi/globals';
|
|
206
|
+
|
|
207
|
+
print('hello', 1, true); // → stdout, GJS String()-join
|
|
208
|
+
const GLib = imports.gi.GLib; // legacy imports.gi (honours .versions)
|
|
209
|
+
imports.gi.versions.Gtk = '4.0';
|
|
210
|
+
console.log(imports.gettext.gettext('x')); // no-translation passthrough
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A follow-up `--app node` build step will inject this automatically for any
|
|
214
|
+
bundle that references those globals (so `const Gtk = imports.gi.Gtk` /
|
|
215
|
+
`print(...)` GJS source runs unmodified on Node); today it is an explicit import.
|
|
216
|
+
|
|
217
|
+
The remaining GJS-compatible surface (`import GLib from 'gi://GLib?version=2.0'`,
|
|
218
|
+
`const GLib = imports.gi.GLib`, the core overrides, `_promisify`, the legacy
|
|
219
|
+
`imports.*` modules) is layered on top of this engine in the gjsify bundler
|
|
220
|
+
integration and subsequent drops.
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "node_gi",
|
|
5
|
+
"sources": [
|
|
6
|
+
"src/addon.cc"
|
|
7
|
+
],
|
|
8
|
+
"include_dirs": [
|
|
9
|
+
"<!@(node -p \"require('node-addon-api').include\")"
|
|
10
|
+
],
|
|
11
|
+
"cflags": [
|
|
12
|
+
"<!@(pkg-config --cflags girepository-2.0)"
|
|
13
|
+
],
|
|
14
|
+
"cflags_cc": [
|
|
15
|
+
"<!@(pkg-config --cflags girepository-2.0)",
|
|
16
|
+
"-std=c++17"
|
|
17
|
+
],
|
|
18
|
+
"libraries": [
|
|
19
|
+
"<!@(pkg-config --libs girepository-2.0)"
|
|
20
|
+
],
|
|
21
|
+
"defines": [
|
|
22
|
+
"NAPI_VERSION=8",
|
|
23
|
+
"NAPI_DISABLE_CPP_EXCEPTIONS"
|
|
24
|
+
],
|
|
25
|
+
"xcode_settings": {
|
|
26
|
+
"OTHER_CFLAGS": [
|
|
27
|
+
"<!@(pkg-config --cflags girepository-2.0)"
|
|
28
|
+
],
|
|
29
|
+
"CLANG_CXX_LANGUAGE_STANDARD": "c++17",
|
|
30
|
+
"GCC_ENABLE_CPP_EXCEPTIONS": "NO"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
package/gi.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi/gi — L1 GJS-compatibility layer type surface.
|
|
3
|
+
//
|
|
4
|
+
// The runtime returns a Proxy whose members are resolved dynamically from
|
|
5
|
+
// introspection, so the precise per-namespace shape is not statically known
|
|
6
|
+
// here. Under gjsify's bundler integration the call site
|
|
7
|
+
// `import Ns from 'gi://Ns?version=X'` is typed by the platform-neutral ambient
|
|
8
|
+
// `@girs/*` module declarations; this declaration types the bare runtime entry.
|
|
9
|
+
|
|
10
|
+
/** A GJS-shaped namespace object (members resolved dynamically). */
|
|
11
|
+
export type GiNamespace = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Require a GObject-Introspection namespace and return a GJS-shaped namespace
|
|
15
|
+
* object. The Node twin of `import Ns from 'gi://Ns?version=X'` /
|
|
16
|
+
* `imports.gi.Ns`. Members are resolved lazily from introspection: namespace
|
|
17
|
+
* functions become callables, GObject types become constructors whose instances
|
|
18
|
+
* expose `.method(...)`, `.prop` get/set and `.connect()/.emit()/.disconnect()`.
|
|
19
|
+
*/
|
|
20
|
+
export function requireGi(namespace: string, version?: string): GiNamespace;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract the raw native GObject handle from a wrapped instance (advanced /
|
|
24
|
+
* interop use). Returns the value unchanged if it is not a wrapped instance.
|
|
25
|
+
*/
|
|
26
|
+
export function unwrap(value: unknown): unknown;
|
|
27
|
+
|
|
28
|
+
export default requireGi;
|
package/gi.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi/gi — the L1 GJS-compatibility layer over the native engine.
|
|
3
|
+
//
|
|
4
|
+
// Turns the low-level primitives (requireNamespace / findInfo / callFunction /
|
|
5
|
+
// newObject / callMethod / get|setProperty / connect|emit|disconnectSignal) into
|
|
6
|
+
// a GJS-shaped namespace object: `requireGi('Gio', '2.0')` returns an object
|
|
7
|
+
// where `Gio.SomeFunction(...)` calls a namespace function, `new Gio.SomeClass({
|
|
8
|
+
// prop: value })` constructs a GObject, and the instance exposes `.method(...)`,
|
|
9
|
+
// `.prop` (property get/set) and `.connect()/.emit()/.disconnect()` — the same
|
|
10
|
+
// surface `import Gio from 'gi://Gio?version=2.0'` gives under GJS.
|
|
11
|
+
//
|
|
12
|
+
// This is the seam the bundler's --app node target rewrites `gi://Ns?version=X`
|
|
13
|
+
// onto. Reference: GJS's gi module (gjs/modules/esm/gi.js) for the require shape;
|
|
14
|
+
// node-gtk (romgrk, MIT) for the binding lineage. Hand-authored JS (the package
|
|
15
|
+
// ships no build step). Milestone 1 surfaces functions + GObject classes; enums,
|
|
16
|
+
// flags, constants, structs/boxed, interface static methods and camelCase
|
|
17
|
+
// aliases land in subsequent drops.
|
|
18
|
+
import * as native from './index.js';
|
|
19
|
+
|
|
20
|
+
// Symbol carrying the raw native GObject handle on a wrapped instance, so it can
|
|
21
|
+
// be unwrapped again when passed back into the engine as a GI argument.
|
|
22
|
+
const HANDLE = Symbol('nodeGiHandle');
|
|
23
|
+
|
|
24
|
+
// JS-builtin / interop names that must NEVER be treated as a GI method or
|
|
25
|
+
// property — otherwise awaiting, printing or inspecting a wrapper would call
|
|
26
|
+
// into GObject (e.g. a stray `then` would make a wrapper look thenable).
|
|
27
|
+
const RESERVED = new Set(['then', 'toString', 'valueOf', 'constructor', 'inspect']);
|
|
28
|
+
|
|
29
|
+
// GJS accepts both snake_case and camelCase for methods/properties. Map a JS
|
|
30
|
+
// accessor to the GI method name (snake_case) and to a GObject property name
|
|
31
|
+
// (kebab-case); a name that is already in the target case passes through.
|
|
32
|
+
function camelToSnake(name) {
|
|
33
|
+
return name.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toKebab(name) {
|
|
37
|
+
return name.replace(/([A-Z])/g, '-$1').replace(/_/g, '-').toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unwrapArg(value) {
|
|
41
|
+
if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
|
|
42
|
+
return value[HANDLE];
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unwrapArgs(args) {
|
|
48
|
+
return args.map(unwrapArg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function unwrapProps(props) {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const key of Object.keys(props)) out[key] = unwrapArg(props[key]);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Object-typed return values become chainable instance proxies; boxed/struct
|
|
58
|
+
// handles (e.g. GMainLoop) become method-routing proxies; everything else
|
|
59
|
+
// (primitives, strings, null) passes through.
|
|
60
|
+
function wrapReturn(value) {
|
|
61
|
+
if (native.isGObjectHandle(value)) return wrapInstance(value);
|
|
62
|
+
if (native.isBoxedHandle(value)) return wrapBoxed(value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wrap a boxed/struct handle so its methods are callable GJS-style
|
|
67
|
+
// (`mainLoop.run()`, `mainLoop.quit()`, snake_case or camelCase). Boxed types
|
|
68
|
+
// have no GObject properties/signals, so only method routing is provided.
|
|
69
|
+
function wrapBoxed(handle) {
|
|
70
|
+
const target = { [HANDLE]: handle };
|
|
71
|
+
return new Proxy(target, {
|
|
72
|
+
get(t, prop) {
|
|
73
|
+
if (prop === HANDLE) return handle;
|
|
74
|
+
if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
|
|
75
|
+
return (...args) =>
|
|
76
|
+
wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
|
|
77
|
+
},
|
|
78
|
+
has(t, prop) {
|
|
79
|
+
return prop === HANDLE || prop in t;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function wrapInstance(handle) {
|
|
85
|
+
const target = { [HANDLE]: handle };
|
|
86
|
+
return new Proxy(target, {
|
|
87
|
+
get(t, prop) {
|
|
88
|
+
if (prop === HANDLE) return handle;
|
|
89
|
+
if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
|
|
90
|
+
switch (prop) {
|
|
91
|
+
case 'connect':
|
|
92
|
+
return (signal, cb) => native.connectSignal(handle, signal, cb, false);
|
|
93
|
+
case 'connect_after':
|
|
94
|
+
return (signal, cb) => native.connectSignal(handle, signal, cb, true);
|
|
95
|
+
case 'emit':
|
|
96
|
+
return (signal, ...args) =>
|
|
97
|
+
wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
|
|
98
|
+
case 'disconnect':
|
|
99
|
+
return (id) => native.disconnectSignal(handle, id);
|
|
100
|
+
default:
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const propName = toKebab(prop);
|
|
104
|
+
if (native.hasProperty(handle, propName)) {
|
|
105
|
+
return wrapReturn(native.getProperty(handle, propName));
|
|
106
|
+
}
|
|
107
|
+
return (...args) => wrapReturn(native.callMethod(handle, camelToSnake(prop), unwrapArgs(args)));
|
|
108
|
+
},
|
|
109
|
+
set(t, prop, value) {
|
|
110
|
+
if (typeof prop === 'string') {
|
|
111
|
+
const propName = toKebab(prop);
|
|
112
|
+
if (native.hasProperty(handle, propName)) {
|
|
113
|
+
native.setProperty(handle, propName, unwrapArg(value));
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
t[prop] = value;
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
has(t, prop) {
|
|
121
|
+
return prop === HANDLE || prop in t;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function makeClass(namespace, typeName) {
|
|
127
|
+
const ctor = function ctor(props) {
|
|
128
|
+
const handle = native.newObject(namespace, typeName, props ? unwrapProps(props) : {});
|
|
129
|
+
return wrapInstance(handle);
|
|
130
|
+
};
|
|
131
|
+
Object.defineProperty(ctor, 'name', { value: typeName, configurable: true });
|
|
132
|
+
ctor.$gtypeName = `${namespace}.${typeName}`;
|
|
133
|
+
// Expose constructor/static methods lazily: Ns.Class.new(...) /
|
|
134
|
+
// Ns.Class.new_for_path(...) (and the camelCase aliases). `new Ns.Class({...})`
|
|
135
|
+
// still goes through the target's [[Construct]] (the default Proxy behaviour).
|
|
136
|
+
return new Proxy(ctor, {
|
|
137
|
+
get(t, prop) {
|
|
138
|
+
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
139
|
+
const giName = camelToSnake(prop);
|
|
140
|
+
return (...args) =>
|
|
141
|
+
wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Surface a boxed/struct type (e.g. GLib.MainLoop) as a class-like object whose
|
|
147
|
+
// static/constructor methods route through the engine: `GLib.MainLoop.new(...)`
|
|
148
|
+
// and the camelCase alias, plus `new GLib.MainLoop(...)` mapped to the `new`
|
|
149
|
+
// constructor. Returned boxed instances are wrapped by {@link wrapBoxed}.
|
|
150
|
+
function makeStruct(namespace, typeName) {
|
|
151
|
+
const base = function () {};
|
|
152
|
+
Object.defineProperty(base, 'name', { value: typeName, configurable: true });
|
|
153
|
+
base.$gtypeName = `${namespace}.${typeName}`;
|
|
154
|
+
return new Proxy(base, {
|
|
155
|
+
get(t, prop) {
|
|
156
|
+
if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
|
|
157
|
+
const giName = camelToSnake(prop);
|
|
158
|
+
return (...args) =>
|
|
159
|
+
wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
|
|
160
|
+
},
|
|
161
|
+
construct(_t, args) {
|
|
162
|
+
return wrapReturn(native.callStaticMethod(namespace, typeName, 'new', unwrapArgs(args)));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build a frozen enum/flags object keyed GJS-style: member names UPPER_CASED
|
|
168
|
+
// with '-' → '_' (e.g. Gio.BusType.SYSTEM, Gio.ApplicationFlags.HANDLES_OPEN).
|
|
169
|
+
function makeEnum(namespace, typeName) {
|
|
170
|
+
const raw = native.getEnumValues(namespace, typeName);
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const key of Object.keys(raw)) {
|
|
173
|
+
out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze(out);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createNamespace(namespace) {
|
|
179
|
+
const cache = new Map();
|
|
180
|
+
return new Proxy(Object.create(null), {
|
|
181
|
+
get(_t, prop) {
|
|
182
|
+
if (typeof prop !== 'string' || RESERVED.has(prop)) return undefined;
|
|
183
|
+
if (cache.has(prop)) return cache.get(prop);
|
|
184
|
+
const info = native.findInfo(namespace, prop);
|
|
185
|
+
let value;
|
|
186
|
+
if (info === null) {
|
|
187
|
+
value = undefined;
|
|
188
|
+
} else if (info.kind === 'function') {
|
|
189
|
+
value = (...args) => wrapReturn(native.callFunction(namespace, prop, unwrapArgs(args)));
|
|
190
|
+
} else if (info.kind === 'object' || info.kind === 'interface') {
|
|
191
|
+
// Interfaces are not constructible (`new` throws via newObject) but
|
|
192
|
+
// carry static/constructor methods, e.g. Gio.File.new_for_path.
|
|
193
|
+
value = makeClass(namespace, prop);
|
|
194
|
+
} else if (info.kind === 'enum' || info.kind === 'flags') {
|
|
195
|
+
value = makeEnum(namespace, prop);
|
|
196
|
+
} else if (info.kind === 'constant') {
|
|
197
|
+
value = native.getConstantValue(namespace, prop);
|
|
198
|
+
} else if (info.kind === 'struct' || info.kind === 'union') {
|
|
199
|
+
// Boxed/struct types: static/constructor methods + boxed instances
|
|
200
|
+
// (GLib.MainLoop, …). Field access lands with the broader structs drop.
|
|
201
|
+
value = makeStruct(namespace, prop);
|
|
202
|
+
} else {
|
|
203
|
+
// interface / callback: surfaced in a later drop.
|
|
204
|
+
value = undefined;
|
|
205
|
+
}
|
|
206
|
+
cache.set(prop, value);
|
|
207
|
+
return value;
|
|
208
|
+
},
|
|
209
|
+
has(_t, prop) {
|
|
210
|
+
return typeof prop === 'string' && native.findInfo(namespace, prop) !== null;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const namespaceCache = new Map();
|
|
216
|
+
|
|
217
|
+
// Whether the libuv↔GLib bridge has been attached this process. The native
|
|
218
|
+
// startMainLoop is itself idempotent; this just avoids the extra call.
|
|
219
|
+
let loopAttached = false;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Require a GObject-Introspection namespace and return a GJS-shaped namespace
|
|
223
|
+
* object. The Node twin of `import Ns from 'gi://Ns?version=X'` /
|
|
224
|
+
* `imports.gi.Ns`.
|
|
225
|
+
* @param {string} namespace e.g. "Gio"
|
|
226
|
+
* @param {string} [version] e.g. "2.0"
|
|
227
|
+
* @returns {Record<string, unknown>}
|
|
228
|
+
*/
|
|
229
|
+
export function requireGi(namespace, version) {
|
|
230
|
+
native.requireNamespace(namespace, version);
|
|
231
|
+
// Attach the libuv-in-GLib bridge once, so any later blocking GLib loop
|
|
232
|
+
// (GLib.MainLoop.run / Gio.Application.run) keeps Node's event loop alive.
|
|
233
|
+
if (!loopAttached) {
|
|
234
|
+
native.startMainLoop();
|
|
235
|
+
loopAttached = true;
|
|
236
|
+
}
|
|
237
|
+
const key = version ? `${namespace}@${version}` : namespace;
|
|
238
|
+
let ns = namespaceCache.get(key);
|
|
239
|
+
if (ns === undefined) {
|
|
240
|
+
ns = createNamespace(namespace);
|
|
241
|
+
namespaceCache.set(key, ns);
|
|
242
|
+
}
|
|
243
|
+
return ns;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extract the raw native GObject handle from a wrapped instance (advanced /
|
|
248
|
+
* interop use). Returns the value unchanged if it is not a wrapped instance.
|
|
249
|
+
* @param {unknown} value
|
|
250
|
+
* @returns {unknown}
|
|
251
|
+
*/
|
|
252
|
+
export function unwrap(value) {
|
|
253
|
+
return unwrapArg(value);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default requireGi;
|
package/globals.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi/globals — types for the GJS ambient-globals shim.
|
|
3
|
+
|
|
4
|
+
/** The legacy GJS `imports` object (a minimal Node-backed subset). */
|
|
5
|
+
export interface GjsImports {
|
|
6
|
+
/** `imports.gi.<Ns>` resolves a namespace; `imports.gi.versions.<Ns>` pins a version. */
|
|
7
|
+
gi: {
|
|
8
|
+
versions: Record<string, string | undefined>;
|
|
9
|
+
[namespace: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
/** `imports.system` — process identity + lifecycle (Node-backed subset). */
|
|
12
|
+
system: {
|
|
13
|
+
exit(code?: number): void;
|
|
14
|
+
gc(): void;
|
|
15
|
+
readonly programInvocationName: string;
|
|
16
|
+
readonly programPath: string | null;
|
|
17
|
+
version: number;
|
|
18
|
+
addressOf(): string;
|
|
19
|
+
refcount(): number;
|
|
20
|
+
breakpoint(): void;
|
|
21
|
+
dumpHeap(): void;
|
|
22
|
+
dumpMemoryInfo(): void;
|
|
23
|
+
};
|
|
24
|
+
/** `imports.gettext` — a no-translation passthrough. */
|
|
25
|
+
gettext: {
|
|
26
|
+
gettext(s: string): string;
|
|
27
|
+
dgettext(domain: string, s: string): string;
|
|
28
|
+
dcgettext(domain: string, s: string): string;
|
|
29
|
+
ngettext(s: string, p: string, n: number): string;
|
|
30
|
+
dngettext(domain: string, s: string, p: string, n: number): string;
|
|
31
|
+
pgettext(ctx: string, s: string): string;
|
|
32
|
+
dpgettext(domain: string, ctx: string, s: string): string;
|
|
33
|
+
domain(domain: string): {
|
|
34
|
+
gettext(s: string): string;
|
|
35
|
+
ngettext(s: string, p: string, n: number): string;
|
|
36
|
+
pgettext(ctx: string, s: string): string;
|
|
37
|
+
};
|
|
38
|
+
setlocale(): null;
|
|
39
|
+
bindtextdomain(): null;
|
|
40
|
+
textdomain(): null;
|
|
41
|
+
bindtextdomainCodeset(): null;
|
|
42
|
+
LocaleCategory: Record<string, number>;
|
|
43
|
+
};
|
|
44
|
+
versions: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare global {
|
|
48
|
+
// eslint-disable-next-line no-var
|
|
49
|
+
var print: (...args: unknown[]) => void;
|
|
50
|
+
// eslint-disable-next-line no-var
|
|
51
|
+
var printerr: (...args: unknown[]) => void;
|
|
52
|
+
// eslint-disable-next-line no-var
|
|
53
|
+
var log: (...args: unknown[]) => void;
|
|
54
|
+
// eslint-disable-next-line no-var
|
|
55
|
+
var logError: (error: unknown, prefix?: string) => void;
|
|
56
|
+
// eslint-disable-next-line no-var
|
|
57
|
+
var ARGV: string[];
|
|
58
|
+
// eslint-disable-next-line no-var
|
|
59
|
+
var imports: GjsImports;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Install the GJS ambient globals on `globalThis` (idempotent). */
|
|
63
|
+
export function installGjsGlobals(): GjsImports;
|
|
64
|
+
|
|
65
|
+
declare const _default: typeof installGjsGlobals;
|
|
66
|
+
export default _default;
|