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.
- package/package.json +3 -4
- package/server.js +28 -19
- package/index.js +0 -472
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termites",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "Local multi-terminal manager with web interface",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "server.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"start": "node
|
|
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
|
-
|
|
29
|
-
|
|
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 (
|
|
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">×</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));
|