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 +6 -1
- package/builder.js +13 -0
- package/core/mac/main.swift +97 -20
- package/core/win/PositronRuntime.csproj +1 -1
- package/core/win/main.cs +164 -21
- package/index.js +149 -38
- package/package.json +9 -2
- package/packager.js +30 -7
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");
|
package/core/mac/main.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
*
|
|
621
|
-
* @param {string} command
|
|
622
|
-
* @param {
|
|
623
|
-
* @
|
|
636
|
+
* @overload
|
|
637
|
+
* @param {string} command
|
|
638
|
+
* @param {RequestOptions} options
|
|
639
|
+
* @param {...any} args
|
|
640
|
+
* @returns {Promise<any>}
|
|
624
641
|
*/
|
|
625
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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(!
|
|
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
|
-
|
|
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"
|
|
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",
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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",
|
|
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",
|
|
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",
|
|
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"
|
|
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",
|
|
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.
|
|
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
|
-
|
|
74
|
-
fs.
|
|
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,
|
|
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,
|
|
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);
|