positron.js 1.0.4 → 1.0.5

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/bin/positron.js CHANGED
@@ -5,6 +5,7 @@ const performPackager = require("../packager");
5
5
  const { spawn } = require("child_process");
6
6
  const [, , command] = process.argv;
7
7
  const { info, success, error } = require("../logs");
8
+ const fs = require("fs");
8
9
 
9
10
  switch (command) {
10
11
  case "build":
@@ -15,7 +16,11 @@ switch (command) {
15
16
 
16
17
  case "dev":
17
18
  info("Starting Positron in development mode...");
18
- performNativeBuild();
19
+ const buildSuccess = performNativeBuild();
20
+ if (!buildSuccess) {
21
+ error("Development build failed. Please fix the errors and try again.");
22
+ process.exit(1);
23
+ }
19
24
  spawn("node", ["."], { stdio: "inherit" });
20
25
  break;
21
26
 
package/builder.js CHANGED
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const cp = require("child_process");
4
4
  const { success, error, info, warn } = require("./logs");
5
+ const semver = require("semver");
5
6
 
6
7
  const arch = process.argv.includes("--x64") ? "x64" : process.argv.includes("--arm64") ? "arm64" : process.arch;
7
8
 
@@ -30,6 +31,18 @@ function performNativeBuild() {
30
31
  if (depPackage.positron) {
31
32
  const depDir = path.dirname(depPackagePath);
32
33
 
34
+ if(depPackage.positron.requiredVersion) {
35
+ const requiredVersion = depPackage.positron.requiredVersion;
36
+ const rootVersion = rootPackage.dependencies["positron.js"];
37
+ if(rootVersion.startsWith("file:")) {
38
+ warn(`[Builder] Dependency "${dep}" specifies a required positron.js version of ${requiredVersion}, but the project is using a local file reference. Skipping version compatibility check for this dependency.`);
39
+ } else {
40
+ if(!semver.satisfies(rootVersion, requiredVersion)) {
41
+ warn(`[Builder] Dependency "${dep}" requires positron.js version ${requiredVersion}, but the project has version ${rootVersion}. This may lead to compatibility issues.`);
42
+ }
43
+ }
44
+ }
45
+
33
46
  let missing = [];
34
47
  if(!depPackage.positron.className) missing.push("className");
35
48
  if(!depPackage.positron.command) missing.push("command");
@@ -3,6 +3,9 @@ import WebKit
3
3
  import Network
4
4
  import Darwin
5
5
  import UserNotifications
6
+ import Foundation
7
+ import IOKit.pwr_mgt
8
+
6
9
 
7
10
  // MARK: - Globals
8
11
 
@@ -20,8 +23,31 @@ let AUTH_TOKEN: String = {
20
23
  var windowObservations: [Int: NSKeyValueObservation] = [:]
21
24
  var navigationDelegates: [Int: WebViewNavigationDelegate] = [:]
22
25
 
26
+ private var assertionID: IOPMAssertionID?
23
27
 
24
- import Foundation
28
+ func blockPowerSave() {
29
+ guard assertionID == nil else { return }
30
+
31
+ var id: IOPMAssertionID = 0
32
+
33
+ let result = IOPMAssertionCreateWithName(
34
+ kIOPMAssertionTypePreventUserIdleSystemSleep as CFString,
35
+ IOPMAssertionLevel(kIOPMAssertionLevelOn),
36
+ "Power Save Blocked" as CFString,
37
+ &id
38
+ )
39
+
40
+ if result == kIOReturnSuccess {
41
+ assertionID = id
42
+ }
43
+ }
44
+
45
+ func unblockPowerSave() {
46
+ guard let id = assertionID else { return }
47
+
48
+ IOPMAssertionRelease(id)
49
+ assertionID = nil
50
+ }
25
51
 
26
52
  final class PositronWebView: WKWebView {
27
53
  override func rightMouseDown(with event: NSEvent) {
@@ -292,7 +318,7 @@ case "isFullscreen":
292
318
  guard let window = windows[windowId] else { return }
293
319
  let isFullscreen = window.styleMask.contains(.fullScreen)
294
320
  AppDelegate.shared?.ipcClient.send(
295
- IPCResponse(windowId: windowId, event: "isFullscreen-reply-\(windowId)", data: ["isFullscreen": isFullscreen ? "true" : "false"])
321
+ IPCResponse(windowId: windowId, event: args.last ?? "isFullscreen-reply-\(windowId)", data: ["isFullscreen": isFullscreen ? "true" : "false"])
296
322
  )
297
323
 
298
324
  case "setSwipeNav":
@@ -305,7 +331,7 @@ case "setSwipeNav":
305
331
  webView.allowsBackForwardNavigationGestures = enable
306
332
 
307
333
  GetIPCClient().send(
308
- IPCResponse(windowId: windowId, event: "setSwipeNav-reply-\(windowId)", data: ["enabled": enable ? "true" : "false"])
334
+ IPCResponse(windowId: windowId, event: args.last ?? "setSwipeNav-reply-\(windowId)", data: ["enabled": enable ? "true" : "false"])
309
335
  )
310
336
 
311
337
  case "forceCloseWindow":
@@ -402,7 +428,7 @@ case "forceCloseWindow":
402
428
  let frame = window.frame
403
429
  let bounds = ["x": "\(Int(frame.origin.x))", "y": "\(Int(frame.origin.y))", "width": "\(Int(frame.size.width))", "height": "\(Int(frame.size.height))"]
404
430
  AppDelegate.shared?.ipcClient.send(
405
- IPCResponse(windowId: windowId, event: "getBounds-reply-\(windowId)", data: bounds)
431
+ IPCResponse(windowId: windowId, event: args.last ?? "getBounds-reply-\(windowId)", data: bounds)
406
432
  )
407
433
 
408
434
  case "setResizable":
@@ -461,16 +487,64 @@ case "forceCloseWindow":
461
487
  guard let window = windows[windowId] else { return }
462
488
  let canGoBack = (window.contentView as? WKWebView)?.canGoBack ?? false
463
489
  AppDelegate.shared?.ipcClient.send(
464
- IPCResponse(windowId: windowId, event: "canGoBack-reply-\(windowId)", data: ["canGoBack": canGoBack ? "true" : "false"])
490
+ IPCResponse(windowId: windowId, event: args.last ?? "canGoBack-reply-\(windowId)", data: ["canGoBack": canGoBack ? "true" : "false"])
465
491
  )
466
492
 
467
493
  case "canGoForward":
468
494
  guard let window = windows[windowId] else { return }
469
495
  let canGoForward = (window.contentView as? WKWebView)?.canGoForward ?? false
470
496
  AppDelegate.shared?.ipcClient.send(
471
- IPCResponse(windowId: windowId, event: "canGoForward-reply-\(windowId)", data: ["canGoForward": canGoForward ? "true" : "false"])
497
+ IPCResponse(windowId: windowId, event: args.last ?? "canGoForward-reply-\(windowId)", data: ["canGoForward": canGoForward ? "true" : "false"])
472
498
  )
473
499
 
500
+ case "showFileOpenDialog":
501
+ guard let window = windows[windowId] else { return }
502
+ let panel = NSOpenPanel()
503
+ panel.allowsMultipleSelection = false
504
+ panel.canChooseDirectories = false
505
+ panel.canChooseFiles = true
506
+ panel.beginSheetModal(for: window) { response in
507
+ if response == .OK, let url = panel.url {
508
+ AppDelegate.shared?.ipcClient.send(
509
+ IPCResponse(windowId: windowId, event: args.last ?? "showFileOpenDialog-reply-\(windowId)", data: ["filePath": url.path])
510
+ )
511
+ } else {
512
+ AppDelegate.shared?.ipcClient.send(
513
+ IPCResponse(windowId: windowId, event: args.last ?? "showFileOpenDialog-reply-\(windowId)", data: ["filePath": ""])
514
+ )
515
+ }
516
+ }
517
+
518
+ case "readFromClipboard":
519
+ let pasteboard = NSPasteboard.general
520
+ let clipboardText = pasteboard.string(forType: .string) ?? "notext"
521
+ AppDelegate.shared?.ipcClient.send(
522
+ IPCResponse(windowId: windowId, event: args.last ?? "readFromClipboard-reply-\(windowId)", data: ["text": clipboardText])
523
+ )
524
+
525
+ case "blockPowerSave":
526
+ blockPowerSave()
527
+
528
+ case "unblockPowerSave":
529
+ unblockPowerSave()
530
+
531
+ case "isDarkMode":
532
+ guard let window = windows[windowId] else { return }
533
+ let isDarkMode = window.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
534
+ AppDelegate.shared?.ipcClient.send(
535
+ IPCResponse(windowId: windowId, event: args.last ?? "isDarkMode-reply-\(windowId)", data: ["isDarkMode": isDarkMode ? "true" : "false"])
536
+ )
537
+
538
+
539
+ case "writeToClipboard":
540
+ guard let text = args.first else {
541
+ printError("writeToClipboard — missing text argument")
542
+ return
543
+ }
544
+ let pasteboard = NSPasteboard.general
545
+ pasteboard.clearContents()
546
+ pasteboard.setString(text, forType: .string)
547
+
474
548
  case "showNotification":
475
549
 
476
550
  UNUserNotificationCenter.current().requestAuthorization(
@@ -501,14 +575,14 @@ UNUserNotificationCenter.current().requestAuthorization(
501
575
  guard let window = windows[windowId] else { return }
502
576
  let url = (window.contentView as? WKWebView)?.url?.absoluteString ?? ""
503
577
  AppDelegate.shared?.ipcClient.send(
504
- IPCResponse(windowId: windowId, event: "getURL-reply-\(windowId)", data: ["url": url])
578
+ IPCResponse(windowId: windowId, event: args.last ?? "getURL-reply-\(windowId)", data: ["url": url])
505
579
  )
506
580
 
507
581
  case "getTitle":
508
582
  guard let window = windows[windowId] else { return }
509
583
  let title = window.title
510
584
  AppDelegate.shared?.ipcClient.send(
511
- IPCResponse(windowId: windowId, event: "getTitle-reply-\(windowId)", data: ["title": title])
585
+ IPCResponse(windowId: windowId, event: args.last ?? "getTitle-reply-\(windowId)", data: ["title": title])
512
586
  )
513
587
 
514
588
  case "executeAppleScript":
@@ -528,7 +602,7 @@ case "isVisible":
528
602
  guard let window = windows[windowId] else { return }
529
603
  let isVisible = window.isVisible
530
604
  AppDelegate.shared?.ipcClient.send(
531
- IPCResponse(windowId: windowId, event: "isVisible-reply-\(windowId)", data: ["isVisible": isVisible ? "true" : "false"])
605
+ IPCResponse(windowId: windowId, event: args.last ?? "isVisible-reply-\(windowId)", data: ["isVisible": isVisible ? "true" : "false"])
532
606
  )
533
607
 
534
608
  case "addToContentBlocker":
@@ -601,7 +675,7 @@ case "addToContentBlocker":
601
675
  }
602
676
 
603
677
  GetIPCClient().send(
604
- IPCResponse(windowId: windowId, event: "addToContentBlocker-reply-\(windowId)", data: ["status": "success"])
678
+ IPCResponse(windowId: windowId, event: args.last ?? "addToContentBlocker-reply-\(windowId)", data: ["status": "success"])
605
679
  )
606
680
 
607
681
  case "isSwipeNavEnabled":
@@ -612,7 +686,7 @@ case "addToContentBlocker":
612
686
  }
613
687
  let enabled = webView.allowsBackForwardNavigationGestures
614
688
  AppDelegate.shared?.ipcClient.send(
615
- IPCResponse(windowId: windowId, event: "isSwipeNavEnabled-reply-\(windowId)", data: ["enabled": enabled ? "true" : "false"])
689
+ IPCResponse(windowId: windowId, event: args.last ?? "isSwipeNavEnabled-reply-\(windowId)", data: ["enabled": enabled ? "true" : "false"])
616
690
  )
617
691
 
618
692
  case "forward":
@@ -698,7 +772,7 @@ case "addToContentBlocker":
698
772
  resultStr = "null"
699
773
  }
700
774
  AppDelegate.shared?.ipcClient.send(
701
- IPCResponse(windowId: windowId, event: "evaluateJS-reply-\(windowId)", data: ["result": resultStr])
775
+ IPCResponse(windowId: windowId, event: args.last ?? "evaluateJS-reply-\(windowId)", data: ["result": resultStr])
702
776
  )
703
777
  }
704
778
 
@@ -724,11 +798,11 @@ case "addToContentBlocker":
724
798
  if response == .alertFirstButtonReturn {
725
799
  let userInput = inputField.stringValue
726
800
  AppDelegate.shared?.ipcClient.send(
727
- IPCResponse(windowId: windowId, event: "prompt-reply-\(windowId)", data: ["input": userInput])
801
+ IPCResponse(windowId: windowId, event: args.last ?? "prompt-reply-\(windowId)", data: ["input": userInput])
728
802
  )
729
803
  } else {
730
804
  AppDelegate.shared?.ipcClient.send(
731
- IPCResponse(windowId: windowId, event: "prompt-reply-\(windowId)", data: ["input": ""])
805
+ IPCResponse(windowId: windowId, event: args.last ?? "prompt-reply-\(windowId)", data: ["input": ""])
732
806
  )
733
807
  }
734
808
  }
@@ -747,7 +821,7 @@ case "addToContentBlocker":
747
821
  alert.beginSheetModal(for: window) { response in
748
822
  let confirmed = (response == .alertFirstButtonReturn)
749
823
  AppDelegate.shared?.ipcClient.send(
750
- IPCResponse(windowId: windowId, event: "confirm-reply-\(windowId)", data: ["confirmed": confirmed ? "true" : "false"])
824
+ IPCResponse(windowId: windowId, event: args.last ?? "confirm-reply-\(windowId)", data: ["confirmed": confirmed ? "true" : "false"])
751
825
  )
752
826
  }
753
827
 
@@ -755,7 +829,7 @@ case "addToContentBlocker":
755
829
  guard let window = windows[windowId] else { return }
756
830
  let isFocused = window.isKeyWindow
757
831
  AppDelegate.shared?.ipcClient.send(
758
- IPCResponse(windowId: windowId, event: "isFocused-reply-\(windowId)", data: ["isFocused": isFocused ? "true" : "false"])
832
+ IPCResponse(windowId: windowId, event: args.last ?? "isFocused-reply-\(windowId)", data: ["isFocused": isFocused ? "true" : "false"])
759
833
  )
760
834
 
761
835
  case "emitToRenderer":
@@ -843,7 +917,7 @@ final class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
843
917
  let isFile = webView.url?.isFileURL ?? false
844
918
  let eventName = isFile ? "loadFile-reply-\(windowId)" : "loadURL-reply-\(windowId)"
845
919
  AppDelegate.shared?.ipcClient.send(
846
- IPCResponse(windowId: windowId, event: eventName, data: [:])
920
+ IPCResponse(windowId: windowId, event: eventName, data: ["url": webView.url?.absoluteString ?? "", "title": webView.title ?? "", "canGoBack": (webView.canGoBack ? "true" : "false"), "canGoForward": (webView.canGoForward ? "true" : "false")])
847
921
  )
848
922
  }
849
923
  }
@@ -1088,8 +1162,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1088
1162
 
1089
1163
  cd "\(resourcePath)"
1090
1164
 
1091
- if [ -f "positron-backend" ]; then
1092
- exec "./positron-backend"
1165
+ BACKEND_BIN=$(find . -maxdepth 1 -name "*-backend" | head -n 1)
1166
+ if [ -n "$BACKEND_BIN" ] && [ -f "$BACKEND_BIN" ]; then
1167
+ exec "$BACKEND_BIN"
1093
1168
  else
1094
1169
  exec "node" "."
1095
1170
  fi
@@ -1311,20 +1386,22 @@ setbuf(__stdoutp, nil)
1311
1386
  setbuf(__stderrp, nil)
1312
1387
 
1313
1388
  signal(SIGINT) { _ in
1314
- printError("INFO: Received SIGINT, shutting down…")
1315
1389
  AppDelegate.shared?.nodeProcess?.terminate()
1390
+ unblockPowerSave()
1316
1391
  exit(0)
1317
1392
  }
1318
1393
 
1319
1394
  signal(SIGTERM) { _ in
1320
1395
  printError("INFO: Received SIGTERM, shutting down…")
1321
1396
  AppDelegate.shared?.nodeProcess?.terminate()
1397
+ unblockPowerSave()
1322
1398
  exit(0)
1323
1399
  }
1324
1400
 
1325
1401
  signal(SIGSEGV) { _ in
1326
1402
  printError("ERROR: Caught SIGSEGV (segmentation fault). This likely indicates a bug in the native code. Attempting to shut down gracefully…")
1327
1403
  AppDelegate.shared?.nodeProcess?.terminate()
1404
+ unblockPowerSave()
1328
1405
  signal(SIGSEGV, SIG_DFL)
1329
1406
  raise(SIGSEGV)
1330
1407
  }
@@ -1,7 +1,7 @@
1
1
  <Project Sdk="Microsoft.NET.Sdk">
2
2
  <PropertyGroup>
3
3
  <OutputType>WinExe</OutputType>
4
- <TargetFramework>net8.0-windows</TargetFramework>
4
+ <TargetFramework>net10.0-windows</TargetFramework>
5
5
  <Nullable>enable</Nullable>
6
6
  <UseWpf>true</UseWpf>
7
7
  <UseWindowsForms>true</UseWindowsForms>
package/core/win/main.cs CHANGED
@@ -10,13 +10,49 @@ using System.Threading;
10
10
  using System.Threading.Tasks;
11
11
  using System.Windows;
12
12
  using System.Windows.Controls;
13
- using System.Windows.Input;
14
13
  using Microsoft.Web.WebView2.Core;
15
14
  using Microsoft.Web.WebView2.Wpf;
16
15
  using System.Text.Json.Serialization;
17
16
  using System.Net;
18
17
  using System.Net.Sockets;
19
18
  using Microsoft.VisualBasic;
19
+ using System.Runtime.InteropServices;
20
+ using Microsoft.Win32;
21
+
22
+ class PowerSaveBlocker
23
+ {
24
+ // Import the Win32 API
25
+ [DllImport("kernel32.dll")]
26
+ private static extern uint SetThreadExecutionState(uint esFlags);
27
+
28
+ // Flags
29
+ private const uint ES_CONTINUOUS = 0x80000000;
30
+ private const uint ES_SYSTEM_REQUIRED = 0x00000001;
31
+ private const uint ES_DISPLAY_REQUIRED = 0x00000002;
32
+
33
+ private static uint currentState = 0;
34
+
35
+ public static void BlockPowerSave(bool keepDisplayOn = false)
36
+ {
37
+ uint flags = ES_CONTINUOUS | ES_SYSTEM_REQUIRED;
38
+ if (keepDisplayOn)
39
+ {
40
+ flags |= ES_DISPLAY_REQUIRED;
41
+ }
42
+
43
+ currentState = SetThreadExecutionState(flags);
44
+ if (currentState == 0)
45
+ {
46
+ Console.WriteLine("Failed to set execution state!");
47
+ }
48
+ }
49
+
50
+ public static void UnblockPowerSave()
51
+ {
52
+ SetThreadExecutionState(ES_CONTINUOUS);
53
+ currentState = 0;
54
+ }
55
+ }
20
56
 
21
57
 
22
58
  namespace PositronWindows
@@ -137,10 +173,18 @@ namespace PositronWindows
137
173
  ? Path.Combine(basePath, "resources")
138
174
  : basePath;
139
175
 
140
- if (File.Exists(Path.Combine(targetDir, "positron-backend.exe")))
176
+ string backendExeName = "positron-backend.exe";
177
+ if (Directory.Exists(targetDir)) {
178
+ string[] files = Directory.GetFiles(targetDir, "*-backend.exe");
179
+ if (files.Length > 0) {
180
+ backendExeName = Path.GetFileName(files[0]);
181
+ }
182
+ }
183
+
184
+ if (File.Exists(Path.Combine(targetDir, backendExeName)))
141
185
  {
142
186
  // PACKAGED MODE — C# is the entry point; launch the Node backend
143
- StartNodeProcess(targetDir);
187
+ StartNodeProcess(targetDir, backendExeName);
144
188
  }
145
189
  else
146
190
  {
@@ -153,7 +197,7 @@ namespace PositronWindows
153
197
  }
154
198
  else
155
199
  {
156
- error("No positron-backend.exe found and POSITRON_IPC_PORT not set. Cannot start.");
200
+ error($"No {backendExeName} found and POSITRON_IPC_PORT not set. Cannot start.");
157
201
  Shutdown();
158
202
  return;
159
203
  }
@@ -189,13 +233,13 @@ namespace PositronWindows
189
233
 
190
234
  private static int _ipcPort = 9000;
191
235
 
192
- private void StartNodeProcess(string workingDirectory)
236
+ private void StartNodeProcess(string workingDirectory, string backendExeName)
193
237
  {
194
238
  IsPackaged = true;
195
239
 
196
240
  _ipcPort = GetRandomOpenPort();
197
241
 
198
- string backendExe = Path.Combine(workingDirectory, "positron-backend.exe");
242
+ string backendExe = Path.Combine(workingDirectory, backendExeName);
199
243
 
200
244
  _nodeProcess = new Process
201
245
  {
@@ -335,7 +379,8 @@ private void StartNodeProcess(string workingDirectory)
335
379
  _ipcClient.Send(new IPCResponse
336
380
  {
337
381
  windowId = windowId,
338
- @event = eventName
382
+ @event = eventName,
383
+ data = new() { { "url", webView.Source?.ToString() ?? "" }, { "title", webView.CoreWebView2.DocumentTitle }, { "canGoBack", webView.CoreWebView2.CanGoBack.ToString().ToLower() }, { "canGoForward", webView.CoreWebView2.CanGoForward.ToString().ToLower() } }
339
384
  });
340
385
  };
341
386
 
@@ -359,6 +404,7 @@ private void StartNodeProcess(string workingDirectory)
359
404
  break;
360
405
  }
361
406
 
407
+
362
408
  case "setContextMenu":
363
409
  if (!LayoutMap.TryGetValue(windowId, out var layout)) break;
364
410
  if (args.Count == 0)
@@ -394,12 +440,41 @@ private void StartNodeProcess(string workingDirectory)
394
440
  GetIPCClient().Send(new IPCResponse
395
441
  {
396
442
  windowId = windowId,
397
- @event = "setSwipeNav-reply-" + windowId,
443
+ @event = args[^1] ?? "setSwipeNav-reply-" + windowId,
398
444
  data = new() { { "enabled", (wvSwipeNav?.CoreWebView2.Settings.IsSwipeNavigationEnabled ?? false).ToString().ToLower() } }
399
445
  });
400
446
 
401
447
  break;
402
448
 
449
+ case "blockPowerSave":
450
+ PowerSaveBlocker.BlockPowerSave();
451
+ break;
452
+
453
+ case "unblockPowerSave":
454
+ PowerSaveBlocker.UnblockPowerSave();
455
+ break;
456
+
457
+ case "isDarkMode":
458
+ bool isLightTheme = true;
459
+ using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"))
460
+ {
461
+ if (key != null)
462
+ {
463
+ object? value = key?.GetValue("AppsUseLightTheme");
464
+ if (value != null && (int)value == 0)
465
+ {
466
+ isLightTheme = false; // Dark Mode
467
+ }
468
+ }
469
+ }
470
+ GetIPCClient().Send(new IPCResponse
471
+ {
472
+ windowId = windowId,
473
+ @event = args[^1] ?? "isDarkMode-reply-" + windowId,
474
+ data = new() { { "isDarkMode", (!isLightTheme).ToString().ToLower() } }
475
+ });
476
+ break;
477
+
403
478
  case "isSwipeNavEnabled":
404
479
  if (!WindowsMap.TryGetValue(windowId, out var winCheckSwipe)) break;
405
480
  var wvCheckSwipe = GetWebView(windowId);
@@ -408,19 +483,75 @@ private void StartNodeProcess(string workingDirectory)
408
483
  GetIPCClient().Send(new IPCResponse
409
484
  {
410
485
  windowId = windowId,
411
- @event = "isSwipeNavEnabled-reply-" + windowId,
486
+ @event = args[^1] ?? "isSwipeNavEnabled-reply-" + windowId,
412
487
  data = new() { { "enabled", isEnabled.ToString().ToLower() } }
413
488
  }
414
489
  );
415
490
  }
416
491
  break;
417
492
 
493
+ case "showFileOpenDialog":
494
+ {
495
+ var dialog = new Microsoft.Win32.OpenFileDialog
496
+ {
497
+ Multiselect = args.Count > 0 && args[0].ToLower() == "true"
498
+ };
499
+ bool? result = dialog.ShowDialog();
500
+ if (result == true)
501
+ {
502
+ string[] files = dialog.FileNames;
503
+ GetIPCClient().Send(new IPCResponse
504
+ {
505
+ windowId = windowId,
506
+ @event = args[^1] ?? "showFileOpenDialog-reply-" + windowId,
507
+ data = new() { { "files", JsonSerializer.Serialize(files) } }
508
+ });
509
+ }
510
+ }
511
+ break;
512
+
513
+ case "readFromClipboard":
514
+ string clipboardText = "";
515
+ Current.Dispatcher.Invoke(() =>
516
+ {
517
+ try
518
+ {
519
+ clipboardText = Clipboard.GetText();
520
+ }
521
+ catch (Exception ex)
522
+ {
523
+ error($"readFromClipboard failed: {ex.Message}");
524
+ }
525
+ });
526
+ GetIPCClient().Send(new IPCResponse
527
+ {
528
+ windowId = windowId,
529
+ @event = args[^1] ?? "readFromClipboard-reply-" + windowId,
530
+ data = new() { { "text", clipboardText } }
531
+ });
532
+ break;
533
+
534
+ case "writeToClipboard":
535
+ if (args.Count == 0) break;
536
+ Current.Dispatcher.Invoke(() =>
537
+ {
538
+ try
539
+ {
540
+ Clipboard.SetText(args[0]);
541
+ }
542
+ catch (Exception ex)
543
+ {
544
+ error($"writeToClipboard failed: {ex.Message}");
545
+ }
546
+ });
547
+ break;
548
+
418
549
  case "isVisible":
419
550
  if (!WindowsMap.TryGetValue(windowId, out var winVisible)) break;
420
551
  bool isVisible = winVisible.IsVisible;
421
552
  GetIPCClient().Send(new IPCResponse
422
553
  { windowId = windowId,
423
- @event = "isVisible-reply-" + windowId,
554
+ @event = args[^1] ?? "isVisible-reply-" + windowId,
424
555
  data = new() { { "isVisible", isVisible.ToString().ToLower() } }
425
556
  });
426
557
  break;
@@ -430,14 +561,17 @@ private void StartNodeProcess(string workingDirectory)
430
561
  bool isFullscreen = winFullscreen.WindowState == WindowState.Maximized;
431
562
  GetIPCClient().Send(new IPCResponse
432
563
  { windowId = windowId,
433
- @event = "isFullscreen-reply-" + windowId,
564
+ @event = args[^1] ?? "isFullscreen-reply-" + windowId,
434
565
  data = new() { { "isFullscreen", isFullscreen.ToString().ToLower() } }
435
566
  });
436
567
  break;
437
568
 
438
569
  case "closeWindow":
439
570
  if (WindowsMap.TryGetValue(windowId, out var winToClose))
571
+ {
572
+ _forceClosing.Add(windowId);
440
573
  winToClose.Close(); // Triggers Closed → cleanup above
574
+ }
441
575
  else
442
576
  error($"closeWindow — no window found with ID {windowId}");
443
577
  break;
@@ -500,6 +634,15 @@ case "forceCloseWindow":
500
634
  }
