@atom-js-org/runtime 0.4.5-alpha.0 → 0.5.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.
package/README.md CHANGED
@@ -1,17 +1,25 @@
1
1
  # @atom-js-org/runtime
2
2
 
3
- The lightweight AtomJS runtime. It uses Node.js for the main process and the operating system WebView for rendering.
3
+ The AtomJS runtime provides an Electron-style main-process API powered by operating-system WebViews.
4
4
 
5
- AtomJS does not install or execute the Electron runtime and does not bundle a private Chromium copy. On Windows, the system WebView is Microsoft Edge WebView2; on macOS it is WKWebView; on Linux it is WebKitGTK through the current native binding.
5
+ ```js
6
+ const { app, BrowserWindow } = require('electron');
7
+
8
+ app.whenReady().then(() => {
9
+ const main = new BrowserWindow({ width: 1000, height: 700 });
10
+ const auth = new BrowserWindow({ parent: main, modal: true, width: 520, height: 720 });
11
+ auth.loadURL('https://example.com/login');
12
+ });
13
+ ```
14
+
15
+ Supported window options include parent/modal relationships, frame and resize controls, always-on-top, opacity, transparency, min/max sizes, taskbar behavior, macOS title-bar styles and traffic-light positioning.
6
16
 
7
- Most applications should import the Electron-compatible alias instead:
17
+ AtomJS does not install or execute Electron and does not bundle a private Chromium copy. Windows uses WebView2, macOS uses WKWebView, and Linux uses WebKitGTK.
18
+
19
+ Most applications should install the Electron-compatible alias:
8
20
 
9
21
  ```bash
10
22
  npm install electron@npm:@atom-js-org/electron@alpha
11
23
  ```
12
24
 
13
- ```js
14
- const { app, BrowserWindow, ipcMain } = require('electron');
15
- ```
16
-
17
25
  Project: https://github.com/Atom-js-org/atom
