positron.js 1.0.3 → 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/builder.js +11 -2
- package/core/mac/main.swift +115 -6
- package/core/win/main.cs +35 -1
- package/index.js +94 -8
- package/package.json +1 -1
package/builder.js
CHANGED
|
@@ -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()
|
|
@@ -292,6 +288,13 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
|
|
|
292
288
|
|
|
293
289
|
window.performClose(nil)
|
|
294
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
|
+
|
|
295
298
|
case "setSwipeNav":
|
|
296
299
|
guard let window = windows[windowId],
|
|
297
300
|
let webView = window.contentView as? WKWebView else {
|
|
@@ -521,6 +524,97 @@ UNUserNotificationCenter.current().requestAuthorization(
|
|
|
521
524
|
printError("executeAppleScript failed: \(errorMessage)")
|
|
522
525
|
}
|
|
523
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
|
+
|
|
524
618
|
case "forward":
|
|
525
619
|
guard let window = windows[windowId] else { return }
|
|
526
620
|
(window.contentView as? WKWebView)?.goForward()
|
|
@@ -1067,6 +1161,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
1067
1161
|
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
1068
1162
|
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
1069
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
|
+
|
|
1070
1173
|
NSApp.mainMenu = mainMenu
|
|
1071
1174
|
}
|
|
1072
1175
|
}
|
|
@@ -1145,6 +1248,12 @@ func buildMenu(from descriptor: [[String: Any]], windowId: Int) -> NSMenu {
|
|
|
1145
1248
|
let sub = NSMenu(title: topItem.title)
|
|
1146
1249
|
topItem.submenu = sub
|
|
1147
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
|
+
|
|
1148
1257
|
if let items = topLevel["items"] as? [[String: Any]] {
|
|
1149
1258
|
populateMenu(sub, with: items, windowId: windowId, isContextMenu: false)
|
|
1150
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; }
|
|
@@ -399,6 +398,41 @@ private void StartNodeProcess(string workingDirectory)
|
|
|
399
398
|
data = new() { { "enabled", (wvSwipeNav?.CoreWebView2.Settings.IsSwipeNavigationEnabled ?? false).ToString().ToLower() } }
|
|
400
399
|
});
|
|
401
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
|
+
});
|
|
402
436
|
break;
|
|
403
437
|
|
|
404
438
|
case "closeWindow":
|
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
|
*/
|
|
@@ -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";
|
|
@@ -965,6 +999,57 @@ const res = await this.request("setSwipeNav", `setSwipeNav-reply-${this.id}`, St
|
|
|
965
999
|
this.emit("swipe-navigation-updated", enabled);
|
|
966
1000
|
}
|
|
967
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
|
+
|
|
968
1053
|
}
|
|
969
1054
|
|
|
970
1055
|
const app = {
|
|
@@ -1047,9 +1132,10 @@ const app = {
|
|
|
1047
1132
|
userData: {
|
|
1048
1133
|
/**
|
|
1049
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.
|
|
1050
1136
|
* @returns {string} The path to the user data directory.
|
|
1051
1137
|
*/
|
|
1052
|
-
getPath() {
|
|
1138
|
+
getPath(append) {
|
|
1053
1139
|
let userPath = null;
|
|
1054
1140
|
|
|
1055
1141
|
if (process.platform === "win32") {
|
|
@@ -1074,7 +1160,7 @@ userData: {
|
|
|
1074
1160
|
fs.mkdirSync(userPath, { recursive: true });
|
|
1075
1161
|
}
|
|
1076
1162
|
|
|
1077
|
-
return userPath;
|
|
1163
|
+
return append ? path.join(userPath, append) : userPath;
|
|
1078
1164
|
},
|
|
1079
1165
|
|
|
1080
1166
|
/**
|