@atom-js-org/runtime 0.2.0-alpha.0 → 0.3.0-alpha.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,519 @@
1
+ #import <Cocoa/Cocoa.h>
2
+ #import <WebKit/WebKit.h>
3
+ #include <string.h>
4
+
5
+ static NSString *const AtomJSEventPrefix = @"__ATOMJS_EVENT__";
6
+
7
+ @class AtomJSWindowController;
8
+ static NSMutableDictionary<NSNumber *, AtomJSWindowController *> *atomWindows;
9
+ static NSString *atomAppName = @"AtomJS App";
10
+
11
+ static void AtomJSEmit(NSDictionary *payload) {
12
+ if (![NSJSONSerialization isValidJSONObject:payload]) return;
13
+
14
+ NSError *error = nil;
15
+ NSData *json = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
16
+ if (!json || error) return;
17
+
18
+ NSMutableData *output = [NSMutableData data];
19
+ [output appendData:[AtomJSEventPrefix dataUsingEncoding:NSUTF8StringEncoding]];
20
+ [output appendData:json];
21
+ [output appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
22
+
23
+ @synchronized([NSFileHandle fileHandleWithStandardOutput]) {
24
+ [[NSFileHandle fileHandleWithStandardOutput] writeData:output];
25
+ }
26
+ }
27
+
28
+ static NSString *AtomJSString(id value, NSString *fallback) {
29
+ return [value isKindOfClass:[NSString class]] ? value : fallback;
30
+ }
31
+
32
+ static NSNumber *AtomJSNumber(id value, NSNumber *fallback) {
33
+ return [value isKindOfClass:[NSNumber class]] ? value : fallback;
34
+ }
35
+
36
+ static BOOL AtomJSBoolean(id value, BOOL fallback) {
37
+ return [value isKindOfClass:[NSNumber class]] ? [value boolValue] : fallback;
38
+ }
39
+
40
+ static NSColor *AtomJSColor(NSString *hex) {
41
+ if (![hex isKindOfClass:[NSString class]]) return [NSColor windowBackgroundColor];
42
+
43
+ NSString *value = [hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
44
+ if ([value hasPrefix:@"#"]) value = [value substringFromIndex:1];
45
+ if (value.length != 6 && value.length != 8) return [NSColor windowBackgroundColor];
46
+
47
+ unsigned int rgba = 0;
48
+ if (![[NSScanner scannerWithString:value] scanHexInt:&rgba]) return [NSColor windowBackgroundColor];
49
+
50
+ CGFloat red;
51
+ CGFloat green;
52
+ CGFloat blue;
53
+ CGFloat alpha = 1.0;
54
+
55
+ if (value.length == 8) {
56
+ red = ((rgba >> 24) & 0xff) / 255.0;
57
+ green = ((rgba >> 16) & 0xff) / 255.0;
58
+ blue = ((rgba >> 8) & 0xff) / 255.0;
59
+ alpha = (rgba & 0xff) / 255.0;
60
+ } else {
61
+ red = ((rgba >> 16) & 0xff) / 255.0;
62
+ green = ((rgba >> 8) & 0xff) / 255.0;
63
+ blue = (rgba & 0xff) / 255.0;
64
+ }
65
+
66
+ return [NSColor colorWithSRGBRed:red green:green blue:blue alpha:alpha];
67
+ }
68
+
69
+ @interface AtomJSWindowController : NSObject <NSWindowDelegate, WKNavigationDelegate>
70
+ @property(nonatomic, strong) NSNumber *windowId;
71
+ @property(nonatomic, strong) NSWindow *window;
72
+ @property(nonatomic, strong) WKWebView *webView;
73
+ - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config;
74
+ - (void)navigate:(NSString *)urlString;
75
+ @end
76
+
77
+ @implementation AtomJSWindowController
78
+
79
+ - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config {
80
+ self = [super init];
81
+ if (!self) return nil;
82
+
83
+ _windowId = windowId;
84
+
85
+ CGFloat width = MAX(320.0, [AtomJSNumber(config[@"width"], @800) doubleValue]);
86
+ CGFloat height = MAX(240.0, [AtomJSNumber(config[@"height"], @600) doubleValue]);
87
+ NSRect frame = NSMakeRect(0, 0, width, height);
88
+
89
+ NSWindowStyleMask style = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
90
+ if (AtomJSBoolean(config[@"frame"], YES)) style |= NSWindowStyleMaskTitled;
91
+ if (AtomJSBoolean(config[@"resizable"], YES)) style |= NSWindowStyleMaskResizable;
92
+
93
+ _window = [[NSWindow alloc]
94
+ initWithContentRect:frame
95
+ styleMask:style
96
+ backing:NSBackingStoreBuffered
97
+ defer:NO];
98
+ _window.releasedWhenClosed = NO;
99
+ _window.delegate = self;
100
+ _window.title = AtomJSString(config[@"title"], atomAppName);
101
+ _window.backgroundColor = AtomJSColor(config[@"backgroundColor"]);
102
+ _window.tabbingMode = NSWindowTabbingModeDisallowed;
103
+
104
+ if (AtomJSBoolean(config[@"center"], YES)) [_window center];
105
+
106
+ WKUserContentController *contentController = [[WKUserContentController alloc] init];
107
+ NSString *bridgeScript = AtomJSString(config[@"bridgeScript"], @"");
108
+ if (bridgeScript.length > 0) {
109
+ WKUserScript *script = [[WKUserScript alloc]
110
+ initWithSource:bridgeScript
111
+ injectionTime:WKUserScriptInjectionTimeAtDocumentStart
112
+ forMainFrameOnly:YES];
113
+ [contentController addUserScript:script];
114
+ }
115
+
116
+ WKWebViewConfiguration *webConfiguration = [[WKWebViewConfiguration alloc] init];
117
+ webConfiguration.userContentController = contentController;
118
+
119
+ _webView = [[WKWebView alloc] initWithFrame:frame configuration:webConfiguration];
120
+ _webView.navigationDelegate = self;
121
+ _webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
122
+ _window.contentView = _webView;
123
+
124
+ [self navigate:AtomJSString(config[@"url"], @"about:blank")];
125
+
126
+ if (AtomJSBoolean(config[@"show"], YES)) {
127
+ [_window makeKeyAndOrderFront:nil];
128
+ [NSApp activateIgnoringOtherApps:YES];
129
+ }
130
+
131
+ return self;
132
+ }
133
+
134
+ - (void)navigate:(NSString *)urlString {
135
+ NSURL *url = [NSURL URLWithString:urlString ?: @"about:blank"];
136
+ if (!url) {
137
+ AtomJSEmit(@{
138
+ @"type": @"did-fail-load",
139
+ @"windowId": self.windowId,
140
+ @"url": urlString ?: @"",
141
+ @"error": @"Invalid URL"
142
+ });
143
+ return;
144
+ }
145
+
146
+ [self.webView loadRequest:[NSURLRequest requestWithURL:url]];
147
+ }
148
+
149
+ - (NSString *)currentURL {
150
+ return self.webView.URL.absoluteString ?: @"";
151
+ }
152
+
153
+ - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
154
+ AtomJSEmit(@{
155
+ @"type": @"did-start-loading",
156
+ @"windowId": self.windowId,
157
+ @"url": [self currentURL]
158
+ });
159
+ }
160
+
161
+ - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
162
+ AtomJSEmit(@{
163
+ @"type": @"did-finish-load",
164
+ @"windowId": self.windowId,
165
+ @"url": [self currentURL]
166
+ });
167
+ }
168
+
169
+ - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
170
+ AtomJSEmit(@{
171
+ @"type": @"did-fail-load",
172
+ @"windowId": self.windowId,
173
+ @"url": [self currentURL],
174
+ @"error": error.localizedDescription ?: @"Navigation failed"
175
+ });
176
+ }
177
+
178
+ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
179
+ [self webView:webView didFailNavigation:navigation withError:error];
180
+ }
181
+
182
+ - (void)windowWillClose:(NSNotification *)notification {
183
+ [atomWindows removeObjectForKey:self.windowId];
184
+ AtomJSEmit(@{
185
+ @"type": @"closed",
186
+ @"windowId": self.windowId
187
+ });
188
+ }
189
+
190
+ - (void)windowDidBecomeKey:(NSNotification *)notification {
191
+ AtomJSEmit(@{
192
+ @"type": @"focus",
193
+ @"windowId": self.windowId
194
+ });
195
+ }
196
+
197
+ - (void)windowDidResignKey:(NSNotification *)notification {
198
+ AtomJSEmit(@{
199
+ @"type": @"blur",
200
+ @"windowId": self.windowId
201
+ });
202
+ }
203
+
204
+ - (void)windowDidMiniaturize:(NSNotification *)notification {
205
+ AtomJSEmit(@{
206
+ @"type": @"minimize",
207
+ @"windowId": self.windowId
208
+ });
209
+ }
210
+
211
+ - (void)windowDidDeminiaturize:(NSNotification *)notification {
212
+ AtomJSEmit(@{
213
+ @"type": @"restore",
214
+ @"windowId": self.windowId
215
+ });
216
+ }
217
+
218
+ @end
219
+
220
+ static AtomJSWindowController *AtomJSController(NSDictionary *message) {
221
+ NSNumber *windowId = AtomJSNumber(message[@"windowId"], nil);
222
+ return windowId ? atomWindows[windowId] : nil;
223
+ }
224
+
225
+ static NSArray<NSString *> *AtomJSAllowedExtensions(NSDictionary *options) {
226
+ NSArray *filters = [options[@"filters"] isKindOfClass:[NSArray class]] ? options[@"filters"] : @[];
227
+ NSMutableArray<NSString *> *extensions = [NSMutableArray array];
228
+
229
+ for (id filter in filters) {
230
+ if (![filter isKindOfClass:[NSDictionary class]]) continue;
231
+ NSArray *items = [filter[@"extensions"] isKindOfClass:[NSArray class]] ? filter[@"extensions"] : @[];
232
+ for (id item in items) {
233
+ if ([item isKindOfClass:[NSString class]] && ![item isEqualToString:@"*"]) {
234
+ [extensions addObject:item];
235
+ }
236
+ }
237
+ }
238
+
239
+ return extensions;
240
+ }
241
+
242
+ static void AtomJSRespond(NSString *requestId, BOOL ok, id result, NSString *error) {
243
+ if (!requestId) return;
244
+
245
+ NSMutableDictionary *response = [@{
246
+ @"type": @"response",
247
+ @"requestId": requestId,
248
+ @"ok": @(ok)
249
+ } mutableCopy];
250
+
251
+ if (result) response[@"result"] = result;
252
+ if (error) response[@"error"] = error;
253
+ AtomJSEmit(response);
254
+ }
255
+
256
+ static NSDictionary *AtomJSOpenDialog(NSDictionary *options) {
257
+ NSOpenPanel *panel = [NSOpenPanel openPanel];
258
+ panel.title = AtomJSString(options[@"title"], @"");
259
+ panel.prompt = AtomJSString(options[@"buttonLabel"], panel.prompt);
260
+
261
+ NSArray *properties = [options[@"properties"] isKindOfClass:[NSArray class]] ? options[@"properties"] : @[];
262
+ panel.allowsMultipleSelection = [properties containsObject:@"multiSelections"];
263
+ panel.canChooseDirectories = [properties containsObject:@"openDirectory"];
264
+ panel.canChooseFiles = !panel.canChooseDirectories || [properties containsObject:@"openFile"];
265
+ panel.canCreateDirectories = [properties containsObject:@"createDirectory"];
266
+
267
+ NSString *defaultPath = AtomJSString(options[@"defaultPath"], nil);
268
+ if (defaultPath.length > 0) {
269
+ BOOL isDirectory = NO;
270
+ if ([[NSFileManager defaultManager] fileExistsAtPath:defaultPath isDirectory:&isDirectory]) {
271
+ panel.directoryURL = [NSURL fileURLWithPath:isDirectory ? defaultPath : [defaultPath stringByDeletingLastPathComponent]];
272
+ }
273
+ }
274
+
275
+ #pragma clang diagnostic push
276
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
277
+ NSArray *extensions = AtomJSAllowedExtensions(options);
278
+ if (extensions.count > 0) panel.allowedFileTypes = extensions;
279
+ #pragma clang diagnostic pop
280
+
281
+ NSModalResponse response = [panel runModal];
282
+ if (response != NSModalResponseOK) {
283
+ return @{ @"canceled": @YES, @"filePaths": @[] };
284
+ }
285
+
286
+ NSMutableArray<NSString *> *paths = [NSMutableArray array];
287
+ for (NSURL *url in panel.URLs) {
288
+ if (url.path) [paths addObject:url.path];
289
+ }
290
+ return @{ @"canceled": @(paths.count == 0), @"filePaths": paths };
291
+ }
292
+
293
+ static NSDictionary *AtomJSSaveDialog(NSDictionary *options) {
294
+ NSSavePanel *panel = [NSSavePanel savePanel];
295
+ panel.title = AtomJSString(options[@"title"], @"");
296
+ panel.prompt = AtomJSString(options[@"buttonLabel"], panel.prompt);
297
+ panel.canCreateDirectories = YES;
298
+
299
+ NSString *defaultPath = AtomJSString(options[@"defaultPath"], nil);
300
+ if (defaultPath.length > 0) {
301
+ panel.directoryURL = [NSURL fileURLWithPath:[defaultPath stringByDeletingLastPathComponent]];
302
+ panel.nameFieldStringValue = [defaultPath lastPathComponent];
303
+ }
304
+
305
+ #pragma clang diagnostic push
306
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
307
+ NSArray *extensions = AtomJSAllowedExtensions(options);
308
+ if (extensions.count > 0) panel.allowedFileTypes = extensions;
309
+ #pragma clang diagnostic pop
310
+
311
+ NSModalResponse response = [panel runModal];
312
+ if (response != NSModalResponseOK || !panel.URL.path) {
313
+ return @{ @"canceled": @YES };
314
+ }
315
+
316
+ return @{ @"canceled": @NO, @"filePath": panel.URL.path };
317
+ }
318
+
319
+ static NSDictionary *AtomJSMessageDialog(NSDictionary *options) {
320
+ NSAlert *alert = [[NSAlert alloc] init];
321
+ alert.messageText = AtomJSString(options[@"message"], atomAppName);
322
+ alert.informativeText = AtomJSString(options[@"detail"], @"");
323
+
324
+ NSString *type = AtomJSString(options[@"type"], @"none");
325
+ if ([type isEqualToString:@"error"]) alert.alertStyle = NSAlertStyleCritical;
326
+ else if ([type isEqualToString:@"warning"]) alert.alertStyle = NSAlertStyleWarning;
327
+ else alert.alertStyle = NSAlertStyleInformational;
328
+
329
+ NSArray *buttons = [options[@"buttons"] isKindOfClass:[NSArray class]] ? options[@"buttons"] : @[];
330
+ if (buttons.count == 0) buttons = @[ @"OK" ];
331
+ for (id button in buttons) {
332
+ [alert addButtonWithTitle:AtomJSString(button, @"OK")];
333
+ }
334
+
335
+ NSString *checkboxLabel = AtomJSString(options[@"checkboxLabel"], nil);
336
+ NSButton *checkbox = nil;
337
+ if (checkboxLabel.length > 0) {
338
+ checkbox = [NSButton checkboxWithTitle:checkboxLabel target:nil action:nil];
339
+ checkbox.state = AtomJSBoolean(options[@"checkboxChecked"], NO) ? NSControlStateValueOn : NSControlStateValueOff;
340
+ alert.accessoryView = checkbox;
341
+ }
342
+
343
+ NSModalResponse response = [alert runModal];
344
+ NSInteger index = MAX(0, response - NSAlertFirstButtonReturn);
345
+ return @{
346
+ @"response": @(index),
347
+ @"checkboxChecked": @(checkbox && checkbox.state == NSControlStateValueOn)
348
+ };
349
+ }
350
+
351
+ static void AtomJSHandleMessage(NSDictionary *message) {
352
+ NSString *command = AtomJSString(message[@"command"], @"");
353
+ NSString *requestId = AtomJSString(message[@"requestId"], nil);
354
+
355
+ if ([command isEqualToString:@"create"]) {
356
+ NSNumber *windowId = AtomJSNumber(message[@"windowId"], nil);
357
+ NSDictionary *config = [message[@"config"] isKindOfClass:[NSDictionary class]] ? message[@"config"] : @{};
358
+ if (!windowId) return;
359
+
360
+ AtomJSWindowController *controller = [[AtomJSWindowController alloc] initWithWindowId:windowId config:config];
361
+ atomWindows[windowId] = controller;
362
+ return;
363
+ }
364
+
365
+ if ([command isEqualToString:@"dialog-open"]) {
366
+ AtomJSRespond(requestId, YES, AtomJSOpenDialog([message[@"options"] isKindOfClass:[NSDictionary class]] ? message[@"options"] : @{}), nil);
367
+ return;
368
+ }
369
+
370
+ if ([command isEqualToString:@"dialog-save"]) {
371
+ AtomJSRespond(requestId, YES, AtomJSSaveDialog([message[@"options"] isKindOfClass:[NSDictionary class]] ? message[@"options"] : @{}), nil);
372
+ return;
373
+ }
374
+
375
+ if ([command isEqualToString:@"dialog-message"]) {
376
+ AtomJSRespond(requestId, YES, AtomJSMessageDialog([message[@"options"] isKindOfClass:[NSDictionary class]] ? message[@"options"] : @{}), nil);
377
+ return;
378
+ }
379
+
380
+ if ([command isEqualToString:@"quit"]) {
381
+ [NSApp terminate:nil];
382
+ return;
383
+ }
384
+
385
+ AtomJSWindowController *controller = AtomJSController(message);
386
+ if (!controller) {
387
+ AtomJSRespond(requestId, NO, nil, @"Unknown AtomJS window.");
388
+ return;
389
+ }
390
+
391
+ if ([command isEqualToString:@"navigate"]) {
392
+ [controller navigate:AtomJSString(message[@"url"], @"about:blank")];
393
+ } else if ([command isEqualToString:@"show"]) {
394
+ [controller.window makeKeyAndOrderFront:nil];
395
+ [NSApp activateIgnoringOtherApps:YES];
396
+ } else if ([command isEqualToString:@"hide"]) {
397
+ [controller.window orderOut:nil];
398
+ } else if ([command isEqualToString:@"focus"]) {
399
+ [controller.window makeKeyAndOrderFront:nil];
400
+ [NSApp activateIgnoringOtherApps:YES];
401
+ } else if ([command isEqualToString:@"close"] || [command isEqualToString:@"destroy"]) {
402
+ [controller.window close];
403
+ } else if ([command isEqualToString:@"set-title"]) {
404
+ controller.window.title = AtomJSString(message[@"title"], atomAppName);
405
+ } else if ([command isEqualToString:@"minimize"]) {
406
+ [controller.window miniaturize:nil];
407
+ } else if ([command isEqualToString:@"restore"]) {
408
+ [controller.window deminiaturize:nil];
409
+ [controller.window makeKeyAndOrderFront:nil];
410
+ } else if ([command isEqualToString:@"maximize"]) {
411
+ if (!controller.window.zoomed) [controller.window zoom:nil];
412
+ } else if ([command isEqualToString:@"unmaximize"]) {
413
+ if (controller.window.zoomed) [controller.window zoom:nil];
414
+ } else if ([command isEqualToString:@"fullscreen"]) {
415
+ BOOL requested = AtomJSBoolean(message[@"value"], YES);
416
+ BOOL active = (controller.window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
417
+ if (requested != active) [controller.window toggleFullScreen:nil];
418
+ } else if ([command isEqualToString:@"set-bounds"]) {
419
+ NSDictionary *bounds = [message[@"bounds"] isKindOfClass:[NSDictionary class]] ? message[@"bounds"] : @{};
420
+ NSRect frame = controller.window.frame;
421
+ if ([bounds[@"x"] isKindOfClass:[NSNumber class]]) frame.origin.x = [bounds[@"x"] doubleValue];
422
+ if ([bounds[@"y"] isKindOfClass:[NSNumber class]]) frame.origin.y = [bounds[@"y"] doubleValue];
423
+ if ([bounds[@"width"] isKindOfClass:[NSNumber class]]) frame.size.width = MAX(1.0, [bounds[@"width"] doubleValue]);
424
+ if ([bounds[@"height"] isKindOfClass:[NSNumber class]]) frame.size.height = MAX(1.0, [bounds[@"height"] doubleValue]);
425
+ [controller.window setFrame:frame display:YES animate:NO];
426
+ } else {
427
+ AtomJSRespond(requestId, NO, nil, [NSString stringWithFormat:@"Unsupported AtomJS native command: %@", command]);
428
+ return;
429
+ }
430
+
431
+ AtomJSRespond(requestId, YES, @{}, nil);
432
+ }
433
+
434
+ static void AtomJSConsumeInput(void) {
435
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
436
+ NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput];
437
+ NSMutableData *buffer = [NSMutableData data];
438
+
439
+ while (YES) {
440
+ @autoreleasepool {
441
+ NSData *chunk = [input readDataOfLength:4096];
442
+ if (chunk.length == 0) break;
443
+ [buffer appendData:chunk];
444
+
445
+ while (YES) {
446
+ const unsigned char *bytes = buffer.bytes;
447
+ NSUInteger newline = NSNotFound;
448
+ for (NSUInteger index = 0; index < buffer.length; index += 1) {
449
+ if (bytes[index] == '\n') {
450
+ newline = index;
451
+ break;
452
+ }
453
+ }
454
+
455
+ if (newline == NSNotFound) break;
456
+
457
+ NSData *lineData = [buffer subdataWithRange:NSMakeRange(0, newline)];
458
+ [buffer replaceBytesInRange:NSMakeRange(0, newline + 1) withBytes:NULL length:0];
459
+ if (lineData.length == 0) continue;
460
+
461
+ NSError *error = nil;
462
+ id value = [NSJSONSerialization JSONObjectWithData:lineData options:0 error:&error];
463
+ if (![value isKindOfClass:[NSDictionary class]] || error) continue;
464
+
465
+ NSDictionary *message = value;
466
+ dispatch_async(dispatch_get_main_queue(), ^{
467
+ AtomJSHandleMessage(message);
468
+ });
469
+ }
470
+ }
471
+ }
472
+
473
+ dispatch_async(dispatch_get_main_queue(), ^{
474
+ [NSApp terminate:nil];
475
+ });
476
+ });
477
+ }
478
+
479
+ static void AtomJSInstallMenu(void) {
480
+ NSMenu *mainMenu = [[NSMenu alloc] initWithTitle:@""];
481
+ NSMenuItem *applicationItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
482
+ [mainMenu addItem:applicationItem];
483
+
484
+ NSMenu *applicationMenu = [[NSMenu alloc] initWithTitle:atomAppName];
485
+ NSString *quitTitle = [NSString stringWithFormat:@"Quit %@", atomAppName];
486
+ NSMenuItem *quitItem = [[NSMenuItem alloc]
487
+ initWithTitle:quitTitle
488
+ action:@selector(terminate:)
489
+ keyEquivalent:@"q"];
490
+ [applicationMenu addItem:quitItem];
491
+ applicationItem.submenu = applicationMenu;
492
+ NSApp.mainMenu = mainMenu;
493
+ }
494
+
495
+ int main(int argc, const char *argv[]) {
496
+ @autoreleasepool {
497
+ for (int index = 1; index + 1 < argc; index += 1) {
498
+ if (strcmp(argv[index], "--app-name") == 0) {
499
+ atomAppName = [NSString stringWithUTF8String:argv[index + 1]] ?: @"AtomJS App";
500
+ index += 1;
501
+ }
502
+ }
503
+
504
+ atomWindows = [NSMutableDictionary dictionary];
505
+ NSApplication *application = [NSApplication sharedApplication];
506
+ [application setActivationPolicy:NSApplicationActivationPolicyRegular];
507
+ AtomJSInstallMenu();
508
+ AtomJSConsumeInput();
509
+
510
+ AtomJSEmit(@{
511
+ @"type": @"ready",
512
+ @"pid": @([[NSProcessInfo processInfo] processIdentifier])
513
+ });
514
+
515
+ [application run];
516
+ }
517
+
518
+ return 0;
519
+ }
@@ -1,7 +1,4 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { spawn } from 'node:child_process';
4
- import { fileURLToPath } from 'node:url';
5
2
 
