positron.js 1.0.2 → 1.0.4
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 +1 -1
- package/builder.js +12 -3
- package/core/mac/main.swift +166 -6
- package/core/win/main.cs +79 -1
- package/index.js +163 -26
- package/package.json +1 -1
package/bin/positron.js
CHANGED
package/builder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const cp = require("child_process");
|
|
4
|
-
const { success, error, info } = require("./logs");
|
|
4
|
+
const { success, error, info, warn } = require("./logs");
|
|
5
5
|
|
|
6
6
|
const arch = process.argv.includes("--x64") ? "x64" : process.argv.includes("--arm64") ? "arm64" : process.arch;
|
|
7
7
|
|
|
@@ -144,10 +144,19 @@ function performNativeBuild() {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
try {
|
|
147
|
-
|
|
147
|
+
let swiftScript = "";
|
|
148
|
+
if(fs.existsSync(path.join(appRoot, "icon.icns"))) {
|
|
149
|
+
const iconPathEscaped = path.join(appRoot, "icon.icns").replace(/"/g, '\\"');
|
|
148
150
|
const binPathEscaped = path.join(outBinaryDir, binaryName).replace(/"/g, '\\"');
|
|
149
|
-
|
|
151
|
+
swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []); `;
|
|
150
152
|
cp.execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
|
|
153
|
+
return true;
|
|
154
|
+
} else if(fs.existsSync(path.join(__dirname, ['positronicon', 'png'].join('.')))) {
|
|
155
|
+
const iconPathEscaped = path.join(__dirname, ['positronicon', 'png'].join('.')).replace(/"/g, '\\"');
|
|
156
|
+
const binPathEscaped = path.join(outBinaryDir, binaryName).replace(/"/g, '\\"');
|
|
157
|
+
swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []);`;
|
|
158
|
+
}
|
|
159
|
+
cp.execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
|
|
151
160
|
} catch (err) {
|
|
152
161
|
error("Failed to set custom icon on native binary:", err);
|
|
153
162
|
}
|
package/core/mac/main.swift
CHANGED
|
@@ -141,13 +141,9 @@ func getBuiltInHandlers() -> [String: (Int, [String]) -> Void] {
|
|
|
141
141
|
return
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
webView.perform(selector, with: nil)
|
|
147
|
-
} else {
|
|
148
|
-
printError("openDevTools failed: _showDeveloperTools: selector not found on WKWebView (windowId \(windowId))")
|
|
144
|
+
let inspector = webView.value(forKey: "inspector") as? NSObject
|
|
145
|
+
inspector?.perform(NSSelectorFromString("show"))
|
|
149
146
|
}
|
|
150
|
-
},
|
|
151
147
|
]
|
|
152
148
|
|
|
153
149
|
return baseHandlers + getExtensionRegistry()
|
|
@@ -169,6 +165,26 @@ struct IPCResponse: Codable {
|
|
|
169
165
|
let data: [String: String]
|
|
170
166
|
}
|
|
171
167
|
|
|
168
|
+
func GetIPCClient() -> IPCClient {
|
|
169
|
+
return AppDelegate.shared?.ipcClient ?? IPCClient()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func GetWebView(windowId: Int) -> WKWebView? {
|
|
173
|
+
guard let window = windows[windowId],
|
|
174
|
+
let webView = window.contentView as? WKWebView else {
|
|
175
|
+
printError("GetWebView failed: no webview found for window \(windowId)")
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
return webView
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func GetWindow(windowId: Int) -> NSWindow? {
|
|
182
|
+
guard let window = windows[windowId] else {
|
|
183
|
+
printError("GetWindow failed: no window found with ID \(windowId)")
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
return window
|
|
187
|
+
}
|
|
172
188
|
|
|
173
189
|
// MARK: - Command Handler
|
|
174
190
|
|
|
@@ -272,6 +288,26 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
|
|
|
272
288
|
|
|
273
289
|
window.performClose(nil)
|
|
274
290
|
|
|
291
|
+
case "isFullscreen":
|
|
292
|
+
guard let window = windows[windowId] else { return }
|
|
293
|
+
let isFullscreen = window.styleMask.contains(.fullScreen)
|
|
294
|
+
AppDelegate.shared?.ipcClient.send(
|
|
295
|
+
IPCResponse(windowId: windowId, event: "isFullscreen-reply-\(windowId)", data: ["isFullscreen": isFullscreen ? "true" : "false"])
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
case "setSwipeNav":
|
|
299
|
+
guard let window = windows[windowId],
|
|
300
|
+
let webView = window.contentView as? WKWebView else {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let enable = args.first?.lowercased() != "false"
|
|
305
|
+
webView.allowsBackForwardNavigationGestures = enable
|
|
306
|
+
|
|
307
|
+
GetIPCClient().send(
|
|
308
|
+
IPCResponse(windowId: windowId, event: "setSwipeNav-reply-\(windowId)", data: ["enabled": enable ? "true" : "false"])
|
|
309
|
+
)
|
|
310
|
+
|
|
275
311
|
case "forceCloseWindow":
|
|
276
312
|
guard let window = windows[windowId] else { return }
|
|
277
313
|
|
|
@@ -488,6 +524,97 @@ UNUserNotificationCenter.current().requestAuthorization(
|
|
|
488
524
|
printError("executeAppleScript failed: \(errorMessage)")
|
|
489
525
|
}
|
|
490
526
|
|
|
527
|
+
case "isVisible":
|
|
528
|
+
guard let window = windows[windowId] else { return }
|
|
529
|
+
let isVisible = window.isVisible
|
|
530
|
+
AppDelegate.shared?.ipcClient.send(
|
|
531
|
+
IPCResponse(windowId: windowId, event: "isVisible-reply-\(windowId)", data: ["isVisible": isVisible ? "true" : "false"])
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
case "addToContentBlocker":
|
|
535
|
+
guard let window = windows[windowId],
|
|
536
|
+
let webView = window.contentView as? WKWebView,
|
|
537
|
+
let input = args.first
|
|
538
|
+
else {
|
|
539
|
+
printError("addToContentBlocker — missing rules")
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let jsonStr: String
|
|
544
|
+
|
|
545
|
+
if FileManager.default.fileExists(atPath: input) {
|
|
546
|
+
do {
|
|
547
|
+
jsonStr = try String(contentsOfFile: input, encoding: .utf8)
|
|
548
|
+
} catch {
|
|
549
|
+
printError("Failed to read rule file: \(error.localizedDescription)")
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
jsonStr = input
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
guard let data = jsonStr.data(using: .utf8),
|
|
557
|
+
(try? JSONSerialization.jsonObject(with: data)) != nil
|
|
558
|
+
else {
|
|
559
|
+
printError("addToContentBlocker — invalid JSON rules")
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let reload = args.count > 1
|
|
564
|
+
? args[1].lowercased() == "true"
|
|
565
|
+
: true
|
|
566
|
+
|
|
567
|
+
let clearAll = args.count > 2
|
|
568
|
+
? args[2].lowercased() == "true"
|
|
569
|
+
: false
|
|
570
|
+
|
|
571
|
+
let identifier = "dynamicRules-\(windowId)-\(UUID().uuidString)"
|
|
572
|
+
|
|
573
|
+
WKContentRuleListStore.default().compileContentRuleList(
|
|
574
|
+
forIdentifier: identifier,
|
|
575
|
+
encodedContentRuleList: jsonStr
|
|
576
|
+
) { [weak webView] ruleList, error in
|
|
577
|
+
|
|
578
|
+
guard let webView else { return }
|
|
579
|
+
|
|
580
|
+
if let error {
|
|
581
|
+
printError("Failed to compile content blocker rules: \(error.localizedDescription)")
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
guard let ruleList else {
|
|
586
|
+
printError("Failed to compile content blocker rules: no rule list returned")
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let controller = webView.configuration.userContentController
|
|
591
|
+
|
|
592
|
+
if clearAll {
|
|
593
|
+
controller.removeAllContentRuleLists()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
controller.add(ruleList)
|
|
597
|
+
|
|
598
|
+
if reload {
|
|
599
|
+
webView.reload()
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
GetIPCClient().send(
|
|
604
|
+
IPCResponse(windowId: windowId, event: "addToContentBlocker-reply-\(windowId)", data: ["status": "success"])
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
case "isSwipeNavEnabled":
|
|
608
|
+
guard let window = windows[windowId],
|
|
609
|
+
let webView = window.contentView as? WKWebView else {
|
|
610
|
+
printError("isSwipeNavEnabled — webview not found for window \(windowId)")
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
let enabled = webView.allowsBackForwardNavigationGestures
|
|
614
|
+
AppDelegate.shared?.ipcClient.send(
|
|
615
|
+
IPCResponse(windowId: windowId, event: "isSwipeNavEnabled-reply-\(windowId)", data: ["enabled": enabled ? "true" : "false"])
|
|
616
|
+
)
|
|
617
|
+
|
|
491
618
|
case "forward":
|
|
492
619
|
guard let window = windows[windowId] else { return }
|
|
493
620
|
(window.contentView as? WKWebView)?.goForward()
|
|
@@ -606,6 +733,24 @@ UNUserNotificationCenter.current().requestAuthorization(
|
|
|
606
733
|
}
|
|
607
734
|
}
|
|
608
735
|
|
|
736
|
+
case "confirm":
|
|
737
|
+
guard let window = windows[windowId] else { return }
|
|
738
|
+
guard let message = args.first else {
|
|
739
|
+
printError("confirm — missing message argument")
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
let alert = NSAlert()
|
|
743
|
+
alert.messageText = message
|
|
744
|
+
alert.addButton(withTitle: "OK")
|
|
745
|
+
alert.addButton(withTitle: "Cancel")
|
|
746
|
+
|
|
747
|
+
alert.beginSheetModal(for: window) { response in
|
|
748
|
+
let confirmed = (response == .alertFirstButtonReturn)
|
|
749
|
+
AppDelegate.shared?.ipcClient.send(
|
|
750
|
+
IPCResponse(windowId: windowId, event: "confirm-reply-\(windowId)", data: ["confirmed": confirmed ? "true" : "false"])
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
609
754
|
case "isFocused":
|
|
610
755
|
guard let window = windows[windowId] else { return }
|
|
611
756
|
let isFocused = window.isKeyWindow
|
|
@@ -1016,6 +1161,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
1016
1161
|
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
1017
1162
|
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
1018
1163
|
|
|
1164
|
+
// Window menu
|
|
1165
|
+
let windowMenuItem = NSMenuItem()
|
|
1166
|
+
mainMenu.addItem(windowMenuItem)
|
|
1167
|
+
let windowMenu = NSMenu(title: "Window")
|
|
1168
|
+
windowMenuItem.submenu = windowMenu
|
|
1169
|
+
windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m")
|
|
1170
|
+
windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "")
|
|
1171
|
+
NSApp.windowsMenu = windowMenu
|
|
1172
|
+
|
|
1019
1173
|
NSApp.mainMenu = mainMenu
|
|
1020
1174
|
}
|
|
1021
1175
|
}
|
|
@@ -1094,6 +1248,12 @@ func buildMenu(from descriptor: [[String: Any]], windowId: Int) -> NSMenu {
|
|
|
1094
1248
|
let sub = NSMenu(title: topItem.title)
|
|
1095
1249
|
topItem.submenu = sub
|
|
1096
1250
|
|
|
1251
|
+
if let role = topLevel["role"] as? String, role.lowercased() == "window" {
|
|
1252
|
+
NSApp.windowsMenu = sub
|
|
1253
|
+
} else if topItem.title.lowercased() == "window" {
|
|
1254
|
+
NSApp.windowsMenu = sub
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1097
1257
|
if let items = topLevel["items"] as? [[String: Any]] {
|
|
1098
1258
|
populateMenu(sub, with: items, windowId: windowId, isContextMenu: false)
|
|
1099
1259
|
}
|
package/core/win/main.cs
CHANGED
|
@@ -24,7 +24,6 @@ namespace PositronWindows
|
|
|
24
24
|
|
|
25
25
|
// MARK: - IPC Message Types
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
public class IPCMessage
|
|
29
28
|
{
|
|
30
29
|
public int windowId { get; set; }
|
|
@@ -382,6 +381,58 @@ private void StartNodeProcess(string workingDirectory)
|
|
|
382
381
|
|
|
383
382
|
// 3. Attach it to the layout
|
|
384
383
|
layout.ContextMenu = contextMenu;
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case "setSwipeNav":
|
|
387
|
+
if (!WindowsMap.TryGetValue(windowId, out var winSwipeNav)) break;
|
|
388
|
+
var wvSwipeNav = GetWebView(windowId);
|
|
389
|
+
if (wvSwipeNav != null) {
|
|
390
|
+
bool enable = args.Count == 0 || args[0].ToLower() != "false";
|
|
391
|
+
wvSwipeNav.CoreWebView2.Settings.IsSwipeNavigationEnabled = enable;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
GetIPCClient().Send(new IPCResponse
|
|
395
|
+
{
|
|
396
|
+
windowId = windowId,
|
|
397
|
+
@event = "setSwipeNav-reply-" + windowId,
|
|
398
|
+
data = new() { { "enabled", (wvSwipeNav?.CoreWebView2.Settings.IsSwipeNavigationEnabled ?? false).ToString().ToLower() } }
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
break;
|
|
402
|
+
|
|
403
|
+
case "isSwipeNavEnabled":
|
|
404
|
+
if (!WindowsMap.TryGetValue(windowId, out var winCheckSwipe)) break;
|
|
405
|
+
var wvCheckSwipe = GetWebView(windowId);
|
|
406
|
+
if (wvCheckSwipe != null) {
|
|
407
|
+
bool isEnabled = wvCheckSwipe.CoreWebView2.Settings.IsSwipeNavigationEnabled;
|
|
408
|
+
GetIPCClient().Send(new IPCResponse
|
|
409
|
+
{
|
|
410
|
+
windowId = windowId,
|
|
411
|
+
@event = "isSwipeNavEnabled-reply-" + windowId,
|
|
412
|
+
data = new() { { "enabled", isEnabled.ToString().ToLower() } }
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case "isVisible":
|
|
419
|
+
if (!WindowsMap.TryGetValue(windowId, out var winVisible)) break;
|
|
420
|
+
bool isVisible = winVisible.IsVisible;
|
|
421
|
+
GetIPCClient().Send(new IPCResponse
|
|
422
|
+
{ windowId = windowId,
|
|
423
|
+
@event = "isVisible-reply-" + windowId,
|
|
424
|
+
data = new() { { "isVisible", isVisible.ToString().ToLower() } }
|
|
425
|
+
});
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case "isFullscreen":
|
|
429
|
+
if (!WindowsMap.TryGetValue(windowId, out var winFullscreen)) break;
|
|
430
|
+
bool isFullscreen = winFullscreen.WindowState == WindowState.Maximized;
|
|
431
|
+
GetIPCClient().Send(new IPCResponse
|
|
432
|
+
{ windowId = windowId,
|
|
433
|
+
@event = "isFullscreen-reply-" + windowId,
|
|
434
|
+
data = new() { { "isFullscreen", isFullscreen.ToString().ToLower() } }
|
|
435
|
+
});
|
|
385
436
|
break;
|
|
386
437
|
|
|
387
438
|
case "closeWindow":
|
|
@@ -621,6 +672,24 @@ case "setBounds":
|
|
|
621
672
|
}
|
|
622
673
|
break;
|
|
623
674
|
|
|
675
|
+
case "confirm":
|
|
676
|
+
if (args.Count < 1)
|
|
677
|
+
{
|
|
678
|
+
error("confirm — expected message argument");
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
{
|
|
682
|
+
var message = args[0];
|
|
683
|
+
var result = MessageBox.Show(message, "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
|
|
684
|
+
_ipcClient.Send(new IPCResponse
|
|
685
|
+
{
|
|
686
|
+
windowId = windowId,
|
|
687
|
+
@event = "confirm-reply-" + windowId,
|
|
688
|
+
data = new() { { "confirmed", result.ToString().ToLower() } }
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
|
|
624
693
|
case "emitToRenderer":
|
|
625
694
|
if (args.Count < 2)
|
|
626
695
|
{
|
|
@@ -841,6 +910,15 @@ case "setBounds":
|
|
|
841
910
|
return null;
|
|
842
911
|
}
|
|
843
912
|
|
|
913
|
+
public static Window? GetWindow(int windowId)
|
|
914
|
+
{
|
|
915
|
+
if (WindowsMap.TryGetValue(windowId, out var window))
|
|
916
|
+
return window;
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
public static IPCClient GetIPCClient() => _ipcClient;
|
|
921
|
+
|
|
844
922
|
// MARK: - Menu Management
|
|
845
923
|
|
|
846
924
|
private static void BuildAndAttachMenu(int windowId, string jsonStr)
|
package/index.js
CHANGED
|
@@ -267,7 +267,8 @@ class Window extends Events.EventEmitter {
|
|
|
267
267
|
* @param {string} command
|
|
268
268
|
* @param {string[]} args
|
|
269
269
|
*/
|
|
270
|
-
sendCommand(command, args
|
|
270
|
+
sendCommand(command, ...args) {
|
|
271
|
+
if(args[0] instanceof Array) args = args[0].concat(args.slice(1));
|
|
271
272
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [String(args)];
|
|
272
273
|
|
|
273
274
|
const payload = JSON.stringify({
|
|
@@ -338,7 +339,7 @@ class Window extends Events.EventEmitter {
|
|
|
338
339
|
* @param {string} channel The IPC channel to send the message on.
|
|
339
340
|
* @param {string[]} args The arguments to send with the message.
|
|
340
341
|
*/
|
|
341
|
-
|
|
342
|
+
emitToRenderer(channel, args = []) {
|
|
342
343
|
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
|
|
343
344
|
const payload = JSON.stringify({
|
|
344
345
|
windowId: this.id,
|
|
@@ -352,6 +353,15 @@ sendIpc(channel, args = []) {
|
|
|
352
353
|
}
|
|
353
354
|
}
|
|
354
355
|
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @deprecated Use emitToRenderer instead.
|
|
359
|
+
*/
|
|
360
|
+
sendIpc = (channel, args = []) => {
|
|
361
|
+
warn("sendIpc is deprecated. Use emitToRenderer instead.");
|
|
362
|
+
this.emitToRenderer(channel, args);
|
|
363
|
+
}
|
|
364
|
+
|
|
355
365
|
#created = false;
|
|
356
366
|
|
|
357
367
|
/**
|
|
@@ -405,6 +415,10 @@ create(width, height, darwinOptions = {
|
|
|
405
415
|
this.sendCommand("forceCloseWindow");
|
|
406
416
|
}
|
|
407
417
|
|
|
418
|
+
isClosed() {
|
|
419
|
+
return !activeWindows.has(this);
|
|
420
|
+
}
|
|
421
|
+
|
|
408
422
|
|
|
409
423
|
/**
|
|
410
424
|
* Sets the application menu for this window.
|
|
@@ -488,10 +502,6 @@ resize(width, height) {
|
|
|
488
502
|
* Opens the developer tools for the window. Emits a "devtools-opened" event when done. Does not work on macOS. For macOS, right-click the window and select "Inspect Element" to open dev tools for that window.
|
|
489
503
|
*/
|
|
490
504
|
openDevTools() {
|
|
491
|
-
if(process.platform === "darwin") {
|
|
492
|
-
warn("The openDevTools command is not supported on macOS due to OS limitations. Please right-click the window and select 'Inspect Element' to access developer tools.");
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
505
|
this.sendCommand("openDevTools");
|
|
496
506
|
this.emit("devtools-opened");
|
|
497
507
|
}
|
|
@@ -560,6 +570,26 @@ focus() {
|
|
|
560
570
|
this.emit("focused");
|
|
561
571
|
}
|
|
562
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Checks if the window is currently visible. Returns a Promise that resolves to true if the window is visible, or false if it is hidden. Emits an "is-visible-checked" event with the result as data when done.
|
|
575
|
+
* @returns {Promise<boolean>} True if the window is visible, false otherwise.
|
|
576
|
+
*/
|
|
577
|
+
async isVisible() {
|
|
578
|
+
const res = await this.request("isVisible", `isVisible-reply-${this.id}`);
|
|
579
|
+
return res?.isVisible === "true";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Checks if the window is currently in fullscreen mode.
|
|
584
|
+
* @returns {Promise<boolean>} True if the window is fullscreen, false otherwise.
|
|
585
|
+
*/
|
|
586
|
+
async isFullscreen() {
|
|
587
|
+
const res = await this.request("isFullscreen", `isFullscreen-reply-${this.id}`);
|
|
588
|
+
return res?.isFullscreen === "true";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
563
593
|
/**
|
|
564
594
|
* Reloads the window. Emits a "reloaded" event when done.
|
|
565
595
|
*/
|
|
@@ -627,7 +657,7 @@ if (timeoutArg) {
|
|
|
627
657
|
|
|
628
658
|
args = args.filter(arg => arg !== timeoutArg);
|
|
629
659
|
|
|
630
|
-
timeoutDuration = parseInt(timeoutArg.split("=")[1]
|
|
660
|
+
timeoutDuration = parseInt(timeoutArg.split("=")[1]);
|
|
631
661
|
}
|
|
632
662
|
|
|
633
663
|
timeout = setTimeout(() => {
|
|
@@ -747,6 +777,10 @@ setContextMenu(menuTemplate) {
|
|
|
747
777
|
this.emit("context-menu-updated", menuTemplate);
|
|
748
778
|
}
|
|
749
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Checks if the window is currently focused.
|
|
782
|
+
* @returns {Promise<boolean>} True if the window is focused, false otherwise.
|
|
783
|
+
*/
|
|
750
784
|
async isFocused() {
|
|
751
785
|
const res = await this.request("isFocused", `isFocused-reply-${this.id}`);
|
|
752
786
|
return res?.isFocused === "true";
|
|
@@ -765,7 +799,7 @@ async getBounds() {
|
|
|
765
799
|
* @returns {Promise<string>} The current URL loaded in the window.
|
|
766
800
|
*/
|
|
767
801
|
async getURL() {
|
|
768
|
-
return await this.request("getURL", `getURL-reply-${this.id}`);
|
|
802
|
+
return (await this.request("getURL", `getURL-reply-${this.id}`))?.url || "";
|
|
769
803
|
}
|
|
770
804
|
|
|
771
805
|
/**
|
|
@@ -773,7 +807,7 @@ async getURL() {
|
|
|
773
807
|
* @returns {Promise<string>} The current title of the window.
|
|
774
808
|
*/
|
|
775
809
|
async getTitle() {
|
|
776
|
-
return await this.request("getTitle", `getTitle-reply-${this.id}`);
|
|
810
|
+
return (await this.request("getTitle", `getTitle-reply-${this.id}`)).title || "";
|
|
777
811
|
}
|
|
778
812
|
|
|
779
813
|
/**
|
|
@@ -801,7 +835,7 @@ setTitlebarTransparent(isTransparent) {
|
|
|
801
835
|
*/
|
|
802
836
|
async evaluateJavaScript(script) {
|
|
803
837
|
const res = await this.request("evaluateJS", `evaluateJS-reply-${this.id}`, script);
|
|
804
|
-
return res;
|
|
838
|
+
return res.result;
|
|
805
839
|
}
|
|
806
840
|
|
|
807
841
|
/**
|
|
@@ -809,8 +843,7 @@ return res;
|
|
|
809
843
|
* @returns {Promise<string>} The user agent string of the window.
|
|
810
844
|
*/
|
|
811
845
|
async getUserAgent() {
|
|
812
|
-
|
|
813
|
-
return res;
|
|
846
|
+
return await this.evaluateJavaScript("navigator.userAgent");
|
|
814
847
|
}
|
|
815
848
|
|
|
816
849
|
/**
|
|
@@ -822,10 +855,14 @@ async getUserAgent() {
|
|
|
822
855
|
async setStyleOf(selector, style) {
|
|
823
856
|
const styleString = Object.entries(style).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
824
857
|
const script = `
|
|
858
|
+
(function() {
|
|
825
859
|
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
826
860
|
elements.forEach(el => {
|
|
827
|
-
|
|
861
|
+
Object.entries(${JSON.stringify(style)}).forEach(([key, value]) => {
|
|
862
|
+
el.style[key] = value;
|
|
863
|
+
});
|
|
828
864
|
});
|
|
865
|
+
})();
|
|
829
866
|
`;
|
|
830
867
|
await this.evaluateJavaScript(script);
|
|
831
868
|
this.emit("style-updated", { selector, style });
|
|
@@ -840,10 +877,12 @@ async setStyleOf(selector, style) {
|
|
|
840
877
|
*/
|
|
841
878
|
async setAttributeOf(selector, attribute, value) {
|
|
842
879
|
const script = `
|
|
880
|
+
(function() {
|
|
843
881
|
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
844
882
|
elements.forEach(el => {
|
|
845
883
|
el.setAttribute(${JSON.stringify(attribute)}, ${JSON.stringify(value)});
|
|
846
884
|
});
|
|
885
|
+
})();
|
|
847
886
|
`;
|
|
848
887
|
await this.evaluateJavaScript(script);
|
|
849
888
|
this.emit("attribute-updated", { selector, attribute, value });
|
|
@@ -857,10 +896,12 @@ async setAttributeOf(selector, attribute, value) {
|
|
|
857
896
|
*/
|
|
858
897
|
async removeAttributeOf(selector, attribute) {
|
|
859
898
|
const script = `
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
899
|
+
(function() {
|
|
900
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
901
|
+
elements.forEach(el => {
|
|
902
|
+
el.removeAttribute(${JSON.stringify(attribute)});
|
|
903
|
+
});
|
|
904
|
+
})();
|
|
864
905
|
`;
|
|
865
906
|
await this.evaluateJavaScript(script);
|
|
866
907
|
this.emit("attribute-removed", { selector, attribute });
|
|
@@ -875,12 +916,14 @@ async removeAttributeOf(selector, attribute) {
|
|
|
875
916
|
async removeStyleOf(selector, styleProperties) {
|
|
876
917
|
const propertiesString = styleProperties.map(prop => `${prop}:`).join("|");
|
|
877
918
|
const script = `
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
919
|
+
(function() {
|
|
920
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
921
|
+
elements.forEach(el => {
|
|
922
|
+
el.style.cssText = el.style.cssText.split(";").filter(rule => {
|
|
923
|
+
return !${JSON.stringify(propertiesString)}.includes(rule.trim().split(":")[0] + ":");
|
|
924
|
+
}).join(";");
|
|
925
|
+
});
|
|
926
|
+
})();
|
|
884
927
|
`;
|
|
885
928
|
await this.evaluateJavaScript(script);
|
|
886
929
|
this.emit("style-removed", { selector, styleProperties });
|
|
@@ -896,6 +939,7 @@ async removeStyleOf(selector, styleProperties) {
|
|
|
896
939
|
*/
|
|
897
940
|
async onClick(selector, channel, { replace = true } = {}) {
|
|
898
941
|
const script = `
|
|
942
|
+
(function() {
|
|
899
943
|
const selector = ${JSON.stringify(selector)};
|
|
900
944
|
const channel = ${JSON.stringify(channel)};
|
|
901
945
|
const replace = ${replace};
|
|
@@ -913,6 +957,7 @@ async onClick(selector, channel, { replace = true } = {}) {
|
|
|
913
957
|
});
|
|
914
958
|
}
|
|
915
959
|
})
|
|
960
|
+
})();
|
|
916
961
|
`;
|
|
917
962
|
|
|
918
963
|
await this.evaluateJavaScript(script);
|
|
@@ -933,6 +978,78 @@ async removeOnClick(selector) {
|
|
|
933
978
|
await this.evaluateJavaScript(script);
|
|
934
979
|
}
|
|
935
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Displays a confirmation dialog with the given message. Returns a Promise that resolves to true if the user confirmed, or false if the user cancelled. Emits a "confirm" event with the message as data when done.
|
|
983
|
+
* @param {string} message The message to display in the confirmation dialog.
|
|
984
|
+
* @returns {Promise<boolean>} True if the user confirmed, false if the user cancelled.
|
|
985
|
+
*/
|
|
986
|
+
async confirm(message) {
|
|
987
|
+
const res = await this.request("confirm", `confirm-reply-${this.id}`, message);
|
|
988
|
+
this.emit("confirm", message);
|
|
989
|
+
return res?.confirmed === "true";
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Enables or disables swipe navigation for the window. When enabled, users can navigate back and forward through their history by swiping left or right on a trackpad or touchscreen. Emits a "swipe-navigation-updated" event with the new value when done.
|
|
994
|
+
* @param {boolean} enabled Whether swipe navigation should be enabled.
|
|
995
|
+
* @returns {Promise<void>} A Promise that resolves when the swipe navigation setting has been updated.
|
|
996
|
+
*/
|
|
997
|
+
async setSwipeNavigation(enabled) {
|
|
998
|
+
const res = await this.request("setSwipeNav", `setSwipeNav-reply-${this.id}`, String(enabled));
|
|
999
|
+
this.emit("swipe-navigation-updated", enabled);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Checks if swipe navigation is enabled for the window. Returns a Promise that resolves to true if swipe navigation is enabled, or false if it is disabled. Emits an "is-swipe-navigation-enabled-checked" event with the result as data when done.
|
|
1004
|
+
* @returns {Promise<boolean>} True if swipe navigation is enabled, false otherwise.
|
|
1005
|
+
*/
|
|
1006
|
+
async isSwipeNavigationEnabled() {
|
|
1007
|
+
const res = await this.request("isSwipeNavEnabled", `isSwipeNavEnabled-reply-${this.id}`);
|
|
1008
|
+
return res?.enabled === "true";
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Adds content blocker rules to the window. The rules can be provided as a JSON object, loaded from a URL that returns JSON, or loaded from a local file.
|
|
1013
|
+
* Once the rules are added, the window will block content according to the specified rules. Emits a "content-blocker-updated" event with the new rules as data when done.
|
|
1014
|
+
* @param {Object} config The configuration for adding content blocker rules.
|
|
1015
|
+
* @param {Object[]} [config.json] An array of content blocker rules as JSON objects. Each rule should follow the format specified by the native layer's content blocking implementation.
|
|
1016
|
+
* @param {string} [config.url] A URL that returns a JSON array of content blocker rules. If provided, the rules will be loaded from this URL instead of using the `json` property.
|
|
1017
|
+
* @param {string} [config.file] A local file path containing the content blocker rules in JSON format. If provided, the rules will be loaded from this file instead of using the `json` or `url` properties.
|
|
1018
|
+
* @param {boolean} [config.reload] Whether to reload the window after adding the content blocker rules. Reloading may be necessary for the new rules to take effect immediately, but it can also be set to false if you want to add rules without interrupting the user's current session.
|
|
1019
|
+
* @param {boolean} [config.clearExisting] Whether to clear existing content blocker rules before adding the new ones. If false, the new rules will be added alongside any existing rules. If true, all existing rules will be removed before adding the new ones.
|
|
1020
|
+
* @platform macOS only
|
|
1021
|
+
* @see https://webkit.org/blog/3476/content-blockers-first-look/
|
|
1022
|
+
*/
|
|
1023
|
+
async addToContentBlocker(config={ json:[], url:"", file:"", reload:true, clearExisting: false }) {
|
|
1024
|
+
|
|
1025
|
+
if(process.platform !== "darwin") return;
|
|
1026
|
+
|
|
1027
|
+
let json = config.json || [];
|
|
1028
|
+
|
|
1029
|
+
if(config.file) {
|
|
1030
|
+
json = config.file
|
|
1031
|
+
} else {
|
|
1032
|
+
if(config.url) {
|
|
1033
|
+
let req = (await fetch(config.url));
|
|
1034
|
+
let _json = await req.json();
|
|
1035
|
+
|
|
1036
|
+
if(json.length) {
|
|
1037
|
+
json = json.concat(_json);
|
|
1038
|
+
} else {
|
|
1039
|
+
json = _json;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
json = JSON.stringify(json);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
const res = await this.request("addToContentBlocker", `addToContentBlocker-reply-${this.id}`, json, config.reload, config.clearExisting);
|
|
1048
|
+
this.emit("content-blocker-updated", json);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
|
|
936
1053
|
}
|
|
937
1054
|
|
|
938
1055
|
const app = {
|
|
@@ -963,7 +1080,7 @@ const app = {
|
|
|
963
1080
|
},
|
|
964
1081
|
|
|
965
1082
|
/**
|
|
966
|
-
* Adds an event listener for application-level events.
|
|
1083
|
+
* Adds an event listener for application-level events.
|
|
967
1084
|
* @param {string} event The name of the event to listen for.
|
|
968
1085
|
* @param {Function} listener The callback function to invoke when the event is emitted.
|
|
969
1086
|
*/
|
|
@@ -989,11 +1106,19 @@ const app = {
|
|
|
989
1106
|
this.events.once(event, listener);
|
|
990
1107
|
},
|
|
991
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Gets the currently focused window. Returns a Promise that resolves to the focused Window instance, or null if no windows are currently focused. Emits a "focused-window-retrieved" event with the focused window as data when done.
|
|
1111
|
+
* @returns {Promise<Window|null>} The currently focused window, or null if no windows are focused.
|
|
1112
|
+
*/
|
|
992
1113
|
async getFocusedWindow() {
|
|
993
1114
|
const results = await Promise.all([...activeWindows].map(win => win.isFocused().then(isFocused => ({ win, isFocused }))));
|
|
994
1115
|
return results.find(({ isFocused }) => isFocused)?.win || null;
|
|
995
1116
|
},
|
|
996
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Sets the application name, which is used for things like the user data directory and may be used by the native layer for other purposes.
|
|
1120
|
+
* @param {*} name The new name for the application. This will be converted to a string before being used.
|
|
1121
|
+
*/
|
|
997
1122
|
setName(name) {
|
|
998
1123
|
process.env.POSITRON_APP_NAME = name;
|
|
999
1124
|
this.events.emit("name-updated", name);
|
|
@@ -1005,7 +1130,12 @@ const app = {
|
|
|
1005
1130
|
},
|
|
1006
1131
|
|
|
1007
1132
|
userData: {
|
|
1008
|
-
|
|
1133
|
+
/**
|
|
1134
|
+
* Gets the path to the user data directory for the application. The path is determined based on the operating system and the application name. If the directory does not exist, it will be created. Emits a "user-data-path-retrieved" event with the path as data when done.
|
|
1135
|
+
* @param {string} [append] An optional string to append to the user data path. This can be used to create subdirectories within the user data directory for organizing different types of data. If provided, the appended path will also be created if it does not exist.
|
|
1136
|
+
* @returns {string} The path to the user data directory.
|
|
1137
|
+
*/
|
|
1138
|
+
getPath(append) {
|
|
1009
1139
|
let userPath = null;
|
|
1010
1140
|
|
|
1011
1141
|
if (process.platform === "win32") {
|
|
@@ -1030,9 +1160,12 @@ userData: {
|
|
|
1030
1160
|
fs.mkdirSync(userPath, { recursive: true });
|
|
1031
1161
|
}
|
|
1032
1162
|
|
|
1033
|
-
return userPath;
|
|
1163
|
+
return append ? path.join(userPath, append) : userPath;
|
|
1034
1164
|
},
|
|
1035
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* Creates the user data directory if it does not already exist. Emits a "user-data-created" event when the directory is created successfully.
|
|
1168
|
+
*/
|
|
1036
1169
|
create() {
|
|
1037
1170
|
const userPath = this.getPath();
|
|
1038
1171
|
|
|
@@ -1042,6 +1175,10 @@ userData: {
|
|
|
1042
1175
|
}
|
|
1043
1176
|
},
|
|
1044
1177
|
|
|
1178
|
+
/**
|
|
1179
|
+
* Deletes the user data directory and all of its contents. Use with caution, as this will permanently remove all user data for the application. Emits a "user-data-deleted" event when the directory is deleted successfully.
|
|
1180
|
+
*/
|
|
1181
|
+
|
|
1045
1182
|
delete() {
|
|
1046
1183
|
const userPath = this.getPath();
|
|
1047
1184
|
|