501
635
  break;
502
636
 
637
+ case "addToContentBlocker":
638
+ _ipcClient.Send(new IPCResponse
639
+ {
640
+ windowId = windowId,
641
+ @event = args[^1] ?? "addToContentBlocker-reply-" + windowId,
642
+ data = new() { { "status", "success" }, { "warning", "Content blocker not supported on Windows." } }
643
+ });
644
+ break;
645
+
503
646
  case "loadFile":
504
647
  if (!WindowsMap.TryGetValue(windowId, out _)) break;
505
648
  if (args.Count == 0)
@@ -550,7 +693,7 @@ case "forceCloseWindow":
550
693
  _ipcClient.Send(new IPCResponse
551
694
  {
552
695
  windowId = windowId,
553
- @event = "evaluateJS-reply-" + windowId,
696
+ @event = args[^1] ?? "evaluateJS-reply-" + windowId,
554
697
  data = new() { { "result", result ?? "null" } }
555
698
  });
556
699
  }
@@ -560,7 +703,7 @@ case "forceCloseWindow":
560
703
  _ipcClient.Send(new IPCResponse
561
704
  {
562
705
  windowId = windowId,
563
- @event = "evaluateJS-reply-" + windowId,
706
+ @event = args[^1] ?? "evaluateJS-reply-" + windowId,
564
707
  data = new() { { "error", ex.Message } }
565
708
  });
