positron.js 1.0.3 → 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");
@@ -144,10 +157,19 @@ function performNativeBuild() {
144
157
  });
145
158
 
146
159
  try {
147
- const iconPathEscaped = path.join(__dirname, "positronicon.png").replace(/"/g, '\\"');
160
+ let swiftScript = "";
161
+ if(fs.existsSync(path.join(appRoot, "icon.icns"))) {
162
+ const iconPathEscaped = path.join(appRoot, "icon.icns").replace(/"/g, '\\"');
148
163
  const binPathEscaped = path.join(outBinaryDir, binaryName).replace(/"/g, '\\"');
149
- const swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: [])`;
164
+ swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []); `;
150
165
  cp.execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
166
+ return true;
167
+ } else if(fs.existsSync(path.join(__dirname, ['positronicon', 'png'].join('.')))) {
168
+ const iconPathEscaped = path.join(__dirname, ['positronicon', 'png'].join('.')).replace(/"/g, '\\"');
169
+ const binPathEscaped = path.join(outBinaryDir, binaryName).replace(/"/g, '\\"');
170
+ swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []);`;
171
+ }
172
+ cp.execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
151
173
  } catch (err) {
152
174
  error("Failed to set custom icon on native binary:", err);
153
175
  }
@@ -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) {
@@ -141,13 +167,9 @@ func getBuiltInHandlers() -> [String: (Int, [String]) -> Void] {
141
167
  return
142
168
  }
143
169
 
144
- let selector = Selector(("_showDeveloperTools:"))
145
- if webView.responds(to: selector) {
146
- webView.perform(selector, with: nil)
147
- } else {
148
- printError("openDevTools failed: _showDeveloperTools: selector not found on WKWebView (windowId \(windowId))")
170
+ let inspector = webView.value(forKey: "inspector") as? NSObject
171
+ inspector?.perform(NSSelectorFromString("show"))
149
172
  }
150
- },
151
173
  ]
152
174
 
153
175
  return baseHandlers + getExtensionRegistry()
@@ -292,6 +314,13 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
292
314
 
293
315
  window.performClose(nil)
294
316
 
317
+ case "isFullscreen":
318
+ guard let window = windows[windowId] else { return }
319
+ let isFullscreen = window.styleMask.contains(.fullScreen)
320
+ AppDelegate.shared?.ipcClient.send(
321
+ IPCResponse(windowId: windowId, event: args.last ?? "isFullscreen-reply-\(windowId)", data: ["isFullscreen": isFullscreen ? "true" : "false"])
322
+ )
323
+
295
324
  case "setSwipeNav":
296
325
  guard let window = windows[windowId],
297
326
  let webView = window.contentView as? WKWebView else {
@@ -302,7 +331,7 @@ case "setSwipeNav":
302
331
  webView.allowsBackForwardNavigationGestures = enable
303
332
 
304
333
  GetIPCClient().send(
305
- 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"])
306
335
  )
307
336
 
308
337
  case "forceCloseWindow":
@@ -399,7 +428,7 @@ case "forceCloseWindow":
399
428
  let frame = window.frame
400
429
  let bounds = ["x": "\(Int(frame.origin.x))", "y": "\(Int(frame.origin.y))", "width": "\(Int(frame.size.width))", "height": "\(Int(frame.size.height))"]
401
430
  AppDelegate.shared?.ipcClient.send(
402
- IPCResponse(windowId: windowId, event: "getBounds-reply-\(windowId)", data: bounds)
431
+ IPCResponse(windowId: windowId, event: args.last ?? "getBounds-reply-\(windowId)", data: bounds)
403
432
  )
404
433
 
405
434
  case "setResizable":
@@ -458,16 +487,64 @@ case "forceCloseWindow":
458
487
  guard let window = windows[windowId] else { return }
459
488
  let canGoBack = (window.contentView as? WKWebView)?.canGoBack ?? false
460
489
  AppDelegate.shared?.ipcClient.send(
461
- 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"])
462
491
  )
463
492
 
464
493
  case "canGoForward":
465
494
  guard let window = windows[windowId] else { return }
466
495
  let canGoForward = (window.contentView as? WKWebView)?.canGoForward ?? false
467
496
  AppDelegate.shared?.ipcClient.send(
468
- 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])
469
523
  )
470
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
+
471
548
  case "showNotification":
472
549
 
473
550
  UNUserNotificationCenter.current().requestAuthorization(
@@ -498,14 +575,14 @@ UNUserNotificationCenter.current().requestAuthorization(
498
575
  guard let window = windows[windowId] else { return }
499
576
  let url = (window.contentView as? WKWebView)?.url?.absoluteString ?? ""
500
577
  AppDelegate.shared?.ipcClient.send(
501
- IPCResponse(windowId: windowId, event: "getURL-reply-\(windowId)", data: ["url": url])
578
+ IPCResponse(windowId: windowId, event: args.last ?? "getURL-reply-\(windowId)", data: ["url": url])
502
579
  )
503
580
 
504
581
  case "getTitle":
505
582
  guard let window = windows[windowId] else { return }
506
583
  let title = window.title
507
584
  AppDelegate.shared?.ipcClient.send(
508
- IPCResponse(windowId: windowId, event: "getTitle-reply-\(windowId)", data: ["title": title])
585
+ IPCResponse(windowId: windowId, event: args.last ?? "getTitle-reply-\(windowId)", data: ["title": title])
509
586
  )
510
587
 
511
588
  case "executeAppleScript":
@@ -521,6 +598,97 @@ UNUserNotificationCenter.current().requestAuthorization(
521
598
  printError("executeAppleScript failed: \(errorMessage)")
522
599
  }
523
600
 
601
+ case "isVisible":
602
+ guard let window = windows[windowId] else { return }
603
+ let isVisible = window.isVisible
604
+ AppDelegate.shared?.ipcClient.send(
605
+ IPCResponse(windowId: windowId, event: args.last ?? "isVisible-reply-\(windowId)", data: ["isVisible": isVisible ? "true" : "false"])
606
+ )
607
+
608
+ case "addToContentBlocker":
609
+ guard let window = windows[windowId],
610
+ let webView = window.contentView as? WKWebView,
611
+ let input = args.first
612
+ else {
613
+ printError("addToContentBlocker — missing rules")
614
+ return
615
+ }
616
+
617
+ let jsonStr: String
618
+
619
+ if FileManager.default.fileExists(atPath: input) {
620
+ do {
621
+ jsonStr = try String(contentsOfFile: input, encoding: .utf8)
622
+ } catch {
623
+ printError("Failed to read rule file: \(error.localizedDescription)")
624
+ return
625
+ }
626
+ } else {
627
+ jsonStr = input
628
+ }
629
+
630
+ guard let data = jsonStr.data(using: .utf8),
631
+ (try? JSONSerialization.jsonObject(with: data)) != nil
632
+ else {
633
+ printError("addToContentBlocker — invalid JSON rules")
634
+ return
635
+ }
636
+
637
+ let reload = args.count > 1
638
+ ? args[1].lowercased() == "true"
639
+ : true
640
+
641
+ let clearAll = args.count > 2
642
+ ? args[2].lowercased() == "true"
643
+ : false
644
+
645
+ let identifier = "dynamicRules-\(windowId)-\(UUID().uuidString)"
646
+
647
+ WKContentRuleListStore.default().compileContentRuleList(
648
+ forIdentifier: identifier,
649
+ encodedContentRuleList: jsonStr
650
+ ) { [weak webView] ruleList, error in
651
+
652
+ guard let webView else { return }
653
+
654
+ if let error {
655
+ printError("Failed to compile content blocker rules: \(error.localizedDescription)")
656
+ return
657
+ }
658
+
659
+ guard let ruleList else {
660
+ printError("Failed to compile content blocker rules: no rule list returned")
661
+ return
662
+ }
663
+
664
+ let controller = webView.configuration.userContentController
665
+
666
+ if clearAll {
667
+ controller.removeAllContentRuleLists()
668
+ }
669
+
670
+ controller.add(ruleList)
671
+
672
+ if reload {
673
+ webView.reload()
674
+ }
675
+ }
676
+
677
+ GetIPCClient().send(
678
+ IPCResponse(windowId: windowId, event: args.last ?? "addToContentBlocker-reply-\(windowId)", data: ["status": "success"])
679
+ )
680
+
681
+ case "isSwipeNavEnabled":
682
+ guard let window = windows[windowId],
683
+ let webView = window.contentView as? WKWebView else {
684
+ printError("isSwipeNavEnabled — webview not found for window \(windowId)")
685
+ return
686
+ }
687
+ let enabled = webView.allowsBackForwardNavigationGestures
688
+ AppDelegate.shared?.ipcClient.send(
689
+ IPCResponse(windowId: windowId, event: args.last ?? "isSwipeNavEnabled-reply-\(windowId)", data: ["enabled": enabled ? "true" : "false"])
690
+ )
691
+
524
692
  case "forward":
525
693
  guard let window = windows[windowId] else { return }
526
694
  (window.contentView as? WKWebView)?.goForward()
@@ -604,7 +772,7 @@ UNUserNotificationCenter.current().requestAuthorization(
604
772
  resultStr = "null"
605
773
  }
606
774
  AppDelegate.shared?.ipcClient.send(
607
- IPCResponse(windowId: windowId, event: "evaluateJS-reply-\(windowId)", data: ["result": resultStr])
775
+ IPCResponse(windowId: windowId, event: args.last ?? "evaluateJS-reply-\(windowId)", data: ["result": resultStr])
608
776
  )
609
777
  }
610
778
 
@@ -630,11 +798,11 @@ UNUserNotificationCenter.current().requestAuthorization(
630
798
  if response == .alertFirstButtonReturn {
631
799
  let userInput = inputField.stringValue
632
800
  AppDelegate.shared?.ipcClient.send(
633
- IPCResponse(windowId: windowId, event: "prompt-reply-\(windowId)", data: ["input": userInput])
801
+ IPCResponse(windowId: windowId, event: args.last ?? "prompt-reply-\(windowId)", data: ["input": userInput])
634
802
  )
635
803
  } else {
636
804
  AppDelegate.shared?.ipcClient.send(
637
- IPCResponse(windowId: windowId, event: "prompt-reply-\(windowId)", data: ["input": ""])
805
+ IPCResponse(windowId: windowId, event: args.last ?? "prompt-reply-\(windowId)", data: ["input": ""])
638
806
  )
639
807
  }
640
808
  }
@@ -653,7 +821,7 @@ UNUserNotificationCenter.current().requestAuthorization(
653
821
  alert.beginSheetModal(for: window) { response in
654
822
  let confirmed = (response == .alertFirstButtonReturn)
655
823
  AppDelegate.shared?.ipcClient.send(
656
- 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"])
657
825
  )
658
826
  }
659
827
 
@@ -661,7 +829,7 @@ UNUserNotificationCenter.current().requestAuthorization(
661
829
  guard let window = windows[windowId] else { return }
662
830
  let isFocused = window.isKeyWindow
663
831
  AppDelegate.shared?.ipcClient.send(
664
- 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"])
665
833
  )
666
834
 
667
835
  case "emitToRenderer":
@@ -749,7 +917,7 @@ final class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
749
917
  let isFile = webView.url?.isFileURL ?? false
750
918
  let eventName = isFile ? "loadFile-reply-\(windowId)" : "loadURL-reply-\(windowId)"
751
919
  AppDelegate.shared?.ipcClient.send(
752
- 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")])
753
921
  )
754
922
  }
755
923
  }
@@ -994,8 +1162,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
994
1162
 
995
1163
  cd "\(resourcePath)"
996
1164
 
997
- if [ -f "positron-backend" ]; then
998
- 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"
999
1168
  else
1000
1169
  exec "node" "."
1001
1170
  fi
@@ -1067,6 +1236,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1067
1236
  editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
1068
1237
  editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
1069
1238
 
1239
+ // Window menu
1240
+ let windowMenuItem = NSMenuItem()
1241
+ mainMenu.addItem(windowMenuItem)
1242
+ let windowMenu = NSMenu(title: "Window")
1243
+ windowMenuItem.submenu = windowMenu
1244
+ windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m")
1245
+ windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "")
1246
+ NSApp.windowsMenu = windowMenu
1247
+
1070
1248
  NSApp.mainMenu = mainMenu
1071
1249
  }
1072
1250
  }
@@ -1145,6 +1323,12 @@ func buildMenu(from descriptor: [[String: Any]], windowId: Int) -> NSMenu {
1145
1323
  let sub = NSMenu(title: topItem.title)
1146
1324
  topItem.submenu = sub
1147
1325
 
1326
+ if let role = topLevel["role"] as? String, role.lowercased() == "window" {
1327
+ NSApp.windowsMenu = sub
1328
+ } else if topItem.title.lowercased() == "window" {
1329
+ NSApp.windowsMenu = sub
1330
+ }
1331
+
1148
1332
  if let items = topLevel["items"] as? [[String: Any]] {
1149
1333
  populateMenu(sub, with: items, windowId: windowId, isContextMenu: false)
1150
1334
  }
@@ -1202,20 +1386,22 @@ setbuf(__stdoutp, nil)
1202
1386
  setbuf(__stderrp, nil)
1203
1387
 
1204
1388
  signal(SIGINT) { _ in
1205
- printError("INFO: Received SIGINT, shutting down…")
1206
1389
  AppDelegate.shared?.nodeProcess?.terminate()
1390
+ unblockPowerSave()
1207
1391
  exit(0)
1208
1392
  }
1209
1393
 
1210
1394
  signal(SIGTERM) { _ in
1211
1395
  printError("INFO: Received SIGTERM, shutting down…")
1212
1396
  AppDelegate.shared?.nodeProcess?.terminate()
1397
+ unblockPowerSave()
1213
1398
  exit(0)
1214
1399
  }
1215
1400
 
1216
1401
  signal(SIGSEGV) { _ in
1217
1402
  printError("ERROR: Caught SIGSEGV (segmentation fault). This likely indicates a bug in the native code. Attempting to shut down gracefully…")
1218
1403
  AppDelegate.shared?.nodeProcess?.terminate()
1404
+ unblockPowerSave()
1219
1405
  signal(SIGSEGV, SIG_DFL)
1220
1406
  raise(SIGSEGV)
1221
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>