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 CHANGED
@@ -35,6 +35,6 @@ switch (command) {
35
35
  break;
36
36
 
37
37
  default:
38
- console.log("Usage: npx positron [build | dev | run]");
38
+ console.log("Usage: npx positron [build | dev | run | package]");
39
39
  process.exit(0);
40
40
  }
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
- const iconPathEscaped = path.join(__dirname, "positronicon.png").replace(/"/g, '\\"');
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
- const swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: [])`;
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
  }
@@ -141,13 +141,9 @@ func getBuiltInHandlers() -> [String: (Int, [String]) -> Void] {
141
141
  return
142
142
  }
143
143
 
144
- let selector = Selector(("_showDeveloperTools:"))
145
- if webView.responds(to: selector) {
146
- webView.perform(selector, with: nil)
147
- } else {
148
- printError("openDevTools failed: _showDeveloperTools: selector not found on WKWebView (windowId \(windowId))")
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
- sendIpc(channel, args = []) {
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], 10);
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
- const res = await this.evaluateJavaScript("navigator.userAgent");
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
- el.style.cssText += ${JSON.stringify(styleString)};
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
- const elements = document.querySelectorAll(${JSON.stringify(selector)});
861
- elements.forEach(el => {
862
- el.removeAttribute(${JSON.stringify(attribute)});
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
- const elements = document.querySelectorAll(${JSON.stringify(selector)});
879
- elements.forEach(el => {
880
- el.style.cssText = el.style.cssText.split(";").filter(rule => {
881
- return !${JSON.stringify(propertiesString)}.includes(rule.trim().split(":")[0] + ":");
882
- }).join(";");
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. Supported events include "before-quit" and "quit".
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
- getPath() {
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
 
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.2",
8
+ "version": "1.0.4",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"