positron.js 1.0.5 → 1.1.0

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/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;
@@ -36,12 +37,11 @@ const binaryPath = path.join(appRoot, "bin", binaryName);
36
37
 
37
38
  const appEvents = new Events.EventEmitter();
38
39
 
39
-
40
40
  const isPackaged = process.env.POSITRON_PACKAGED === "true";
41
41
 
42
42
  if(isPackaged) {
43
43
  if (typeof process.pkg !== 'undefined') {
44
- if (process.platform === 'darwin') {
44
+ if (process.platform === 'darwin' || process.platform === 'linux') {
45
45
  __dirname = path.join(path.dirname(process.execPath), '.');
46
46
  } else {
47
47
  __dirname = path.dirname(process.execPath);
@@ -51,11 +51,6 @@ if (typeof process.pkg !== 'undefined') {
51
51
 
52
52
  const EXPECTED_TOKEN = process.env.POSITRON_AUTH_TOKEN;
53
53
 
54
- const parseRes = (obj) => {
55
- if (Object.keys(obj) > 1) return obj;
56
-
57
- return Object.values(obj)[0];
58
- }
59
54
 
60
55
  if (!isPackaged) {
61
56
  // DEV MODE
@@ -68,6 +63,8 @@ if (!isPackaged) {
68
63
  }
69
64
  }
70
65
 
66
+ // setTimeout(() => { // FOR HIJACK TESTING
67
+
71
68
  info("Starting Positron render process...");
72
69
  const renderProcess = cp.spawn(binaryPath, {
73
70
  env: {
@@ -101,12 +98,20 @@ process.on("uncaughtException", (err) => {
101
98
  info(`[Positron] Render process exited with code ${code}`);
102
99
  process.exit(code);
103
100
  });
101
+ // }, 60000);
104
102
  } else {
105
103
  // PRODUCTION MODE
106
104
  info("[Positron] Packaged mode detected. Skipping native binary spawn.");
107
105
  }
108
106
 
109
107
  const httpServer = http.createServer((req, res) => {
108
+ const clientToken = req.headers["x-positron-auth-token"];
109
+ if (clientToken !== EXPECTED_TOKEN) {
110
+ res.writeHead(401, { 'Content-Type': 'text/plain' });
111
+ res.end('Unauthorized');
112
+ return;
113
+ }
114
+
110
115
  if (req.method === 'GET' && req.url === '/running') {
111
116
  res.writeHead(200, { 'Content-Type': 'text/plain' });
112
117
  res.end('true');
@@ -116,7 +121,25 @@ const httpServer = http.createServer((req, res) => {
116
121
  }
117
122
  });
118
123
 
119
- const _ipcWS = new WebSocket.Server({ server: httpServer });
124
+ const MAX_CONNECTIONS = 1;
125
+
126
+ const _ipcWS = new WebSocket.Server({ server: httpServer, verifyClient: (info, cb) => {
127
+
128
+ const clientToken = info.req.headers["x-positron-auth-token"];
129
+ if (clientToken !== EXPECTED_TOKEN) {
130
+ warn("[Security] Unauthorized local connection attempt rejected.");
131
+ cb(false, 401, "Unauthorized token match failure.");
132
+ return
133
+ }
134
+
135
+ if (_ipcWS.clients.size >= MAX_CONNECTIONS) {
136
+ return cb(false, 503, 'IPC client already connected. Only one client allowed at a time.');
137
+ }
138
+
139
+ cb(true);
140
+ }
141
+
142
+ });
120
143
  let activeSocket = null;
121
144
  const pendingWindows = new Set();
122
145
 
@@ -126,14 +149,6 @@ const commandQueue = [];
126
149
  let activeWindows = new Set();
127
150
 
128
151
  _ipcWS.on("connection", (ws, req) => {
129
- const clientToken = req.headers["x-positron-auth-token"];
130
-
131
- if (clientToken !== EXPECTED_TOKEN) {
132
- warn("[Security] Unauthorized local connection attempt rejected. Token:", clientToken, "Expected:", EXPECTED_TOKEN);
133
- ws.close(4001, "Unauthorized token match failure.");
134
- return;
135
- }
136
-
137
152
  activeSocket = ws;
138
153
  success("Client connected to IPC");
139
154
 
@@ -153,7 +168,7 @@ ws.on("message", raw => {
153
168
 
154
169
  if(process.env.POSITRON_LOG_IPC) console.log("Received IPC message:", msg);
155
170
 
156
- if (msg.event === "ipcMessage" || msg.event.includes("-reply-") || msg.event.includes("-result-")) {
171
+ if (msg.event === "ipcMessage" || msg.event.includes("-reply-") || msg.event.includes("-result-") || msg.event === "nativeError") {
157
172
 
158
173
  const simulatedMsg = msg.event === "ipcMessage" ? msg : {
159
174
  event: "ipcMessage",
@@ -180,7 +195,7 @@ ws.on("message", raw => {
180
195
  win.destroy();
181
196
  }
182
197
  }
183
- } else if(msg.event == "menu-action" || msg.event == "context-menu-action") {
198
+ } else if(msg.event == "menu-action" || msg.event == "context-menu-action" || msg.event == "tray-menu-action") {
184
199
 
185
200
  const findMenuAction = (items, label, channel) => {
186
201
  if (!items || items.length === 0) return null;
@@ -199,7 +214,12 @@ ws.on("message", raw => {
199
214
  return null;
200
215
  }
201
216
 
202
- const menuAction = findMenuAction((msg.event === "menu-action" ? currMenu : contextMenu), msg.data.label, msg.data.channel);
217
+ let searchMenu = msg.event === "menu-action" ? currMenu : (msg.event === "context-menu-action" ? contextMenu : trayMenu);
218
+ let menuAction = findMenuAction(searchMenu, msg.data.label, msg.data.channel);
219
+
220
+ if (!menuAction && msg.event === "context-menu-action") {
221
+ menuAction = findMenuAction(trayMenu, msg.data.label, msg.data.channel);
222
+ }
203
223
 
204
224
  if (menuAction) {
205
225
  menuAction.click();
@@ -210,7 +230,7 @@ ws.on("message", raw => {
210
230
  appEvents.emit(msg.event, msg.data);
211
231
  }
212
232
  } catch (err) {
213
- error("Failed to process incoming IPC network frame:", err);
233
+ error("Failed to process incoming IPC network frame:", err, err.stack.split('\n').slice(1).join('\n'));
214
234
  }
215
235
  });
216
236
 
@@ -237,12 +257,20 @@ class Window extends Events.EventEmitter {
237
257
  minimizable: true,
238
258
  titlebarTransparent: false,
239
259
  titlebarVisible: true
240
- }
260
+ },
261
+ linuxOptions: {
262
+ closable: true,
263
+ resizable: true,
264
+ minimizable: true,
265
+ titlebarTransparent: false,
266
+ titlebarVisible: true
267
+ },
268
+ allowEvaluateJS: false
241
269
 
242
270
  }) {
243
271
  super();
244
272
  this.id = ++_windowCounter;
245
- this.options = options;
273
+ this.options = { allowEvaluateJS: false, ...options };
246
274
  activeWindows.add(this);
247
275
 
248
276
  if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
@@ -255,7 +283,11 @@ class Window extends Events.EventEmitter {
255
283
  const height = options.height ? String(options.height) : "600";
256
284
 
257
285
  if(!this.options.skipCreate) {
258
- this.create(width, height, options.darwinOptions);
286
+ if (process.platform === "linux") {
287
+ this.create(width, height, options.linuxOptions || options.darwinOptions);
288
+ } else {
289
+ this.create(width, height, options.darwinOptions);
290
+ }
259
291
  }
260
292
 
261
293
  }
@@ -692,7 +724,8 @@ async request(command, ...args) {
692
724
  }
693
725
  });
694
726
 
695
- let timeout;
727
+ let timeout;
728
+
696
729
 
697
730
  if(!options.noTimeout) {
698
731
  let timeoutDuration = 7000;
@@ -706,7 +739,8 @@ if (options.timeout) {
706
739
  if (!settled) {
707
740
  settled = true;
708
741
  unsubscribe();
709
- reject(new Error(`Request timed out waiting for reply on channel "${replyChannel}"`));
742
+ // reject(new Error(`Request timed out waiting for reply on channel "${replyChannel}"`));
743
+ resolve({ error: `Request timed out waiting for reply on channel "${replyChannel}"` });
710
744
  }
711
745
  }, timeoutDuration);
712
746
  } else {
@@ -876,8 +910,16 @@ setTitlebarTransparent(isTransparent) {
876
910
  * @returns {Promise<*>} A Promise that resolves to the result of the evaluation.
877
911
  */
878
912
  async evaluateJavaScript(script) {
879
- const res = await this.request("evaluateJS", script);
880
- return res.result;
913
+ if (!this.options.allowEvaluateJS) {
914
+ throw new Error("evaluateJavaScript is disabled by default for security. Set allowEvaluateJS: true in window options to enable it.");
915
+ }
916
+ return await this.#evaluateJavaScriptInternal(script);
917
+ }
918
+
919
+
920
+ async #evaluateJavaScriptInternal(script) {
921
+ const res = await this.request("evaluateJS", script);
922
+ return res.result;
881
923
  }
882
924
 
883
925
  /**
@@ -885,7 +927,7 @@ return res.result;
885
927
  * @returns {Promise<string>} The user agent string of the window.
886
928
  */
887
929
  async getUserAgent() {
888
- return await this.evaluateJavaScript("navigator.userAgent");
930
+ return await this.#evaluateJavaScriptInternal("navigator.userAgent");
889
931
  }
890
932
 
891
933
  /**
@@ -906,7 +948,7 @@ async setStyleOf(selector, style) {
906
948
  });
907
949
  })();
908
950
  `;
909
- await this.evaluateJavaScript(script);
951
+ await this.#evaluateJavaScriptInternal(script);
910
952
  this.emit("style-updated", { selector, style });
911
953
  }
912
954
 
@@ -926,7 +968,7 @@ async setAttributeOf(selector, attribute, value) {
926
968
  });
927
969
  })();
928
970
  `;
929
- await this.evaluateJavaScript(script);
971
+ await this.#evaluateJavaScriptInternal(script);
930
972
  this.emit("attribute-updated", { selector, attribute, value });
931
973
  }
932
974
 
@@ -945,7 +987,7 @@ async removeAttributeOf(selector, attribute) {
945
987
  });
946
988
  })();
947
989
  `;
948
- await this.evaluateJavaScript(script);
990
+ await this.#evaluateJavaScriptInternal(script);
949
991
  this.emit("attribute-removed", { selector, attribute });
950
992
  }
951
993
 
@@ -967,7 +1009,7 @@ async removeStyleOf(selector, styleProperties) {
967
1009
  });
968
1010
  })();
969
1011
  `;
970
- await this.evaluateJavaScript(script);
1012
+ await this.#evaluateJavaScriptInternal(script);
971
1013
  this.emit("style-removed", { selector, styleProperties });
972
1014
  }
973
1015
 
@@ -1002,7 +1044,7 @@ async onClick(selector, channel, { replace = true } = {}) {
1002
1044
  })();
1003
1045
  `;
1004
1046
 
1005
- await this.evaluateJavaScript(script);
1047
+ await this.#evaluateJavaScriptInternal(script);
1006
1048
  }
1007
1049
 
1008
1050
  /**
@@ -1017,7 +1059,7 @@ async removeOnClick(selector) {
1017
1059
  el.onclick = null;
1018
1060
  });
1019
1061
  `;
1020
- await this.evaluateJavaScript(script);
1062
+ await this.#evaluateJavaScriptInternal(script);
1021
1063
  }
1022
1064
 
1023
1065
  /**
@@ -1027,8 +1069,8 @@ async removeOnClick(selector) {
1027
1069
  */
1028
1070
  async confirm(message) {
1029
1071
  const res = await this.request("confirm", message);
1030
- this.emit("confirm", message);
1031
- return res?.confirmed === "true";
1072
+ this.emit("confirm", res?.confirmed);
1073
+ return res?.confirmed == true || res?.confirmed === "true";
1032
1074
  }
1033
1075
 
1034
1076
  /**
@@ -1106,6 +1148,10 @@ async showFileOpenDialog(options = {}) {
1106
1148
  const app = {
1107
1149
 
1108
1150
  name:"PositronApp",
1151
+
1152
+ setTrayMenu(menuTemplate) {
1153
+ trayMenu = menuTemplate;
1154
+ },
1109
1155
 
1110
1156
  /**
1111
1157
  * 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 +1208,10 @@ const app = {
1162
1208
  * @returns {Promise<Window|null>} The currently focused window, or null if no windows are focused.
1163
1209
  */
1164
1210
  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;
1211
+ const getFocused = await this.requestFromNative("getFocusedWindowId");
1212
+ const winId = getFocused?.focusedWindowId ? parseInt(getFocused.focusedWindowId, 10) : -1;
1213
+ const focusedWin = [...activeWindows].find(w => w.id === winId);
1214
+ return focusedWin || { error: "No focused window found" };
1167
1215
  },
1168
1216
 
1169
1217
  /**
@@ -1205,6 +1253,11 @@ userData: {
1205
1253
  "Application Support",
1206
1254
  process.env.POSITRON_APP_NAME
1207
1255
  );
1256
+ } else {
1257
+ // Linux / other POSIX — follow XDG Base Directory spec
1258
+ const xdgDataHome = process.env.XDG_DATA_HOME
1259
+ || path.join(process.env.HOME, ".local", "share");
1260
+ userPath = path.join(xdgDataHome, process.env.POSITRON_APP_NAME);
1208
1261
  }
1209
1262
 
1210
1263
  if(!fs.existsSync(userPath)) {
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.1.0",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"
@@ -19,7 +19,8 @@
19
19
  "javascript",
20
20
  "node",
21
21
  "macos",
22
- "windows"
22
+ "windows",
23
+ "linux"
23
24
  ],
24
25
  "author": "Bryce",
25
26
  "license": "MIT",
package/packager.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { execSync, execFileSync } = require("child_process");
3
+ const { execSync, execFileSync, spawnSync } = require("child_process");
4
4
  const { info, error, success } = require("./logs");
5
5
  const https = require("https");
6
6
  const esbuild = require("esbuild");
@@ -8,7 +8,8 @@ const findPackageJson = require("./findpackage");
8
8
 
9
9
  const MAJOR_NODE_V = 24;
10
10
 
11
- const ob = process.argv.includes("--obfuscate");
11
+ const min = process.argv.includes("--minify") || process.argv.includes("--min");
12
+ const ob = process.argv.includes("--obfuscate") || process.argv.includes("--ob");
12
13
 
13
14
  const arch = process.argv.includes("--x64") ? "x64" : process.argv.includes("--arm64") ? "arm64" : process.arch;
14
15
 
@@ -22,35 +23,50 @@ function performPackager() {
22
23
  if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true, force: true });
23
24
  fs.mkdirSync(distDir, { recursive: true });
24
25
 
26
+ let selectedPlatform = false;
27
+
25
28
  if (process.argv.includes("--mac") || process.argv.includes("--m")) {
26
29
  packageMacOS(appRoot, distDir, appName);
27
- } else if (process.argv.includes("--windows") || process.argv.includes("--w")) {
30
+ selectedPlatform = true;
31
+ }
32
+ if (process.argv.includes("--windows") || process.argv.includes("--w")) {
28
33
  packageWindows(appRoot, distDir, appName);
29
- } else {
30
- process.platform === "win32" ? packageWindows(appRoot, distDir, appName) : packageMacOS(appRoot, distDir, appName);
31
- }
34
+ selectedPlatform = true;
35
+ }
36
+ if (process.argv.includes("--linux") || process.argv.includes("--l")) {
37
+ packageLinux(appRoot, distDir, appName);
38
+ selectedPlatform = true;
39
+ }
40
+
41
+ if (selectedPlatform) return;
42
+ if (process.platform === "win32") packageWindows(appRoot, distDir, appName);
43
+ else if (process.platform === "linux") packageLinux(appRoot, distDir, appName);
44
+ else packageMacOS(appRoot, distDir, appName);
32
45
  }
33
46
 
34
47
  function handleJavaScriptPipeline(appRoot, resourcesPath) {
35
48
  const targetOutputFile = path.join(resourcesPath, "index.js");
49
+ let bundledFiles = [];
36
50
 
37
51
  info(`[Packager] Bundling JavaScript code with esbuild...`);
38
52
  try {
39
- esbuild.buildSync({
53
+ const result = esbuild.buildSync({
40
54
  entryPoints: [path.join(appRoot, "index.js")],
41
55
  bundle: true,
42
56
  platform: "node",
43
57
  target: `node${MAJOR_NODE_V}`,
44
58
  outfile: targetOutputFile,
45
- minify: ob,
59
+ minify: true,
46
60
  sourcemap: false,
61
+ metafile: true,
47
62
  });
63
+ bundledFiles = Object.keys(result.metafile.inputs).map(f => path.resolve(appRoot, f));
48
64
  } catch (err) {
49
65
  error("Fatal: esbuild bundling failed.");
50
66
  process.exit(1);
51
67
  }
52
68
 
53
- copyAppAssets(appRoot, resourcesPath);
69
+ copyAppAssets(appRoot, resourcesPath, bundledFiles);
54
70
 
55
71
  }
56
72
 
@@ -86,7 +102,7 @@ async function packageMacOS(appRoot, distDir, appName) {
86
102
  <key>CFBundleExecutable</key>
87
103
  <string>${appName}</string>
88
104
  <key>CFBundleIdentifier</key>
89
- <string>com.${package.author || "positron"}.${appName.toLowerCase()}</string>
105
+ <string>${package.bundleIdentifier || `com.${package.author || "positron"}.${appName.toLowerCase()}`}</string>
90
106
  <key>CFBundleName</key>
91
107
  <string>${appName}</string>
92
108
  <key>CFBundlePackageType</key>
@@ -212,7 +228,58 @@ async function packageWindows(appRoot, distDir, appName) {
212
228
  success(`Successfully packaged Windows app directory at: ${outputFolder}`);
213
229
  }
214
230
 
215
- function copyAppAssets(src, dest) {
231
+ async function packageLinux(appRoot, distDir, appName) {
232
+ const outputFolder = path.join(distDir, `${appName}-linux`);
233
+ fs.mkdirSync(outputFolder, { recursive: true });
234
+
235
+ info(`[Packager] Creating Linux App structure...`);
236
+
237
+ const binFolder = path.join(appRoot, "bin");
238
+ const compiledBinary = path.join(binFolder, "positron-runtime");
239
+
240
+ if (!fs.existsSync(compiledBinary)) {
241
+ error("Fatal: Native compiled binary missing from bin/. Run build first.");
242
+ process.exit(1);
243
+ }
244
+
245
+ const binaryPath = path.join(outputFolder, appName);
246
+ fs.copyFileSync(compiledBinary, binaryPath);
247
+ fs.chmodSync(binaryPath, "755");
248
+
249
+ const resourcesPath = path.join(outputFolder, "resources");
250
+ fs.mkdirSync(resourcesPath, { recursive: true });
251
+
252
+ handleJavaScriptPipeline(appRoot, resourcesPath);
253
+
254
+ const bundledJs = path.join(resourcesPath, "index.js");
255
+ const backendName = appName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase() + '-backend';
256
+
257
+ if (fs.existsSync(bundledJs)) {
258
+ await compileWithPkg(bundledJs, "linux", resourcesPath, backendName);
259
+ } else {
260
+ error(`[Packager] Fatal: Bundled JavaScript entry point missing at ${bundledJs}`);
261
+ process.exit(1);
262
+ }
263
+
264
+ const desktopContent = `[Desktop Entry]
265
+ Name=${appName}
266
+ Exec=./${appName}
267
+ Icon=utilities-terminal
268
+ Type=Application
269
+ Categories=Utility;`;
270
+ fs.writeFileSync(path.join(outputFolder, `${appName}.desktop`), desktopContent);
271
+
272
+ if(!process.argv.includes('--keep-package-json') || !process.argv.includes('--kpj')) {
273
+ fs.rmSync(path.join(resourcesPath, "package.json"), { force: true });
274
+ }
275
+
276
+ fs.rmSync(path.join(resourcesPath, "icon.icns"), { force: true });
277
+ fs.rmSync(path.join(resourcesPath, "icon.ico"), { force: true });
278
+
279
+ success(`Successfully packaged Linux app directory at: ${outputFolder}`);
280
+ }
281
+
282
+ function copyAppAssets(src, dest, ignoredFiles = []) {
216
283
  const ignoreList = ["node_modules", "dist", "bin", ".git"];
217
284
 
218
285
  function copyRecursive(currentSrc, currentDest) {
@@ -221,6 +288,8 @@ function copyAppAssets(src, dest) {
221
288
  if (ignoreList.includes(item)) continue;
222
289
 
223
290
  const srcPath = path.join(currentSrc, item);
291
+ if (ignoredFiles.includes(srcPath)) continue;
292
+
224
293
  const destPath = path.join(currentDest, item);
225
294
  const stat = fs.statSync(srcPath);
226
295
 
@@ -232,8 +301,35 @@ function copyAppAssets(src, dest) {
232
301
  fs.mkdirSync(destPath, { recursive: true });
233
302
  copyRecursive(srcPath, destPath);
234
303
  } else {
235
- if (item.endsWith(".js")) continue;
236
304
  fs.copyFileSync(srcPath, destPath);
305
+
306
+ if(min && item.endsWith(".js")) {
307
+ info(`Minifying JavaScript file: ${destPath}`);
308
+ try {
309
+ esbuild.buildSync({
310
+ "bundle": false,
311
+ "minify": true,
312
+ "sourcemap": false,
313
+ "target": `node${MAJOR_NODE_V}`,
314
+ "entryPoints": [srcPath],
315
+ "outfile": destPath,
316
+ })
317
+ } catch (err) {
318
+ error(`JavaScript minification failed for ${destPath}:`, err);
319
+ }
320
+ }
321
+
322
+ if(ob && item.endsWith(".js")) {
323
+ info(`Obfuscating JavaScript file: ${destPath}`);
324
+ try {
325
+ const obResult = spawnSync("npx", ["javascript-obfuscator", destPath, "--compact", "true", "--self-defending", "true", "--string-array", "true", "--string-array-encoding", "base64", "--string-array-threshold", "1", "--output", destPath], { stdio:"inherit" });
326
+ if (obResult.error) throw obResult.error;
327
+ if (obResult.status !== 0) throw new Error("javascript-obfuscator failed");
328
+ } catch (err) {
329
+ error(`JavaScript obfuscation failed for ${destPath}:`, err);
330
+ }
331
+ }
332
+
237
333
  }
238
334
  }
239
335
  }
@@ -244,6 +340,14 @@ function copyAppAssets(src, dest) {
244
340
  const { exec } = require("@yao-pkg/pkg");
245
341
 
246
342
  async function compileWithPkg(bundledJsPath, targetPlatform, outputFolder, appName) {
343
+
344
+ if(process.argv.includes("--no-pkg")) {
345
+ const finalPath = path.join(outputFolder, "index.js");
346
+ fs.copyFileSync(bundledJsPath, finalPath);
347
+ success(`[Packager] Skipped pkg compilation. Copied bundled JavaScript to: ${finalPath}`);
348
+ return;
349
+ }
350
+
247
351
  info(`[Packager] Packaging application into a standalone binary...`);
248
352
 
249
353
  let pkgTarget = "";
@@ -256,6 +360,8 @@ async function compileWithPkg(bundledJsPath, targetPlatform, outputFolder, appNa
256
360
  finalBinaryName = `${appName}.exe`;
257
361
  } else if (targetPlatform === "darwin") {
258
362
  pkgTarget = `node${MAJOR_NODE_V}-macos-${arch}`;
363
+ } else if (targetPlatform === "linux") {
364
+ pkgTarget = `node${MAJOR_NODE_V}-linux-${arch}`;
259
365
  }
260
366
 
261
367
  const outputPath = path.join(outputFolder, finalBinaryName);
package/screen.js CHANGED
@@ -1,4 +1,4 @@
1
- const { execSync } = require('child_process');
1
+ const { spawnSync } = require('child_process');
2
2
 
3
3
  /**
4
4
  * Gets the screen size of the primary display. The implementation varies based on the operating system:
@@ -11,16 +11,18 @@ function getScreenSize() {
11
11
 
12
12
  try {
13
13
  if (platform === 'win32') {
14
- const cmd = "powershell -command \"Get-CimInstance Win32_VideoController | Select-Object CurrentHorizontalResolution, CurrentVerticalResolution | Format-List\"";
15
- const output = execSync(cmd).toString();
14
+ const result = spawnSync("powershell", ["-command", "Get-CimInstance Win32_VideoController | Select-Object CurrentHorizontalResolution, CurrentVerticalResolution | Format-List"]);
15
+ if (result.error || result.status !== 0) throw new Error("Failed to execute powershell");
16
+ const output = result.stdout.toString();
16
17
  const width = output.match(/CurrentHorizontalResolution\s*:\s*(\d+)/)?.[1];
17
18
  const height = output.match(/CurrentVerticalResolution\s*:\s*(\d+)/)?.[1];
18
19
  return { width: parseInt(width), height: parseInt(height) };
19
20
  }
20
21
 
21
22
  if (platform === 'darwin') {
22
- const cmd = "system_profiler SPDisplaysDataType | grep Resolution";
23
- const output = execSync(cmd).toString();
23
+ const result = spawnSync("system_profiler", ["SPDisplaysDataType"]);
24
+ if (result.error || result.status !== 0) throw new Error("Failed to execute system_profiler");
25
+ const output = result.stdout.toString().split('\\n').filter(line => line.includes('Resolution')).join('\\n');
24
26
  const match = output.match(/(\d+) x (\d+)/);
25
27
  return { width: parseInt(match[1]), height: parseInt(match[2]) };
26
28
  }
package/tray.js ADDED
@@ -0,0 +1,72 @@
1
+ const { app } = require("./index");
2
+ const { Menu } = require("./menu");
3
+ const { warn } = require("./logs");
4
+
5
+ let createdTray = false;
6
+
7
+ module.exports = {
8
+
9
+ create(menu, title = "", icon = "") {
10
+
11
+ if(process.platform == "linux") return warn("Tray is not supported on Linux at this time.");
12
+
13
+ if(createdTray) {
14
+ warn("Tray already created. Use setMenu, setTitle, or setIcon to update the existing tray.");
15
+ return;
16
+ }
17
+
18
+ createdTray = true;
19
+
20
+ if(menu instanceof Menu) {
21
+ menu = menu.template
22
+ }
23
+
24
+ const stripClick = (items) => {
25
+ if (!items) return null;
26
+ return items.map(i => {
27
+ const newItem = { ...i, click: undefined };
28
+ if (newItem.items) {
29
+ newItem.items = stripClick(newItem.items);
30
+ }
31
+ return newItem;
32
+ });
33
+ };
34
+
35
+ app.setTrayMenu(menu);
36
+ app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), title, icon]);
37
+ },
38
+
39
+ setMenu(menu) {
40
+
41
+ if(process.platform == "linux") return warn("Tray is not supported on Linux at this time.");
42
+
43
+ if(menu instanceof Menu) {
44
+ menu = menu.template
45
+ }
46
+
47
+ const stripClick = (items) => {
48
+ if (!items) return null;
49
+ return items.map(i => {
50
+ const newItem = { ...i, click: undefined };
51
+ if (newItem.items) {
52
+ newItem.items = stripClick(newItem.items);
53
+ }
54
+ return newItem;
55
+ });
56
+ };
57
+
58
+ app.setTrayMenu(menu);
59
+ app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), "setMenu"]);
60
+ },
61
+
62
+ setTitle(title) {
63
+ if(process.platform == "linux") return warn("Tray is not supported on Linux at this time.");
64
+ app.sendToNative("createTray", [title, "setTitle"]);
65
+ },
66
+
67
+ setIcon(iconPath) {
68
+ if(process.platform == "linux") return warn("Tray is not supported on Linux at this time.");
69
+ app.sendToNative("createTray", [iconPath, "setIcon"]);
70
+ }
71
+
72
+ }