termites 1.0.35 → 1.0.37

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.
Files changed (3) hide show
  1. package/package.json +3 -4
  2. package/server.js +28 -19
  3. package/index.js +0 -472
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "termites",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Local multi-terminal manager with web interface",
5
- "main": "index.js",
5
+ "main": "server.js",
6
6
  "scripts": {
7
- "start": "node index.js",
8
- "server": "node server.js"
7
+ "start": "node server.js"
9
8
  },
10
9
  "bin": {
11
10
  "termites": "bin/termites.js"
package/server.js CHANGED
@@ -25,8 +25,21 @@ function saveConfig(config) {
25
25
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
26
26
  }
27
27
 
28
- function hashPassword(password) {
29
- return crypto.createHash('sha256').update(password).digest('hex');
28
+ // Use scrypt for secure password hashing
29
+ const SCRYPT_SALT_LENGTH = 16;
30
+ const SCRYPT_KEY_LENGTH = 64;
31
+
32
+ function hashPassword(password, salt) {
33
+ salt = salt || crypto.randomBytes(SCRYPT_SALT_LENGTH).toString('hex');
34
+ const hash = crypto.scryptSync(password, salt, SCRYPT_KEY_LENGTH).toString('hex');
35
+ return `${salt}:${hash}`;
36
+ }
37
+
38
+ function verifyPassword(password, storedHash) {
39
+ const [salt, hash] = storedHash.split(':');
40
+ if (!salt || !hash) return false;
41
+ const newHash = crypto.scryptSync(password, salt, SCRYPT_KEY_LENGTH).toString('hex');
42
+ return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(newHash, 'hex'));
30
43
  }
31
44
 
32
45
  class TermitesServer {
@@ -385,7 +398,7 @@ class TermitesServer {
385
398
  req.on('end', () => {
386
399
  try {
387
400
  const { password } = JSON.parse(body);
388
- if (hashPassword(password) === this.config.passwordHash) {
401
+ if (verifyPassword(password, this.config.passwordHash)) {
389
402
  res.writeHead(200, {
390
403
  'Content-Type': 'application/json',
391
404
  'Set-Cookie': `session=${this.sessionToken}; Path=/; HttpOnly; SameSite=Strict`
@@ -1095,6 +1108,9 @@ class TermitesServer {
1095
1108
  };
1096
1109
 
1097
1110
  toolbar.querySelectorAll('button').forEach(btn => {
1111
+ // Skip hist button, it has its own handler
1112
+ if (btn.id === 'hist-btn') return;
1113
+
1098
1114
  let repeatInterval = null;
1099
1115
  const isArrowKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(btn.dataset.key);
1100
1116
 
@@ -1143,6 +1159,13 @@ class TermitesServer {
1143
1159
 
1144
1160
  if (data && ws?.readyState === WebSocket.OPEN && selectedClientId) {
1145
1161
  ws.send(JSON.stringify({ type: 'input', clientId: selectedClientId, text: data }));
1162
+ // Reset modifiers after sending (for non-modifier keys)
1163
+ if (!btn.dataset.mod) {
1164
+ modifiers.ctrl = false;
1165
+ modifiers.alt = false;
1166
+ if (window.ctrlTimeout) { clearTimeout(window.ctrlTimeout); window.ctrlTimeout = null; }
1167
+ toolbar.querySelectorAll('.mod-btn').forEach(b => b.classList.remove('active'));
1168
+ }
1146
1169
  }
1147
1170
  };
1148
1171
 
@@ -1150,17 +1173,10 @@ class TermitesServer {
1150
1173
  e.preventDefault();
1151
1174
  e.stopPropagation();
1152
1175
  sendKey();
1153
- // For arrow keys, start repeating after initial press
1154
- if (isArrowKey && !btn.dataset.mod) {
1176
+ // For arrow keys, start repeating after initial press (but not with modifiers)
1177
+ if (isArrowKey && !btn.dataset.mod && !modifiers.ctrl && !modifiers.alt) {
1155
1178
  repeatInterval = setInterval(sendKey, 100);
1156
1179
  }
1157
- // Reset modifiers after use (except for arrow keys during repeat)
1158
- if (!isArrowKey) {
1159
- modifiers.ctrl = false;
1160
- modifiers.alt = false;
1161
- if (window.ctrlTimeout) { clearTimeout(window.ctrlTimeout); window.ctrlTimeout = null; }
1162
- toolbar.querySelectorAll('.mod-btn').forEach(b => b.classList.remove('active'));
1163
- }
1164
1180
  };
1165
1181
 
1166
1182
  const stopRepeat = () => {
@@ -1168,13 +1184,6 @@ class TermitesServer {
1168
1184
  clearInterval(repeatInterval);
1169
1185
  repeatInterval = null;
1170
1186
  }
1171
- // Reset modifiers after arrow key release
1172
- if (isArrowKey) {
1173
- modifiers.ctrl = false;
1174
- modifiers.alt = false;
1175
- if (window.ctrlTimeout) { clearTimeout(window.ctrlTimeout); window.ctrlTimeout = null; }
1176
- toolbar.querySelectorAll('.mod-btn').forEach(b => b.classList.remove('active'));
1177
- }
1178
1187
  term.focus();
1179
1188
  };
1180
1189
 
package/index.js DELETED
@@ -1,472 +0,0 @@
1
- #!/usr/bin/env node
2
- const http = require('http');
3
- const os = require('os');
4
- const WebSocket = require('ws');
5
-
6
- const PORT = process.env.PORT || 6789;
7
- const SHELL = process.env.SHELL || '/bin/bash';
8
-
9
- function getSystemInfo() {
10
- const username = os.userInfo().username;
11
- const hostname = os.hostname();
12
- const cwd = process.cwd().replace(os.homedir(), '~');
13
- return { username, hostname, cwd };
14
- }
15
-
16
- class WebTerminalServer {
17
- constructor(port) {
18
- this.port = port;
19
- this.pty = null;
20
- this.outputBuffer = [];
21
- this.clients = new Set();
22
- this.isRunning = false;
23
- this.systemInfo = getSystemInfo();
24
-
25
- this.startServer();
26
- }
27
-
28
- startServer() {
29
- this.httpServer = http.createServer((req, res) => {
30
- this.handleHttpRequest(req, res);
31
- });
32
-
33
- this.wss = new WebSocket.Server({ server: this.httpServer });
34
-
35
- this.wss.on('connection', (ws) => {
36
- console.log('新的 WebSocket 连接');
37
- this.clients.add(ws);
38
-
39
- ws.send(JSON.stringify({
40
- type: 'status',
41
- isRunning: this.isRunning,
42
- systemInfo: this.systemInfo
43
- }));
44
-
45
- this.outputBuffer.forEach(data => {
46
- ws.send(JSON.stringify({ type: 'output', data }));
47
- });
48
-
49
- ws.on('message', (message) => {
50
- try {
51
- const data = JSON.parse(message);
52
- this.handleClientMessage(data);
53
- } catch (e) {
54
- console.error('解析消息失败:', e);
55
- }
56
- });
57
-
58
- ws.on('close', () => {
59
- this.clients.delete(ws);
60
- });
61
- });
62
-
63
- this.httpServer.listen(this.port, () => {
64
- console.log(`Web Terminal 启动: http://localhost:${this.port}`);
65
- // 自动启动终端
66
- this.startTerminal();
67
- });
68
- }
69
-
70
- handleClientMessage(data) {
71
- switch (data.type) {
72
- case 'start':
73
- this.startTerminal();
74
- break;
75
- case 'stop':
76
- this.stopTerminal();
77
- break;
78
- case 'input':
79
- if (this.pty) this.pty.write(data.text);
80
- break;
81
- case 'resize':
82
- if (this.pty) this.pty.resize(data.cols, data.rows);
83
- break;
84
- }
85
- }
86
-
87
- broadcast(data) {
88
- const msg = JSON.stringify(data);
89
- this.clients.forEach(c => {
90
- if (c.readyState === WebSocket.OPEN) c.send(msg);
91
- });
92
- }
93
-
94
- startTerminal() {
95
- if (this.isRunning) return;
96
-
97
- const pty = require('node-pty');
98
- this.pty = pty.spawn(SHELL, ['-l'], {
99
- name: 'xterm-256color',
100
- cols: 120,
101
- rows: 40,
102
- cwd: process.cwd(),
103
- env: process.env
104
- });
105
-
106
- this.isRunning = true;
107
- this.broadcast({ type: 'started' });
108
-
109
- this.pty.onData((data) => {
110
- this.outputBuffer.push(data);
111
- if (this.outputBuffer.length > 1000) {
112
- this.outputBuffer = this.outputBuffer.slice(-500);
113
- }
114
- this.broadcast({ type: 'output', data });
115
- });
116
-
117
- this.pty.onExit(() => {
118
- this.isRunning = false;
119
- this.pty = null;
120
- this.broadcast({ type: 'stopped' });
121
- });
122
- }
123
-
124
- stopTerminal() {
125
- if (this.pty) {
126
- this.pty.kill();
127
- this.pty = null;
128
- this.isRunning = false;
129
- this.broadcast({ type: 'stopped' });
130
- }
131
- }
132
-
133
- handleHttpRequest(req, res) {
134
- if (req.url === '/' || req.url === '/index.html') {
135
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
136
- res.end(this.getHtmlPage());
137
- } else {
138
- res.writeHead(404);
139
- res.end('Not Found');
140
- }
141
- }
142
-
143
- getHtmlPage() {
144
- return `<!DOCTYPE html>
145
- <html lang="zh-CN">
146
- <head>
147
- <meta charset="UTF-8">
148
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
- <title>Web Terminal</title>
150
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
151
- <style>
152
- * { margin: 0; padding: 0; box-sizing: border-box; }
153
- body { height: 100vh; display: flex; flex-direction: column; transition: background 0.3s; }
154
- .header {
155
- padding: 8px 16px;
156
- display: flex; justify-content: space-between; align-items: center;
157
- border-bottom: 1px solid; font-family: monospace; transition: all 0.3s;
158
- }
159
- .header-left { display: flex; align-items: center; gap: 12px; }
160
- .path-info { font-size: 14px; }
161
- .path-info .user { font-weight: bold; color: var(--user-color, #859900); }
162
- .path-info .host { color: var(--host-color, #859900); }
163
- .path-info .sep { color: var(--sep-color, #657b83); }
164
- .path-info .path { color: var(--path-color, #268bd2); }
165
- .status.running { background: var(--status-running, #859900); color: #fff; }
166
- .status.stopped { background: var(--status-stopped, #dc322f); color: #fff; }
167
- .status { padding: 3px 10px; border-radius: 10px; font-size: 11px; }
168
- #terminal-container { flex: 1; padding: 5px; transition: background 0.3s; }
169
- .xterm { height: 100%; }
170
- .settings-btn {
171
- background: none; border: 1px solid; padding: 4px 10px;
172
- border-radius: 4px; cursor: pointer; font-size: 12px;
173
- font-family: monospace; transition: all 0.2s;
174
- }
175
- .settings-btn:hover { opacity: 0.8; }
176
- .settings-panel {
177
- position: fixed; top: 0; right: -320px; width: 300px; height: 100%;
178
- padding: 20px; box-shadow: -2px 0 10px rgba(0,0,0,0.2);
179
- transition: right 0.3s; z-index: 100; overflow-y: auto;
180
- }
181
- .settings-panel.open { right: 0; }
182
- .settings-panel h3 { margin-bottom: 20px; font-size: 16px; }
183
- .setting-group { margin-bottom: 20px; }
184
- .setting-group label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: bold; }
185
- .setting-group select, .setting-group input {
186
- width: 100%; padding: 8px; border: 1px solid; border-radius: 4px;
187
- font-size: 13px; background: inherit; color: inherit;
188
- }
189
- .close-btn {
190
- position: absolute; top: 15px; right: 15px; background: none;
191
- border: none; font-size: 20px; cursor: pointer; color: inherit;
192
- }
193
- .overlay {
194
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
195
- background: rgba(0,0,0,0.3); z-index: 99; display: none;
196
- }
197
- .overlay.open { display: block; }
198
- </style>
199
- </head>
200
- <body>
201
- <div class="overlay" id="overlay"></div>
202
- <div class="settings-panel" id="settings-panel">
203
- <button class="close-btn" id="close-settings">&times;</button>
204
- <h3>设置</h3>
205
- <div class="setting-group">
206
- <label>主题</label>
207
- <select id="theme-select">
208
- <option value="solarized-light">Solarized Light</option>
209
- <option value="solarized-dark">Solarized Dark</option>
210
- <option value="monokai">Monokai</option>
211
- <option value="dracula">Dracula</option>
212
- <option value="nord">Nord</option>
213
- <option value="github-dark">GitHub Dark</option>
214
- </select>
215
- </div>
216
- <div class="setting-group">
217
- <label>字体</label>
218
- <select id="font-select">
219
- <option value="Monaco, Menlo, monospace">Monaco</option>
220
- <option value="'Fira Code', monospace">Fira Code</option>
221
- <option value="'JetBrains Mono', monospace">JetBrains Mono</option>
222
- <option value="'Source Code Pro', monospace">Source Code Pro</option>
223
- <option value="Consolas, monospace">Consolas</option>
224
- <option value="'Courier New', monospace">Courier New</option>
225
- </select>
226
- </div>
227
- <div class="setting-group">
228
- <label>字体大小</label>
229
- <input type="range" id="font-size" min="10" max="24" value="14">
230
- <span id="font-size-value">14px</span>
231
- </div>
232
- </div>
233
- <div class="header">
234
- <div class="header-left">
235
- <span id="path-info" class="path-info"></span>
236
- <span id="status" class="status stopped">已停止</span>
237
- </div>
238
- <button class="settings-btn" id="settings-btn">⚙ 设置</button>
239
- </div>
240
- <div id="terminal-container"></div>
241
-
242
- <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
243
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
244
- <script>
245
- let ws, term, fitAddon, isRunning = false;
246
-
247
- const themes = {
248
- 'solarized-light': {
249
- background: '#fdf6e3', foreground: '#657b83', cursor: '#657b83',
250
- cursorAccent: '#fdf6e3', selectionBackground: '#93a1a1',
251
- black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
252
- blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
253
- brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
254
- brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
255
- brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
256
- headerBg: '#eee8d5', headerBorder: '#93a1a1', userColor: '#859900',
257
- hostColor: '#859900', sepColor: '#657b83', pathColor: '#268bd2',
258
- statusRunning: '#859900', statusStopped: '#dc322f'
259
- },
260
- 'solarized-dark': {
261
- background: '#002b36', foreground: '#839496', cursor: '#839496',
262
- cursorAccent: '#002b36', selectionBackground: '#073642',
263
- black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
264
- blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
265
- brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
266
- brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
267
- brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
268
- headerBg: '#073642', headerBorder: '#586e75', userColor: '#859900',
269
- hostColor: '#859900', sepColor: '#839496', pathColor: '#268bd2',
270
- statusRunning: '#859900', statusStopped: '#dc322f'
271
- },
272
- 'monokai': {
273
- background: '#272822', foreground: '#f8f8f2', cursor: '#f8f8f2',
274
- cursorAccent: '#272822', selectionBackground: '#49483e',
275
- black: '#272822', red: '#f92672', green: '#a6e22e', yellow: '#f4bf75',
276
- blue: '#66d9ef', magenta: '#ae81ff', cyan: '#a1efe4', white: '#f8f8f2',
277
- brightBlack: '#75715e', brightRed: '#f92672', brightGreen: '#a6e22e',
278
- brightYellow: '#f4bf75', brightBlue: '#66d9ef', brightMagenta: '#ae81ff',
279
- brightCyan: '#a1efe4', brightWhite: '#f9f8f5',
280
- headerBg: '#1e1f1c', headerBorder: '#49483e', userColor: '#a6e22e',
281
- hostColor: '#a6e22e', sepColor: '#f8f8f2', pathColor: '#66d9ef',
282
- statusRunning: '#a6e22e', statusStopped: '#f92672'
283
- },
284
- 'dracula': {
285
- background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2',
286
- cursorAccent: '#282a36', selectionBackground: '#44475a',
287
- black: '#21222c', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
288
- blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2',
289
- brightBlack: '#6272a4', brightRed: '#ff6e6e', brightGreen: '#69ff94',
290
- brightYellow: '#ffffa5', brightBlue: '#d6acff', brightMagenta: '#ff92df',
291
- brightCyan: '#a4ffff', brightWhite: '#ffffff',
292
- headerBg: '#21222c', headerBorder: '#44475a', userColor: '#50fa7b',
293
- hostColor: '#50fa7b', sepColor: '#f8f8f2', pathColor: '#bd93f9',
294
- statusRunning: '#50fa7b', statusStopped: '#ff5555'
295
- },
296
- 'nord': {
297
- background: '#2e3440', foreground: '#d8dee9', cursor: '#d8dee9',
298
- cursorAccent: '#2e3440', selectionBackground: '#434c5e',
299
- black: '#3b4252', red: '#bf616a', green: '#a3be8c', yellow: '#ebcb8b',
300
- blue: '#81a1c1', magenta: '#b48ead', cyan: '#88c0d0', white: '#e5e9f0',
301
- brightBlack: '#4c566a', brightRed: '#bf616a', brightGreen: '#a3be8c',
302
- brightYellow: '#ebcb8b', brightBlue: '#81a1c1', brightMagenta: '#b48ead',
303
- brightCyan: '#8fbcbb', brightWhite: '#eceff4',
304
- headerBg: '#3b4252', headerBorder: '#4c566a', userColor: '#a3be8c',
305
- hostColor: '#a3be8c', sepColor: '#d8dee9', pathColor: '#81a1c1',
306
- statusRunning: '#a3be8c', statusStopped: '#bf616a'
307
- },
308
- 'github-dark': {
309
- background: '#0d1117', foreground: '#c9d1d9', cursor: '#c9d1d9',
310
- cursorAccent: '#0d1117', selectionBackground: '#3b5070',
311
- black: '#0d1117', red: '#ff7b72', green: '#7ee787', yellow: '#d29922',
312
- blue: '#79c0ff', magenta: '#d2a8ff', cyan: '#a5d6ff', white: '#c9d1d9',
313
- brightBlack: '#484f58', brightRed: '#ffa198', brightGreen: '#aff5b4',
314
- brightYellow: '#e3b341', brightBlue: '#a5d6ff', brightMagenta: '#d2a8ff',
315
- brightCyan: '#b6e3ff', brightWhite: '#f0f6fc',
316
- headerBg: '#161b22', headerBorder: '#30363d', userColor: '#7ee787',
317
- hostColor: '#7ee787', sepColor: '#c9d1d9', pathColor: '#79c0ff',
318
- statusRunning: '#7ee787', statusStopped: '#ff7b72'
319
- }
320
- };
321
-
322
- let currentTheme = 'solarized-light';
323
- let currentFont = 'Monaco, Menlo, monospace';
324
- let currentFontSize = 14;
325
-
326
- function loadSettings() {
327
- const saved = localStorage.getItem('webterm-settings');
328
- if (saved) {
329
- const s = JSON.parse(saved);
330
- currentTheme = s.theme || currentTheme;
331
- currentFont = s.font || currentFont;
332
- currentFontSize = s.fontSize || currentFontSize;
333
- }
334
- document.getElementById('theme-select').value = currentTheme;
335
- document.getElementById('font-select').value = currentFont;
336
- document.getElementById('font-size').value = currentFontSize;
337
- document.getElementById('font-size-value').textContent = currentFontSize + 'px';
338
- }
339
-
340
- function saveSettings() {
341
- localStorage.setItem('webterm-settings', JSON.stringify({
342
- theme: currentTheme, font: currentFont, fontSize: currentFontSize
343
- }));
344
- }
345
-
346
- function applyTheme(themeName) {
347
- const t = themes[themeName];
348
- if (!t) return;
349
- currentTheme = themeName;
350
- document.body.style.background = t.background;
351
- document.getElementById('terminal-container').style.background = t.background;
352
- const header = document.querySelector('.header');
353
- header.style.background = t.headerBg;
354
- header.style.borderColor = t.headerBorder;
355
- header.style.color = t.foreground;
356
- document.querySelector('.settings-btn').style.borderColor = t.foreground;
357
- document.querySelector('.settings-btn').style.color = t.foreground;
358
- const panel = document.getElementById('settings-panel');
359
- panel.style.background = t.headerBg;
360
- panel.style.color = t.foreground;
361
- const style = document.documentElement.style;
362
- style.setProperty('--user-color', t.userColor);
363
- style.setProperty('--host-color', t.hostColor);
364
- style.setProperty('--sep-color', t.sepColor);
365
- style.setProperty('--path-color', t.pathColor);
366
- style.setProperty('--status-running', t.statusRunning);
367
- style.setProperty('--status-stopped', t.statusStopped);
368
- if (term) {
369
- term.options.theme = t;
370
- }
371
- saveSettings();
372
- }
373
-
374
- function applyFont(font) {
375
- currentFont = font;
376
- if (term) {
377
- term.options.fontFamily = font;
378
- fitAddon.fit();
379
- }
380
- saveSettings();
381
- }
382
-
383
- function applyFontSize(size) {
384
- currentFontSize = size;
385
- document.getElementById('font-size-value').textContent = size + 'px';
386
- if (term) {
387
- term.options.fontSize = size;
388
- fitAddon.fit();
389
- }
390
- saveSettings();
391
- }
392
-
393
- function setupSettings() {
394
- const btn = document.getElementById('settings-btn');
395
- const panel = document.getElementById('settings-panel');
396
- const overlay = document.getElementById('overlay');
397
- const closeBtn = document.getElementById('close-settings');
398
-
399
- btn.onclick = () => { panel.classList.add('open'); overlay.classList.add('open'); };
400
- closeBtn.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
401
- overlay.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
402
-
403
- document.getElementById('theme-select').onchange = e => applyTheme(e.target.value);
404
- document.getElementById('font-select').onchange = e => applyFont(e.target.value);
405
- document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
406
- }
407
-
408
- function init() {
409
- loadSettings();
410
- setupSettings();
411
-
412
- term = new Terminal({
413
- theme: themes[currentTheme], fontFamily: currentFont,
414
- fontSize: currentFontSize, cursorBlink: true
415
- });
416
- fitAddon = new FitAddon.FitAddon();
417
- term.loadAddon(fitAddon);
418
- term.open(document.getElementById('terminal-container'));
419
- fitAddon.fit();
420
-
421
- term.onData(data => {
422
- if (ws?.readyState === WebSocket.OPEN) {
423
- ws.send(JSON.stringify({ type: 'input', text: data }));
424
- }
425
- });
426
-
427
- window.addEventListener('resize', () => {
428
- fitAddon.fit();
429
- if (ws?.readyState === WebSocket.OPEN) {
430
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
431
- }
432
- });
433
-
434
- connect();
435
- applyTheme(currentTheme);
436
- }
437
-
438
- function connect() {
439
- ws = new WebSocket('ws://' + location.host);
440
- ws.onopen = () => {
441
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
442
- };
443
- ws.onmessage = e => {
444
- const d = JSON.parse(e.data);
445
- if (d.type === 'output') term.write(d.data);
446
- else if (d.type === 'status' || d.type === 'started' || d.type === 'stopped') {
447
- isRunning = d.type === 'started' || (d.type === 'status' && d.isRunning);
448
- document.getElementById('status').textContent = isRunning ? '运行中' : '已停止';
449
- document.getElementById('status').className = 'status ' + (isRunning ? 'running' : 'stopped');
450
- if (d.systemInfo) {
451
- const i = d.systemInfo;
452
- document.getElementById('path-info').innerHTML =
453
- '<span class="user">' + i.username + '</span>' +
454
- '<span class="sep">@</span><span class="host">' + i.hostname + '</span>' +
455
- '<span class="sep">:</span><span class="path">' + i.cwd + '</span>';
456
- }
457
- }
458
- };
459
- ws.onclose = () => setTimeout(connect, 3000);
460
- }
461
-
462
- init();
463
- </script>
464
- </body>
465
- </html>`;
466
- }
467
- }
468
-
469
- new WebTerminalServer(PORT);
470
-
471
- process.on('SIGINT', () => process.exit(0));
472
- process.on('SIGTERM', () => process.exit(0));