positron.js 1.0.2 → 1.0.3

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
 
@@ -169,6 +169,26 @@ struct IPCResponse: Codable {
169
169
  let data: [String: String]
170
170
  }
171
171
 
172
+ func GetIPCClient() -> IPCClient {
173
+ return AppDelegate.shared?.ipcClient ?? IPCClient()
174
+ }
175
+
176
+ func GetWebView(windowId: Int) -> WKWebView? {
177
+ guard let window = windows[windowId],
178
+ let webView = window.contentView as? WKWebView else {
179
+ printError("GetWebView failed: no webview found for window \(windowId)")
180
+ return nil
181
+ }
182
+ return webView
183
+ }
184
+
185
+ func GetWindow(windowId: Int) -> NSWindow? {
186
+ guard let window = windows[windowId] else {
187
+ printError("GetWindow failed: no window found with ID \(windowId)")
188
+ return nil
189
+ }
190
+ return window
191
+ }
172
192
 
173
193
  // MARK: - Command Handler
174
194
 
@@ -272,6 +292,19 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
272
292
 
273
293
  window.performClose(nil)
274
294
 
295
+ case "setSwipeNav":
296
+ guard let window = windows[windowId],
297
+ let webView = window.contentView as? WKWebView else {
298
+ return
299
+ }
300
+
301
+ let enable = args.first?.lowercased() != "false"
302
+ webView.allowsBackForwardNavigationGestures = enable
303
+
304
+ GetIPCClient().send(
305
+ IPCResponse(windowId: windowId, event: "setSwipeNav-reply-\(windowId)", data: ["enabled": enable ? "true" : "false"])
306
+ )
307
+
275
308
  case "forceCloseWindow":
276
309
  guard let window = windows[windowId] else { return }
277
310
 
@@ -606,6 +639,24 @@ UNUserNotificationCenter.current().requestAuthorization(
606
639
  }
607
640
  }
608
641
 
642
+ case "confirm":
643
+ guard let window = windows[windowId] else { return }
644
+ guard let message = args.first else {
645
+ printError("confirm — missing message argument")
646
+ return
647
+ }
648
+ let alert = NSAlert()
649
+ alert.messageText = message
650
+ alert.addButton(withTitle: "OK")
651
+ alert.addButton(withTitle: "Cancel")
652
+
653
+ alert.beginSheetModal(for: window) { response in
654
+ let confirmed = (response == .alertFirstButtonReturn)
655
+ AppDelegate.shared?.ipcClient.send(
656
+ IPCResponse(windowId: windowId, event: "confirm-reply-\(windowId)", data: ["confirmed": confirmed ? "true" : "false"])
657
+ )
658
+ }
659
+
609
660
  case "isFocused":
610
661
  guard let window = windows[windowId] else { return }
611
662
  let isFocused = window.isKeyWindow
package/core/win/main.cs CHANGED
@@ -382,6 +382,23 @@ private void StartNodeProcess(string workingDirectory)
382
382
 
383
383
  // 3. Attach it to the layout
384
384
  layout.ContextMenu = contextMenu;
385
+ break;
386
+
387
+ case "setSwipeNav":
388
+ if (!WindowsMap.TryGetValue(windowId, out var winSwipeNav)) break;
389
+ var wvSwipeNav = GetWebView(windowId);
390
+ if (wvSwipeNav != null) {
391
+ bool enable = args.Count == 0 || args[0].ToLower() != "false";
392
+ wvSwipeNav.CoreWebView2.Settings.IsSwipeNavigationEnabled = enable;
393
+ }
394
+
395
+ GetIPCClient().Send(new IPCResponse
396
+ {
397
+ windowId = windowId,
398
+ @event = "setSwipeNav-reply-" + windowId,
399
+ data = new() { { "enabled", (wvSwipeNav?.CoreWebView2.Settings.IsSwipeNavigationEnabled ?? false).ToString().ToLower() } }
400
+ });
401
+
385
402
  break;
386
403
 
387
404
  case "closeWindow":
@@ -621,6 +638,24 @@ case "setBounds":
621
638
  }
622
639
  break;
623
640
 
