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/README.md +7 -8
- package/builder.js +160 -3
- package/core/linux/main.cpp +1123 -0
- package/core/mac/main.swift +52 -21
- package/core/mac/tray.swift +95 -0
- package/core/win/main.cs +58 -13
- package/core/win/tray.cs +142 -0
- package/index.js +91 -38
- package/package.json +3 -2
- package/packager.js +118 -12
- package/screen.js +7 -5
- package/tray.js +72 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
1166
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
30
|
+
selectedPlatform = true;
|
|
31
|
+
}
|
|
32
|
+
if (process.argv.includes("--windows") || process.argv.includes("--w")) {
|
|
28
33
|
packageWindows(appRoot, distDir, appName);
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
15
|
-
|
|
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
|
|
23
|
-
|
|
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
|
+
}
|