positron.js 1.0.5 → 1.0.6

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 CHANGED
@@ -91,7 +91,17 @@ function performNativeBuild() {
91
91
  }
92
92
 
93
93
  if (buildingForMac) {
94
- info(`[Builder] Stitching ${nativeExtensionsMac.length} native extensions...`);
94
+
95
+ const coreMacDir = path.join(__dirname, "core", "mac");
96
+
97
+ nativeExtensionsMac.push({
98
+ command:"createTray",
99
+ className:"TrayExtension",
100
+ sourceFile:path.join(coreMacDir, "tray.swift")
101
+ });
102
+
103
+ // -1 to account for the built-in tray extension
104
+ info(`[Builder] Stitching ${nativeExtensionsMac.length-1} native extensions...`);
95
105
 
96
106
  let registryContent = `// Auto-generated by Positron. Do not edit.\n`;
97
107
  registryContent += `func getExtensionRegistry() -> [String: (Int, [String]) -> Void] {\n`;
@@ -112,7 +122,6 @@ function performNativeBuild() {
112
122
 
113
123
  registryContent += `}\n`;
114
124
 
115
- const coreMacDir = path.join(__dirname, "core", "mac");
116
125
  fs.writeFileSync(path.join(coreMacDir, "Registry.swift"), registryContent);
117
126
 
118
127
  info("[Builder] Compiling native binary...");
@@ -189,6 +198,7 @@ function performNativeBuild() {
189
198
  const coreWinDir = path.join(__dirname, "core", "win");
190
199
  const extensionsDir = path.join(coreWinDir, "extensions");
191
200
 
201
+
192
202
  // 1. Clean and prepare a staging folder for all native extensions
193
203
  if (fs.existsSync(extensionsDir)) fs.rmSync(extensionsDir, { recursive: true, force: true });
194
204
  fs.mkdirSync(extensionsDir, { recursive: true });
@@ -832,6 +832,12 @@ case "addToContentBlocker":
832
832
  IPCResponse(windowId: windowId, event: args.last ?? "isFocused-reply-\(windowId)", data: ["isFocused": isFocused ? "true" : "false"])
833
833
  )
834
834
 
835
+ case "getFocusedWindowId":
836
+ let focusedWindowId = windows.first(where: { $0.value.isKeyWindow })?.key ?? -1
837
+ AppDelegate.shared?.ipcClient.send(
838
+ IPCResponse(windowId: windowId, event: args.last ?? "getFocusedWindowId-reply-\(windowId)", data: ["focusedWindowId": "\(focusedWindowId)"])
839
+ )
840
+
835
841
  case "emitToRenderer":
836
842
  guard let window = windows[windowId] else { return }
837
843
  guard args.count >= 2 else {
@@ -1005,7 +1011,7 @@ func makePreloadScript(windowId: Int) -> String {
1005
1011
  /** Called internally by Swift's evaluateJS to deliver a push message. */
1006
1012
  _emit(channel, payload) {
1007
1013
  (_listeners[channel] || []).forEach(fn => {
1008
- try { fn(payload); } catch(e) { console.printError('[ipc] listener error:', e); }
1014
+ try { fn(payload); } catch(e) { console.error('[ipc] listener error:', e); }
1009
1015
  });
1010
1016
  },
1011
1017
 
@@ -0,0 +1,95 @@
1
+ import Cocoa
2
+
3
+ class TrayManager {
4
+ static let shared = TrayManager()
5
+ var statusItem: NSStatusItem?
6
+
7
+ func setupTray() {
8
+ if statusItem == nil {
9
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
10
+ if let button = statusItem?.button {
11
+ button.title = "App"
12
+ }
13
+ }
14
+ }
15
+
16
+ func setMenu(_ menu: NSMenu) {
17
+ statusItem?.menu = menu
18
+ }
19
+
20
+ func setTitle(_ title: String) {
21
+ statusItem?.button?.title = title
22
+ }
23
+
24
+ func setIcon(_ iconPath: String) {
25
+ guard let button = statusItem?.button else { return printError("Tray button not initialized") }
26
+
27
+ if iconPath.isEmpty {
28
+ DispatchQueue.main.async {
29
+ button.image = nil
30
+ button.imagePosition = .imageLeft
31
+ }
32
+ return
33
+ }
34
+
35
+ guard FileManager.default.fileExists(atPath: iconPath) else {
36
+ printError("Icon path does not exist: \(iconPath)")
37
+ return
38
+ }
39
+
40
+ DispatchQueue.main.async {
41
+ if let img = NSImage(contentsOfFile: iconPath) {
42
+ img.size = NSSize(width: 18, height: 18)
43
+ img.isTemplate = true
44
+ button.image = img
45
+ button.imagePosition = .imageLeft
46
+ } else {
47
+ button.image = nil
48
+ button.imagePosition = .imageLeft
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ public struct TrayExtension {
55
+ public static func handle(windowId: Int, args: [String]) {
56
+
57
+ if(args.last == "setTitle") {
58
+ TrayManager.shared.setTitle(args[0])
59
+ return
60
+ }
61
+
62
+ if(args.last == "setIcon") {
63
+ TrayManager.shared.setIcon(args[0])
64
+ return
65
+ }
66
+
67
+ guard let descString = args.first,
68
+ let data = descString.data(using: .utf8),
69
+ let desc = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
70
+ printError("tray setMenu — invalid JSON descriptor")
71
+ return
72
+ }
73
+
74
+ if args.last == "setMenu" {
75
+ TrayManager.shared.setMenu(buildContextMenu(from: desc, windowId: windowId))
76
+ return
77
+ }
78
+
79
+ DispatchQueue.main.async {
80
+
81
+ TrayManager.shared.setupTray()
82
+
83
+ let title = args.count > 1 ? args[1] : ""
84
+ TrayManager.shared.setTitle(title)
85
+
86
+ let imagePath = args.count > 2 ? args[2] : nil
87
+ if let imagePath = imagePath {
88
+ TrayManager.shared.setIcon(imagePath)
89
+ }
90
+
91
+ let menu = buildContextMenu(from: desc, windowId: windowId)
92
+ TrayManager.shared.setMenu(menu)
93
+ }
94
+ }
95
+ }
package/core/win/main.cs CHANGED
@@ -18,6 +18,7 @@ using System.Net.Sockets;
18
18
  using Microsoft.VisualBasic;
19
19
  using System.Runtime.InteropServices;
20
20
  using Microsoft.Win32;
21
+ using System.Linq;
21
22
 
22
23
  class PowerSaveBlocker
23
24
  {
@@ -722,6 +723,16 @@ case "forceCloseWindow":
722
723
  });
723
724
  break;
724
725
 
726
+ case "getFocusedWindowId":
727
+ int focusedWindowId = WindowsMap.FirstOrDefault(kv => kv.Value.IsActive).Key;
728
+ _ipcClient.Send(new IPCResponse
729
+ {
730
+ windowId = windowId,
731
+ @event = args[^1] ?? "getFocusedWindowId-reply-" + windowId,
732
+ data = new() { { "focusedWindowId", focusedWindowId.ToString() } }
733
+ });
734
+ break;
735
+
725
736
  case "showNotification":
726
737
  if (args.Count < 2)
727
738
  {
@@ -1094,7 +1105,7 @@ case "setBounds":
1094
1105
  MenuMap[windowId] = menu;
1095
1106
  }
1096
1107
 
1097
- private static void PopulateMenu(ItemCollection parentItems, JsonArray items, int windowId, string eventType = "menu-action")
1108
+ internal static void PopulateMenu(ItemCollection parentItems, JsonArray items, int windowId, string eventType = "menu-action")
1098
1109
  {
1099
1110
  foreach (var item in items)
1100
1111
  {
@@ -0,0 +1,142 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Windows.Controls;
4
+ using System.Text.Json;
5
+ using System.Text.Json.Nodes;
6
+ using System.Windows;
7
+ using System.Drawing;
8
+
9
+ namespace PositronWindows
10
+ {
11
+ public class TrayManager
12
+ {
13
+ private static TrayManager? _shared;
14
+ public static TrayManager Shared => _shared ??= new TrayManager();
15
+
16
+ public System.Windows.Forms.NotifyIcon? NotifyIcon { get; private set; }
17
+ private ContextMenu? _wpfContextMenu;
18
+
19
+ public void SetupTray()
20
+ {
21
+ if (NotifyIcon == null)
22
+ {
23
+ NotifyIcon = new System.Windows.Forms.NotifyIcon();
24
+ NotifyIcon.Visible = true;
25
+ NotifyIcon.Text = "App";
26
+ NotifyIcon.Icon = SystemIcons.Application;
27
+
28
+ NotifyIcon.MouseUp += (s, e) =>
29
+ {
30
+ if (e.Button == System.Windows.Forms.MouseButtons.Right || e.Button == System.Windows.Forms.MouseButtons.Left)
31
+ {
32
+ if (_wpfContextMenu != null)
33
+ {
34
+ _wpfContextMenu.IsOpen = true;
35
+ if (Application.Current.MainWindow != null)
36
+ {
37
+ Application.Current.MainWindow.Activate();
38
+ }
39
+ }
40
+ }
41
+ };
42
+ }
43
+ }
44
+
45
+ public void SetMenu(ContextMenu menu)
46
+ {
47
+ _wpfContextMenu = menu;
48
+ }
49
+
50
+ public void SetTitle(string title)
51
+ {
52
+ if (NotifyIcon != null && !string.IsNullOrEmpty(title))
53
+ {
54
+ NotifyIcon.Text = title.Length > 63 ? title.Substring(0, 63) : title;
55
+ }
56
+ }
57
+
58
+ public void SetIcon(string iconPath)
59
+ {
60
+ if (NotifyIcon != null && !string.IsNullOrEmpty(iconPath))
61
+ {
62
+ try
63
+ {
64
+ NotifyIcon.Icon = new Icon(iconPath);
65
+ }
66
+ catch (Exception ex)
67
+ {
68
+ Console.WriteLine($"Failed to set tray icon: {ex.Message}");
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ public static class TrayExtension
75
+ {
76
+ public static void Handle(int windowId, List<string> args)
77
+ {
78
+ if (args.Count == 0)
79
+ {
80
+ Console.WriteLine("tray:setMenu — missing JSON descriptor");
81
+ return;
82
+ }
83
+
84
+ if (args[^1] == "setTitle")
85
+ {
86
+ string title = args[0];
87
+ Application.Current.Dispatcher.Invoke(() =>
88
+ {
89
+ TrayManager.Shared.SetTitle(title);
90
+ });
91
+ return;
92
+ }
93
+
94
+ if (args[^1] == "setIcon")
95
+ {
96
+ string iconPath = args[0];
97
+ Application.Current.Dispatcher.Invoke(() =>
98
+ {
99
+ TrayManager.Shared.SetIcon(iconPath);
100
+ });
101
+ return;
102
+ }
103
+
104
+ var descString = args[0];
105
+ var ctxDescriptor = JsonSerializer.Deserialize<JsonArray>(descString);
106
+ if (ctxDescriptor == null)
107
+ {
108
+ Console.WriteLine("tray:setMenu — invalid JSON descriptor");
109
+ return;
110
+ }
111
+
112
+ if(args[^1] == "setMenu")
113
+ {
114
+ Application.Current.Dispatcher.Invoke(() =>
115
+ {
116
+ var contextMenu = new ContextMenu();
117
+ App.PopulateMenu(contextMenu.Items, ctxDescriptor, windowId, "context-menu-action");
118
+ TrayManager.Shared.SetMenu(contextMenu);
119
+ });
120
+ return;
121
+ }
122
+
123
+ Application.Current.Dispatcher.Invoke(() =>
124
+ {
125
+ TrayManager.Shared.SetupTray();
126
+
127
+ string? title = args.Count > 1 ? args[1] : "";
128
+ TrayManager.Shared.SetTitle(title);
129
+
130
+ string? imagePath = args.Count > 2 ? args[2] : null;
131
+ if (imagePath != null)
132
+ {
133
+ TrayManager.Shared.SetIcon(imagePath);
134
+ }
135
+
136
+ var contextMenu = new ContextMenu();
137
+ App.PopulateMenu(contextMenu.Items, ctxDescriptor, windowId, "context-menu-action");
138
+ TrayManager.Shared.SetMenu(contextMenu);
139
+ });
140
+ }
141
+ }
142
+ }
package/index.js CHANGED
@@ -13,6 +13,7 @@ const { info, error, warn, success } = require("./logs");
13
13
 
14
14
  let currMenu = []
15
15
  let contextMenu = [];
16
+ let trayMenu = [];
16
17
 
17
18
  const randomPort = () => {
18
19
  const min = 1024;
@@ -180,7 +181,7 @@ ws.on("message", raw => {
180
181
  win.destroy();
181
182
  }
182
183
  }
183
- } else if(msg.event == "menu-action" || msg.event == "context-menu-action") {
184
+ } else if(msg.event == "menu-action" || msg.event == "context-menu-action" || msg.event == "tray-menu-action") {
184
185
 
185
186
  const findMenuAction = (items, label, channel) => {
186
187
  if (!items || items.length === 0) return null;
@@ -199,7 +200,12 @@ ws.on("message", raw => {
199
200
  return null;
200
201
  }
201
202
 
202
- const menuAction = findMenuAction((msg.event === "menu-action" ? currMenu : contextMenu), msg.data.label, msg.data.channel);
203
+ let searchMenu = msg.event === "menu-action" ? currMenu : (msg.event === "context-menu-action" ? contextMenu : trayMenu);
204
+ let menuAction = findMenuAction(searchMenu, msg.data.label, msg.data.channel);
205
+
206
+ if (!menuAction && msg.event === "context-menu-action") {
207
+ menuAction = findMenuAction(trayMenu, msg.data.label, msg.data.channel);
208
+ }
203
209
 
204
210
  if (menuAction) {
205
211
  menuAction.click();
@@ -1106,6 +1112,10 @@ async showFileOpenDialog(options = {}) {
1106
1112
  const app = {
1107
1113
 
1108
1114
  name:"PositronApp",
1115
+
1116
+ setTrayMenu(menuTemplate) {
1117
+ trayMenu = menuTemplate;
1118
+ },
1109
1119
 
1110
1120
  /**
1111
1121
  * Quits the application by sending a terminate command to the native layer and then exiting the process. Emits a "before-quit" event before sending the command, and a "quit" event after initiating the quit sequence.
@@ -1162,8 +1172,10 @@ const app = {
1162
1172
  * @returns {Promise<Window|null>} The currently focused window, or null if no windows are focused.
1163
1173
  */
1164
1174
  async getFocusedWindow() {
1165
- const results = await Promise.all([...activeWindows].map(win => win.isFocused().then(isFocused => ({ win, isFocused }))));
1166
- return results.find(({ isFocused }) => isFocused)?.win || null;
1175
+ const getFocused = await this.requestFromNative("getFocusedWindowId");
1176
+ const winId = getFocused?.focusedWindowId ? parseInt(getFocused.focusedWindowId, 10) : -1;
1177
+ const focusedWin = [...activeWindows].find(w => w.id === winId);
1178
+ return focusedWin || { error: "No focused window found" };
1167
1179
  },
1168
1180
 
1169
1181
  /**
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.5",
8
+ "version": "1.0.6",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"
package/packager.js CHANGED
@@ -33,24 +33,27 @@ function performPackager() {
33
33
 
34
34
  function handleJavaScriptPipeline(appRoot, resourcesPath) {
35
35
  const targetOutputFile = path.join(resourcesPath, "index.js");
36
+ let bundledFiles = [];
36
37
 
37
38
  info(`[Packager] Bundling JavaScript code with esbuild...`);
38
39
  try {
39
- esbuild.buildSync({
40
+ const result = esbuild.buildSync({
40
41
  entryPoints: [path.join(appRoot, "index.js")],
41
42
  bundle: true,
42
43
  platform: "node",
43
44
  target: `node${MAJOR_NODE_V}`,
44
45
  outfile: targetOutputFile,
45
- minify: ob,
46
+ minify: true,
46
47
  sourcemap: false,
48
+ metafile: true,
47
49
  });
50
+ bundledFiles = Object.keys(result.metafile.inputs).map(f => path.resolve(appRoot, f));
48
51
  } catch (err) {
49
52
  error("Fatal: esbuild bundling failed.");
50
53
  process.exit(1);
51
54
  }
52
55
 
53
- copyAppAssets(appRoot, resourcesPath);
56
+ copyAppAssets(appRoot, resourcesPath, bundledFiles);
54
57
 
55
58
  }
56
59
 
@@ -212,7 +215,7 @@ async function packageWindows(appRoot, distDir, appName) {
212
215
  success(`Successfully packaged Windows app directory at: ${outputFolder}`);
213
216
  }
214
217
 
215
- function copyAppAssets(src, dest) {
218
+ function copyAppAssets(src, dest, ignoredFiles = []) {
216
219
  const ignoreList = ["node_modules", "dist", "bin", ".git"];
217
220
 
218
221
  function copyRecursive(currentSrc, currentDest) {
@@ -221,6 +224,8 @@ function copyAppAssets(src, dest) {
221
224
  if (ignoreList.includes(item)) continue;
222
225
 
223
226
  const srcPath = path.join(currentSrc, item);
227
+ if (ignoredFiles.includes(srcPath)) continue;
228
+
224
229
  const destPath = path.join(currentDest, item);
225
230
  const stat = fs.statSync(srcPath);
226
231
 
@@ -232,7 +237,6 @@ function copyAppAssets(src, dest) {
232
237
  fs.mkdirSync(destPath, { recursive: true });
233
238
  copyRecursive(srcPath, destPath);
234
239
  } else {
235
- if (item.endsWith(".js")) continue;
236
240
  fs.copyFileSync(srcPath, destPath);
237
241
  }
238
242
  }
@@ -244,6 +248,14 @@ function copyAppAssets(src, dest) {
244
248
  const { exec } = require("@yao-pkg/pkg");
245
249
 
246
250
  async function compileWithPkg(bundledJsPath, targetPlatform, outputFolder, appName) {
251
+
252
+ if(process.argv.includes("--no-pkg")) {
253
+ const finalPath = path.join(outputFolder, "index.js");
254
+ fs.copyFileSync(bundledJsPath, finalPath);
255
+ success(`[Packager] Skipped pkg compilation. Copied bundled JavaScript to: ${finalPath}`);
256
+ return;
257
+ }
258
+
247
259
  info(`[Packager] Packaging application into a standalone binary...`);
248
260
 
249
261
  let pkgTarget = "";
package/tray.js ADDED
@@ -0,0 +1,64 @@
1
+ const { app } = require("./index");
2
+ const { Menu } = require("./menu");
3
+
4
+ let createdTray = false;
5
+
6
+ module.exports = {
7
+
8
+ create(menu, title = "", icon = "") {
9
+
10
+ if(createdTray) {
11
+ console.warn("Tray already created. Use setMenu, setTitle, or setIcon to update the existing tray.");
12
+ return;
13
+ }
14
+
15
+ createdTray = true;
16
+
17
+ if(menu instanceof Menu) {
18
+ menu = menu.template
19
+ }
20
+
21
+ const stripClick = (items) => {
22
+ if (!items) return null;
23
+ return items.map(i => {
24
+ const newItem = { ...i, click: undefined };
25
+ if (newItem.items) {
26
+ newItem.items = stripClick(newItem.items);
27
+ }
28
+ return newItem;
29
+ });
30
+ };
31
+
32
+ app.setTrayMenu(menu);
33
+ app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), title, icon]);
34
+ },
35
+
36
+ setMenu(menu) {
37
+ if(menu instanceof Menu) {
38
+ menu = menu.template
39
+ }
40
+
41
+ const stripClick = (items) => {
42
+ if (!items) return null;
43
+ return items.map(i => {
44
+ const newItem = { ...i, click: undefined };
45
+ if (newItem.items) {
46
+ newItem.items = stripClick(newItem.items);
47
+ }
48
+ return newItem;
49
+ });
50
+ };
51
+
52
+ app.setTrayMenu(menu);
53
+ app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), "setMenu"]);
54
+ },
55
+
56
+ setTitle(title) {
57
+ app.sendToNative("createTray", [title, "setTitle"]);
58
+ },
59
+
60
+ setIcon(iconPath) {
61
+ app.sendToNative("createTray", [iconPath, "setIcon"]);
62
+ }
63
+
64
+ }