641
+ case "confirm":
642
+ if (args.Count < 1)
643
+ {
644
+ error("confirm — expected message argument");
645
+ break;
646
+ }
647
+ {
648
+ var message = args[0];
649
+ var result = MessageBox.Show(message, "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
650
+ _ipcClient.Send(new IPCResponse
651
+ {
652
+ windowId = windowId,
653
+ @event = "confirm-reply-" + windowId,
654
+ data = new() { { "confirmed", result.ToString().ToLower() } }
655
+ });
656
+ }
657
+ break;
658
+
624
659
  case "emitToRenderer":
625
660
  if (args.Count < 2)
626
661
  {
@@ -841,6 +876,15 @@ case "setBounds":
841
876
  return null;
842
877
  }
843
878
 
879
+ public static Window? GetWindow(int windowId)
880
+ {
881
+ if (WindowsMap.TryGetValue(windowId, out var window))
882
+ return window;
883
+ return null;
884
+ }
885
+
886
+ public static IPCClient GetIPCClient() => _ipcClient;
887
+
844
888
  // MARK: - Menu Management
845
889
 
846
890
  private static void BuildAndAttachMenu(int windowId, string jsonStr)
package/index.js CHANGED
@@ -627,7 +627,7 @@ if (timeoutArg) {
627
627
 
628
628
  args = args.filter(arg => arg !== timeoutArg);
629
629
 
630
- timeoutDuration = parseInt(timeoutArg.split("=")[1], 10);
630
+ timeoutDuration = parseInt(timeoutArg.split("=")[1]);
631
631
  }
632
632
 
633
633
  timeout = setTimeout(() => {
@@ -765,7 +765,7 @@ async getBounds() {
765
765
  * @returns {Promise<string>} The current URL loaded in the window.
766
766
  */
767
767
  async getURL() {
768
- return await this.request("getURL", `getURL-reply-${this.id}`);
768
+ return (await this.request("getURL", `getURL-reply-${this.id}`))?.url || "";
769
769
  }
770
770
 
771
771
  /**
@@ -773,7 +773,7 @@ async getURL() {
773
773
  * @returns {Promise<string>} The current title of the window.
774
774
  */
775
775
  async getTitle() {
776
- return await this.request("getTitle", `getTitle-reply-${this.id}`);
776
+ return (await this.request("getTitle", `getTitle-reply-${this.id}`)).title || "";
777
777
  }
778
778
 
779
779
  /**
@@ -801,7 +801,7 @@ setTitlebarTransparent(isTransparent) {
801
801
  */
802
802
  async evaluateJavaScript(script) {
803
803
  const res = await this.request("evaluateJS", `evaluateJS-reply-${this.id}`, script);
804
- return res;
804
+ return res.result;
805
805
  }
806
806
 
807
807
  /**
@@ -809,8 +809,7 @@ return res;
809
809
  * @returns {Promise<string>} The user agent string of the window.
810
810
  */
811
811
  async getUserAgent() {
812
- const res = await this.evaluateJavaScript("navigator.userAgent");
813
- return res;
812
+ return await this.evaluateJavaScript("navigator.userAgent");
814
813
  }
815
814
 
816
815
  /**
@@ -822,10 +821,14 @@ async getUserAgent() {
822
821
  async setStyleOf(selector, style) {
823
822
  const styleString = Object.entries(style).map(([key, value]) => `${key}: ${value};`).join(" ");
824
823
  const script = `
824
+ (function() {
825
825
  const elements = document.querySelectorAll(${JSON.stringify(selector)});
826
826
  elements.forEach(el => {
827
- el.style.cssText += ${JSON.stringify(styleString)};
827
+ Object.entries(${JSON.stringify(style)}).forEach(([key, value]) => {
828
+ el.style[key] = value;
829
+ });
828
830
  });
831
+ })();
829
832
  `;
830
833
  await this.evaluateJavaScript(script);
831
834
  this.emit("style-updated", { selector, style });
@@ -840,10 +843,12 @@ async setStyleOf(selector, style) {
840
843
  */
841
844
  async setAttributeOf(selector, attribute, value) {
842
845
  const script = `
846
+ (function() {
843
847
  const elements = document.querySelectorAll(${JSON.stringify(selector)});
844
848
  elements.forEach(el => {
845
849
  el.setAttribute(${JSON.stringify(attribute)}, ${JSON.stringify(value)});
846
850
  });
851
+ })();
847
852
  `;
848
853
  await this.evaluateJavaScript(script);
849
854
  this.emit("attribute-updated", { selector, attribute, value });
@@ -857,10 +862,12 @@ async setAttributeOf(selector, attribute, value) {
857
862
  */
858
863
  async removeAttributeOf(selector, attribute) {
859
864
  const script = `
860
- const elements = document.querySelectorAll(${JSON.stringify(selector)});
861
- elements.forEach(el => {
862
- el.removeAttribute(${JSON.stringify(attribute)});
863
- });
865
+ (function() {
866
+ const elements = document.querySelectorAll(${JSON.stringify(selector)});
867
+ elements.forEach(el => {
868
+ el.removeAttribute(${JSON.stringify(attribute)});
869
+ });
870
+ })();
864
871
  `;
865
872
  await this.evaluateJavaScript(script);
866
873
  this.emit("attribute-removed", { selector, attribute });
@@ -875,12 +882,14 @@ async removeAttributeOf(selector, attribute) {
875
882
  async removeStyleOf(selector, styleProperties) {
876
883
  const propertiesString = styleProperties.map(prop => `${prop}:`).join("|");
877
884
  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
- });
885
+ (function() {
886
+ const elements = document.querySelectorAll(${JSON.stringify(selector)});
887
+ elements.forEach(el => {
888
+ el.style.cssText = el.style.cssText.split(";").filter(rule => {
889
+ return !${JSON.stringify(propertiesString)}.includes(rule.trim().split(":")[0] + ":");
890
+ }).join(";");
891
+ });
892
+ })();
884
893
  `;
885
894
  await this.evaluateJavaScript(script);
886
895
  this.emit("style-removed", { selector, styleProperties });
@@ -896,6 +905,7 @@ async removeStyleOf(selector, styleProperties) {
896
905
  */
897
906
  async onClick(selector, channel, { replace = true } = {}) {
898
907
  const script = `
908
+ (function() {
899
909
  const selector = ${JSON.stringify(selector)};
900
910
  const channel = ${JSON.stringify(channel)};
901
911
  const replace = ${replace};
@@ -913,6 +923,7 @@ async onClick(selector, channel, { replace = true } = {}) {
913
923
  });
914
924
  }
915
925
  })
926
+ })();
916
927
  `;
917
928
 
918
929
  await this.evaluateJavaScript(script);
@@ -933,6 +944,27 @@ async removeOnClick(selector) {
933
944
  await this.evaluateJavaScript(script);
934
945
  }
935
946
 
947
+ /**
948
+ * 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.
949
+ * @param {string} message The message to display in the confirmation dialog.
950
+ * @returns {Promise<boolean>} True if the user confirmed, false if the user cancelled.
951
+ */
952
+ async confirm(message) {
953
+ const res = await this.request("confirm", `confirm-reply-${this.id}`, message);
954
+ this.emit("confirm", message);
955
+ return res?.confirmed === "true";
956
+ }
957
+
958
+ /**
959
+ * 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.
960
+ * @param {boolean} enabled Whether swipe navigation should be enabled.
961
+ * @returns {Promise<void>} A Promise that resolves when the swipe navigation setting has been updated.
962
+ */
963
+ async setSwipeNavigation(enabled) {
964
+ const res = await this.request("setSwipeNav", `setSwipeNav-reply-${this.id}`, String(enabled));
965
+ this.emit("swipe-navigation-updated", enabled);
966
+ }
967
+
936
968
  }
937
969
 
938
970
  const app = {
@@ -963,7 +995,7 @@ const app = {
963
995
  },
964
996
 
965
997
  /**
966
- * Adds an event listener for application-level events. Supported events include "before-quit" and "quit".
998
+ * Adds an event listener for application-level events.
967
999
  * @param {string} event The name of the event to listen for.
968
1000
  * @param {Function} listener The callback function to invoke when the event is emitted.
969
1001
  */
@@ -989,11 +1021,19 @@ const app = {
989
1021
  this.events.once(event, listener);
990
1022
  },
991
1023
 
1024
+ /**
1025
+ * 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.
1026
+ * @returns {Promise<Window|null>} The currently focused window, or null if no windows are focused.
1027
+ */
992
1028
  async getFocusedWindow() {
993
1029
  const results = await Promise.all([...activeWindows].map(win => win.isFocused().then(isFocused => ({ win, isFocused }))));
994
1030
  return results.find(({ isFocused }) => isFocused)?.win || null;
995
1031
  },
996
1032
 
1033
+ /**
1034
+ * 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.
1035
+ * @param {*} name The new name for the application. This will be converted to a string before being used.
1036
+ */
997
1037
  setName(name) {
998
1038
  process.env.POSITRON_APP_NAME = name;
999
1039
  this.events.emit("name-updated", name);
@@ -1005,6 +1045,10 @@ const app = {
1005
1045
  },
1006
1046
 
1007
1047
  userData: {
1048
+ /**
1049
+ * 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.
1050
+ * @returns {string} The path to the user data directory.
1051
+ */
1008
1052
  getPath() {
1009
1053
  let userPath = null;
1010
1054
 
@@ -1033,6 +1077,9 @@ userData: {
1033
1077
  return userPath;
1034
1078
  },
1035
1079
 
1080
+ /**
1081
+ * Creates the user data directory if it does not already exist. Emits a "user-data-created" event when the directory is created successfully.
1082
+ */
1036
1083
  create() {
1037
1084
  const userPath = this.getPath();
1038
1085
 
@@ -1042,6 +1089,10 @@ userData: {
1042
1089
  }
1043
1090
  },
1044
1091
 
1092
+ /**
1093
+ * 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.
1094
+ */
1095
+
1045
1096
  delete() {
1046
1097
  const userPath = this.getPath();
1047
1098
 
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.3",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"