566
709
  }
@@ -574,7 +717,7 @@ case "forceCloseWindow":
574
717
  _ipcClient.Send(new IPCResponse
575
718
  {
576
719
  windowId = windowId,
577
- @event = "isFocused-reply-" + windowId,
720
+ @event = args[^1] ?? "isFocused-reply-" + windowId,
578
721
  data = new() { { "isFocused", isFocused.ToString().ToLower() } }
579
722
  });
580
723
  break;
@@ -666,7 +809,7 @@ case "setBounds":
666
809
  _ipcClient.Send(new IPCResponse
667
810
  {
668
811
  windowId = windowId,
669
- @event = "prompt-reply-" + windowId,
812
+ @event = args[^1] ?? "prompt-reply-" + windowId,
670
813
  data = new() { { "input", result } }
671
814
  });
672
815
  }
@@ -684,7 +827,7 @@ case "setBounds":
684
827
  _ipcClient.Send(new IPCResponse
685
828
  {
686
829
  windowId = windowId,
687
- @event = "confirm-reply-" + windowId,
830
+ @event = args[^1] ?? "confirm-reply-" + windowId,
688
831
  data = new() { { "confirmed", result.ToString().ToLower() } }
689
832
  });
690
833
  }
@@ -810,7 +953,7 @@ case "setBounds":
810
953
  _ipcClient.Send(new IPCResponse
811
954
  {
812
955
  windowId = windowId,
813
- @event = "capture-page-result-" + windowId,
956
+ @event = args[^1] ?? "capture-page-result-" + windowId,
814
957
  data = new() { { "imageData", base64 } }
815
958
  });
