positron.js 1.0.4 → 1.0.6

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");
@@ -78,7 +91,17 @@ function performNativeBuild() {
78
91
  }
79
92
 
80
93
  if (buildingForMac) {
81
- info(`[Builder] Stitching ${nativeExtensionsMac.length} native extensions...`);
94
+
95
+ const coreMacDir = path.join(__dirname, "core", "mac");
96
+
97
+ nativeExtensionsMac.push({
98
+ command:"createTray",
99
+ className:"TrayExtension",
100
+ sourceFile:path.join(coreMacDir, "tray.swift")
101
+ });
102
+
103
+ // -1 to account for the built-in tray extension
104
+ info(`[Builder] Stitching ${nativeExtensionsMac.length-1} native extensions...`);
82
105
 
83
106
  let registryContent = `// Auto-generated by Positron. Do not edit.\n`;
84
107
  registryContent += `func getExtensionRegistry() -> [String: (Int, [String]) -> Void] {\n`;
@@ -99,7 +122,6 @@ function performNativeBuild() {
99
122
 
100
123
  registryContent += `}\n`;
101
124
 
102
- const coreMacDir = path.join(__dirname, "core", "mac");
103
125
  fs.writeFileSync(path.join(coreMacDir, "Registry.swift"), registryContent);
104
126
 
105
127
  info("[Builder] Compiling native binary...");
@@ -176,6 +198,7 @@ function performNativeBuild() {
176
198
  const coreWinDir = path.join(__dirname, "core", "win");
177
199
  const extensionsDir = path.join(coreWinDir, "extensions");
178
200
 
201
+
179
202
  // 1. Clean and prepare a staging folder for all native extensions
180
203
  if (fs.existsSync(extensionsDir)) fs.rmSync(extensionsDir, { recursive: true, force: true });
181
204
  fs.mkdirSync(extensionsDir, { recursive: true });
@@ -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"])
498
+ )
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])
472
523
  )
473
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,13 @@ 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"])
833
+ )
834
+
835
+ case "getFocusedWindowId":
836
+ let focusedWindowId = windows.first(where: { $0.value.isKeyWindow })?.key ?? -1
837
+ AppDelegate.shared?.ipcClient.send(
838
+ IPCResponse(windowId: windowId, event: args.last ?? "getFocusedWindowId-reply-\(windowId)", data: ["focusedWindowId": "\(focusedWindowId)"])
759
839
  )
760
840
 
761
841
  case "emitToRenderer":
@@ -843,7 +923,7 @@ final class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
843
923
  let isFile = webView.url?.isFileURL ?? false
844
924
  let eventName = isFile ? "loadFile-reply-\(windowId)" : "loadURL-reply-\(windowId)"
845
925
  AppDelegate.shared?.ipcClient.send(
846
- IPCResponse(windowId: windowId, event: eventName, data: [:])
926
+ IPCResponse(windowId: windowId, event: eventName, data: ["url": webView.url?.absoluteString ?? "", "title": webView.title ?? "", "canGoBack": (webView.canGoBack ? "true" : "false"), "canGoForward": (webView.canGoForward ? "true" : "false")])
847
927
  )
848
928
  }
849
929
  }
