zerogterm 0.4.0-alpha.1 → 0.6.0-alpha.1
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/LICENSE +21 -21
- package/README.md +193 -153
- package/bin/zerogterm.cjs +23 -23
- package/dist/main/main/main.js +11 -2
- package/dist/main/main/preload.cjs +33 -33
- package/dist/main/main/session-service.js +74 -10
- package/dist/renderer/assets/index-BJsoL2QL.js +17 -0
- package/dist/renderer/assets/index-BPfYUrvm.css +1 -0
- package/dist/renderer/index.html +4 -4
- package/package.json +67 -67
- package/dist/renderer/assets/index-DSL4JtQn.css +0 -1
- package/dist/renderer/assets/index-x_BAzbUO.js +0 -17
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
5
7
|
import { promisify } from 'node:util';
|
|
6
8
|
import { defaultShellBackend, isLocalShellBackend, resolveShellBackend } from './shell-catalog.js';
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -14,6 +16,37 @@ const NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,48}$/;
|
|
|
14
16
|
* -chosen config file (hence ProxyCommand) without any shell involvement.
|
|
15
17
|
*/
|
|
16
18
|
const SSH_TARGET = /^(?:([A-Za-z0-9][A-Za-z0-9._-]*)@)?([A-Za-z0-9][A-Za-z0-9.-]*)(?::(\d{1,5}))?$/;
|
|
19
|
+
function resolveSshExecutable() {
|
|
20
|
+
if (process.platform !== 'win32')
|
|
21
|
+
return 'ssh';
|
|
22
|
+
const windowsRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(windowsRoot, 'System32', 'OpenSSH', 'ssh.exe'),
|
|
25
|
+
join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Git', 'usr', 'bin', 'ssh.exe'),
|
|
26
|
+
join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Git', 'usr', 'bin', 'ssh.exe')
|
|
27
|
+
];
|
|
28
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? 'ssh.exe';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Size a pty starts at when the pane could not be measured — a placeholder,
|
|
32
|
+
* not a preference. Panes pass their real size through attach().
|
|
33
|
+
*/
|
|
34
|
+
const FALLBACK_SIZE = { cols: 120, rows: 32 };
|
|
35
|
+
/**
|
|
36
|
+
* A size a pty can actually be given. Sizes cross the IPC boundary from the
|
|
37
|
+
* renderer, and a zero or fractional one is rejected by ConPTY (and makes
|
|
38
|
+
* every full-screen program wrap in the wrong place on Unix).
|
|
39
|
+
*/
|
|
40
|
+
function usableSize(size) {
|
|
41
|
+
if (!size)
|
|
42
|
+
return undefined;
|
|
43
|
+
const { cols, rows } = size;
|
|
44
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (cols < 2 || rows < 2 || cols > 2000 || rows > 2000)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { cols, rows };
|
|
49
|
+
}
|
|
17
50
|
export function validateSessionName(name) {
|
|
18
51
|
const value = name.trim();
|
|
19
52
|
if (!NAME.test(value)) {
|
|
@@ -176,10 +209,23 @@ export class ScreenService {
|
|
|
176
209
|
this.onEvent?.('created', session, true);
|
|
177
210
|
return session;
|
|
178
211
|
}
|
|
179
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Attach a pane to a session, starting its pty if it does not have one.
|
|
214
|
+
*
|
|
215
|
+
* `size` is the pane's own measurement. A pty spawned at some stock size and
|
|
216
|
+
* resized a moment later shows its first frame at the wrong width, and a
|
|
217
|
+
* full-screen program redrawing over that frame leaves pieces of it behind —
|
|
218
|
+
* so the shell is started at the size it will actually be displayed at.
|
|
219
|
+
*/
|
|
220
|
+
attach(id, onData, onExit, size) {
|
|
180
221
|
const existing = this.getSession(id);
|
|
181
|
-
if (this.ptys.has(id))
|
|
222
|
+
if (this.ptys.has(id)) {
|
|
223
|
+
// Re-attaching pane may be a different size than the one that started it.
|
|
224
|
+
const measured = usableSize(size);
|
|
225
|
+
if (measured)
|
|
226
|
+
this.resize(id, measured.cols, measured.rows);
|
|
182
227
|
return { ...existing, status: 'connected' };
|
|
228
|
+
}
|
|
183
229
|
if (id.startsWith('local:')) {
|
|
184
230
|
const fallback = this.fallbackLocalSessions.get(id);
|
|
185
231
|
if (fallback) {
|
|
@@ -188,14 +234,14 @@ export class ScreenService {
|
|
|
188
234
|
const shell = isLocalShellBackend(fallback.backend)
|
|
189
235
|
? resolveShellBackend(fallback.backend, fallback.wslDistribution)
|
|
190
236
|
: defaultShellBackend();
|
|
191
|
-
this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd);
|
|
237
|
+
this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd, size);
|
|
192
238
|
this.onEvent?.('attached', fallback, true);
|
|
193
239
|
return { ...fallback };
|
|
194
240
|
}
|
|
195
241
|
// Session ids cross the IPC boundary from the renderer, so re-validate
|
|
196
242
|
// rather than trusting that createLocal produced this one.
|
|
197
243
|
const name = validateSessionName(id.slice('local:'.length));
|
|
198
|
-
this.spawnCommand(id, 'screen', ['-x', name], onData, onExit);
|
|
244
|
+
this.spawnCommand(id, 'screen', ['-x', name], onData, onExit, homedir(), size);
|
|
199
245
|
this.onEvent?.('attached', existing, true);
|
|
200
246
|
return { ...existing, status: 'connected', persistence: 'screen' };
|
|
201
247
|
}
|
|
@@ -207,7 +253,7 @@ export class ScreenService {
|
|
|
207
253
|
session.status = 'connected';
|
|
208
254
|
session.lastSeen = new Date().toISOString();
|
|
209
255
|
try {
|
|
210
|
-
this.spawnCommand(id,
|
|
256
|
+
this.spawnCommand(id, resolveSshExecutable(), args, onData, onExit, homedir(), size);
|
|
211
257
|
}
|
|
212
258
|
catch (error) {
|
|
213
259
|
session.status = 'error';
|
|
@@ -239,11 +285,12 @@ export class ScreenService {
|
|
|
239
285
|
throw new Error(`Unknown session: ${id}`);
|
|
240
286
|
return session;
|
|
241
287
|
}
|
|
242
|
-
spawnCommand(sessionId, file, args, onData, onExit, cwd = homedir()) {
|
|
288
|
+
spawnCommand(sessionId, file, args, onData, onExit, cwd = homedir(), size) {
|
|
289
|
+
const { cols, rows } = usableSize(size) ?? FALLBACK_SIZE;
|
|
243
290
|
const proc = this.spawnPty(file, args, {
|
|
244
291
|
name: 'xterm-256color',
|
|
245
|
-
cols
|
|
246
|
-
rows
|
|
292
|
+
cols,
|
|
293
|
+
rows,
|
|
247
294
|
cwd,
|
|
248
295
|
env: process.env
|
|
249
296
|
});
|
|
@@ -259,7 +306,7 @@ export class ScreenService {
|
|
|
259
306
|
}
|
|
260
307
|
}
|
|
261
308
|
};
|
|
262
|
-
this.ptys.set(sessionId, { handle, kind: sessionId.startsWith('ssh:') ? 'ssh' : 'local' });
|
|
309
|
+
this.ptys.set(sessionId, { handle, kind: sessionId.startsWith('ssh:') ? 'ssh' : 'local', cols, rows });
|
|
263
310
|
proc.onData(onData);
|
|
264
311
|
proc.onExit(() => {
|
|
265
312
|
this.ptys.delete(sessionId);
|
|
@@ -281,8 +328,25 @@ export class ScreenService {
|
|
|
281
328
|
write(sessionId, data) {
|
|
282
329
|
this.ptys.get(sessionId)?.handle.write(data);
|
|
283
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Tell a session's pty how big its pane is now.
|
|
333
|
+
*
|
|
334
|
+
* A resize is not free and not invisible: it makes ConPTY reflow and re-emit
|
|
335
|
+
* its screen, and every full-screen program redraw on SIGWINCH. Panes refit
|
|
336
|
+
* on layout, font and status changes, which mostly produce the size the pty
|
|
337
|
+
* already has, so a repeat of the current size is dropped rather than paid
|
|
338
|
+
* for in a torn redraw.
|
|
339
|
+
*/
|
|
284
340
|
resize(sessionId, cols, rows) {
|
|
285
|
-
this.ptys.get(sessionId)
|
|
341
|
+
const pty = this.ptys.get(sessionId);
|
|
342
|
+
if (!pty)
|
|
343
|
+
return;
|
|
344
|
+
const size = usableSize({ cols, rows });
|
|
345
|
+
if (!size || (size.cols === pty.cols && size.rows === pty.rows))
|
|
346
|
+
return;
|
|
347
|
+
pty.cols = size.cols;
|
|
348
|
+
pty.rows = size.rows;
|
|
349
|
+
pty.handle.resize(size.cols, size.rows);
|
|
286
350
|
}
|
|
287
351
|
detach(sessionId) {
|
|
288
352
|
this.ptys.get(sessionId)?.handle.kill();
|