816
959
  }
@@ -831,7 +974,7 @@ case "setBounds":
831
974
  _ipcClient.Send(new IPCResponse
832
975
  {
833
976
  windowId = windowId,
834
- @event = "canGoBack-reply-" + windowId,
977
+ @event = args[^1] ?? "canGoBack-reply-" + windowId,
835
978
  data = new() { { "canGoBack", canGoBack.ToString().ToLower() } }
836
979
  });
837
980
  }
@@ -847,7 +990,7 @@ case "setBounds":
847
990
  _ipcClient.Send(new IPCResponse
848
991
  {
849
992
  windowId = windowId,
850
- @event = "canGoForward-reply-" + windowId,
993
+ @event = args[^1] ?? "canGoForward-reply-" + windowId,
851
994
  data = new() { { "canGoForward", canGoForward.ToString().ToLower() } }
852
995
  });
853
996
  }
@@ -863,7 +1006,7 @@ case "setBounds":
863
1006
  _ipcClient.Send(new IPCResponse
864
1007
  {
865
1008
  windowId = windowId,
866
- @event = "getURL-reply-" + windowId,
1009
+ @event = args[^1] ?? "getURL-reply-" + windowId,
867
1010
  data = new() { { "url", url } }
868
1011
  });
869
1012
  }
