positron.js 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1123 @@
1
+ #include <gtk/gtk.h>
2
+ #include <webkit2/webkit2.h>
3
+ #include <libsoup/soup.h>
4
+ #include <json-glib/json-glib.h>
5
+ #include <string>
6
+ #include <unordered_map>
7
+ #include <vector>
8
+ #include <iostream>
9
+ #include <cstdlib>
10
+ #include <sstream>
11
+ #include <fstream>
12
+ #include <climits>
13
+ #include <unistd.h>
14
+ #include <sys/types.h>
15
+ #include <sys/socket.h>
16
+ #include <netinet/in.h>
17
+ #include <signal.h>
18
+ #include <gdk/gdk.h>
19
+ #include <gdk-pixbuf/gdk-pixbuf.h>
20
+ #include <libnotify/notify.h>
21
+
22
+ using namespace std;
23
+
24
+ // ─── Globals ────────────────────────────────────────────────────────────────
25
+
26
+ struct WindowState {
27
+ GtkWidget* window;
28
+ WebKitWebView* webview;
29
+ };
30
+
31
+ unordered_map<int, WindowState> windowMap;
32
+ SoupWebsocketConnection* ws_conn = nullptr;
33
+ string auth_token = "";
34
+
35
+ extern unordered_map<string, void(*)(int, vector<string>)> getExtensionRegistry();
36
+
37
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
38
+
39
+ static string js_escape(const string& s) {
40
+ string out;
41
+ out.reserve(s.size());
42
+ for (char c : s) {
43
+ if (c == '\\') out += "\\\\";
44
+ else if (c == '`') out += "\\`";
45
+ else out += c;
46
+ }
47
+ return out;
48
+ }
49
+
50
+ void send_ipc(int window_id, const string& event, JsonBuilder* payload_builder) {
51
+ if (!ws_conn) return;
52
+
53
+ JsonNode* data_node = json_builder_get_root(payload_builder);
54
+
55
+ JsonBuilder* root_builder = json_builder_new();
56
+ json_builder_begin_object(root_builder);
57
+ json_builder_set_member_name(root_builder, "windowId");
58
+ json_builder_add_int_value(root_builder, window_id);
59
+ json_builder_set_member_name(root_builder, "event");
60
+ json_builder_add_string_value(root_builder, event.c_str());
61
+ json_builder_set_member_name(root_builder, "data");
62
+ json_builder_add_value(root_builder, data_node);
63
+ json_builder_end_object(root_builder);
64
+
65
+ JsonNode* root = json_builder_get_root(root_builder);
66
+ JsonGenerator* gen = json_generator_new();
67
+ json_generator_set_root(gen, root);
68
+ gsize length;
69
+ gchar* str = json_generator_to_data(gen, &length);
70
+
71
+ soup_websocket_connection_send_text(ws_conn, str);
72
+
73
+ g_free(str);
74
+ g_object_unref(gen);
75
+ json_node_free(root);
76
+ g_object_unref(root_builder);
77
+ g_object_unref(payload_builder);
78
+ }
79
+
80
+ void send_reply(int window_id, const string& event, const string& key, const string& val) {
81
+ JsonBuilder* b = json_builder_new();
82
+ json_builder_begin_object(b);
83
+ json_builder_set_member_name(b, key.c_str());
84
+ json_builder_add_string_value(b, val.c_str());
85
+ json_builder_end_object(b);
86
+ send_ipc(window_id, event, b);
87
+ }
88
+
89
+ static string get_last_arg(const vector<string>& args, const string& fallback) {
90
+ return args.empty() ? fallback : args.back();
91
+ }
92
+
93
+ // ─── Forward-declare JS evaluation ──────────────────────────────────────────
94
+
95
+ static void evaluate_js(int window_id, const string& script);
96
+
97
+ // ─── Preload script ───────────────────────────────────────────────────────────
98
+
99
+ static string make_preload(int window_id) {
100
+ return R"JS(
101
+ (function () {
102
+ if (window.__ipcInstalled) return;
103
+ window.__ipcInstalled = true;
104
+
105
+ const _listeners = {};
106
+
107
+ window.ipc = {
108
+ send(channel, payload = null) {
109
+ if (typeof channel !== 'string') { console.warn('[ipc] send() failed: channel must be a string'); return; }
110
+ window.webkit.messageHandlers.ipc.postMessage({ channel, payload });
111
+ },
112
+ on(channel, fn) {
113
+ if (!_listeners[channel]) _listeners[channel] = [];
114
+ _listeners[channel].push(fn);
115
+ },
116
+ off(channel, fn) {
117
+ if (!_listeners[channel]) return;
118
+ _listeners[channel] = _listeners[channel].filter(f => f !== fn);
119
+ },
120
+ _emit(channel, payload) {
121
+ (_listeners[channel] || []).forEach(fn => fn(payload));
122
+ }
123
+ };
124
+ })();
125
+ )JS";
126
+ }
127
+
128
+ // ─── WebView message callback (renderer → Node) ──────────────────────────────
129
+
130
+ static void on_script_message(WebKitUserContentManager* mgr,
131
+ WebKitJavascriptResult* result,
132
+ gpointer user_data) {
133
+ int window_id = GPOINTER_TO_INT(user_data);
134
+
135
+ JSCValue* val = webkit_javascript_result_get_js_value(result);
136
+ if (!val || !jsc_value_is_object(val)) {
137
+ webkit_javascript_result_unref(result);
138
+ return;
139
+ }
140
+
141
+ JSCValue* ch_val = jsc_value_object_get_property(val, "channel");
142
+ JSCValue* pay_val = jsc_value_object_get_property(val, "payload");
143
+
144
+ gchar* channel = jsc_value_to_string(ch_val);
145
+ gchar* payload = jsc_value_is_null(pay_val) ? g_strdup("null") : jsc_value_to_json(pay_val, 0);
146
+
147
+ JsonBuilder* b = json_builder_new();
148
+ json_builder_begin_object(b);
149
+ json_builder_set_member_name(b, "channel");
150
+ json_builder_add_string_value(b, channel ? channel : "");
151
+ json_builder_set_member_name(b, "payload");
152
+ json_builder_add_string_value(b, payload ? payload : "null");
153
+ json_builder_end_object(b);
154
+ send_ipc(window_id, "ipcMessage", b);
155
+
156
+ g_free(channel);
157
+ g_free(payload);
158
+ g_object_unref(ch_val);
159
+ g_object_unref(pay_val);
160
+ webkit_javascript_result_unref(result);
161
+ }
162
+
163
+ // ─── Navigation callbacks ─────────────────────────────────────────────────────
164
+
165
+ static void on_load_changed(WebKitWebView* webview,
166
+ WebKitLoadEvent event,
167
+ gpointer user_data) {
168
+ if (event != WEBKIT_LOAD_FINISHED) return;
169
+
170
+ int window_id = GPOINTER_TO_INT(user_data);
171
+ const gchar* uri = webkit_web_view_get_uri(webview);
172
+ bool is_file = uri && g_str_has_prefix(uri, "file://");
173
+ string reply_event = is_file
174
+ ? ("loadFile-reply-" + to_string(window_id))
175
+ : ("loadURL-reply-" + to_string(window_id));
176
+
177
+ const gchar* title = webkit_web_view_get_title(webview);
178
+ bool can_back = webkit_web_view_can_go_back(webview);
179
+ bool can_forward = webkit_web_view_can_go_forward(webview);
180
+
181
+ JsonBuilder* b = json_builder_new();
182
+ json_builder_begin_object(b);
183
+ json_builder_set_member_name(b, "url"); json_builder_add_string_value(b, uri ? uri : "");
184
+ json_builder_set_member_name(b, "title"); json_builder_add_string_value(b, title ? title : "");
185
+ json_builder_set_member_name(b, "canGoBack"); json_builder_add_string_value(b, can_back ? "true" : "false");
186
+ json_builder_set_member_name(b, "canGoForward"); json_builder_add_string_value(b, can_forward ? "true" : "false");
187
+ json_builder_end_object(b);
188
+ send_ipc(window_id, reply_event, b);
189
+ }
190
+
191
+ // ─── Window close callback ────────────────────────────────────────────────────
192
+
193
+ static gboolean on_delete_event(GtkWidget* widget, GdkEvent* event, gpointer data) {
194
+ int window_id = GPOINTER_TO_INT(data);
195
+
196
+ JsonBuilder* b = json_builder_new();
197
+ json_builder_begin_object(b);
198
+ json_builder_end_object(b);
199
+ send_ipc(window_id, "window-close-requested", b);
200
+
201
+ // Prevent default destroy — let JS decide
202
+ return TRUE;
203
+ }
204
+
205
+ static void on_destroy(GtkWidget* widget, gpointer data) {
206
+ int window_id = GPOINTER_TO_INT(data);
207
+ windowMap.erase(window_id);
208
+
209
+ JsonBuilder* b = json_builder_new();
210
+ json_builder_begin_object(b);
211
+ json_builder_end_object(b);
212
+ send_ipc(window_id, "windowClosed", b);
213
+
214
+ if (windowMap.empty()) gtk_main_quit();
215
+ }
216
+
217
+ // ─── JS evaluation ────────────────────────────────────────────────────────────
218
+
219
+ static void evaluate_js(int window_id, const string& script) {
220
+ auto it = windowMap.find(window_id);
221
+ if (it == windowMap.end()) return;
222
+ #if WEBKIT_CHECK_VERSION(2, 40, 0)
223
+ webkit_web_view_evaluate_javascript(it->second.webview, script.c_str(), -1, nullptr, nullptr, nullptr, nullptr, nullptr);
224
+ #else
225
+ webkit_web_view_run_javascript(it->second.webview, script.c_str(), nullptr, nullptr, nullptr);
226
+ #endif
227
+ }
228
+
229
+ // ─── Screenshot callback ──────────────────────────────────────────────────────
230
+
231
+ static void on_snapshot_ready(GObject* source, GAsyncResult* res, gpointer user_data) {
232
+ int window_id = GPOINTER_TO_INT(user_data);
233
+ GError* error = nullptr;
234
+
235
+ cairo_surface_t* surface = webkit_web_view_get_snapshot_finish(
236
+ WEBKIT_WEB_VIEW(source), res, &error);
237
+
238
+ if (error) {
239
+ cerr << "[Linux Core] capturePage failed: " << error->message << endl;
240
+ g_error_free(error);
241
+ return;
242
+ }
243
+
244
+ // Encode to PNG in memory
245
+ GdkPixbuf* pixbuf = gdk_pixbuf_get_from_surface(
246
+ surface, 0, 0,
247
+ cairo_image_surface_get_width(surface),
248
+ cairo_image_surface_get_height(surface));
249
+ cairo_surface_destroy(surface);
250
+
251
+ if (!pixbuf) return;
252
+
253
+ gchar* buffer = nullptr;
254
+ gsize size = 0;
255
+ gdk_pixbuf_save_to_buffer(pixbuf, &buffer, &size, "png", &error, nullptr);
256
+ g_object_unref(pixbuf);
257
+
258
+ if (error) { g_error_free(error); return; }
259
+
260
+ // Base64 encode
261
+ gchar* b64 = g_base64_encode((guchar*)buffer, size);
262
+ g_free(buffer);
263
+
264
+ send_reply(window_id, "capture-page-result-" + to_string(window_id), "image", b64);
265
+ g_free(b64);
266
+ }
267
+
268
+ // ─── Context-menu builder ─────────────────────────────────────────────────────
269
+
270
+ struct MenuActionData {
271
+ int window_id;
272
+ string label;
273
+ string channel;
274
+ };
275
+
276
+ static void on_menu_item_activate(GtkMenuItem* item, gpointer data) {
277
+ auto* d = static_cast<MenuActionData*>(data);
278
+ JsonBuilder* b = json_builder_new();
279
+ json_builder_begin_object(b);
280
+ json_builder_set_member_name(b, "label"); json_builder_add_string_value(b, d->label.c_str());
281
+ json_builder_set_member_name(b, "channel"); json_builder_add_string_value(b, d->channel.c_str());
282
+ json_builder_end_object(b);
283
+ send_ipc(d->window_id, "context-menu-action", b);
284
+ delete d;
285
+ }
286
+
287
+ static GtkWidget* build_context_menu(JsonArray* items, int window_id) {
288
+ GtkWidget* menu = gtk_menu_new();
289
+ guint len = json_array_get_length(items);
290
+ for (guint i = 0; i < len; i++) {
291
+ JsonObject* item = json_array_get_object_element(items, i);
292
+ const gchar* label = json_object_get_string_member(item, "label");
293
+ if (!label || g_strcmp0(label, "-") == 0) {
294
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
295
+ continue;
296
+ }
297
+ GtkWidget* mi = gtk_menu_item_new_with_label(label);
298
+ auto* d = new MenuActionData{ window_id, label,
299
+ json_object_has_member(item, "channel")
300
+ ? json_object_get_string_member(item, "channel") : "" };
301
+ g_signal_connect(mi, "activate", G_CALLBACK(on_menu_item_activate), d);
302
+ if (json_object_has_member(item, "items")) {
303
+ GtkWidget* sub = build_context_menu(json_object_get_array_member(item, "items"), window_id);
304
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), sub);
305
+ }
306
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
307
+ }
308
+ gtk_widget_show_all(menu);
309
+ return menu;
310
+ }
311
+
312
+ // ─── Command Handler ──────────────────────────────────────────────────────────
313
+
314
+ void handle_command(int window_id, const string& command, const vector<string>& args) {
315
+
316
+ // Convenience accessors
317
+ auto get_win = [&]() -> GtkWidget* {
318
+ auto it = windowMap.find(window_id);
319
+ return it != windowMap.end() ? it->second.window : nullptr;
320
+ };
321
+ auto get_wv = [&]() -> WebKitWebView* {
322
+ auto it = windowMap.find(window_id);
323
+ return it != windowMap.end() ? it->second.webview : nullptr;
324
+ };
325
+
326
+ // ── createWindow ──────────────────────────────────────────────────────────
327
+ if (command == "createWindow") {
328
+ int width = args.size() > 0 ? stoi(args[0]) : 800;
329
+ int height = args.size() > 1 ? stoi(args[1]) : 600;
330
+ bool closable = args.size() > 2 ? args[2] == "true" : true;
331
+ bool resizable = args.size() > 3 ? args[3] == "true" : true;
332
+ bool minimizable = args.size() > 4 ? args[4] == "true" : true;
333
+
334
+ GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
335
+ gtk_window_set_default_size(GTK_WINDOW(window), width, height);
336
+ gtk_window_set_resizable(GTK_WINDOW(window), resizable);
337
+
338
+ // Minimise button
339
+ string hints = "";
340
+ if (!minimizable) {
341
+ gtk_widget_realize(window);
342
+ GdkWMFunction funcs = (GdkWMFunction)(GDK_FUNC_MOVE | GDK_FUNC_RESIZE |
343
+ (closable ? GDK_FUNC_CLOSE : (GdkWMFunction)0));
344
+ gdk_window_set_functions(gtk_widget_get_window(window), funcs);
345
+ }
346
+
347
+ // User content manager for renderer IPC
348
+ WebKitUserContentManager* ucm = webkit_user_content_manager_new();
349
+ g_signal_connect(ucm, "script-message-received::ipc",
350
+ G_CALLBACK(on_script_message), GINT_TO_POINTER(window_id));
351
+ webkit_user_content_manager_register_script_message_handler(ucm, "ipc");
352
+
353
+ // Inject preload
354
+ string preload_src = make_preload(window_id);
355
+ WebKitUserScript* preload = webkit_user_script_new(
356
+ preload_src.c_str(),
357
+ WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
358
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
359
+ nullptr, nullptr);
360
+ webkit_user_content_manager_add_script(ucm, preload);
361
+ webkit_user_script_unref(preload);
362
+
363
+ WebKitWebView* webview = WEBKIT_WEB_VIEW(
364
+ webkit_web_view_new_with_user_content_manager(ucm));
365
+
366
+ g_signal_connect(webview, "load-changed", G_CALLBACK(on_load_changed), GINT_TO_POINTER(window_id));
367
+ g_signal_connect(window, "delete-event", G_CALLBACK(on_delete_event), GINT_TO_POINTER(window_id));
368
+ g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), GINT_TO_POINTER(window_id));
369
+
370
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(webview));
371
+ windowMap[window_id] = { window, webview };
372
+ gtk_widget_show_all(window);
373
+ return;
374
+ }
375
+
376
+ // ── terminate ──────────────────────────────────────────────────────────────
377
+ if (command == "terminate") { gtk_main_quit(); return; }
378
+
379
+ // ── triggerCloseSequence ───────────────────────────────────────────────────
380
+ if (command == "triggerCloseSequence") {
381
+ GtkWidget* w = get_win();
382
+ if (w) {
383
+ // Emit delete-event so JS can intercept
384
+ gboolean handled = FALSE;
385
+ g_signal_emit_by_name(w, "delete-event", nullptr, &handled);
386
+ }
387
+ return;
388
+ }
389
+
390
+ // ── forceCloseWindow ──────────────────────────────────────────────────────
391
+ if (command == "forceCloseWindow") {
392
+ GtkWidget* w = get_win();
393
+ if (w) {
394
+ g_signal_handlers_disconnect_by_func(w, (gpointer)on_delete_event, GINT_TO_POINTER(window_id));
395
+ gtk_widget_destroy(w);
396
+ windowMap.erase(window_id);
397
+ if (windowMap.empty()) gtk_main_quit();
398
+ }
399
+ return;
400
+ }
401
+
402
+ // ── setTitle ──────────────────────────────────────────────────────────────
403
+ if (command == "setTitle") {
404
+ GtkWidget* w = get_win();
405
+ if (w && !args.empty()) gtk_window_set_title(GTK_WINDOW(w), args[0].c_str());
406
+ return;
407
+ }
408
+
409
+ // ── getTitle ──────────────────────────────────────────────────────────────
410
+ if (command == "getTitle") {
411
+ GtkWidget* w = get_win();
412
+ const gchar* title = w ? gtk_window_get_title(GTK_WINDOW(w)) : "";
413
+ send_reply(window_id, get_last_arg(args, "getTitle-reply-" + to_string(window_id)), "title", title ? title : "");
414
+ return;
415
+ }
416
+
417
+ // ── loadURL ───────────────────────────────────────────────────────────────
418
+ if (command == "loadURL") {
419
+ WebKitWebView* wv = get_wv();
420
+ if (wv && !args.empty()) webkit_web_view_load_uri(wv, args[0].c_str());
421
+ return;
422
+ }
423
+
424
+ // ── loadFile ──────────────────────────────────────────────────────────────
425
+ if (command == "loadFile") {
426
+ WebKitWebView* wv = get_wv();
427
+ if (wv && !args.empty()) {
428
+ string file_uri = "file://" + args[0];
429
+ webkit_web_view_load_uri(wv, file_uri.c_str());
430
+ }
431
+ return;
432
+ }
433
+
434
+ // ── getURL ────────────────────────────────────────────────────────────────
435
+ if (command == "getURL") {
436
+ WebKitWebView* wv = get_wv();
437
+ const gchar* uri = wv ? webkit_web_view_get_uri(wv) : "";
438
+ send_reply(window_id, get_last_arg(args, "getURL-reply-" + to_string(window_id)), "url", uri ? uri : "");
439
+ return;
440
+ }
441
+
442
+ // ── hideWindow ────────────────────────────────────────────────────────────
443
+ if (command == "hideWindow" || command == "hide") {
444
+ GtkWidget* w = get_win();
445
+ if (w) gtk_widget_hide(w);
446
+ return;
447
+ }
448
+
449
+ // ── showWindow ────────────────────────────────────────────────────────────
450
+ if (command == "showWindow" || command == "show") {
451
+ GtkWidget* w = get_win();
452
+ if (w) { gtk_widget_show_all(w); gtk_window_present(GTK_WINDOW(w)); }
453
+ return;
454
+ }
455
+
456
+ // ── focus ────────────────────────────────────────────────────────────────
457
+ if (command == "focus") {
458
+ GtkWidget* w = get_win();
459
+ if (w) gtk_window_present(GTK_WINDOW(w));
460
+ return;
461
+ }
462
+
463
+ // ── isVisible ────────────────────────────────────────────────────────────
464
+ if (command == "isVisible") {
465
+ GtkWidget* w = get_win();
466
+ bool vis = w && gtk_widget_is_visible(w);
467
+ send_reply(window_id, get_last_arg(args, "isVisible-reply-" + to_string(window_id)), "isVisible", vis ? "true" : "false");
468
+ return;
469
+ }
470
+
471
+ // ── isFocused ────────────────────────────────────────────────────────────
472
+ if (command == "isFocused") {
473
+ GtkWidget* w = get_win();
474
+ bool focused = w && gtk_window_is_active(GTK_WINDOW(w));
475
+ send_reply(window_id, get_last_arg(args, "isFocused-reply-" + to_string(window_id)), "isFocused", focused ? "true" : "false");
476
+ return;
477
+ }
478
+
479
+ // ── getFocusedWindowId ───────────────────────────────────────────────────
480
+ if (command == "getFocusedWindowId") {
481
+ int focused_id = -1;
482
+ for (auto& kv : windowMap)
483
+ if (gtk_window_is_active(GTK_WINDOW(kv.second.window))) { focused_id = kv.first; break; }
484
+ send_reply(window_id, get_last_arg(args, "getFocusedWindowId-reply-" + to_string(window_id)), "focusedWindowId", to_string(focused_id));
485
+ return;
486
+ }
487
+
488
+ // ── isFullscreen ─────────────────────────────────────────────────────────
489
+ if (command == "isFullscreen") {
490
+ GtkWidget* w = get_win();
491
+ GdkWindow* gdk_win = w ? gtk_widget_get_window(w) : nullptr;
492
+ bool fs = gdk_win &&
493
+ (gdk_window_get_state(gdk_win) & GDK_WINDOW_STATE_FULLSCREEN);
494
+ send_reply(window_id, get_last_arg(args, "isFullscreen-reply-" + to_string(window_id)), "isFullscreen", fs ? "true" : "false");
495
+ return;
496
+ }
497
+
498
+ // ── fullscreen ───────────────────────────────────────────────────────────
499
+ if (command == "fullscreen" || command == "toggleFullscreen") {
500
+ GtkWidget* w = get_win();
501
+ if (!w) return;
502
+ GdkWindow* gdk_win = gtk_widget_get_window(w);
503
+ bool fs = gdk_win && (gdk_window_get_state(gdk_win) & GDK_WINDOW_STATE_FULLSCREEN);
504
+ if (command == "toggleFullscreen" ? true : !fs) {
505
+ if (fs) gtk_window_unfullscreen(GTK_WINDOW(w));
506
+ else gtk_window_fullscreen(GTK_WINDOW(w));
507
+ } else {
508
+ gtk_window_fullscreen(GTK_WINDOW(w));
509
+ }
510
+ return;
511
+ }
512
+
513
+ // ── exitFullscreen ───────────────────────────────────────────────────────
514
+ if (command == "exitFullscreen") {
515
+ GtkWidget* w = get_win();
516
+ if (w) gtk_window_unfullscreen(GTK_WINDOW(w));
517
+ return;
518
+ }
519
+
520
+ // ── resizeWindow / resize ────────────────────────────────────────────────
521
+ if (command == "resizeWindow" || command == "resize") {
522
+ GtkWidget* w = get_win();
523
+ if (w && args.size() >= 2)
524
+ gtk_window_resize(GTK_WINDOW(w), stoi(args[0]), stoi(args[1]));
525
+ return;
526
+ }
527
+
528
+ // ── setBounds ────────────────────────────────────────────────────────────
529
+ if (command == "setBounds") {
530
+ GtkWidget* w = get_win();
531
+ if (w && args.size() >= 4) {
532
+ gtk_window_move(GTK_WINDOW(w), stoi(args[0]), stoi(args[1]));
533
+ gtk_window_resize(GTK_WINDOW(w), stoi(args[2]), stoi(args[3]));
534
+ }
535
+ return;
536
+ }
537
+
538
+ // ── getBounds ────────────────────────────────────────────────────────────
539
+ if (command == "getBounds") {
540
+ GtkWidget* w = get_win();
541
+ int x = 0, y = 0, width = 0, height = 0;
542
+ if (w) { gtk_window_get_position(GTK_WINDOW(w), &x, &y); gtk_window_get_size(GTK_WINDOW(w), &width, &height); }
543
+ JsonBuilder* b = json_builder_new();
544
+ json_builder_begin_object(b);
545
+ json_builder_set_member_name(b, "x"); json_builder_add_string_value(b, to_string(x).c_str());
546
+ json_builder_set_member_name(b, "y"); json_builder_add_string_value(b, to_string(y).c_str());
547
+ json_builder_set_member_name(b, "width"); json_builder_add_string_value(b, to_string(width).c_str());
548
+ json_builder_set_member_name(b, "height"); json_builder_add_string_value(b, to_string(height).c_str());
549
+ json_builder_end_object(b);
550
+ send_ipc(window_id, get_last_arg(args, "getBounds-reply-" + to_string(window_id)), b);
551
+ return;
552
+ }
553
+
554
+ // ── setResizable ─────────────────────────────────────────────────────────
555
+ if (command == "setResizable") {
556
+ GtkWidget* w = get_win();
557
+ if (w && !args.empty()) gtk_window_set_resizable(GTK_WINDOW(w), args[0] == "true");
558
+ return;
559
+ }
560
+
561
+ // ── setAlwaysOnTop ────────────────────────────────────────────────────────
562
+ if (command == "setAlwaysOnTop") {
563
+ GtkWidget* w = get_win();
564
+ if (w && !args.empty()) gtk_window_set_keep_above(GTK_WINDOW(w), args[0] == "true");
565
+ return;
566
+ }
567
+
568
+ // ── reload ───────────────────────────────────────────────────────────────
569
+ if (command == "reload") {
570
+ WebKitWebView* wv = get_wv();
571
+ if (wv) webkit_web_view_reload(wv);
572
+ return;
573
+ }
574
+
575
+ // ── forward / back ───────────────────────────────────────────────────────
576
+ if (command == "forward") {
577
+ WebKitWebView* wv = get_wv();
578
+ if (wv) webkit_web_view_go_forward(wv);
579
+ return;
580
+ }
581
+ if (command == "back") {
582
+ WebKitWebView* wv = get_wv();
583
+ if (wv) webkit_web_view_go_back(wv);
584
+ return;
585
+ }
586
+
587
+ // ── canGoBack / canGoForward ──────────────────────────────────────────────
588
+ if (command == "canGoBack") {
589
+ WebKitWebView* wv = get_wv();
590
+ bool can = wv && webkit_web_view_can_go_back(wv);
591
+ send_reply(window_id, get_last_arg(args, "canGoBack-reply-" + to_string(window_id)), "canGoBack", can ? "true" : "false");
592
+ return;
593
+ }
594
+ if (command == "canGoForward") {
595
+ WebKitWebView* wv = get_wv();
596
+ bool can = wv && webkit_web_view_can_go_forward(wv);
597
+ send_reply(window_id, get_last_arg(args, "canGoForward-reply-" + to_string(window_id)), "canGoForward", can ? "true" : "false");
598
+ return;
599
+ }
600
+
601
+ // ── setUserAgent ─────────────────────────────────────────────────────────
602
+ if (command == "setUserAgent") {
603
+ WebKitWebView* wv = get_wv();
604
+ if (wv && !args.empty()) {
605
+ WebKitSettings* s = webkit_web_view_get_settings(wv);
606
+ webkit_settings_set_user_agent(s, args[0].c_str());
607
+ }
608
+ return;
609
+ }
610
+
611
+ // ── evaluateJS ───────────────────────────────────────────────────────────
612
+ if (command == "evaluateJS") {
613
+ if (args.empty()) return;
614
+ const string& script = args[0];
615
+ const string reply_ch = get_last_arg(args, "evaluateJS-reply-" + to_string(window_id));
616
+ WebKitWebView* wv = get_wv();
617
+ if (!wv) return;
618
+ // capture reply_ch by copy
619
+ struct Ctx { int wid; string reply; };
620
+ auto* ctx = new Ctx{ window_id, reply_ch };
621
+ #if WEBKIT_CHECK_VERSION(2, 40, 0)
622
+ webkit_web_view_evaluate_javascript(wv, script.c_str(), -1, nullptr, nullptr, nullptr,
623
+ [](GObject* src, GAsyncResult* res, gpointer data) {
624
+ auto* ctx = static_cast<Ctx*>(data);
625
+ GError* err = nullptr;
626
+ JSCValue* val = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(src), res, &err);
627
+ string result_str = "null";
628
+ if (err) { g_error_free(err); }
629
+ else if (val) {
630
+ gchar* str = jsc_value_to_json(val, 0);
631
+ if (str) { result_str = str; g_free(str); }
632
+ g_object_unref(val);
633
+ }
634
+ send_reply(ctx->wid, ctx->reply, "result", result_str);
635
+ delete ctx;
636
+ }, ctx);
637
+ #else
638
+ webkit_web_view_run_javascript(wv, script.c_str(), nullptr,
639
+ [](GObject* src, GAsyncResult* res, gpointer data) {
640
+ auto* ctx = static_cast<Ctx*>(data);
641
+ GError* err = nullptr;
642
+ WebKitJavascriptResult* js_result =
643
+ webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(src), res, &err);
644
+ string result_str = "null";
645
+ if (err) { g_error_free(err); }
646
+ else if (js_result) {
647
+ JSCValue* val = webkit_javascript_result_get_js_value(js_result);
648
+ if (val) {
649
+ gchar* str = jsc_value_to_json(val, 0);
650
+ if (str) { result_str = str; g_free(str); }
651
+ }
652
+ webkit_javascript_result_unref(js_result);
653
+ }
654
+ send_reply(ctx->wid, ctx->reply, "result", result_str);
655
+ delete ctx;
656
+ }, ctx);
657
+ #endif
658
+ return;
659
+ }
660
+
661
+ // ── emitToRenderer ────────────────────────────────────────────────────────
662
+ if (command == "emitToRenderer") {
663
+ if (args.size() < 2) return;
664
+ string escaped = js_escape(args[1]);
665
+ string script = "window.ipc._emit(`" + js_escape(args[0]) + "`, JSON.parse(`" + escaped + "`));";
666
+ evaluate_js(window_id, script);
667
+ return;
668
+ }
669
+
670
+ // ── addUserScript ────────────────────────────────────────────────────────
671
+ if (command == "addUserScript") {
672
+ WebKitWebView* wv = get_wv();
673
+ if (!wv || args.empty()) return;
674
+ WebKitUserContentManager* ucm = webkit_web_view_get_user_content_manager(wv);
675
+ WebKitUserScript* script = webkit_user_script_new(
676
+ args[0].c_str(),
677
+ WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
678
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
679
+ nullptr, nullptr);
680
+ webkit_user_content_manager_add_script(ucm, script);
681
+ webkit_user_script_unref(script);
682
+ return;
683
+ }
684
+
685
+ // ── openDevTools ─────────────────────────────────────────────────────────
686
+ if (command == "openDevTools") {
687
+ WebKitWebView* wv = get_wv();
688
+ if (!wv) return;
689
+ WebKitSettings* s = webkit_web_view_get_settings(wv);
690
+ webkit_settings_set_enable_developer_extras(s, TRUE);
691
+ WebKitWebInspector* inspector = webkit_web_view_get_inspector(wv);
692
+ webkit_web_inspector_show(inspector);
693
+ return;
694
+ }
695
+
696
+ // ── alert ────────────────────────────────────────────────────────────────
697
+ if (command == "alert") {
698
+ GtkWidget* w = get_win();
699
+ if (!w || args.empty()) return;
700
+ GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(w),
701
+ GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", args[0].c_str());
702
+ g_signal_connect(dialog, "response", G_CALLBACK(+[](GtkDialog* dlg, gint response, gpointer user_data) {
703
+ gtk_widget_destroy(GTK_WIDGET(dlg));
704
+ }), nullptr);
705
+ gtk_widget_show_all(dialog);
706
+ return;
707
+ }
708
+
709
+ // ── confirm ──────────────────────────────────────────────────────────────
710
+ if (command == "confirm") {
711
+ GtkWidget* w = get_win();
712
+ if (!w || args.empty()) return;
713
+ GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(w),
714
+ GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, "%s", args[0].c_str());
715
+
716
+ struct ConfirmData { int wid; string ch; };
717
+ auto* d = new ConfirmData{ window_id, get_last_arg(args, "confirm-reply-" + to_string(window_id)) };
718
+
719
+ g_signal_connect(dialog, "response", G_CALLBACK(+[](GtkDialog* dlg, gint response, gpointer user_data) {
720
+ auto* data = static_cast<ConfirmData*>(user_data);
721
+ bool confirmed = (response == GTK_RESPONSE_OK);
722
+ send_reply(data->wid, data->ch, "confirmed", confirmed ? "true" : "false");
723
+ delete data;
724
+ gtk_widget_destroy(GTK_WIDGET(dlg));
725
+ }), d);
726
+ gtk_widget_show_all(dialog);
727
+ return;
728
+ }
729
+
730
+ // ── prompt ───────────────────────────────────────────────────────────────
731
+ if (command == "prompt") {
732
+ GtkWidget* w = get_win();
733
+ if (!w || args.empty()) return;
734
+ GtkWidget* dialog = gtk_dialog_new_with_buttons(
735
+ "Input", GTK_WINDOW(w),
736
+ GTK_DIALOG_MODAL,
737
+ "_OK", GTK_RESPONSE_OK,
738
+ "_Cancel", GTK_RESPONSE_CANCEL,
739
+ nullptr);
740
+ GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
741
+ GtkWidget* label = gtk_label_new(args[0].c_str());
742
+ GtkWidget* entry = gtk_entry_new();
743
+ if (args.size() > 1) gtk_entry_set_text(GTK_ENTRY(entry), args[1].c_str());
744
+ gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 4);
745
+ gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 4);
746
+
747
+ struct PromptData { int wid; string ch; GtkWidget* entry; };
748
+ auto* d = new PromptData{ window_id, get_last_arg(args, "prompt-reply-" + to_string(window_id)), entry };
749
+
750
+ g_signal_connect(dialog, "response", G_CALLBACK(+[](GtkDialog* dlg, gint response, gpointer user_data) {
751
+ auto* data = static_cast<PromptData*>(user_data);
752
+ string input = (response == GTK_RESPONSE_OK)
753
+ ? gtk_entry_get_text(GTK_ENTRY(data->entry)) : "";
754
+ send_reply(data->wid, data->ch, "input", input);
755
+ delete data;
756
+ gtk_widget_destroy(GTK_WIDGET(dlg));
757
+ }), d);
758
+ gtk_widget_show_all(dialog);
759
+ return;
760
+ }
761
+
762
+ // ── showFileOpenDialog ───────────────────────────────────────────────────
763
+ if (command == "showFileOpenDialog") {
764
+ GtkWidget* w = get_win();
765
+ GtkWidget* chooser = gtk_file_chooser_dialog_new("Open File",
766
+ w ? GTK_WINDOW(w) : nullptr,
767
+ GTK_FILE_CHOOSER_ACTION_OPEN,
768
+ "_Cancel", GTK_RESPONSE_CANCEL,
769
+ "_Open", GTK_RESPONSE_ACCEPT,
770
+ nullptr);
771
+
772
+ struct FileOpenData { int wid; string ch; };
773
+ auto* d = new FileOpenData{ window_id, get_last_arg(args, "showFileOpenDialog-reply-" + to_string(window_id)) };
774
+
775
+ g_signal_connect(chooser, "response", G_CALLBACK(+[](GtkDialog* dlg, gint response, gpointer user_data) {
776
+ auto* data = static_cast<FileOpenData*>(user_data);
777
+ string file_path = "";
778
+ if (response == GTK_RESPONSE_ACCEPT) {
779
+ gchar* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
780
+ if (filename) { file_path = filename; g_free(filename); }
781
+ }
782
+ send_reply(data->wid, data->ch, "filePath", file_path);
783
+ delete data;
784
+ gtk_widget_destroy(GTK_WIDGET(dlg));
785
+ }), d);
786
+ gtk_widget_show_all(chooser);
787
+ return;
788
+ }
789
+
790
+ // ── readFromClipboard ────────────────────────────────────────────────────
791
+ if (command == "readFromClipboard") {
792
+ GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
793
+ gchar* text = gtk_clipboard_wait_for_text(cb);
794
+ string result = text ? text : "";
795
+ if (text) g_free(text);
796
+ send_reply(window_id, get_last_arg(args, "readFromClipboard-reply-" + to_string(window_id)), "text", result);
797
+ return;
798
+ }
799
+
800
+ // ── writeToClipboard ─────────────────────────────────────────────────────
801
+ if (command == "writeToClipboard") {
802
+ if (args.empty()) return;
803
+ GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
804
+ gtk_clipboard_set_text(cb, args[0].c_str(), -1);
805
+ gtk_clipboard_store(cb);
806
+ return;
807
+ }
808
+
809
+ // ── isDarkMode ───────────────────────────────────────────────────────────
810
+ if (command == "isDarkMode") {
811
+ GtkSettings* settings = gtk_settings_get_default();
812
+ gchar* theme_name = nullptr;
813
+ g_object_get(settings, "gtk-theme-name", &theme_name, nullptr);
814
+ bool dark = theme_name && g_str_has_suffix(g_ascii_strdown(theme_name, -1), "dark");
815
+ if (theme_name) g_free(theme_name);
816
+ // Also check prefer-dark-theme
817
+ gboolean prefer_dark = FALSE;
818
+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer_dark, nullptr);
819
+ send_reply(window_id, get_last_arg(args, "isDarkMode-reply-" + to_string(window_id)), "isDarkMode", (dark || prefer_dark) ? "true" : "false");
820
+ return;
821
+ }
822
+
823
+ // ── showNotification ─────────────────────────────────────────────────────
824
+ if (command == "showNotification") {
825
+ if (args.empty()) return;
826
+ notify_init("Positron");
827
+ NotifyNotification* notif = notify_notification_new(
828
+ args[0].c_str(),
829
+ args.size() > 1 ? args[1].c_str() : nullptr,
830
+ nullptr);
831
+ notify_notification_show(notif, nullptr);
832
+ g_object_unref(notif);
833
+ return;
834
+ }
835
+
836
+ // ── capturePage ──────────────────────────────────────────────────────────
837
+ if (command == "capturePage") {
838
+ WebKitWebView* wv = get_wv();
839
+ if (!wv) return;
840
+ webkit_web_view_get_snapshot(wv,
841
+ WEBKIT_SNAPSHOT_REGION_VISIBLE,
842
+ WEBKIT_SNAPSHOT_OPTIONS_NONE,
843
+ nullptr, on_snapshot_ready, GINT_TO_POINTER(window_id));
844
+ return;
845
+ }
846
+
847
+ // ── setContextMenu ───────────────────────────────────────────────────────
848
+ if (command == "setContextMenu") {
849
+ WebKitWebView* wv = get_wv();
850
+ if (!wv || args.empty()) return;
851
+ JsonParser* parser = json_parser_new();
852
+ if (json_parser_load_from_data(parser, args[0].c_str(), -1, nullptr)) {
853
+ JsonNode* root = json_parser_get_root(parser);
854
+ if (JSON_NODE_HOLDS_ARRAY(root)) {
855
+ GtkWidget* menu = build_context_menu(json_node_get_array(root), window_id);
856
+ // Store reference on webview for right-click (handled via policy)
857
+ g_object_set_data_full(G_OBJECT(wv), "context-menu", menu, (GDestroyNotify)gtk_widget_destroy);
858
+ }
859
+ }
860
+ g_object_unref(parser);
861
+ return;
862
+ }
863
+
864
+ // ── setCloseable / setMinimizible ────────────────────────────────────────
865
+ // Note: GTK doesn't allow dynamically toggling decorations at runtime easily.
866
+ // These are no-ops on Linux with a note.
867
+ if (command == "setCloseable" || command == "setMinimizible") {
868
+ cerr << "[Linux Core] " << command << " is not supported at runtime on Linux/GTK." << endl;
869
+ return;
870
+ }
871
+
872
+ // ── blockPowerSave / unblockPowerSave ────────────────────────────────────
873
+ // Handled via dbus / systemd inhibit — stub for now
874
+ if (command == "blockPowerSave" || command == "unblockPowerSave") {
875
+ cerr << "[Linux Core] " << command << " is a no-op on Linux (requires systemd-inhibit integration)." << endl;
876
+ return;
877
+ }
878
+
879
+ // ── print ────────────────────────────────────────────────────────────────
880
+ if (command == "print") {
881
+ WebKitWebView* wv = get_wv();
882
+ if (!wv) return;
883
+ WebKitPrintOperation* op = webkit_print_operation_new(wv);
884
+ webkit_print_operation_run_dialog(op, nullptr);
885
+ g_object_unref(op);
886
+ return;
887
+ }
888
+
889
+ // ── fallthrough: extension registry ─────────────────────────────────────
890
+ auto registry = getExtensionRegistry();
891
+ if (registry.count(command)) {
892
+ registry[command](window_id, args);
893
+ } else {
894
+ cerr << "[Linux Core] Unknown command: " << command << endl;
895
+ }
896
+ }
897
+
898
+ // ─── WebSocket message handler ───────────────────────────────────────────────
899
+
900
+ void on_ws_message(SoupWebsocketConnection* conn, gint type, GBytes* message, gpointer) {
901
+ gsize size;
902
+ const gchar* ptr = (const gchar*)g_bytes_get_data(message, &size);
903
+
904
+ JsonParser* parser = json_parser_new();
905
+ if (!json_parser_load_from_data(parser, ptr, (gssize)size, nullptr)) {
906
+ g_object_unref(parser);
907
+ return;
908
+ }
909
+
910
+ JsonNode* root = json_parser_get_root(parser);
911
+ if (!root || !JSON_NODE_HOLDS_OBJECT(root)) {
912
+ g_object_unref(parser);
913
+ return;
914
+ }
915
+
916
+ JsonObject* obj = json_node_get_object(root);
917
+ int window_id = json_object_has_member(obj, "windowId") ? (int)json_object_get_int_member(obj, "windowId") : -1;
918
+ string command = json_object_has_member(obj, "command") ? json_object_get_string_member(obj, "command") : "";
919
+
920
+ vector<string> args;
921
+ if (json_object_has_member(obj, "args")) {
922
+ JsonArray* arr = json_object_get_array_member(obj, "args");
923
+ if (arr) {
924
+ for (guint i = 0; i < json_array_get_length(arr); i++)
925
+ args.push_back(json_array_get_string_element(arr, i));
926
+ }
927
+ }
928
+
929
+ handle_command(window_id, command, args);
930
+ g_object_unref(parser);
931
+ }
932
+
933
+ void on_ws_closed(SoupWebsocketConnection*, gpointer) {
934
+ cout << "[Linux Core] IPC connection closed. Terminating." << endl;
935
+ gtk_main_quit();
936
+ }
937
+
938
+ void on_ws_connected(GObject* source, GAsyncResult* res, gpointer) {
939
+ GError* error = nullptr;
940
+ ws_conn = soup_session_websocket_connect_finish(SOUP_SESSION(source), res, &error);
941
+ if (error) {
942
+ cerr << "[Linux Core] Failed to connect to IPC: " << error->message << endl;
943
+ g_error_free(error);
944
+ gtk_main_quit();
945
+ return;
946
+ }
947
+ g_signal_connect(ws_conn, "message", G_CALLBACK(on_ws_message), nullptr);
948
+ g_signal_connect(ws_conn, "closed", G_CALLBACK(on_ws_closed), nullptr);
949
+ }
950
+
951
+ // ─── Port / UUID helpers ──────────────────────────────────────────────────────
952
+
953
+ static int find_open_port() {
954
+ int sock = socket(AF_INET, SOCK_STREAM, 0);
955
+ if (sock < 0) return 9000;
956
+ struct sockaddr_in addr{};
957
+ addr.sin_family = AF_INET;
958
+ addr.sin_addr.s_addr = INADDR_ANY;
959
+ addr.sin_port = 0;
960
+ if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { close(sock); return 9000; }
961
+ socklen_t len = sizeof(addr);
962
+ getsockname(sock, (struct sockaddr*)&addr, &len);
963
+ int port = ntohs(addr.sin_port);
964
+ close(sock);
965
+ return port;
966
+ }
967
+
968
+ static string generate_uuid() {
969
+ ifstream f("/proc/sys/kernel/random/uuid");
970
+ if (f.good()) { string uuid; f >> uuid; return uuid; }
971
+ unsigned char buf[16];
972
+ ifstream urandom("/dev/urandom", ios::binary);
973
+ urandom.read((char*)buf, 16);
974
+ buf[6] = (buf[6] & 0x0f) | 0x40;
975
+ buf[8] = (buf[8] & 0x3f) | 0x80;
976
+ char out[37];
977
+ snprintf(out, sizeof(out),
978
+ "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
979
+ buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7],
980
+ buf[8],buf[9],buf[10],buf[11],buf[12],buf[13],buf[14],buf[15]);
981
+ return string(out);
982
+ }
983
+
984
+ // ─── Entry point ─────────────────────────────────────────────────────────────
985
+
986
+ static GPid node_pid = 0;
987
+
988
+ static void on_node_exit(GPid pid, gint, gpointer) {
989
+ cerr << "[Linux Core] Node.js backend exited. Terminating." << endl;
990
+ gtk_main_quit();
991
+ g_spawn_close_pid(pid);
992
+ }
993
+
994
+ int main(int argc, char* argv[]) {
995
+ gtk_init(&argc, &argv);
996
+
997
+ // Locate executable directory
998
+ char self_path[PATH_MAX];
999
+ ssize_t self_len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
1000
+ string exe_dir = "";
1001
+ if (self_len >= 0) {
1002
+ self_path[self_len] = '\0';
1003
+ exe_dir = string(self_path);
1004
+ exe_dir = exe_dir.substr(0, exe_dir.rfind('/'));
1005
+
1006
+ // Attempt to load application icon
1007
+ string icon_path = exe_dir + "/resources/icon.png";
1008
+ if (access(icon_path.c_str(), F_OK) == -1) {
1009
+ icon_path = exe_dir + "/icon.png";
1010
+ }
1011
+ if (access(icon_path.c_str(), F_OK) != -1) {
1012
+ gtk_window_set_default_icon_from_file(icon_path.c_str(), nullptr);
1013
+ }
1014
+ }
1015
+
1016
+ const char* port_env = getenv("POSITRON_IPC_PORT");
1017
+ const char* token_env = getenv("POSITRON_AUTH_TOKEN");
1018
+
1019
+ string ipc_port, ipc_token;
1020
+
1021
+ if (port_env && token_env) {
1022
+ // ── Dev mode: Node.js spawned us and passed creds via env ─────────────
1023
+ ipc_port = port_env;
1024
+ ipc_token = token_env;
1025
+ } else {
1026
+ // ── Packaged mode: we are the entry point ─────────────────────────────
1027
+ ipc_port = to_string(find_open_port());
1028
+ ipc_token = generate_uuid();
1029
+
1030
+ if (exe_dir.empty()) { cerr << "[Linux Core] Failed to resolve executable path." << endl; return 1; }
1031
+ string resources_dir = exe_dir + "/resources";
1032
+
1033
+ // Find *-backend binary
1034
+ string backend_bin;
1035
+ GDir* dir = g_dir_open(resources_dir.c_str(), 0, nullptr);
1036
+ if (dir) {
1037
+ const gchar* name;
1038
+ while ((name = g_dir_read_name(dir)) != nullptr) {
1039
+ string n = name;
1040
+ if (n.size() > 8 && n.substr(n.size() - 8) == "-backend") {
1041
+ backend_bin = resources_dir + "/" + n;
1042
+ break;
1043
+ }
1044
+ }
1045
+ g_dir_close(dir);
1046
+ }
1047
+
1048
+ if (backend_bin.empty()) {
1049
+ cerr << "[Linux Core] Could not find *-backend binary in " << resources_dir << endl;
1050
+ return 1;
1051
+ }
1052
+
1053
+ // Build environment
1054
+ vector<string> envp_storage;
1055
+ for (int i = 0; environ[i]; i++) {
1056
+ string e = environ[i];
1057
+ if (e.rfind("POSITRON_IPC_PORT=", 0) == 0) continue;
1058
+ if (e.rfind("POSITRON_AUTH_TOKEN=", 0) == 0) continue;
1059
+ if (e.rfind("POSITRON_PACKAGED=", 0) == 0) continue;
1060
+ envp_storage.push_back(e);
1061
+ }
1062
+ envp_storage.push_back("POSITRON_IPC_PORT=" + ipc_port);
1063
+ envp_storage.push_back("POSITRON_AUTH_TOKEN=" + ipc_token);
1064
+ envp_storage.push_back("POSITRON_PACKAGED=true");
1065
+
1066
+ vector<const gchar*> envp;
1067
+ for (auto& s : envp_storage) envp.push_back(s.c_str());
1068
+ envp.push_back(nullptr);
1069
+
1070
+ string cmd = "\"" + backend_bin + "\"";
1071
+ const gchar* argv_spawn[] = { "/bin/sh", "-c", cmd.c_str(), nullptr };
1072
+ GSpawnFlags flags = (GSpawnFlags)(G_SPAWN_DO_NOT_REAP_CHILD);
1073
+ GError* spawn_err = nullptr;
1074
+ gboolean ok = g_spawn_async(
1075
+ resources_dir.c_str(),
1076
+ (gchar**)argv_spawn,
1077
+ (gchar**)envp.data(),
1078
+ flags,
1079
+ nullptr, nullptr,
1080
+ &node_pid,
1081
+ &spawn_err);
1082
+
1083
+ if (!ok) {
1084
+ cerr << "[Linux Core] Failed to spawn backend: "
1085
+ << (spawn_err ? spawn_err->message : "unknown") << endl;
1086
+ if (spawn_err) g_error_free(spawn_err);
1087
+ return 1;
1088
+ }
1089
+
1090
+ g_child_watch_add(node_pid, on_node_exit, nullptr);
1091
+ cout << "[Linux Core] Spawned backend (pid " << node_pid << "). Waiting for IPC..." << endl;
1092
+ g_usleep(800000); // 0.8 s — give Node time to bind the WebSocket
1093
+ }
1094
+
1095
+ auth_token = ipc_token;
1096
+ string url = "ws://127.0.0.1:" + ipc_port;
1097
+
1098
+ // Build WebSocket upgrade request with auth header
1099
+ SoupSession* session = soup_session_new();
1100
+ SoupMessage* msg = soup_message_new("GET", url.c_str());
1101
+
1102
+ #if defined(SOUP_VERSION_2_4) && !defined(SOUP_VERSION_3_0)
1103
+ soup_message_headers_append(msg->request_headers,
1104
+ "x-positron-auth-token", auth_token.c_str());
1105
+ #else
1106
+ // libsoup 3.x
1107
+ SoupMessageHeaders* headers = soup_message_get_request_headers(msg);
1108
+ soup_message_headers_append(headers, "x-positron-auth-token", auth_token.c_str());
1109
+ #endif
1110
+
1111
+ soup_session_websocket_connect_async(session, msg, nullptr, nullptr,
1112
+ G_PRIORITY_DEFAULT, nullptr, on_ws_connected, nullptr);
1113
+
1114
+ signal(SIGTERM, [](int) { if (node_pid) kill(node_pid, SIGTERM); gtk_main_quit(); });
1115
+ signal(SIGINT, [](int) { if (node_pid) kill(node_pid, SIGTERM); gtk_main_quit(); });
1116
+
1117
+ gtk_main();
1118
+
1119
+ if (node_pid) kill(node_pid, SIGTERM);
1120
+ if (ws_conn) g_object_unref(ws_conn);
1121
+ g_object_unref(session);
1122
+ return 0;
1123
+ }