package/index.d.ts CHANGED
@@ -5,11 +5,30 @@ import { EventEmitter } from 'node:events';
5
5
  export interface BrowserWindowConstructorOptions {
6
6
  width?: number;
7
7
  height?: number;
8
+ x?: number;
9
+ y?: number;
8
10
  title?: string;
9
11
  show?: boolean;
10
12
  resizable?: boolean;
11
13
  center?: boolean;
12
14
  frame?: boolean;
15
+ parent?: BrowserWindow;
16
+ modal?: boolean;
17
+ alwaysOnTop?: boolean;
18
+ focusable?: boolean;
19
+ closable?: boolean;
20
+ minimizable?: boolean;
21
+ maximizable?: boolean;
22
+ fullscreenable?: boolean;
23
+ skipTaskbar?: boolean;
24
+ transparent?: boolean;
25
+ opacity?: number;
26
+ titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
27
+ trafficLightPosition?: { x: number; y: number };
28
+ minWidth?: number;
29
+ minHeight?: number;
30
+ maxWidth?: number;
31
+ maxHeight?: number;
13
32
  backgroundColor?: string;
14
33
  webPreferences?: {
15
34
  preload?: string;
@@ -48,6 +67,8 @@ export class BrowserWindow extends EventEmitter {
48
67
  isDestroyed(): boolean;
49
68
  setTitle(title: string): void;
50
69
  getTitle(): string;
70
+ getParentWindow(): BrowserWindow | null;
71
+ getChildWindows(): BrowserWindow[];
51
72
  setMenu(menu: Menu | null): this;
52
73
  getMenu(): Menu | null;
53
74
  removeMenu(): void;
@@ -55,6 +76,13 @@ export class BrowserWindow extends EventEmitter {
55
76
  isMenuBarVisible(): boolean;
56
77
  setAutoHideMenuBar(hide: boolean): void;
57
78
  isFullScreen(): boolean;
79
+ setAlwaysOnTop(flag?: boolean): void;
80
+ isAlwaysOnTop(): boolean;
81
+ setOpacity(opacity: number): void;
82
+ getOpacity(): number;
83
+ setResizable(flag: boolean): void;
84
+ isResizable(): boolean;
85
+ isModal(): boolean;
58
86
  setFullScreen(flag: boolean): void;
59
87
  maximize(): void;
60
88
  unmaximize(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/runtime",
3
- "version": "0.4.5-alpha.0",
3
+ "version": "0.5.0-alpha.0",
4
4
  "description": "Electron-like desktop runtime powered by the operating system WebView.",
5
5
  "main": "src/index.cjs",
6
6
  "types": "index.d.ts",
@@ -15,6 +15,14 @@ class BrowserWindow extends EventEmitter {
15
15
  constructor(options = {}) {
16
16
  super();
17
17
  this.id = state.nextWindowId++;
18
+ const explicitParent = options.parent instanceof BrowserWindow && !options.parent.isDestroyed()
19
+ ? options.parent
20
+ : null;
21
+ const modalParent = !explicitParent && options.modal === true
22
+ ? BrowserWindow.getFocusedWindow()
23
+ : null;
24
+ this._parent = explicitParent || modalParent || null;
25
+ this._children = new Set();
18
26
  this.options = normalizeOptions(options);
19
27
  this.webContents = new WebContents(this);
20
28
  this._child = null;
@@ -32,12 +40,15 @@ class BrowserWindow extends EventEmitter {
32
40
  this._fullScreen = false;
33
41
  this._maximized = false;
34
42
  this._minimized = false;
43
+ this._alwaysOnTop = this.options.alwaysOnTop;
44
+ this._opacity = this.options.opacity;
35
45
  this._bounds = {
36
- x: 0,
37
- y: 0,
46
+ x: this.options.x == null ? 0 : this.options.x,
47
+ y: this.options.y == null ? 0 : this.options.y,
38
48
  width: this.options.width,
39
49
  height: this.options.height
40
50
  };
51
+ if (this._parent) this._parent._children.add(this);
41
52
  state.windows.set(this.id, this);
42
53
  }
43
54
 
@@ -82,12 +93,33 @@ class BrowserWindow extends EventEmitter {
82
93
  if (preloadPath) preloadCode = await fs.promises.readFile(path.resolve(preloadPath), 'utf8');
83
94
 
84
95
  const config = {
96
+ windowId: this.id,
85
97
  title: this.options.title || app.getName(),
86
98
  width: this.options.width,
87
99
  height: this.options.height,
100
+ x: this.options.x,
101
+ y: this.options.y,
88
102
  resizable: this.options.resizable,
89
103
  center: this.options.center,
90
104
  frame: this.options.frame,
105
+ parentWindowId: this._parent ? this._parent.id : null,
106
+ parentProcessId: this._parent && this._parent._child ? this._parent._child.pid : null,
107
+ modal: this.options.modal,
108
+ alwaysOnTop: this.options.alwaysOnTop,
109
+ focusable: this.options.focusable,
110
+ closable: this.options.closable,
111
+ minimizable: this.options.minimizable,
112
+ maximizable: this.options.maximizable,
113
+ fullscreenable: this.options.fullscreenable,
114
+ skipTaskbar: this.options.skipTaskbar,
115
+ transparent: this.options.transparent,
116
+ opacity: this.options.opacity,
117
+ titleBarStyle: this.options.titleBarStyle,
118
+ trafficLightPosition: this.options.trafficLightPosition,
119
+ minWidth: this.options.minWidth,
120
+ minHeight: this.options.minHeight,
121
+ maxWidth: this.options.maxWidth,
122
+ maxHeight: this.options.maxHeight,
91
123
  show: this.options.show,
92
124
  backgroundColor: this.options.backgroundColor,
93
125
  debug: Boolean(this.options.webPreferences.devTools || process.env.ATOM_DEV === '1'),
@@ -124,7 +156,8 @@ class BrowserWindow extends EventEmitter {
124
156
  this._child = spawn(nodeExecutable, hostArgs, {
125
157
  cwd: state.projectRoot,
126
158
  env: process.env,
127
- stdio: ['ignore', 'pipe', 'inherit']
159
+ stdio: ['ignore', 'pipe', 'inherit'],
160
+ windowsHide: true
128
161
  });
129
162
  this._hostAttached = true;
130
163
  attachHostOutput(this, this._child);
@@ -198,10 +231,12 @@ class BrowserWindow extends EventEmitter {
198
231
  return;
199
232
  }
200
233
  if (event.type === 'focus') {
234
+ state.focusedWindowId = this.id;
201
235
  this.emit('focus');
202
236
  return;
203
237
  }
204
238
  if (event.type === 'blur') {
239
+ if (state.focusedWindowId === this.id) state.focusedWindowId = null;
205
240
  this.emit('blur');
206
241
  return;
207
242
  }
@@ -266,6 +301,10 @@ class BrowserWindow extends EventEmitter {
266
301
  if (this._destroyed) return;
267
302
  this._destroyed = true;
268
303
  this._hostAttached = false;
304
+ if (state.focusedWindowId === this.id) state.focusedWindowId = null;
305
+ if (this._parent) this._parent._children.delete(this);
306
+ for (const child of this._children) child._parent = null;
307
+ this._children.clear();
269
308
  state.windows.delete(this.id);
270
309
  this.emit('closed', details);
271
310
  if (state.windows.size === 0 && !state.isQuitting) app.emit('window-all-closed');
@@ -337,6 +376,14 @@ class BrowserWindow extends EventEmitter {
337
376
  return this.options.title || app.getName();
338
377
  }
339
378
 
379
+ getParentWindow() {
380
+ return this._parent && !this._parent.isDestroyed() ? this._parent : null;
381
+ }
382
+
383
+ getChildWindows() {
384
+ return [...this._children].filter((child) => !child.isDestroyed());
385
+ }
386
+
340
387
  setMenu(menu) {
341
388
  this._menu = menu || null;
342
389
  return this;
@@ -368,6 +415,49 @@ class BrowserWindow extends EventEmitter {
368
415
  return this._fullScreen;
369
416
  }
370
417
 
418
+ setAlwaysOnTop(value = true) {
419
+ this._alwaysOnTop = Boolean(value);
420
+ this.options.alwaysOnTop = this._alwaysOnTop;
421
+ if (!this._sendHostCommand({ command: 'set-always-on-top', value: this._alwaysOnTop })) {
422
+ console.warn('[AtomJS] Changing always-on-top after creation is not supported by the current platform host. Set alwaysOnTop in BrowserWindow options.');
423
+ }
424
+ }
425
+
426
+ isAlwaysOnTop() {
427
+ return this._alwaysOnTop;
428
+ }
429
+
430
+ setOpacity(value) {
431
+ const number = Number(value);
432
+ if (!Number.isFinite(number) || number < 0 || number > 1) {
433
+ throw new TypeError('BrowserWindow.setOpacity expects a number between 0 and 1.');
434
+ }
435
+ this._opacity = number;
436
+ this.options.opacity = number;
437
+ if (!this._sendHostCommand({ command: 'set-opacity', value: number })) {
438
+ console.warn('[AtomJS] Changing opacity after creation is not supported by the current platform host. Set opacity in BrowserWindow options.');
439
+ }
440
+ }
441
+
442
+ getOpacity() {
443
+ return this._opacity;
444
+ }
445
+
446
+ setResizable(value) {
447
+ this.options.resizable = Boolean(value);
448
+ if (!this._sendHostCommand({ command: 'set-resizable', value: this.options.resizable })) {
449
+ console.warn('[AtomJS] Changing resizable after creation is not supported by the current platform host.');
450
+ }
451
+ }
452
+
453
+ isResizable() {
454
+ return this.options.resizable;
455
+ }
456
+
457
+ isModal() {
458
+ return this.options.modal;
459
+ }
460
+
371
461
  setFullScreen(value = true) {
372
462
  this._fullScreen = Boolean(value);
373
463
  if (!this._sendHostCommand({ command: 'fullscreen', value: this._fullScreen })) {
@@ -443,6 +533,10 @@ class BrowserWindow extends EventEmitter {
443
533
  }
444
534
 
445
535
  static getFocusedWindow() {
536
+ if (state.focusedWindowId != null) {
537
+ const focused = BrowserWindow.fromId(state.focusedWindowId);
538
+ if (focused && !focused.isDestroyed()) return focused;
539
+ }
446
540
  const windows = BrowserWindow.getAllWindows();
447
541
  return windows.length ? windows[windows.length - 1] : null;
448
542
  }
@@ -483,9 +577,27 @@ function normalizeOptions(options) {
483
577
  return {
484
578
  width: finiteOr(options.width, 800),
485
579
  height: finiteOr(options.height, 600),
580
+ x: finiteOrNull(options.x),
581
+ y: finiteOrNull(options.y),
486
582
  title: options.title ? String(options.title) : '',
487
583
  show: options.show !== false,
488
584
  resizable: options.resizable !== false,
585
+ modal: options.modal === true,
586
+ alwaysOnTop: options.alwaysOnTop === true,
587
+ focusable: options.focusable !== false,
588
+ closable: options.closable !== false,
589
+ minimizable: options.minimizable !== false,
590
+ maximizable: options.maximizable !== false,
591
+ fullscreenable: options.fullscreenable !== false,
592
+ skipTaskbar: options.skipTaskbar === true,
593
+ transparent: options.transparent === true,
594
+ opacity: clampOpacity(options.opacity),
595
+ titleBarStyle: normalizeTitleBarStyle(options.titleBarStyle),
596
+ trafficLightPosition: normalizePoint(options.trafficLightPosition),
597
+ minWidth: finiteOrZero(options.minWidth),
598
+ minHeight: finiteOrZero(options.minHeight),
599
+ maxWidth: finiteOrZero(options.maxWidth),
600
+ maxHeight: finiteOrZero(options.maxHeight),
489
601
  center: options.center !== false,
490
602
  frame: options.frame !== false,
491
603
  backgroundColor: options.backgroundColor || '#ffffff',
@@ -500,6 +612,38 @@ function normalizeOptions(options) {
500
612
  };
501
613
  }
502
614
 
615
+ function clampOpacity(value) {
616
+ if (value == null) return 1;
617
+ const number = Number(value);
618
+ if (!Number.isFinite(number)) return 1;
619
+ return Math.min(1, Math.max(0, number));
620
+ }
621
+
622
+ function finiteOrZero(value) {
623
+ const number = Number(value);
624
+ return Number.isFinite(number) && number > 0 ? Math.round(number) : 0;
625
+ }
626
+
627
+ function finiteOrNull(value) {
628
+ if (value == null) return null;
629
+ const number = Number(value);
630
+ return Number.isFinite(number) ? Math.round(number) : null;
631
+ }
632
+
633
+ function normalizePoint(value) {
634
+ if (!value || typeof value !== 'object') return null;
635
+ const x = Number(value.x);
636
+ const y = Number(value.y);
637
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
638
+ }
639
+
640
+ function normalizeTitleBarStyle(value) {
641
+ const normalized = String(value || 'default');
642
+ return ['default', 'hidden', 'hiddenInset', 'customButtonsOnHover'].includes(normalized)
643
+ ? normalized
644
+ : 'default';
645
+ }
646
+
503
647
  function finiteOr(value, fallback) {
504
648
  const number = Number(value);
505
649
  return Number.isFinite(number) && number > 0 ? Math.round(number) : fallback;
@@ -114,6 +114,8 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
114
114
  @property(nonatomic, strong) NSNumber *windowId;
115
115
  @property(nonatomic, strong) NSWindow *window;
116
116
  @property(nonatomic, strong) WKWebView *webView;
117
+ @property(nonatomic, weak) AtomJSWindowController *parentController;
118
+ @property(nonatomic, assign) BOOL modal;
117
119
  - (instancetype)initWithWindowId:(NSNumber *)windowId config:(NSDictionary *)config;
118
120
  - (void)navigate:(NSString *)urlString;
119
121
  @end
@@ -130,10 +132,20 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
130
132
  CGFloat height = MAX(240.0, [AtomJSNumber(config[@"height"], @600) doubleValue]);
131
133
  NSRect frame = NSMakeRect(0, 0, width, height);
132
134
 
133
- NSWindowStyleMask style = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
135
+ NSWindowStyleMask style = 0;
136
+ if (AtomJSBoolean(config[@"closable"], YES)) style |= NSWindowStyleMaskClosable;
137
+ if (AtomJSBoolean(config[@"minimizable"], YES)) style |= NSWindowStyleMaskMiniaturizable;
134
138
  if (AtomJSBoolean(config[@"frame"], YES)) style |= NSWindowStyleMaskTitled;
135
139
  if (AtomJSBoolean(config[@"resizable"], YES)) style |= NSWindowStyleMaskResizable;
136
140
 
141
+ NSString *titleBarStyle = AtomJSString(config[@"titleBarStyle"], @"default");
142
+ BOOL usesHiddenTitleBar = [titleBarStyle isEqualToString:@"hidden"]
143
+ || [titleBarStyle isEqualToString:@"hiddenInset"]
144
+ || [titleBarStyle isEqualToString:@"customButtonsOnHover"];
145
+ if (usesHiddenTitleBar) {
146
+ style |= NSWindowStyleMaskFullSizeContentView;
147
+ }
148
+
137
149
  _window = [[NSWindow alloc]
138
150
  initWithContentRect:frame
139
151
  styleMask:style
@@ -144,8 +156,41 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
144
156
  _window.title = AtomJSString(config[@"title"], atomAppName);
145
157
  _window.backgroundColor = AtomJSColor(config[@"backgroundColor"]);
146
158
  _window.tabbingMode = NSWindowTabbingModeDisallowed;
159
+ _window.alphaValue = MIN(1.0, MAX(0.0, [AtomJSNumber(config[@"opacity"], @1) doubleValue]));
160
+ _window.opaque = !AtomJSBoolean(config[@"transparent"], NO);
161
+ if (!_window.opaque) {
162
+ _window.backgroundColor = [NSColor clearColor];
163
+ }
164
+
165
+ if (usesHiddenTitleBar) {
166
+ _window.titleVisibility = NSWindowTitleHidden;
167
+ _window.titlebarAppearsTransparent = YES;
168
+ }
147
169
 
148
- if (AtomJSBoolean(config[@"center"], YES)) [_window center];
170
+ if (!AtomJSBoolean(config[@"fullscreenable"], YES)) {
171
+ _window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenNone;
172
+ }
173
+ [_window standardWindowButton:NSWindowZoomButton].enabled = AtomJSBoolean(config[@"maximizable"], YES);
174
+ if (AtomJSBoolean(config[@"alwaysOnTop"], NO)) {
175
+ _window.level = NSFloatingWindowLevel;
176
+ }
177
+
178
+ CGFloat minWidth = [AtomJSNumber(config[@"minWidth"], @0) doubleValue];
179
+ CGFloat minHeight = [AtomJSNumber(config[@"minHeight"], @0) doubleValue];
180
+ CGFloat maxWidth = [AtomJSNumber(config[@"maxWidth"], @0) doubleValue];
181
+ CGFloat maxHeight = [AtomJSNumber(config[@"maxHeight"], @0) doubleValue];
182
+ if (minWidth > 0 || minHeight > 0) {
183
+ _window.contentMinSize = NSMakeSize(MAX(1.0, minWidth), MAX(1.0, minHeight));
184
+ }
185
+ if (maxWidth > 0 || maxHeight > 0) {
186
+ _window.contentMaxSize = NSMakeSize(maxWidth > 0 ? maxWidth : CGFLOAT_MAX, maxHeight > 0 ? maxHeight : CGFLOAT_MAX);
187
+ }
188
+
189
+ if ([config[@"x"] isKindOfClass:[NSNumber class]] && [config[@"y"] isKindOfClass:[NSNumber class]]) {
190
+ [_window setFrameOrigin:NSMakePoint([config[@"x"] doubleValue], [config[@"y"] doubleValue])];
191
+ } else if (AtomJSBoolean(config[@"center"], YES)) {
192
+ [_window center];
193
+ }
149
194
 
150
195
  WKUserContentController *contentController = [[WKUserContentController alloc] init];
151
196
  NSString *bridgeScript = AtomJSString(config[@"bridgeScript"], @"");
@@ -163,14 +208,40 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
163
208
  _webView = [[WKWebView alloc] initWithFrame:frame configuration:webConfiguration];
164
209
  _webView.navigationDelegate = self;
165
210
  _webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
211
+ if (AtomJSBoolean(config[@"transparent"], NO)) _webView.drawsBackground = NO;
166
212
  _window.contentView = _webView;
167
213
 
214
+ NSNumber *parentWindowId = AtomJSNumber(config[@"parentWindowId"], nil);
215
+ _parentController = parentWindowId ? atomWindows[parentWindowId] : nil;
216
+ _modal = AtomJSBoolean(config[@"modal"], NO) && _parentController != nil;
217
+
218
+ NSDictionary *trafficLightPosition = [config[@"trafficLightPosition"] isKindOfClass:[NSDictionary class]]
219
+ ? config[@"trafficLightPosition"]
220
+ : nil;
221
+ if (trafficLightPosition) {
222
+ CGFloat x = [AtomJSNumber(trafficLightPosition[@"x"], @0) doubleValue];
223
+ CGFloat y = [AtomJSNumber(trafficLightPosition[@"y"], @0) doubleValue];
224
+ NSButton *closeButton = [_window standardWindowButton:NSWindowCloseButton];
225
+ NSView *container = closeButton.superview;
226
+ if (container) {
227
+ NSPoint origin = container.frame.origin;
228
+ origin.x = x;
229
+ origin.y = MAX(0.0, _window.frame.size.height - container.frame.size.height - y);
230
+ [container setFrameOrigin:origin];
231
+ }
232
+ }
233
+
168
234
  [self navigate:AtomJSString(config[@"url"], @"about:blank")];
169
235
 
170
236
  if (AtomJSBoolean(config[@"show"], YES)) {
171
237
  [NSApp unhide:nil];
172
- [_window makeKeyAndOrderFront:nil];
173
- [_window orderFrontRegardless];
238
+ if (_modal) {
239
+ [_parentController.window beginSheet:_window completionHandler:nil];
240
+ } else {
241
+ if (_parentController) [_parentController.window addChildWindow:_window ordered:NSWindowAbove];
242
+ [_window makeKeyAndOrderFront:nil];
243
+ [_window orderFrontRegardless];
244
+ }
174
245
  [NSApp activateIgnoringOtherApps:YES];
175
246
  }
176
247
 
@@ -226,6 +297,13 @@ static void AtomJSConfigureApplicationIdentity(NSApplication *application) {
226
297
  }
227
298
 
228
299
  - (void)windowWillClose:(NSNotification *)notification {
300
+ if (self.parentController) {
301
+ if (self.modal && self.window.sheetParent) {
302
+ [self.window.sheetParent endSheet:self.window];
303
+ } else {
304
+ [self.parentController.window removeChildWindow:self.window];
305
+ }
306
+ }
229
307
  [atomWindows removeObjectForKey:self.windowId];
230
308
  AtomJSEmit(@{
231
309
  @"type": @"closed",
@@ -486,6 +564,14 @@ static void AtomJSHandleMessage(NSDictionary *message) {
486
564
  if ([bounds[@"width"] isKindOfClass:[NSNumber class]]) frame.size.width = MAX(1.0, [bounds[@"width"] doubleValue]);
487
565
  if ([bounds[@"height"] isKindOfClass:[NSNumber class]]) frame.size.height = MAX(1.0, [bounds[@"height"] doubleValue]);
488
566
  [controller.window setFrame:frame display:YES animate:NO];
567
+ } else if ([command isEqualToString:@"set-always-on-top"]) {
568
+ controller.window.level = AtomJSBoolean(message[@"value"], NO) ? NSFloatingWindowLevel : NSNormalWindowLevel;
569
+ } else if ([command isEqualToString:@"set-opacity"]) {
570
+ controller.window.alphaValue = MIN(1.0, MAX(0.0, [AtomJSNumber(message[@"value"], @1) doubleValue]));
571
+ } else if ([command isEqualToString:@"set-resizable"]) {
572
+ BOOL requested = AtomJSBoolean(message[@"value"], YES);
573
+ if (requested) controller.window.styleMask |= NSWindowStyleMaskResizable;
574
+ else controller.window.styleMask &= ~NSWindowStyleMaskResizable;
489
575
  } else {
490
576
  AtomJSRespond(requestId, NO, nil, [NSString stringWithFormat:@"Unsupported AtomJS native command: %@", command]);
491
577
  return;
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
2
3
 
4
+ const EVENT_PREFIX = '__ATOMJS_EVENT__';
3
5
  const configPath = process.argv[2];
4
6
  if (!configPath) {
5
7
  console.error('AtomJS window host expected a configuration file');
@@ -33,18 +35,387 @@ try {
33
35
  process.exit(1);
34
36
  }
35
37
 
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);
38
+ function emit(event) {
39
+ process.stdout.write(`${EVENT_PREFIX}${JSON.stringify(event)}\n`);
40
+ }
41
+
42
+ function configureWindowsWindow(options) {
43
+ const parentProcessId = Number(options.parentProcessId || 0);
44
+ const modal = Boolean(options.modal && parentProcessId > 0);
45
+ const env = {
46
+ ...process.env,
47
+ ATOMJS_CHILD_PID: String(process.pid),
48
+ ATOMJS_PARENT_PID: String(parentProcessId || 0),
49
+ ATOMJS_MODAL: modal ? '1' : '0',
50
+ ATOMJS_FRAME: options.frame === false ? '0' : '1',
51
+ ATOMJS_RESIZABLE: options.resizable === false ? '0' : '1',
52
+ ATOMJS_MINIMIZABLE: options.minimizable === false ? '0' : '1',
53
+ ATOMJS_MAXIMIZABLE: options.maximizable === false ? '0' : '1',
54
+ ATOMJS_CLOSABLE: options.closable === false ? '0' : '1',
55
+ ATOMJS_ALWAYS_ON_TOP: options.alwaysOnTop ? '1' : '0',
56
+ ATOMJS_FOCUSABLE: options.focusable === false ? '0' : '1',
57
+ ATOMJS_SKIP_TASKBAR: options.skipTaskbar ? '1' : '0',
58
+ ATOMJS_TRANSPARENT: options.transparent ? '1' : '0',
59
+ ATOMJS_OPACITY: String(normalizeOpacity(options.opacity)),
60
+ ATOMJS_CENTER: options.center === false ? '0' : '1',
61
+ ATOMJS_SHOW: options.show === false ? '0' : '1',
62
+ ATOMJS_X: Number.isFinite(Number(options.x)) ? String(Math.round(Number(options.x))) : '',
63
+ ATOMJS_Y: Number.isFinite(Number(options.y)) ? String(Math.round(Number(options.y))) : ''
64
+ };
65
+
66
+ const result = spawnSync('powershell.exe', [
67
+ '-NoLogo',
68
+ '-NoProfile',
69
+ '-NonInteractive',
70
+ '-ExecutionPolicy', 'Bypass',
71
+ '-Command', WINDOWS_WINDOW_SETUP
72
+ ], {
73
+ env,
74
+ encoding: 'utf8',
75
+ windowsHide: true,
76
+ stdio: ['ignore', 'pipe', 'pipe']
77
+ });
78
+
79
+ if (result.error) {
80
+ console.warn(`[AtomJS] Could not configure the native Windows window: ${result.error.message}`);
81
+ } else if (result.status !== 0) {
82
+ const detail = String(result.stderr || result.stdout || '').trim();
83
+ if (detail) console.warn(`[AtomJS] Could not configure every native Windows option: ${detail}`);
84
+ }
85
+
86
+ return modal ? { parentProcessId } : null;
87
+ }
88
+
89
+ function releaseWindowsModal(ownership) {
90
+ if (!ownership || !ownership.parentProcessId) return;
91
+ const result = spawnSync('powershell.exe', [
92
+ '-NoLogo',
93
+ '-NoProfile',
94
+ '-NonInteractive',
95
+ '-ExecutionPolicy', 'Bypass',
96
+ '-Command', WINDOWS_MODAL_RELEASE
97
+ ], {
98
+ env: {
99
+ ...process.env,
100
+ ATOMJS_PARENT_PID: String(ownership.parentProcessId)
101
+ },
102
+ encoding: 'utf8',
103
+ windowsHide: true,
104
+ stdio: ['ignore', 'pipe', 'pipe']
105
+ });
106
+ if (result.error) {
107
+ console.warn(`[AtomJS] Could not restore the parent Windows window: ${result.error.message}`);
108
+ }
50
109
  }
110
+
111
+ function normalizeOpacity(value) {
112
+ const number = Number(value);
113
+ if (!Number.isFinite(number)) return 1;
114
+ return Math.min(1, Math.max(0, number));
115
+ }
116
+
117
+ const WINDOWS_INTEROP = String.raw`
118
+ Add-Type -TypeDefinition @'
119
+ using System;
120
+ using System.Runtime.InteropServices;
121
+
122
+ public static class AtomJSNativeWindow {
123
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
124
+
125
+ [DllImport("user32.dll")]
126
+ public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
127
+
128
+ [DllImport("user32.dll")]
129
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
130
+
131
+ [DllImport("user32.dll")]
132
+ public static extern IntPtr GetParent(IntPtr hWnd);
133
+
134
+ [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
135
+ private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int index);
136
+
137
+ [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
138
+ private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int index);
139
+
140
+ [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
141
+ private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int index, IntPtr value);
142
+
143
+ [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
144
+ private static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int index, IntPtr value);
145
+
146
+ [DllImport("user32.dll")]
147
+ public static extern bool SetWindowPos(IntPtr hWnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags);
148
+
149
+ [DllImport("user32.dll")]
150
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
151
+
152
+ [DllImport("user32.dll")]
153
+ public static extern IntPtr GetForegroundWindow();
154
+
155
+ [DllImport("user32.dll")]
156
+ public static extern bool AttachThreadInput(uint attachThreadId, uint attachToThreadId, bool attach);
157
+
158
+ [DllImport("kernel32.dll")]
159
+ public static extern uint GetCurrentThreadId();
160
+
161
+ [DllImport("user32.dll")]
162
+ public static extern IntPtr SetActiveWindow(IntPtr hWnd);
163
+
164
+ [DllImport("user32.dll")]
165
+ public static extern IntPtr SetFocus(IntPtr hWnd);
166
+
167
+ [DllImport("user32.dll")]
168
+ public static extern bool BringWindowToTop(IntPtr hWnd);
169
+
170
+ [DllImport("user32.dll")]
171
+ public static extern bool ShowWindow(IntPtr hWnd, int command);
172
+
173
+ [DllImport("user32.dll")]
174
+ public static extern bool EnableWindow(IntPtr hWnd, bool enabled);
175
+
176
+ [DllImport("user32.dll")]
177
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
178
+
179
+ [DllImport("user32.dll")]
180
+ public static extern int GetSystemMetrics(int index);
181
+
182
+ [DllImport("user32.dll")]
183
+ public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool revert);
184
+
185
+ [DllImport("user32.dll")]
186
+ public static extern bool DeleteMenu(IntPtr menu, uint position, uint flags);
187
+
188
+ [DllImport("user32.dll")]
189
+ public static extern bool DrawMenuBar(IntPtr hWnd);
190
+
191
+ [DllImport("user32.dll")]
192
+ public static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint colorKey, byte alpha, uint flags);
193
+
194
+ [StructLayout(LayoutKind.Sequential)]
195
+ public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
196
+
197
+ public static IntPtr GetWindowLongPtr(IntPtr hWnd, int index) {
198
+ return IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, index) : GetWindowLongPtr32(hWnd, index);
199
+ }
200
+
201
+ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int index, IntPtr value) {
202
+ return IntPtr.Size == 8 ? SetWindowLongPtr64(hWnd, index, value) : SetWindowLongPtr32(hWnd, index, value);
203
+ }
204
+
205
+ public static IntPtr FindWindowForProcess(uint expectedProcessId) {
206
+ IntPtr result = IntPtr.Zero;
207
+ EnumWindows((window, state) => {
208
+ uint processId;
209
+ GetWindowThreadProcessId(window, out processId);
210
+ if (processId == expectedProcessId && GetParent(window) == IntPtr.Zero) {
211
+ result = window;
212
+ return false;
213
+ }
214
+ return true;
215
+ }, IntPtr.Zero);
216
+ return result;
217
+ }
218
+ }
219
+ '@
220
+
221
+ function Find-AtomWindow([uint32] $ProcessId) {
222
+ for ($attempt = 0; $attempt -lt 50; $attempt += 1) {
223
+ $window = [AtomJSNativeWindow]::FindWindowForProcess($ProcessId)
224
+ if ($window -ne [IntPtr]::Zero) { return $window }
225
+ Start-Sleep -Milliseconds 40
226
+ }
227
+ return [IntPtr]::Zero
228
+ }
229
+ `;
230
+
231
+ const WINDOWS_WINDOW_SETUP = WINDOWS_INTEROP + String.raw`
232
+ $child = Find-AtomWindow ([uint32]$env:ATOMJS_CHILD_PID)
233
+ if ($child -eq [IntPtr]::Zero) { throw 'The AtomJS WebView window handle was not found.' }
234
+
235
+ $GWL_STYLE = -16
236
+ $GWL_EXSTYLE = -20
237
+ $GWLP_HWNDPARENT = -8
238
+ $WS_CAPTION = 0x00C00000L
239
+ $WS_THICKFRAME = 0x00040000L
240
+ $WS_MINIMIZEBOX = 0x00020000L
241
+ $WS_MAXIMIZEBOX = 0x00010000L
242
+ $WS_EX_TOOLWINDOW = 0x00000080L
243
+ $WS_EX_APPWINDOW = 0x00040000L
244
+ $WS_EX_LAYERED = 0x00080000L
245
+ $WS_EX_NOACTIVATE = 0x08000000L
246
+ $SWP_NOSIZE = 0x0001
247
+ $SWP_NOMOVE = 0x0002
248
+ $SWP_NOACTIVATE = 0x0010
249
+ $SWP_FRAMECHANGED = 0x0020
250
+ $SWP_SHOWWINDOW = 0x0040
251
+ $SC_CLOSE = 0xF060
252
+ $MF_BYCOMMAND = 0x0000
253
+ $LWA_ALPHA = 0x00000002
254
+
255
+ $style = [AtomJSNativeWindow]::GetWindowLongPtr($child, $GWL_STYLE).ToInt64()
256
+ if ($env:ATOMJS_FRAME -eq '0') { $style = $style -band (-bnot ($WS_CAPTION -bor $WS_THICKFRAME)) }
257
+ if ($env:ATOMJS_RESIZABLE -eq '0') { $style = $style -band (-bnot $WS_THICKFRAME) }
258
+ if ($env:ATOMJS_MINIMIZABLE -eq '0') { $style = $style -band (-bnot $WS_MINIMIZEBOX) }
259
+ if ($env:ATOMJS_MAXIMIZABLE -eq '0') { $style = $style -band (-bnot $WS_MAXIMIZEBOX) }
260
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWL_STYLE, [IntPtr]$style)
261
+
262
+ $extendedStyle = [AtomJSNativeWindow]::GetWindowLongPtr($child, $GWL_EXSTYLE).ToInt64()
263
+ if ($env:ATOMJS_SKIP_TASKBAR -eq '1') {
264
+ $extendedStyle = ($extendedStyle -bor $WS_EX_TOOLWINDOW) -band (-bnot $WS_EX_APPWINDOW)
265
+ }
266
+ if ($env:ATOMJS_FOCUSABLE -eq '0') {
267
+ $extendedStyle = $extendedStyle -bor $WS_EX_NOACTIVATE
268
+ } else {
269
+ $extendedStyle = $extendedStyle -band (-bnot $WS_EX_NOACTIVATE)
270
+ }
271
+ $opacity = [Math]::Max(0.0, [Math]::Min(1.0, [double]$env:ATOMJS_OPACITY))
272
+ if ($env:ATOMJS_TRANSPARENT -eq '1' -or $opacity -lt 1.0) {
273
+ $extendedStyle = $extendedStyle -bor $WS_EX_LAYERED
274
+ }
275
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWL_EXSTYLE, [IntPtr]$extendedStyle)
276
+
277
+ if ($env:ATOMJS_CLOSABLE -eq '0') {
278
+ $menu = [AtomJSNativeWindow]::GetSystemMenu($child, $false)
279
+ if ($menu -ne [IntPtr]::Zero) {
280
+ [void][AtomJSNativeWindow]::DeleteMenu($menu, $SC_CLOSE, $MF_BYCOMMAND)
281
+ [void][AtomJSNativeWindow]::DrawMenuBar($child)
282
+ }
283
+ }
284
+
285
+ $alpha = [byte][Math]::Round($opacity * 255.0)
286
+ if ($env:ATOMJS_TRANSPARENT -eq '1' -or $opacity -lt 1.0) {
287
+ [void][AtomJSNativeWindow]::SetLayeredWindowAttributes($child, 0, $alpha, $LWA_ALPHA)
288
+ }
289
+
290
+ $parent = [IntPtr]::Zero
291
+ if ([uint32]$env:ATOMJS_PARENT_PID -gt 0) {
292
+ $parent = Find-AtomWindow ([uint32]$env:ATOMJS_PARENT_PID)
293
+ if ($parent -ne [IntPtr]::Zero) {
294
+ [void][AtomJSNativeWindow]::SetWindowLongPtr($child, $GWLP_HWNDPARENT, $parent)
295
+ if ($env:ATOMJS_MODAL -eq '1') { [void][AtomJSNativeWindow]::EnableWindow($parent, $false) }
296
+ }
297
+ }
298
+
299
+ $rect = New-Object AtomJSNativeWindow+RECT
300
+ [void][AtomJSNativeWindow]::GetWindowRect($child, [ref]$rect)
301
+ $width = $rect.Right - $rect.Left
302
+ $height = $rect.Bottom - $rect.Top
303
+ $x = $rect.Left
304
+ $y = $rect.Top
305
+ if ($env:ATOMJS_CENTER -eq '1' -and $parent -ne [IntPtr]::Zero) {
306
+ $parentRect = New-Object AtomJSNativeWindow+RECT
307
+ [void][AtomJSNativeWindow]::GetWindowRect($parent, [ref]$parentRect)
308
+ $x = $parentRect.Left + [Math]::Max(0, (($parentRect.Right - $parentRect.Left) - $width) / 2)
309
+ $y = $parentRect.Top + [Math]::Max(0, (($parentRect.Bottom - $parentRect.Top) - $height) / 2)
310
+ } elseif ($env:ATOMJS_CENTER -eq '1') {
311
+ $x = [Math]::Max(0, ([AtomJSNativeWindow]::GetSystemMetrics(0) - $width) / 2)
312
+ $y = [Math]::Max(0, ([AtomJSNativeWindow]::GetSystemMetrics(1) - $height) / 2)
313
+ }
314
+ if ($env:ATOMJS_X -ne '') { $x = [int]$env:ATOMJS_X }
315
+ if ($env:ATOMJS_Y -ne '') { $y = [int]$env:ATOMJS_Y }
316
+
317
+ $insertAfter = if ($env:ATOMJS_ALWAYS_ON_TOP -eq '1') { [IntPtr](-1) } else { [IntPtr]::Zero }
318
+ $positionFlags = $SWP_FRAMECHANGED
319
+ if ($env:ATOMJS_SHOW -ne '0') { $positionFlags = $positionFlags -bor $SWP_SHOWWINDOW }
320
+ [void][AtomJSNativeWindow]::SetWindowPos($child, $insertAfter, [int]$x, [int]$y, $width, $height, $positionFlags)
321
+ if ($env:ATOMJS_SHOW -eq '0') {
322
+ [void][AtomJSNativeWindow]::ShowWindow($child, 0)
323
+ } else {
324
+ [void][AtomJSNativeWindow]::ShowWindow($child, 5)
325
+ [void][AtomJSNativeWindow]::BringWindowToTop($child)
326
+ }
327
+ if ($env:ATOMJS_SHOW -ne '0' -and $env:ATOMJS_FOCUSABLE -ne '0') {
328
+ # WebView windows live in helper processes. Temporarily attach the setup
329
+ # thread to the foreground and WebView UI threads so Windows accepts the
330
+ # activation request instead of leaving OAuth/login windows behind the app.
331
+ $foreground = [AtomJSNativeWindow]::GetForegroundWindow()
332
+ [uint32]$foregroundProcess = 0
333
+ [uint32]$childProcess = 0
334
+ $foregroundThread = if ($foreground -ne [IntPtr]::Zero) {
335
+ [AtomJSNativeWindow]::GetWindowThreadProcessId($foreground, [ref]$foregroundProcess)
336
+ } else { 0 }
337
+ $childThread = [AtomJSNativeWindow]::GetWindowThreadProcessId($child, [ref]$childProcess)
338
+ $currentThread = [AtomJSNativeWindow]::GetCurrentThreadId()
339
+ $attachedForeground = $false
340
+ $attachedChild = $false
341
+
342
+ try {
343
+ if ($foregroundThread -ne 0 -and $foregroundThread -ne $currentThread) {
344
+ $attachedForeground = [AtomJSNativeWindow]::AttachThreadInput($currentThread, $foregroundThread, $true)
345
+ }
346
+ if ($childThread -ne 0 -and $childThread -ne $currentThread) {
347
+ $attachedChild = [AtomJSNativeWindow]::AttachThreadInput($currentThread, $childThread, $true)
348
+ }
349
+ [void][AtomJSNativeWindow]::BringWindowToTop($child)
350
+ [void][AtomJSNativeWindow]::SetActiveWindow($child)
351
+ [void][AtomJSNativeWindow]::SetForegroundWindow($child)
352
+ [void][AtomJSNativeWindow]::SetFocus($child)
353
+ } finally {
354
+ if ($attachedChild) {
355
+ [void][AtomJSNativeWindow]::AttachThreadInput($currentThread, $childThread, $false)
356
+ }
357
+ if ($attachedForeground) {
358
+ [void][AtomJSNativeWindow]::AttachThreadInput($currentThread, $foregroundThread, $false)
359
+ }
360
+ }
361
+ }
362
+ `;
363
+
364
+ const WINDOWS_MODAL_RELEASE = WINDOWS_INTEROP + String.raw`
365
+ $parent = Find-AtomWindow ([uint32]$env:ATOMJS_PARENT_PID)
366
+ if ($parent -ne [IntPtr]::Zero) {
367
+ [void][AtomJSNativeWindow]::EnableWindow($parent, $true)
368
+ [void][AtomJSNativeWindow]::ShowWindow($parent, 5)
369
+ [void][AtomJSNativeWindow]::BringWindowToTop($parent)
370
+ [void][AtomJSNativeWindow]::SetForegroundWindow($parent)
371
+ }
372
+ `;
373
+
374
+ async function main() {
375
+ let view = null;
376
+ let windowsOwnership = null;
377
+ try {
378
+ view = new Webview(Boolean(config.debug));
379
+ view.title(String(config.title || 'AtomJS App'));
380
+
381
+ const width = Number(config.width || 800);
382
+ const height = Number(config.height || 600);
383
+ view.size(
384
+ width,
385
+ height,
386
+ config.resizable === false ? SizeHint.Fixed : SizeHint.None
387
+ );
388
+
389
+ if (Number(config.minWidth) > 0 || Number(config.minHeight) > 0) {
390
+ view.size(
391
+ Number(config.minWidth) > 0 ? Number(config.minWidth) : width,
392
+ Number(config.minHeight) > 0 ? Number(config.minHeight) : height,
393
+ SizeHint.Min
394
+ );
395
+ }
396
+ if (Number(config.maxWidth) > 0 || Number(config.maxHeight) > 0) {
397
+ view.size(
398
+ Number(config.maxWidth) > 0 ? Number(config.maxWidth) : width,
399
+ Number(config.maxHeight) > 0 ? Number(config.maxHeight) : height,
400
+ SizeHint.Max
401
+ );
402
+ }
403
+
404
+ view.init(String(config.bridgeScript || ''));
405
+ view.navigate(String(config.url));
406
+
407
+ if (process.platform === 'win32') {
408
+ windowsOwnership = configureWindowsWindow(config);
409
+ }
410
+
411
+ emit({ type: 'created', windowId: Number(config.windowId || 0), pid: process.pid });
412
+ view.show();
413
+ } catch (error) {
414
+ console.error('AtomJS native window failed:', error && error.stack ? error.stack : error);
415
+ process.exitCode = 1;
416
+ } finally {
417
+ if (process.platform === 'win32') releaseWindowsModal(windowsOwnership);
418
+ }
419
+ }
420
+
421
+ await main();
package/src/state.cjs CHANGED
@@ -6,6 +6,7 @@ module.exports = {
6
6
  windows,
7
7
  bridgeServer: null,
8
8
  nextWindowId: 1,
9
+ focusedWindowId: null,
9
10
  projectRoot: process.env.ATOM_PROJECT_ROOT || process.cwd(),
10
11
  isQuitting: false
11
12
  };