@@ -879,7 +1022,7 @@ case "setBounds":
879
1022
  _ipcClient.Send(new IPCResponse
880
1023
  {
881
1024
  windowId = windowId,
882
- @event = "getTitle-reply-" + windowId,
1025
+ @event = args[^1] ?? "getTitle-reply-" + windowId,
883
1026
  data = new() { { "title", title } }
884
1027
  });
885
1028
  }
package/index.js CHANGED
@@ -317,7 +317,7 @@ class Window extends Events.EventEmitter {
317
317
  * @param {string} path The path to the file to load.
318
318
  */
319
319
  async loadFile(path) {
320
- const res = await this.request("loadFile", `loadFile-reply-${this.id}`, path);
320
+ const res = await this.request("loadFile", { replyChannel: `loadFile-reply-${this.id}` }, path);
321
321
  this.emit("file-loaded", path);
322
322
  this.emit("navigated", path);
323
323
  return res;
@@ -328,7 +328,7 @@ class Window extends Events.EventEmitter {
328
328
  * @param {string} url The URL to load.
329
329
  */
330
330
  async loadURL(url) {
331
- const res = await this.request("loadURL", `loadURL-reply-${this.id}`, url);
331
+ const res = await this.request("loadURL", { replyChannel: `loadURL-reply-${this.id}` }, url);
332
332
  this.emit("url-loaded", url);
333
333
  this.emit("navigated", url);
334
334
  return res;
@@ -349,7 +349,7 @@ emitToRenderer(channel, args = []) {
349
349
  activeSocket.send(payload);
350
350
  this.emit("ipc-sent", { channel, args });
351
351
  } else {
352
- warn(`Cannot send IPC message, socket not ready. Channel: ${channel}`);
352
+ warn(`Cannot send IPC message on ${channel}, socket not ready.`);
353
353
  }
354
354
  }
355
355
 
@@ -575,7 +575,7 @@ focus() {
575
575
  * @returns {Promise<boolean>} True if the window is visible, false otherwise.
576
576
  */
577
577
  async isVisible() {
578
- const res = await this.request("isVisible", `isVisible-reply-${this.id}`);
578
+ const res = await this.request("isVisible");
579
579
  return res?.isVisible === "true";
580
580
  }
581
581
 
@@ -584,7 +584,7 @@ async isVisible() {
584
584
  * @returns {Promise<boolean>} True if the window is fullscreen, false otherwise.
585
585
  */
586
586
  async isFullscreen() {
587
- const res = await this.request("isFullscreen", `isFullscreen-reply-${this.id}`);
587
+ const res = await this.request("isFullscreen");
588
588
  return res?.isFullscreen === "true";
589
589
  }
590
590
 
@@ -603,7 +603,7 @@ reload() {
603
603
  * @returns {Promise<Buffer|null>} The captured screenshot as a Buffer, or null if the capture failed.
604
604
  */
605
605
  async capturePage() {
606
- const response = await this.request("capturePage", `capture-page-result-${this.id}`);
606
+ const response = await this.request("capturePage", { replyChannel: `capture-page-result-${this.id}` });
607
607
  return response.image ? Buffer.from(response.image, "base64") : null;
608
608
  }
609
609
 
@@ -612,52 +612,94 @@ async capturePage() {
612
612
  * @returns {Promise<boolean>} True if the window can navigate back, false otherwise.
613
613
  */
614
614
  async canGoBack() {
615
- const response = await this.request("canGoBack", `canGoBack-reply-${this.id}`);
615
+ const response = await this.request("canGoBack");
616
616
  return response === "true";
617
617
  }
618
618
 
619
+ // @ts-check
620
+
621
+ /**
622
+ * @typedef {Object} RequestOptions
623
+ * @property {number} [timeout]
624
+ * @property {boolean} [noTimeout]
625
+ * @property {string} [replyChannel]
626
+ */
627
+
628
+ /**
629
+ * @overload
630
+ * @param {string} command
631
+ * @param {...any} args
632
+ * @returns {Promise<any>}
633
+ */
634
+
619
635
  /**
620
- * Sends a request/response command to the native layer. The command will be sent, and the method will wait for a response on the specified reply channel. Once a response is received, the promise will resolve with the reply data.
621
- * @param {string} command The command to send.
622
- * @param {string} replyChannel The channel to listen for the reply on.
623
- * @returns {Promise<*>} A promise that resolves to the reply data.
636
+ * @overload
637
+ * @param {string} command
638
+ * @param {RequestOptions} options
639
+ * @param {...any} args
640
+ * @returns {Promise<any>}
624
641
  */
625
- async request(command, replyChannel, ...args) {
642
+
643
+ /**
644
+ * Send an IPC request.
645
+ * @param {string} command
646
+ * @param {...any} args
647
+ */
648
+ async request(command, ...args) {
626
649
  return new Promise((resolve, reject) => {
627
650
  let settled = false;
628
651
 
652
+ let options = {};
653
+
654
+ if(typeof args[0] === "object" && args[0].constructor === Object) {
655
+ options = args[0];
656
+ args = args.slice(1);
657
+ }
658
+
629
659
  if(!command) {
630
660
  reject(new Error("Command is required for request"));
631
661
  return;
632
662
  }
633
663
 
634
- if(!replyChannel) {
635
- replyChannel = `${command}-reply-${this.id}`;
636
- }
664
+ const reqId = crypto.randomUUID();
665
+
666
+ let replyChannel = `${command}-reply-${reqId}`;
667
+
668
+ if (options.replyChannel) {
669
+ replyChannel = options.replyChannel;
670
+ } else if(args[0] && (args[0].includes("-reply-") || args[0].includes("-result-"))) {
671
+ // TEMP TRANSITIONAL LOGIC TO SUPPORT LEGACY REQUESTS THAT PASS REPLY CHANNEL AS FIRST ARGUMENT. WILL BE REMOVED IN A FUTURE RELEASE.
672
+ replyChannel = args[0];
673
+ }
637
674
 
638
675
  const unsubscribe = ipc.handle(replyChannel, (data) => {
639
676
  if (!settled) {
640
677
  settled = true;
641
678
  clearTimeout(timeout);
642
679
  unsubscribe();
680
+
681
+ for (const key in data) {
682
+ if (data[key] === "true") {
683
+ data[key] = true;
684
+ } else if (data[key] === "false") {
685
+ data[key] = false;
686
+ } else if (!isNaN(data[key])) {
687
+ data[key] = Number(data[key]);
688
+ }
689
+ }
690
+
643
691
  resolve(data);
644
692
  }
645
693
  });
646
694
 
647
695
  let timeout;
648
696
 
649
- if(!args.includes("NO_TIMEOUT")) {
697
+ if(!options.noTimeout) {
650
698
  let timeoutDuration = 7000;
651
-
652
- const timeoutArg = args.find(
653
- arg => typeof arg === "string" && arg.startsWith("TIMEOUT=")
654
- );
655
-
656
- if (timeoutArg) {
657
-
658
- args = args.filter(arg => arg !== timeoutArg);
699
+
659
700
 
660
- timeoutDuration = parseInt(timeoutArg.split("=")[1]);
701
+ if (options.timeout) {
702
+ timeoutDuration = options.timeout;
661
703
  }
662
704
 
663
705
  timeout = setTimeout(() => {
@@ -680,7 +722,7 @@ if (timeoutArg) {
680
722
  * @returns {Promise<boolean>} True if the window can navigate forward, false otherwise.
681
723
  */
682
724
  async canGoForward() {
683
- const response = await this.request("canGoForward", `canGoForward-reply-${this.id}`);
725
+ const response = await this.request("canGoForward");
684
726
  return response === "true";
685
727
  }
686
728
 
@@ -745,7 +787,7 @@ setBounds(x, y, width, height) {
745
787
  * @returns {Promise<string|null>} The user's input as a string, or null if the user cancelled the prompt.
746
788
  */
747
789
  async prompt(message, defaultValue = "") {
748
- const res = await this.request("prompt", `prompt-reply-${this.id}`, message, defaultValue);
790
+ const res = await this.request("prompt", message, defaultValue);
749
791
  this.emit("prompt", { message, defaultValue });
750
792
  return res?.input;
751
793
  }
@@ -782,7 +824,7 @@ setContextMenu(menuTemplate) {
782
824
  * @returns {Promise<boolean>} True if the window is focused, false otherwise.
783
825
  */
784
826
  async isFocused() {
785
- const res = await this.request("isFocused", `isFocused-reply-${this.id}`);
827
+ const res = await this.request("isFocused");
786
828
  return res?.isFocused === "true";
787
829
  }
788
830
 
@@ -791,7 +833,7 @@ async isFocused() {
791
833
  * @returns {Promise<{x: number, y: number, width: number, height: number}>} An object containing the window's bounds.
792
834
  */
793
835
  async getBounds() {
794
- return await this.request("getBounds", `getBounds-reply-${this.id}`);
836
+ return await this.request("getBounds");
795
837
  }
796
838
 
797
839
  /**
@@ -799,7 +841,7 @@ async getBounds() {
799
841
  * @returns {Promise<string>} The current URL loaded in the window.
800
842
  */
801
843
  async getURL() {
802
- return (await this.request("getURL", `getURL-reply-${this.id}`))?.url || "";
844
+ return (await this.request("getURL"))?.url || "";
803
845
  }
804
846
 
805
847
  /**
@@ -807,7 +849,7 @@ async getURL() {
807
849
  * @returns {Promise<string>} The current title of the window.
808
850
  */
809
851
  async getTitle() {
810
- return (await this.request("getTitle", `getTitle-reply-${this.id}`)).title || "";
852
+ return (await this.request("getTitle"))?.title || "";
811
853
  }
812
854
 
813
855
  /**
@@ -834,7 +876,7 @@ setTitlebarTransparent(isTransparent) {
834
876
  * @returns {Promise<*>} A Promise that resolves to the result of the evaluation.
835
877
  */
836
878
  async evaluateJavaScript(script) {
837
- const res = await this.request("evaluateJS", `evaluateJS-reply-${this.id}`, script);
879
+ const res = await this.request("evaluateJS", script);
838
880
  return res.result;
839
881
  }
840
882
 
@@ -984,7 +1026,7 @@ async removeOnClick(selector) {
984
1026
  * @returns {Promise<boolean>} True if the user confirmed, false if the user cancelled.
985
1027
  */
986
1028
  async confirm(message) {
987
- const res = await this.request("confirm", `confirm-reply-${this.id}`, message);
1029
+ const res = await this.request("confirm", message);
988
1030
  this.emit("confirm", message);
989
1031
  return res?.confirmed === "true";
990
1032
  }
@@ -995,7 +1037,7 @@ async confirm(message) {
995
1037
  * @returns {Promise<void>} A Promise that resolves when the swipe navigation setting has been updated.
996
1038
  */
997
1039
  async setSwipeNavigation(enabled) {
998
- const res = await this.request("setSwipeNav", `setSwipeNav-reply-${this.id}`, String(enabled));
1040
+ const res = await this.request("setSwipeNav", String(enabled));
999
1041
  this.emit("swipe-navigation-updated", enabled);
1000
1042
  }
1001
1043
 
@@ -1004,7 +1046,7 @@ this.emit("swipe-navigation-updated", enabled);
1004
1046
  * @returns {Promise<boolean>} True if swipe navigation is enabled, false otherwise.
1005
1047
  */
1006
1048
  async isSwipeNavigationEnabled() {
1007
- const res = await this.request("isSwipeNavEnabled", `isSwipeNavEnabled-reply-${this.id}`);
1049
+ const res = await this.request("isSwipeNavEnabled");
1008
1050
  return res?.enabled === "true";
1009
1051
  }
1010
1052
 
@@ -1044,10 +1086,19 @@ async addToContentBlocker(config={ json:[], url:"", file:"", reload:true, clearE
1044
1086
  }
1045
1087
 
1046
1088
 
1047
- const res = await this.request("addToContentBlocker", `addToContentBlocker-reply-${this.id}`, json, config.reload, config.clearExisting);
1089
+ const res = await this.request("addToContentBlocker", json, config.reload, config.clearExisting);
1048
1090
  this.emit("content-blocker-updated", json);
1049
1091
  }
1050
1092
 
1093
+ /**
1094
+ * Displays a file open dialog and returns a Promise that resolves to an array of selected file paths, or null if the user cancelled the dialog.
1095
+ * @param {*} options The options for the file open dialog.
1096
+ * @returns
1097
+ */
1098
+ async showFileOpenDialog(options = {}) {
1099
+ this.emit("show-file-open-dialog", options);
1100
+ return this.request("showFileOpenDialog", options);
1101
+ }
1051
1102
 
1052
1103
 
1053
1104
  }
@@ -1194,11 +1245,71 @@ userData: {
1194
1245
  /**
1195
1246
  * Full access to the underlying event emitter for application-level events, allowing for advanced event handling patterns if needed.
1196
1247
  */
1197
- events: appEvents
1248
+ events: appEvents,
1249
+
1250
+ /**
1251
+ * Sends a command to the native layer. This is a low-level method that can be used to send arbitrary commands, but for most use cases you will want to use the higher-level methods provided by the Window class instead. Emits an "ipc-sent" event with the command and arguments as data when done.
1252
+ * @param {string} command The command to send to the native layer.
1253
+ * @param {any[]} args The arguments to send with the command.
1254
+ */
1255
+ sendToNative(command, args) {
1256
+ const firstWin = activeWindows.values().next().value;
1257
+ if (firstWin) {
1258
+ firstWin.sendCommand(command, args);
1259
+ } else {
1260
+ error("No active windows to send command to native layer");
1261
+ }
1262
+ },
1198
1263
 
1264
+ /**
1265
+ * Sends a request to the native layer and returns a Promise that resolves to the response. This is a low-level method that can be used to send arbitrary requests, but for most use cases you will want to use the higher-level methods provided by the Window class instead. Emits an "ipc-request-sent" event with the command and arguments as data when done.
1266
+ * @param {string} command The command to send to the native layer.
1267
+ * @param {any[]} args The arguments to send with the command.
1268
+ * @returns {Promise<any>} A Promise that resolves to the response from the native layer.
1269
+ */
1270
+ async requestFromNative(command, ...args) {
1271
+ const firstWin = activeWindows.values().next().value;
1272
+ if (firstWin) {
1273
+ return await firstWin.request(command, ...args);
1274
+ } else {
1275
+ error("No active windows to send request to native layer");
1276
+ return null;
1277
+ }
1278
+ },
1279
+
1280
+ async isDarkMode() {
1281
+ const res = await this.requestFromNative("isDarkMode");
1282
+ return res?.isDarkMode;
1283
+ }
1284
+
1285
+ }
1286
+
1287
+ const clipboard = {
1288
+
1289
+ async writeText(text) {
1290
+ app.sendToNative("writeToClipboard", [text]);
1291
+ },
1292
+
1293
+ async readText() {
1294
+ const res = await app.requestFromNative("readFromClipboard");
1295
+ return res.text || "";
1296
+ }
1297
+
1298
+ }
1299
+
1300
+ const blockPowerSave = {
1301
+
1302
+ start() {
1303
+ app.sendToNative("blockPowerSave");
1304
+ },
1305
+
1306
+ stop() {
1307
+ app.sendToNative("unblockPowerSave");
1308
+ }
1309
+
1199
1310
  }
1200
1311
 
1201
- module.exports = { Window, ipc, isPackaged, app, PORT };
1312
+ module.exports = { Window, ipc, isPackaged, app, PORT, clipboard, blockPowerSave };
1202
1313
 
1203
1314
  const findNearestPackageJson = require("./findpackage");
1204
1315
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/systemsoftware/positron.js"
6
6
  },
7
7
  "homepage": "https://positronjs.gitbook.io",
8
- "version": "1.0.4",
8
+ "version": "1.0.5",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"
@@ -13,7 +13,13 @@
13
13
  "keywords": [
14
14
  "app",
15
15
  "framework",
16
- "desktop"
16
+ "desktop",
17
+ "cross-platform",
18
+ "native",
19
+ "javascript",
20
+ "node",
21
+ "macos",
22
+ "windows"
17
23
  ],
18
24
  "author": "Bryce",
19
25
  "license": "MIT",
@@ -21,6 +27,7 @@
21
27
  "dependencies": {
22
28
  "@yao-pkg/pkg": "^6.20.0",
23
29
  "esbuild": "^0.28.0",
30
+ "semver": "^7.8.1",
24
31
  "ws": "^8.20.1"
25
32
  },
26
33
  "bin": {
package/packager.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { execSync } = require("child_process");
3
+ const { execSync, execFileSync } = require("child_process");
4
4
  const { info, error, success } = require("./logs");
5
5
  const https = require("https");
6
6
  const esbuild = require("esbuild");
@@ -70,9 +70,12 @@ async function packageMacOS(appRoot, distDir, appName) {
70
70
  error("Fatal: Native compiled binary missing from bin/. Run build first.");
71
71
  process.exit(1);
72
72
  }
73
- fs.copyFileSync(compiledBinary, path.join(macosPath, appName));
74
- fs.chmodSync(path.join(macosPath, appName), "755");
75
-
73
+ const binaryPath = path.join(macosPath, appName);
74
+ fs.copyFileSync(compiledBinary, binaryPath);
75
+ fs.chmodSync(binaryPath, "755");
76
+
77
+
78
+
76
79
  const packageJsonPath = path.join(appRoot, "package.json");
77
80
  const package = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
78
81
 
@@ -105,14 +108,29 @@ async function packageMacOS(appRoot, distDir, appName) {
105
108
  handleJavaScriptPipeline(appRoot, resourcesPath);
106
109
 
107
110
  const bundledJs = path.join(resourcesPath, "index.js");
111
+ const backendName = appName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase() + '-backend';
108
112
  if (fs.existsSync(bundledJs)) {
109
- await compileWithPkg(bundledJs, "darwin", resourcesPath, "positron-backend");
113
+ await compileWithPkg(bundledJs, "darwin", resourcesPath, backendName);
114
+ const binPathEscaped = path.join(resourcesPath, backendName).replace(/"/g, '\\"');
115
+
116
+ try {
117
+ let swiftScript = "";
118
+ if(fs.existsSync(path.join(__dirname, ['positronicon', 'png'].join('.')))) {
119
+ const iconPathEscaped = path.join(__dirname, ['positronicon', 'png'].join('.')).replace(/"/g, '\\"');
120
+ swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []);`;
121
+ }
122
+ execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
123
+ } catch (err) {
124
+ error("Failed to set custom icon on native binary:", err);
125
+ }
110
126
 
111
- // fs.renameSync(targetNodePath, path.join(resourcesPath, "positron-backend"));
112
127
  }
113
128
 
114
129
  fs.rmSync(path.join(resourcesPath, "icon.ico"), { force: true });
115
130
 
131
+ if(!process.argv.includes('--keep-package-json') || !process.argv.includes('--kpj')) {
132
+ fs.rmSync(path.join(resourcesPath, "package.json"), { force: true });
133
+ }
116
134
 
117
135
  success(`Successfully packaged macOS app at: ${appBundlePath}`);
118
136
  }
@@ -162,9 +180,10 @@ async function packageWindows(appRoot, distDir, appName) {
162
180
  }
163
181
 
164
182
  const bundledJs = path.join(resourcesPath, "index.js");
183
+ const backendName = appName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase() + '-backend';
165
184
 
166
185
  if (fs.existsSync(bundledJs)) {
167
- await compileWithPkg(bundledJs, "win32", resourcesPath, "positron-backend");
186
+ await compileWithPkg(bundledJs, "win32", resourcesPath, backendName);
168
187
  } else {
169
188
  error(`[Packager] Fatal: Bundled JavaScript entry point missing at ${bundledJs}`);
170
189
  process.exit(1);
@@ -173,6 +192,10 @@ async function packageWindows(appRoot, distDir, appName) {
173
192
  fs.rmSync(path.join(resourcesPath, "icon.icns"), { force: true });
174
193
  fs.rmSync(path.join(resourcesPath, "icon.ico"), { force: true });
175
194
 
195
+ if(!process.argv.includes('--keep-package-json') || !process.argv.includes('--kpj')) {
196
+ fs.rmSync(path.join(resourcesPath, "package.json"), { force: true });
197
+ }
198
+
176
199
  const macBinaryPath = path.join(outputFolder, "positron-runtime");
177
200
  if (fs.existsSync(macBinaryPath)) {
178
201
  fs.rmSync(macBinaryPath);