@devscholar/node-with-gjs 0.0.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.md +21 -0
- package/README.md +118 -0
- package/deno.json +8 -0
- package/examples/adwaita/counter/counter.ts +66 -0
- package/examples/console/await-delay/await-delay.ts +11 -0
- package/examples/console/console-input/console-input.ts +27 -0
- package/examples/gtk/counter/counter.ts +62 -0
- package/examples/gtk/drag-box/drag-box.ts +77 -0
- package/examples/gtk-webkit/counter/counter.html +47 -0
- package/examples/gtk-webkit/counter/counter.ts +101 -0
- package/gi-loader.ts +13 -0
- package/hook.js +42 -0
- package/package.json +14 -0
- package/scripts/host.js +162 -0
- package/src/index.ts +180 -0
- package/src/ipc.ts +99 -0
- package/start.js +105 -0
- package/tsconfig.json +11 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) DevScholar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Node with GJS
|
|
2
|
+
|
|
3
|
+
⚠️ This project is still in pre-alpha stage, and API is subject to change.
|
|
4
|
+
|
|
5
|
+
This is a project that brings GNOME's GJS (GObject Introspection JavaScript runtime) to Node.js, allowing you to use GTK4 and WebKit from JavaScript/TypeScript with a Node.js-like API. Since this project uses IPC instead of C++ Addon, it is compatible not only with Node but also with Deno and Bun.
|
|
6
|
+
|
|
7
|
+
# Requirements
|
|
8
|
+
|
|
9
|
+
- Linux with GTK4 and WebKitGTK 6.0 installed
|
|
10
|
+
- Node.js 22+ (or Deno/Bun)
|
|
11
|
+
- bash (for Unix pipe IPC)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Note: These packages are supposed to be pre-installed with GNOME-based Linux distros:
|
|
16
|
+
|
|
17
|
+
For Ubuntu/Debian:
|
|
18
|
+
```bash
|
|
19
|
+
apt install libgtk-4-1 libwebkitgtk-6.0-0 gjs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For Fedora:
|
|
23
|
+
```bash
|
|
24
|
+
dnf install gtk4 webkitgtk6.0 gjs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For Arch Linux:
|
|
28
|
+
```bash
|
|
29
|
+
pacman -S gtk4 webkitgtk-6.0 gjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
# Usage
|
|
33
|
+
|
|
34
|
+
## Runtime Support
|
|
35
|
+
|
|
36
|
+
| Runtime | `gi://` URL Syntax | Loader Hooks |
|
|
37
|
+
|---------|-------------------|--------------|
|
|
38
|
+
| Node.js | ✅ Supported | ✅ `--experimental-loader` |
|
|
39
|
+
| Bun | ❌ Not supported | ❌ No hooks mechanism |
|
|
40
|
+
| Deno | ❌ Not supported | ❌ No hooks mechanism |
|
|
41
|
+
|
|
42
|
+
## Node.js
|
|
43
|
+
|
|
44
|
+
Node.js supports the `gi://` URL syntax, which is consistent with GJS:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import Gtk from 'gi://Gtk?version=4.0';
|
|
48
|
+
import WebKit from 'gi://WebKit?version=6.0';
|
|
49
|
+
|
|
50
|
+
const app = new Gtk.Application({ application_id: 'org.example.app' });
|
|
51
|
+
// ...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Bun / Deno
|
|
55
|
+
|
|
56
|
+
Bun and Deno do not support Node.js loader hooks, so you need to use the `loadGi` function instead:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { loadGi } from './gi-loader.ts';
|
|
60
|
+
|
|
61
|
+
const Gtk = loadGi('Gtk', '4.0');
|
|
62
|
+
const WebKit = loadGi('WebKit', '6.0');
|
|
63
|
+
|
|
64
|
+
const app = new Gtk.Application({ application_id: 'org.example.app' });
|
|
65
|
+
// ...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Note:** The `loadGi` function also works with Node.js, useful for older Node.js versions or when not using experimental loader flags.
|
|
69
|
+
|
|
70
|
+
# Examples
|
|
71
|
+
|
|
72
|
+
## Console Apps
|
|
73
|
+
|
|
74
|
+
### Console Input App
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node start.js examples/console/console-input/console-input.ts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Await Delay App
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node start.js examples/console/await-delay/await-delay.ts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## GUI Apps
|
|
87
|
+
|
|
88
|
+
### GTK4 Counter App
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node start.js examples/gtk/counter/counter.ts
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### GTK4 Drag Box App
|
|
95
|
+
|
|
96
|
+
A drag box example that demonstrates high frequency IPC.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
node start.js examples/gtk/drag-box/drag-box.ts
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### GTK4 WebKit Counter App
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node start.js examples/gtk-webkit/counter/counter.ts
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Adwaita Counter App (libadwaita)
|
|
109
|
+
|
|
110
|
+
A counter example based on [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/), demonstrating how to use Adwaita-specific components like `Adw.ApplicationWindow` and `Adw.Clamp`.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
node start.js examples/adwaita/counter/counter.ts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
# License
|
|
117
|
+
|
|
118
|
+
This project is licensed under the MIT License.
|
package/deno.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Run: node start.js examples/adwaita/counter/counter.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
|
|
4
|
+
const Gtk = loadGi('Gtk', '4.0');
|
|
5
|
+
const Adw = loadGi('Adw', '1');
|
|
6
|
+
|
|
7
|
+
let clickCount = 0;
|
|
8
|
+
|
|
9
|
+
console.log("--- Adwaita Counter ---");
|
|
10
|
+
|
|
11
|
+
const app = new Adw.Application({ application_id: 'org.adwaita.counter' });
|
|
12
|
+
|
|
13
|
+
app.connect('activate', () => {
|
|
14
|
+
const window = new Adw.ApplicationWindow({
|
|
15
|
+
application: app,
|
|
16
|
+
title: 'Adwaita Counter App',
|
|
17
|
+
default_width: 400,
|
|
18
|
+
default_height: 300
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const toolbarView = new Adw.ToolbarView();
|
|
22
|
+
|
|
23
|
+
const headerBar = new Adw.HeaderBar({
|
|
24
|
+
title_widget: new Gtk.Label({ label: 'Adwaita Counter App' })
|
|
25
|
+
});
|
|
26
|
+
toolbarView.add_top_bar(headerBar);
|
|
27
|
+
|
|
28
|
+
const box = new Gtk.Box({
|
|
29
|
+
orientation: Gtk.Orientation.VERTICAL,
|
|
30
|
+
spacing: 10,
|
|
31
|
+
halign: Gtk.Align.CENTER,
|
|
32
|
+
valign: Gtk.Align.CENTER
|
|
33
|
+
});
|
|
34
|
+
box.set_margin_start(20);
|
|
35
|
+
box.set_margin_end(20);
|
|
36
|
+
box.set_margin_top(20);
|
|
37
|
+
box.set_margin_bottom(20);
|
|
38
|
+
|
|
39
|
+
const label = new Gtk.Label({
|
|
40
|
+
label: 'Clicks: 0',
|
|
41
|
+
css_classes: ['title-1']
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const button = new Gtk.Button({
|
|
45
|
+
label: 'Click to Add',
|
|
46
|
+
css_classes: ['suggested-action']
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
button.connect('clicked', () => {
|
|
50
|
+
clickCount++;
|
|
51
|
+
const message = `Clicked ${clickCount} times`;
|
|
52
|
+
label.set_label(message);
|
|
53
|
+
console.log(message);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
box.append(label);
|
|
57
|
+
box.append(button);
|
|
58
|
+
|
|
59
|
+
toolbarView.set_content(box);
|
|
60
|
+
window.set_content(toolbarView);
|
|
61
|
+
window.present();
|
|
62
|
+
|
|
63
|
+
console.log("Click the button to increase the counter...");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
app.run([]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Run: node start.js examples/console/await-delay/await-delay.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
|
|
4
|
+
loadGi('GLib', '2.0');
|
|
5
|
+
|
|
6
|
+
print('0s');
|
|
7
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
8
|
+
print("1s");
|
|
9
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
10
|
+
print("2s");
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Run: node start.js examples/console/console-input/console-input.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
|
|
4
|
+
const Gio = loadGi('Gio', '2.0');
|
|
5
|
+
|
|
6
|
+
let GioUnix;
|
|
7
|
+
try {
|
|
8
|
+
GioUnix = loadGi('GioUnix', '2.0');
|
|
9
|
+
} catch {
|
|
10
|
+
GioUnix = Gio;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
print("=== Greeting Program ===");
|
|
14
|
+
print("Please enter your name: ");
|
|
15
|
+
|
|
16
|
+
const stdin = new GioUnix.InputStream({ fd: 0, close_fd: false });
|
|
17
|
+
const dataInput = new Gio.DataInputStream({ base_stream: stdin });
|
|
18
|
+
|
|
19
|
+
const [name] = dataInput.read_line_utf8(null);
|
|
20
|
+
|
|
21
|
+
if (name && name.trim() !== "") {
|
|
22
|
+
print(`Hello, ${name}! Welcome to this program!`);
|
|
23
|
+
} else {
|
|
24
|
+
print("Hello, friend! Welcome to this program!");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
print("Program ended.");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Run: node start.js examples/gtk/counter/counter.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
|
|
4
|
+
const Gtk = loadGi('Gtk', '4.0');
|
|
5
|
+
|
|
6
|
+
let clickCount = 0;
|
|
7
|
+
|
|
8
|
+
console.log("--- GTK4 Counter ---");
|
|
9
|
+
|
|
10
|
+
const app = new Gtk.Application({ application_id: 'org.gtk.counter' });
|
|
11
|
+
|
|
12
|
+
app.connect('activate', () => {
|
|
13
|
+
const window = new Gtk.ApplicationWindow({
|
|
14
|
+
application: app,
|
|
15
|
+
title: 'GTK Counter App',
|
|
16
|
+
default_width: 400,
|
|
17
|
+
default_height: 300
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
window.connect('close-request', () => {
|
|
21
|
+
app.quit();
|
|
22
|
+
return false;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const box = new Gtk.Box({
|
|
26
|
+
orientation: Gtk.Orientation.VERTICAL,
|
|
27
|
+
spacing: 10,
|
|
28
|
+
halign: Gtk.Align.CENTER,
|
|
29
|
+
valign: Gtk.Align.CENTER
|
|
30
|
+
});
|
|
31
|
+
box.set_margin_start(20);
|
|
32
|
+
box.set_margin_end(20);
|
|
33
|
+
box.set_margin_top(20);
|
|
34
|
+
box.set_margin_bottom(20);
|
|
35
|
+
|
|
36
|
+
const label = new Gtk.Label({
|
|
37
|
+
label: 'Clicks: 0',
|
|
38
|
+
css_classes: ['title-1']
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const button = new Gtk.Button({
|
|
42
|
+
label: 'Click to Add',
|
|
43
|
+
css_classes: ['suggested-action']
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
button.connect('clicked', () => {
|
|
47
|
+
clickCount++;
|
|
48
|
+
const message = `Clicked ${clickCount} times`;
|
|
49
|
+
label.set_label(message);
|
|
50
|
+
console.log(message);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
box.append(label);
|
|
54
|
+
box.append(button);
|
|
55
|
+
|
|
56
|
+
window.set_child(box);
|
|
57
|
+
window.present();
|
|
58
|
+
|
|
59
|
+
console.log("Click the button to increase the counter...");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.run([]);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Run: node start.js examples/gtk/drag-box/drag-box.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
|
|
4
|
+
const Gtk = loadGi('Gtk', '4.0');
|
|
5
|
+
|
|
6
|
+
console.log("--- GTK4 Flicker-free Draggable Square (Cairo) ---");
|
|
7
|
+
|
|
8
|
+
const app = new Gtk.Application({ application_id: 'org.gtk.dragbox' });
|
|
9
|
+
|
|
10
|
+
app.connect('activate', () => {
|
|
11
|
+
const window = new Gtk.ApplicationWindow({
|
|
12
|
+
application: app,
|
|
13
|
+
title: 'Drag Example (High Frequency IPC) ',
|
|
14
|
+
default_width: 600,
|
|
15
|
+
default_height: 400
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const fixed = new Gtk.Fixed();
|
|
19
|
+
fixed.set_hexpand(true);
|
|
20
|
+
fixed.set_vexpand(true);
|
|
21
|
+
|
|
22
|
+
const squareSize = 80;
|
|
23
|
+
const drawingArea = new Gtk.DrawingArea();
|
|
24
|
+
drawingArea.set_size_request(squareSize, squareSize);
|
|
25
|
+
|
|
26
|
+
const drawFunction = (area: any, cr: any, width: number, height: number) => {
|
|
27
|
+
cr.setSourceRGB(1.0, 0.2, 0.2);
|
|
28
|
+
cr.rectangle(0, 0, width, height);
|
|
29
|
+
cr.fill();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
drawingArea.set_draw_func(drawFunction);
|
|
33
|
+
|
|
34
|
+
let currentX = 260;
|
|
35
|
+
let currentY = 160;
|
|
36
|
+
fixed.put(drawingArea, currentX, currentY);
|
|
37
|
+
|
|
38
|
+
const drag = new Gtk.GestureDrag();
|
|
39
|
+
|
|
40
|
+
let isDragging = false;
|
|
41
|
+
let dragStartX = 0;
|
|
42
|
+
let dragStartY = 0;
|
|
43
|
+
|
|
44
|
+
drag.connect('drag-begin', (gesture: any, startX: number, startY: number) => {
|
|
45
|
+
if (startX >= currentX && startX <= currentX + squareSize &&
|
|
46
|
+
startY >= currentY && startY <= currentY + squareSize) {
|
|
47
|
+
isDragging = true;
|
|
48
|
+
dragStartX = currentX;
|
|
49
|
+
dragStartY = currentY;
|
|
50
|
+
console.log(`✅ Drag started at: (${startX}, ${startY})`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
drag.connect('drag-update', (gesture: any, offsetX: number, offsetY: number) => {
|
|
55
|
+
if (!isDragging) return;
|
|
56
|
+
const newX = dragStartX + offsetX;
|
|
57
|
+
const newY = dragStartY + offsetY;
|
|
58
|
+
fixed.move(drawingArea, newX, newY);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
drag.connect('drag-end', (gesture: any, offsetX: number, offsetY: number) => {
|
|
62
|
+
if (!isDragging) return;
|
|
63
|
+
isDragging = false;
|
|
64
|
+
currentX = dragStartX + offsetX;
|
|
65
|
+
currentY = dragStartY + offsetY;
|
|
66
|
+
console.log(`🛑 Drag ended at position: (${currentX}, ${currentY})`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fixed.add_controller(drag);
|
|
70
|
+
|
|
71
|
+
window.set_child(fixed);
|
|
72
|
+
window.present();
|
|
73
|
+
|
|
74
|
+
console.log("Window loaded. Try dragging the red square smoothly!");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
app.run([]);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Counter</title>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: Arial, sans-serif; padding: 20px; text-align: center; }
|
|
8
|
+
h1 { color: #333; }
|
|
9
|
+
#display { font-size: 24px; margin: 20px 0; }
|
|
10
|
+
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
|
|
11
|
+
</style>
|
|
12
|
+
<script>
|
|
13
|
+
(function() {
|
|
14
|
+
const originalConsole = window.console;
|
|
15
|
+
window.console = {
|
|
16
|
+
log: function(...args) {
|
|
17
|
+
window.webkit.messageHandlers.console.postMessage(args.join(' '));
|
|
18
|
+
},
|
|
19
|
+
error: function(...args) {
|
|
20
|
+
window.webkit.messageHandlers.console.postMessage('[ERROR] ' + args.join(' '));
|
|
21
|
+
},
|
|
22
|
+
warn: function(...args) {
|
|
23
|
+
window.webkit.messageHandlers.console.postMessage('[WARN] ' + args.join(' '));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
})();
|
|
27
|
+
</script>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<h1>Counter App (WebKit)</h1>
|
|
31
|
+
<p id="display">Clicks: 0</p>
|
|
32
|
+
<button id="btn">Click to Add</button>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
let clickCount = 0;
|
|
36
|
+
const display = document.getElementById('display');
|
|
37
|
+
const button = document.getElementById('btn');
|
|
38
|
+
|
|
39
|
+
button.addEventListener('click', function() {
|
|
40
|
+
clickCount++;
|
|
41
|
+
const message = 'Button clicked ' + clickCount + ' times';
|
|
42
|
+
display.textContent = message;
|
|
43
|
+
console.log(message);
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Run: node start.js examples/gtk-webkit/counter/counter.ts
|
|
2
|
+
import { loadGi } from '../../../gi-loader.ts';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const Gtk = loadGi('Gtk', '4.0');
|
|
7
|
+
const WebKit = loadGi('WebKit', '6.0');
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
console.log('--- GTK4 WebKit Counter ---');
|
|
13
|
+
|
|
14
|
+
const app = new Gtk.Application({ application_id: 'org.gtk.webkitcounter' });
|
|
15
|
+
|
|
16
|
+
app.connect('activate', () => {
|
|
17
|
+
const window = new Gtk.ApplicationWindow({
|
|
18
|
+
application: app,
|
|
19
|
+
title: 'WebKit Counter App',
|
|
20
|
+
default_width: 500,
|
|
21
|
+
default_height: 400
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const box = new Gtk.Box({
|
|
25
|
+
orientation: Gtk.Orientation.VERTICAL,
|
|
26
|
+
spacing: 0
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const toolbar = new Gtk.Box({
|
|
30
|
+
orientation: Gtk.Orientation.HORIZONTAL,
|
|
31
|
+
spacing: 5
|
|
32
|
+
});
|
|
33
|
+
toolbar.set_margin_start(5);
|
|
34
|
+
toolbar.set_margin_end(5);
|
|
35
|
+
toolbar.set_margin_top(5);
|
|
36
|
+
toolbar.set_margin_bottom(5);
|
|
37
|
+
|
|
38
|
+
const htmlPath = path.join(__dirname, 'counter.html');
|
|
39
|
+
const htmlUri = 'file://' + htmlPath;
|
|
40
|
+
|
|
41
|
+
const backButton = new Gtk.Button({ label: '← Back' });
|
|
42
|
+
const forwardButton = new Gtk.Button({ label: 'Forward →' });
|
|
43
|
+
const urlEntry = new Gtk.Entry({
|
|
44
|
+
placeholder_text: 'Enter URL or use default',
|
|
45
|
+
text: htmlUri,
|
|
46
|
+
hexpand: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
toolbar.append(backButton);
|
|
50
|
+
toolbar.append(forwardButton);
|
|
51
|
+
toolbar.append(urlEntry);
|
|
52
|
+
|
|
53
|
+
const contentManager = new WebKit.UserContentManager();
|
|
54
|
+
|
|
55
|
+
contentManager.connect('script-message-received', (manager, value) => {
|
|
56
|
+
const message = value.to_string();
|
|
57
|
+
if (message) print(`[WebView] ${message}`);
|
|
58
|
+
});
|
|
59
|
+
contentManager.register_script_message_handler('console', null);
|
|
60
|
+
|
|
61
|
+
const webView = new WebKit.WebView({
|
|
62
|
+
vexpand: true,
|
|
63
|
+
hexpand: true,
|
|
64
|
+
user_content_manager: contentManager
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
webView.load_uri(htmlUri);
|
|
68
|
+
|
|
69
|
+
webView.connect('load-changed', (webview, loadEvent) => {
|
|
70
|
+
if (loadEvent === WebKit.LoadEvent.FINISHED) {
|
|
71
|
+
console.log('Page Loaded Successfully');
|
|
72
|
+
urlEntry.set_text(webview.get_uri() || '');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
backButton.connect('clicked', () => {
|
|
77
|
+
if (webView.can_go_back()) webView.go_back();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
forwardButton.connect('clicked', () => {
|
|
81
|
+
if (webView.can_go_forward()) webView.go_forward();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
urlEntry.connect('activate', () => {
|
|
85
|
+
let uri = urlEntry.get_text();
|
|
86
|
+
if (!uri.startsWith('http://') && !uri.startsWith('https://') && !uri.startsWith('file://')) {
|
|
87
|
+
uri = 'https://' + uri;
|
|
88
|
+
}
|
|
89
|
+
webView.load_uri(uri);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
box.append(toolbar);
|
|
93
|
+
box.append(webView);
|
|
94
|
+
|
|
95
|
+
window.set_child(box);
|
|
96
|
+
window.present();
|
|
97
|
+
|
|
98
|
+
console.log("Click the button in the web view to increase the counter...");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.run([]);
|
package/gi-loader.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// gi-loader.ts
|
|
2
|
+
// Universal GI namespace loader for Node.js, Bun, and Deno
|
|
3
|
+
// Usage:
|
|
4
|
+
// import { loadGi } from './gi-loader.ts';
|
|
5
|
+
// const Gtk = loadGi('Gtk', '4.0');
|
|
6
|
+
|
|
7
|
+
import { init, loadGiNamespace } from './src/index.ts';
|
|
8
|
+
|
|
9
|
+
init();
|
|
10
|
+
|
|
11
|
+
export function loadGi(namespace: string, version: string = '') {
|
|
12
|
+
return loadGiNamespace(namespace, version);
|
|
13
|
+
}
|
package/hook.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// hook.js
|
|
2
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
3
|
+
if (specifier.startsWith('gi://')) {
|
|
4
|
+
return {
|
|
5
|
+
url: specifier,
|
|
6
|
+
shortCircuit: true,
|
|
7
|
+
format: 'module'
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return nextResolve(specifier, context);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function load(url, context, nextLoad) {
|
|
14
|
+
if (url.startsWith('gi://')) {
|
|
15
|
+
// Safely parse 'gi://Gtk?version=4.0'
|
|
16
|
+
const bareUrl = url.replace('gi://', '');
|
|
17
|
+
const [namespacePart, queryPart] = bareUrl.split('?');
|
|
18
|
+
|
|
19
|
+
const namespace = namespacePart;
|
|
20
|
+
let version = '';
|
|
21
|
+
|
|
22
|
+
if (queryPart) {
|
|
23
|
+
const params = new URLSearchParams(queryPart);
|
|
24
|
+
version = params.get('version') || '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const coreUrl = new URL('./src/index.ts', import.meta.url).href;
|
|
28
|
+
|
|
29
|
+
const source = `
|
|
30
|
+
import { init, loadGiNamespace } from '${coreUrl}';
|
|
31
|
+
init();
|
|
32
|
+
export default loadGiNamespace('${namespace}', '${version}');
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
format: 'module',
|
|
37
|
+
shortCircuit: true,
|
|
38
|
+
source: source
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return nextLoad(url, context);
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devscholar/node-with-gjs",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Node.js IPC Bridge for GJS",
|
|
5
|
+
"main": "start.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^20.0.0",
|
|
12
|
+
"typescript": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/scripts/host.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// scripts/host.js
|
|
2
|
+
import Gio from 'gi://Gio';
|
|
3
|
+
import GLib from 'gi://GLib';
|
|
4
|
+
import System from 'system';
|
|
5
|
+
|
|
6
|
+
let InputStream, OutputStream;
|
|
7
|
+
try {
|
|
8
|
+
const GioUnix = imports.gi.GioUnix;
|
|
9
|
+
InputStream = GioUnix.InputStream;
|
|
10
|
+
OutputStream = GioUnix.OutputStream;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
InputStream = Gio.UnixInputStream;
|
|
13
|
+
OutputStream = Gio.UnixOutputStream;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!InputStream || !OutputStream) {
|
|
17
|
+
console.error("Critical: Cannot find UnixInputStream/UnixOutputStream in this GJS environment.");
|
|
18
|
+
System.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const inStream = new InputStream({ fd: 3, close_fd: false });
|
|
22
|
+
const outStream = new OutputStream({ fd: 4, close_fd: false });
|
|
23
|
+
|
|
24
|
+
const dataIn = new Gio.DataInputStream({ base_stream: inStream });
|
|
25
|
+
const dataOut = new Gio.DataOutputStream({ base_stream: outStream });
|
|
26
|
+
|
|
27
|
+
const objectStore = new Map();
|
|
28
|
+
let nextObjectId = 1;
|
|
29
|
+
|
|
30
|
+
function ConvertToProtocol(obj) {
|
|
31
|
+
if (obj === null || obj === undefined) return { type: 'null' };
|
|
32
|
+
const t = typeof obj;
|
|
33
|
+
if (t === 'string' || t === 'number' || t === 'boolean') {
|
|
34
|
+
return { type: 'primitive', value: obj };
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(obj)) {
|
|
37
|
+
return { type: 'array', value: obj.map(ConvertToProtocol) };
|
|
38
|
+
}
|
|
39
|
+
const id = `gobj_${nextObjectId++}`;
|
|
40
|
+
objectStore.set(id, obj);
|
|
41
|
+
return { type: 'ref', id: id };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function processNestedCommands() {
|
|
45
|
+
while (true) {
|
|
46
|
+
const [line] = dataIn.read_line_utf8(null);
|
|
47
|
+
if (!line) System.exit(0);
|
|
48
|
+
const cmd = JSON.parse(line);
|
|
49
|
+
if (cmd.type === 'reply') return cmd;
|
|
50
|
+
let response;
|
|
51
|
+
try { response = executeCommand(cmd); }
|
|
52
|
+
catch (e) { response = { type: 'error', message: e.toString() }; }
|
|
53
|
+
dataOut.put_string(JSON.stringify(response) + '\n', null);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ResolveArg(arg) {
|
|
58
|
+
if (arg.type === 'null') return null;
|
|
59
|
+
if (arg.type === 'primitive') return arg.value;
|
|
60
|
+
|
|
61
|
+
if (arg.type === 'uint8array') {
|
|
62
|
+
return new Uint8Array(arg.value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (arg.type === 'ref') return objectStore.get(arg.id);
|
|
66
|
+
if (arg.type === 'array') return arg.value.map(a => ResolveArg(a));
|
|
67
|
+
if (arg.type === 'object') {
|
|
68
|
+
const obj = {};
|
|
69
|
+
for (let k in arg.value) obj[k] = ResolveArg(arg.value[k]);
|
|
70
|
+
return obj;
|
|
71
|
+
}
|
|
72
|
+
if (arg.type === 'callback') {
|
|
73
|
+
return (...cbArgs) => {
|
|
74
|
+
const mappedArgs = cbArgs.map(ConvertToProtocol);
|
|
75
|
+
const msg = { type: 'event', callbackId: arg.callbackId, args: mappedArgs };
|
|
76
|
+
dataOut.put_string(JSON.stringify(msg) + '\n', null);
|
|
77
|
+
const res = processNestedCommands();
|
|
78
|
+
if (res.result && res.result.type === 'primitive') return res.result.value;
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function executeCommand(cmd) {
|
|
85
|
+
if (cmd.action === 'LoadNamespace') {
|
|
86
|
+
imports.gi.versions[cmd.namespace] = cmd.version;
|
|
87
|
+
const ns = imports.gi[cmd.namespace];
|
|
88
|
+
return ConvertToProtocol(ns);
|
|
89
|
+
}
|
|
90
|
+
// Instantiate from direct TypeId
|
|
91
|
+
if (cmd.action === 'New') {
|
|
92
|
+
const Type = objectStore.get(cmd.typeId);
|
|
93
|
+
const argsArray = (cmd.args || []).map(a => ResolveArg(a));
|
|
94
|
+
const instance = new Type(...argsArray);
|
|
95
|
+
return ConvertToProtocol(instance);
|
|
96
|
+
}
|
|
97
|
+
// Instantiate a property belonging to a namespace/object: e.g. new Gtk.Application()
|
|
98
|
+
if (cmd.action === 'NewProp') {
|
|
99
|
+
const target = objectStore.get(cmd.targetId);
|
|
100
|
+
const Type = target[cmd.property];
|
|
101
|
+
if (!Type) throw new Error("Constructor not found: " + cmd.property);
|
|
102
|
+
const argsArray = (cmd.args || []).map(a => ResolveArg(a));
|
|
103
|
+
const instance = new Type(...argsArray);
|
|
104
|
+
return ConvertToProtocol(instance);
|
|
105
|
+
}
|
|
106
|
+
if (cmd.action === 'Get') {
|
|
107
|
+
const target = objectStore.get(cmd.targetId);
|
|
108
|
+
let val;
|
|
109
|
+
try { val = target[cmd.property]; } catch(e) { return { type: 'null' }; }
|
|
110
|
+
if (typeof val === 'function') return { type: 'function' };
|
|
111
|
+
return ConvertToProtocol(val);
|
|
112
|
+
}
|
|
113
|
+
if (cmd.action === 'Invoke') {
|
|
114
|
+
const target = objectStore.get(cmd.targetId);
|
|
115
|
+
const argsArray = (cmd.args || []).map(a => ResolveArg(a));
|
|
116
|
+
const func = target[cmd.methodName];
|
|
117
|
+
if (!func) throw new Error("Method not found: " + cmd.methodName);
|
|
118
|
+
const res = func.apply(target, argsArray);
|
|
119
|
+
return ConvertToProtocol(res);
|
|
120
|
+
}
|
|
121
|
+
if (cmd.action === 'Set') {
|
|
122
|
+
const target = objectStore.get(cmd.targetId);
|
|
123
|
+
target[cmd.property] = ResolveArg(cmd.value);
|
|
124
|
+
return { type: 'void' };
|
|
125
|
+
}
|
|
126
|
+
if (cmd.action === 'Release') {
|
|
127
|
+
objectStore.delete(cmd.targetId);
|
|
128
|
+
return { type: 'void' };
|
|
129
|
+
}
|
|
130
|
+
if (cmd.action === 'Print') {
|
|
131
|
+
print(...cmd.args.map(a => ResolveArg(a)));
|
|
132
|
+
return { type: 'void' };
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unknown Action ${cmd.action}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function bindIPCEvent() {
|
|
138
|
+
const channel = GLib.IOChannel.unix_new(3);
|
|
139
|
+
GLib.io_add_watch(channel, GLib.PRIORITY_DEFAULT, GLib.IOCondition.IN, (channel, condition) => {
|
|
140
|
+
try {
|
|
141
|
+
const [line] = dataIn.read_line_utf8(null);
|
|
142
|
+
if (!line) {
|
|
143
|
+
System.exit(0);
|
|
144
|
+
return GLib.SOURCE_REMOVE;
|
|
145
|
+
}
|
|
146
|
+
const cmd = JSON.parse(line);
|
|
147
|
+
|
|
148
|
+
let response;
|
|
149
|
+
try { response = executeCommand(cmd); }
|
|
150
|
+
catch (e) { response = { type: 'error', message: e.toString() }; }
|
|
151
|
+
|
|
152
|
+
dataOut.put_string(JSON.stringify(response) + '\n', null);
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
return GLib.SOURCE_CONTINUE;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
print("GJS IPC Host: Initialization Complete.");
|
|
159
|
+
bindIPCEvent();
|
|
160
|
+
|
|
161
|
+
const mainLoop = GLib.MainLoop.new(null, false);
|
|
162
|
+
mainLoop.run();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as cp from 'node:child_process';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { IpcSync } from './ipc.ts';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const gcRegistry = new FinalizationRegistry((id: string) => {
|
|
13
|
+
try { if (ipc) ipc.send({ action: 'Release', targetId: id }); } catch {}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const callbackRegistry = new Map<string, Function>();
|
|
17
|
+
|
|
18
|
+
let ipc: IpcSync | null = null;
|
|
19
|
+
let proc: cp.ChildProcess | null = null;
|
|
20
|
+
let initialized = false;
|
|
21
|
+
let reqPath = '';
|
|
22
|
+
let resPath = '';
|
|
23
|
+
|
|
24
|
+
function cleanup() {
|
|
25
|
+
if (!initialized) return;
|
|
26
|
+
initialized = false;
|
|
27
|
+
|
|
28
|
+
if (ipc) try { ipc.close(); } catch {}
|
|
29
|
+
if (proc && !proc.killed) try { proc.kill('SIGKILL'); } catch {}
|
|
30
|
+
if (fs.existsSync(reqPath)) try { fs.unlinkSync(reqPath); } catch {}
|
|
31
|
+
if (fs.existsSync(resPath)) try { fs.unlinkSync(resPath); } catch {}
|
|
32
|
+
|
|
33
|
+
proc = null;
|
|
34
|
+
ipc = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findGjsPath(): string {
|
|
38
|
+
try {
|
|
39
|
+
const result = cp.execSync('which gjs', { encoding: 'utf-8' }).trim();
|
|
40
|
+
return result || 'gjs';
|
|
41
|
+
} catch {
|
|
42
|
+
return 'gjs';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initialize() {
|
|
47
|
+
if (initialized) return;
|
|
48
|
+
|
|
49
|
+
const token = `${process.pid}-${Date.now()}`;
|
|
50
|
+
reqPath = path.join(os.tmpdir(), `gjs-req-${token}.pipe`);
|
|
51
|
+
resPath = path.join(os.tmpdir(), `gjs-res-${token}.pipe`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
cp.execSync(`mkfifo "${reqPath}"`);
|
|
55
|
+
cp.execSync(`mkfifo "${resPath}"`);
|
|
56
|
+
} catch(e) {
|
|
57
|
+
console.error("Failed to create Unix FIFOs");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const scriptPath = path.join(__dirname, '..', 'scripts', 'host.js');
|
|
62
|
+
const gjsPath = findGjsPath();
|
|
63
|
+
proc = cp.spawn('bash', ['-c', `exec "${gjsPath}" -m "${scriptPath}" 3<"${reqPath}" 4>"${resPath}"`], {
|
|
64
|
+
stdio: 'inherit',
|
|
65
|
+
env: process.env
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
proc.unref();
|
|
69
|
+
|
|
70
|
+
process.on('beforeExit', () => { cleanup(); process.exit(0); });
|
|
71
|
+
process.on('exit', cleanup);
|
|
72
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
73
|
+
process.on('uncaughtException', (err) => {
|
|
74
|
+
console.error('Node.js Exception:', err);
|
|
75
|
+
cleanup();
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const fdWrite = fs.openSync(reqPath, 'w');
|
|
80
|
+
const fdRead = fs.openSync(resPath, 'r');
|
|
81
|
+
|
|
82
|
+
ipc = new IpcSync(fdRead, fdWrite, (res: any) => {
|
|
83
|
+
const cb = callbackRegistry.get(res.callbackId!);
|
|
84
|
+
if (cb) {
|
|
85
|
+
const wrappedArgs = (res.args || []).map((arg: any) => createProxy(arg));
|
|
86
|
+
return cb(...wrappedArgs);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
(globalThis as any).print = (...args: any[]) => {
|
|
92
|
+
ipc!.send({ action: 'Print', args: args.map(wrapArg) });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
initialized = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wrapArg(arg: any): any {
|
|
99
|
+
if (arg === null || arg === undefined) return { type: 'null' };
|
|
100
|
+
if (arg.__ref) return { type: 'ref', id: arg.__ref };
|
|
101
|
+
|
|
102
|
+
if (arg instanceof Uint8Array) {
|
|
103
|
+
return { type: 'uint8array', value: Array.from(arg) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof arg === 'function') {
|
|
107
|
+
const cbId = `cb_${Date.now()}_${Math.random()}`;
|
|
108
|
+
callbackRegistry.set(cbId, arg);
|
|
109
|
+
return { type: 'callback', callbackId: cbId };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Array.isArray(arg)) return { type: 'array', value: arg.map(wrapArg) };
|
|
113
|
+
|
|
114
|
+
if (typeof arg === 'object') {
|
|
115
|
+
const plainObj: any = {};
|
|
116
|
+
for (let k in arg) plainObj[k] = wrapArg(arg[k]);
|
|
117
|
+
return { type: 'object', value: plainObj };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { type: 'primitive', value: arg };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createProxy(meta: any): any {
|
|
124
|
+
if (meta.type === 'primitive' || meta.type === 'null') return meta.value;
|
|
125
|
+
if (meta.type === 'array') return meta.value.map((item: any) => createProxy(item));
|
|
126
|
+
if (meta.type !== 'ref') return undefined;
|
|
127
|
+
|
|
128
|
+
const id = meta.id!;
|
|
129
|
+
const stub = function() {};
|
|
130
|
+
|
|
131
|
+
const proxy = new Proxy(stub, {
|
|
132
|
+
get: (target: any, prop: string | symbol) => {
|
|
133
|
+
if (prop === '__ref') return id;
|
|
134
|
+
if (typeof prop !== 'string') return undefined;
|
|
135
|
+
|
|
136
|
+
const val = ipc!.send({ action: 'Get', targetId: id, property: prop });
|
|
137
|
+
|
|
138
|
+
if (val && val.type === 'function') {
|
|
139
|
+
return new Proxy(function() {}, {
|
|
140
|
+
apply: (t, thisArg, args) => {
|
|
141
|
+
const netArgs = args.map(wrapArg);
|
|
142
|
+
const res = ipc!.send({ action: 'Invoke', targetId: id, methodName: prop, args: netArgs });
|
|
143
|
+
return createProxy(res);
|
|
144
|
+
},
|
|
145
|
+
construct: (t, args) => {
|
|
146
|
+
const netArgs = args.map(wrapArg);
|
|
147
|
+
const res = ipc!.send({ action: 'NewProp', targetId: id, property: prop, args: netArgs });
|
|
148
|
+
return createProxy(res);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return createProxy(val);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
set: (target: any, prop: string | symbol, value: any) => {
|
|
156
|
+
if (typeof prop !== 'string') return false;
|
|
157
|
+
ipc!.send({ action: 'Set', targetId: id, property: prop, value: wrapArg(value) });
|
|
158
|
+
return true;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
construct: (target: any, args: any[]) => {
|
|
162
|
+
const netArgs = args.map(wrapArg);
|
|
163
|
+
const res = ipc!.send({ action: 'New', typeId: id, args: netArgs });
|
|
164
|
+
return createProxy(res);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
gcRegistry.register(proxy, id);
|
|
169
|
+
return proxy;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function init() {
|
|
173
|
+
initialize();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function loadGiNamespace(namespace: string, version: string) {
|
|
177
|
+
initialize();
|
|
178
|
+
const res = ipc!.send({ action: 'LoadNamespace', namespace, version });
|
|
179
|
+
return createProxy(res);
|
|
180
|
+
}
|
package/src/ipc.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/ipc.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Use a global/shared read buffer to handle redundant data across calls
|
|
5
|
+
let readBuffer = Buffer.alloc(0);
|
|
6
|
+
|
|
7
|
+
export function readLineSync(fd: number): string | null {
|
|
8
|
+
while (true) {
|
|
9
|
+
// 1. If the buffer already contains a complete line, extract and return it with minimal overhead
|
|
10
|
+
const newlineIdx = readBuffer.indexOf(10); // 10 is the ASCII code for \n
|
|
11
|
+
if (newlineIdx !== -1) {
|
|
12
|
+
const line = readBuffer.subarray(0, newlineIdx).toString('utf8');
|
|
13
|
+
readBuffer = readBuffer.subarray(newlineIdx + 1);
|
|
14
|
+
return line;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Otherwise, try to read a large chunk from the pipe
|
|
18
|
+
const chunk = Buffer.alloc(8192); // Attempt to read 8KB each time
|
|
19
|
+
let bytesRead = 0;
|
|
20
|
+
try {
|
|
21
|
+
bytesRead = fs.readSync(fd, chunk, 0, 8192, null);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (bytesRead === 0) {
|
|
27
|
+
if (readBuffer.length === 0) return null;
|
|
28
|
+
const line = readBuffer.toString('utf8');
|
|
29
|
+
readBuffer = Buffer.alloc(0);
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Append the newly read data to the buffer
|
|
34
|
+
readBuffer = Buffer.concat([readBuffer, chunk.subarray(0, bytesRead)]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class IpcSync {
|
|
39
|
+
private exited: boolean = false;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private fdRead: number,
|
|
43
|
+
private fdWrite: number,
|
|
44
|
+
private onEvent: (msg: any) => any
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
send(cmd: any): any {
|
|
48
|
+
if (this.exited) return { type: 'exit' };
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
fs.writeSync(this.fdWrite, JSON.stringify(cmd) + '\n');
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new Error("Pipe closed (Write failed)");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
const line = readLineSync(this.fdRead);
|
|
58
|
+
if (line === null) throw new Error("Pipe closed (Read EOF)");
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
|
|
61
|
+
let res: any;
|
|
62
|
+
try {
|
|
63
|
+
res = JSON.parse(line);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
throw new Error(`Invalid JSON from host: ${line}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (res.type === 'event') {
|
|
69
|
+
let result = null;
|
|
70
|
+
try {
|
|
71
|
+
result = this.onEvent(res);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error("Callback Error:", e);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reply = { type: 'reply', result: result };
|
|
77
|
+
try {
|
|
78
|
+
fs.writeSync(this.fdWrite, JSON.stringify(reply) + '\n');
|
|
79
|
+
} catch {}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (res.type === 'error') throw new Error(`GJS Host Error: ${res.message}`);
|
|
84
|
+
|
|
85
|
+
if (res.type === 'exit') {
|
|
86
|
+
this.exited = true;
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
close() {
|
|
95
|
+
this.exited = true;
|
|
96
|
+
if (this.fdRead) try { fs.closeSync(this.fdRead); } catch {}
|
|
97
|
+
if (this.fdWrite) try { fs.closeSync(this.fdWrite); } catch {}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/start.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// start.js
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
console.error('Usage: node start.js <script.ts> [--runtime=node|bun|deno]');
|
|
12
|
+
console.error(' bun start.js <script.ts> [--runtime=node|bun|deno]');
|
|
13
|
+
console.error(' deno run start.js <script.ts> [--runtime=node|bun|deno]');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let runtime = null;
|
|
18
|
+
let targetScript = null;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
if (args[i] === '--runtime' && args[i + 1]) {
|
|
22
|
+
runtime = args[i + 1].toLowerCase();
|
|
23
|
+
i++;
|
|
24
|
+
} else if (!args[i].startsWith('--')) {
|
|
25
|
+
targetScript = args[i];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!targetScript) {
|
|
30
|
+
console.error('Error: No script specified');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
targetScript = path.resolve(targetScript);
|
|
35
|
+
const hookUrl = new URL('./hook.js', import.meta.url).href;
|
|
36
|
+
|
|
37
|
+
if (!runtime) {
|
|
38
|
+
runtime = detectRuntime();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const validRuntimes = ['node', 'bun', 'deno'];
|
|
42
|
+
if (!validRuntimes.includes(runtime)) {
|
|
43
|
+
console.error(`Error: Invalid runtime "${runtime}". Must be one of: ${validRuntimes.join(', ')}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`Starting ${runtime.charAt(0).toUpperCase() + runtime.slice(1)}-GJS execution context...`);
|
|
48
|
+
|
|
49
|
+
let proc;
|
|
50
|
+
|
|
51
|
+
switch (runtime) {
|
|
52
|
+
case 'bun':
|
|
53
|
+
proc = spawnBun(targetScript);
|
|
54
|
+
break;
|
|
55
|
+
case 'deno':
|
|
56
|
+
proc = spawnDeno(targetScript);
|
|
57
|
+
break;
|
|
58
|
+
case 'node':
|
|
59
|
+
default:
|
|
60
|
+
proc = spawnNode(targetScript, hookUrl);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
proc.on('exit', (code) => {
|
|
65
|
+
process.exit(code || 0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function detectRuntime() {
|
|
69
|
+
if (typeof Bun !== 'undefined') return 'bun';
|
|
70
|
+
if (typeof Deno !== 'undefined') return 'deno';
|
|
71
|
+
return 'node';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spawnNode(targetScript, hookUrl) {
|
|
75
|
+
return spawn(process.execPath, [
|
|
76
|
+
'--no-warnings',
|
|
77
|
+
'--experimental-loader', hookUrl,
|
|
78
|
+
'--experimental-transform-types',
|
|
79
|
+
targetScript
|
|
80
|
+
], {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
env: process.env
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function spawnBun(targetScript) {
|
|
87
|
+
return spawn('bun', [
|
|
88
|
+
'run',
|
|
89
|
+
targetScript
|
|
90
|
+
], {
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
env: process.env
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function spawnDeno(targetScript) {
|
|
97
|
+
return spawn('deno', [
|
|
98
|
+
'run',
|
|
99
|
+
'--allow-all',
|
|
100
|
+
targetScript
|
|
101
|
+
], {
|
|
102
|
+
stdio: 'inherit',
|
|
103
|
+
env: process.env
|
|
104
|
+
});
|
|
105
|
+
}
|