6
3
  const configPath = process.argv[2];
7
4
  if (!configPath) {
@@ -15,80 +12,39 @@ try {
15
12
  } catch (error) {
16
13
  console.error('Unable to read AtomJS window configuration:', error);
17
14
  process.exit(1);
15
+ } finally {
16
+ fs.rmSync(configPath, { force: true });
18
17
  }
19
18
 
20
19
  if (process.platform === 'darwin') {
21
- await runMacOSHost(configPath);
22
- } else {
23
- fs.rmSync(configPath, { force: true });
24
- await runNativeBindingHost(config);
20
+ console.error('AtomJS macOS windows must use the shared native Cocoa host, not the legacy Node window host.');
21
+ process.exit(1);
25
22
  }
26
23
 
27
- async function runMacOSHost(configurationPath) {
28
- const runtimeDirectory = path.dirname(fileURLToPath(import.meta.url));
29
- const helperPath = path.join(runtimeDirectory, 'macos-window-host.jxa.js');
30
- const osascript = '/usr/bin/osascript';
31
-
32
- if (!fs.existsSync(osascript)) {
33
- console.error('AtomJS could not find /usr/bin/osascript, required for the pure-JavaScript macOS WKWebView host.');
34
- fs.rmSync(configurationPath, { force: true });
35
- process.exit(1);
36
- }
37
-
38
- const child = spawn(osascript, ['-l', 'JavaScript', helperPath, configurationPath], {
39
- cwd: process.cwd(),
40
- env: process.env,
41
- stdio: ['ignore', 'inherit', 'inherit']
42
- });
43
-
44
- const forwardSignal = (signal) => {
45
- if (!child.killed) child.kill(signal);
46
- };
47
- process.once('SIGINT', () => forwardSignal('SIGINT'));
48
- process.once('SIGTERM', () => forwardSignal('SIGTERM'));
49
- process.once('SIGHUP', () => forwardSignal('SIGHUP'));
50
-
51
- await new Promise((resolve) => {
52
- child.once('error', (error) => {
53
- console.error('AtomJS macOS WKWebView host failed to start:', error && error.stack ? error.stack : error);
54
- process.exitCode = 1;
55
- resolve();
56
- });
57
- child.once('exit', (code, signal) => {
58
- if (code !== 0 && signal == null) process.exitCode = code || 1;
59
- resolve();
60
- });
61
- });
62
-
63
- fs.rmSync(configurationPath, { force: true });
24
+ let Webview;
25
+ let SizeHint;
26
+ try {
27
+ ({ Webview, SizeHint } = await import('webview-nodejs'));
28
+ } catch (error) {
29
+ console.error('\nAtomJS could not load the system WebView binding.');
30
+ console.error('Windows and Linux currently require the webview-nodejs package.');
31
+ console.error('Run `atom doctor`, install the platform prerequisites, then install dependencies again.');
32
+ console.error(error && error.stack ? error.stack : error);
33
+ process.exit(1);
64
34
  }
65
35
 
66
- async function runNativeBindingHost(runtimeConfig) {
67
- let Webview;
68
- let SizeHint;
69
- try {
70
- ({ Webview, SizeHint } = await import('webview-nodejs'));
71
- } catch (error) {
72
- console.error('\nAtomJS could not load the system WebView binding.');
73
- console.error('Windows and Linux currently require the webview-nodejs package.');
74
- console.error('Run `atom doctor`, install the platform prerequisites, then install dependencies again.');
75
- console.error(error && error.stack ? error.stack : error);
76
- process.exit(1);
77
- }
78
-
79
- try {
80
- const view = new Webview(Boolean(runtimeConfig.debug));
81
- view.title(String(runtimeConfig.title || 'AtomJS App'));
82
- view.size(
83
- Number(runtimeConfig.width || 800),
84
- Number(runtimeConfig.height || 600),
85
- runtimeConfig.resizable === false ? SizeHint.Fixed : SizeHint.None
86
- );
87
- view.init(String(runtimeConfig.bridgeScript || ''));
88
- view.navigate(String(runtimeConfig.url));
89
- view.show();
90
- } catch (error) {
91
- console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
92
- process.exit(1);
93
- }
36
+ try {
37
+ const view = new Webview(Boolean(config.debug));
38
+ view.title(String(config.title || 'AtomJS App'));
39
+ view.size(
40
+ Number(config.width || 800),
41
+ Number(config.height || 600),
42
+ config.resizable === false ? SizeHint.Fixed : SizeHint.None
43
+ );
44
+ view.init(String(config.bridgeScript || ''));
45
+ view.navigate(String(config.url));
46
+ view.show();
47
+ } catch (error) {
48
+ console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
49
+ process.exit(1);
94
50
  }
@@ -25,12 +25,18 @@ class WebContents extends EventEmitter {
25
25
 
26
26
  reload() {
27
27
  ensureAlive(this.owner);
28
- state.bridgeServer.send(this.owner.id, { type: 'system', command: 'reload' });
28
+ if (!this.owner._sendHostCommand({ command: 'reload' })) {
29
+ state.bridgeServer.send(this.owner.id, { type: 'system', command: 'reload' });
30
+ }
29
31
  }
30
32
 
31
33
  loadURL(url) {
32
34
  ensureAlive(this.owner);
33
- state.bridgeServer.send(this.owner.id, { type: 'system', command: 'navigate', url: String(url) });
35
+ const value = String(url);
36
+ this.owner._currentUrl = value;
37
+ if (!this.owner._sendHostCommand({ command: 'navigate', url: value })) {
38
+ state.bridgeServer.send(this.owner.id, { type: 'system', command: 'navigate', url: value });
39
+ }
34
40
  }
35
41
 
36
42
  openDevTools() {