@@ -931,7 +1011,7 @@ func makePreloadScript(windowId: Int) -> String {
931
1011
  /** Called internally by Swift's evaluateJS to deliver a push message. */
932
1012
  _emit(channel, payload) {
933
1013
  (_listeners[channel] || []).forEach(fn => {
934
- try { fn(payload); } catch(e) { console.printError('[ipc] listener error:', e); }
1014
+ try { fn(payload); } catch(e) { console.error('[ipc] listener error:', e); }
935
1015
  });
936
1016
  },
937
1017
 
@@ -1088,8 +1168,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1088
1168
 
1089
1169
  cd "\(resourcePath)"
1090
1170
 
1091
- if [ -f "positron-backend" ]; then
1092
- exec "./positron-backend"
1171
+ BACKEND_BIN=$(find . -maxdepth 1 -name "*-backend" | head -n 1)
1172
+ if [ -n "$BACKEND_BIN" ] && [ -f "$BACKEND_BIN" ]; then
1173
+ exec "$BACKEND_BIN"
1093
1174
  else
1094
1175
  exec "node" "."
1095
1176
  fi
@@ -1311,20 +1392,22 @@ setbuf(__stdoutp, nil)
1311
1392
  setbuf(__stderrp, nil)
1312
1393
 
1313
1394
  signal(SIGINT) { _ in
1314
- printError("INFO: Received SIGINT, shutting down…")
1315
1395
  AppDelegate.shared?.nodeProcess?.terminate()
1396
+ unblockPowerSave()
1316
1397
  exit(0)
1317
1398
  }
1318
1399
 
1319
1400
  signal(SIGTERM) { _ in
1320
1401
  printError("INFO: Received SIGTERM, shutting down…")
1321
1402
  AppDelegate.shared?.nodeProcess?.terminate()
1403
+ unblockPowerSave()
1322
1404
  exit(0)
1323
1405
  }
1324
1406
 
1325
1407
  signal(SIGSEGV) { _ in
1326
1408
  printError("ERROR: Caught SIGSEGV (segmentation fault). This likely indicates a bug in the native code. Attempting to shut down gracefully…")
1327
1409
  AppDelegate.shared?.nodeProcess?.terminate()
1410
+ unblockPowerSave()
1328
1411
  signal(SIGSEGV, SIG_DFL)
1329
1412
  raise(SIGSEGV)
1330
1413
  }
@@ -0,0 +1,95 @@
1
+ import Cocoa
2
+
3
+ class TrayManager {
4
+ static let shared = TrayManager()
5
+ var statusItem: NSStatusItem?
6
+
7
+ func setupTray() {
8
+ if statusItem == nil {
9
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
10
+ if let button = statusItem?.button {
11
+ button.title = "App"
12
+ }
13
+ }
14
+ }
15
+
16
+ func setMenu(_ menu: NSMenu) {
17
+ statusItem?.menu = menu
18
+ }
19
+
20
+ func setTitle(_ title: String) {
21
+ statusItem?.button?.title = title
22
+ }
23
+
24
+ func setIcon(_ iconPath: String) {
25
+ guard let button = statusItem?.button else { return printError("Tray button not initialized") }
26
+
27
+ if iconPath.isEmpty {
28
+ DispatchQueue.main.async {
29
+ button.image = nil
30
+ button.imagePosition = .imageLeft
31
+ }
32
+ return
33
+ }
34
+
35
+ guard FileManager.default.fileExists(atPath: iconPath) else {
36
+ printError("Icon path does not exist: \(iconPath)")
37
+ return
38
+ }
39
+
40
+ DispatchQueue.main.async {
41
+ if let img = NSImage(contentsOfFile: iconPath) {
42
+ img.size = NSSize(width: 18, height: 18)
43
+ img.isTemplate = true
44
+ button.image = img
45
+ button.imagePosition = .imageLeft
46
+ } else {
47
+ button.image = nil
48
+ button.imagePosition = .imageLeft
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ public struct TrayExtension {
55
+ public static func handle(windowId: Int, args: [String]) {
56
+
57
+ if(args.last == "setTitle") {
58
+ TrayManager.shared.setTitle(args[0])
59
+ return
60
+ }
61
+
62
+ if(args.last == "setIcon") {
63
+ TrayManager.shared.setIcon(args[0])
64
+ return
65
+ }
66
+
67
+ guard let descString = args.first,
68
+ let data = descString.data(using: .utf8),
69
+ let desc = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
70
+ printError("tray setMenu — invalid JSON descriptor")
71
+ return
72
+ }
73
+
74
+ if args.last == "setMenu" {
75
+ TrayManager.shared.setMenu(buildContextMenu(from: desc, windowId: windowId))
76
+ return
77
+ }
78
+
79
+ DispatchQueue.main.async {
80
+
81
+ TrayManager.shared.setupTray()
82
+
83
+ let title = args.count > 1 ? args[1] : ""
84
+ TrayManager.shared.setTitle(title)
85
+
86
+ let imagePath = args.count > 2 ? args[2] : nil
87
+ if let imagePath = imagePath {
88
+ TrayManager.shared.setIcon(imagePath)
89
+ }
90
+
91
+ let menu = buildContextMenu(from: desc, windowId: windowId)
92
+ TrayManager.shared.setMenu(menu)
93
+ }
94
+ }
95
+ }
@@ -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>