quick-ssh-new 1.0.0 → 1.0.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/Quick-SSH.psm1 CHANGED
@@ -1,4 +1,4 @@
1
- # Quick-SSH.psm1 - PowerShell SSH Connection Manager
1
+ # Quick-SSH.psm1 - PowerShell SSH Connection Manager
2
2
  # 仿 Docker 命令行风格的 SSH 连接管理工具
3
3
  # 配置文件路径: %USERPROFILE%\.quickssh\hosts.json
4
4
 
@@ -6,8 +6,10 @@
6
6
  # 内部函数 - 配置管理
7
7
  # ============================================================
8
8
 
9
- $Script:ConfigDir = Join-Path $env:USERPROFILE ".quickssh"
10
- $Script:ConfigFile = Join-Path $Script:ConfigDir "hosts.json"
9
+ $Script:ConfigDir = Join-Path $env:USERPROFILE ".quickssh"
10
+ $Script:ConfigFile = Join-Path $Script:ConfigDir "hosts.json"
11
+ $Script:ModuleRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
12
+ $Script:TUIScript = Join-Path $Script:ModuleRoot "qssh-tui.js"
11
13
 
12
14
  # 初始化配置目录和空 JSON 文件
13
15
  function Initialize-QuickSSHConfig {
@@ -162,7 +164,7 @@ function Invoke-QuickSSHConnect {
162
164
  param([string]$Alias)
163
165
 
164
166
  if (-not $Alias) {
165
- Show-QuickSSHHelp
167
+ Invoke-QuickSSHTUI
166
168
  return
167
169
  }
168
170
 
@@ -248,6 +250,29 @@ function Invoke-QuickSSHImport {
248
250
  Write-QSSuccess "✔ 导入完成:新增 $added 个,跳过 $skipped 个(别名重复)。"
249
251
  }
250
252
 
253
+ # ============================================================
254
+ # TUI 终端界面
255
+ # ============================================================
256
+
257
+ function Invoke-QuickSSHTUI {
258
+ # 检测 Node.js 是否可用
259
+ $nodePath = (Get-Command "node" -ErrorAction SilentlyContinue).Source
260
+ if (-not $nodePath) {
261
+ Write-QSError "错误:启动 TUI 需要 Node.js,请先安装 Node.js (https://nodejs.org)"
262
+ Write-QSError "或者使用命令行模式: qssh help"
263
+ return
264
+ }
265
+
266
+ if (-not (Test-Path $Script:TUIScript)) {
267
+ Write-QSError "错误:未找到 TUI 脚本: $Script:TUIScript"
268
+ return
269
+ }
270
+
271
+ # 启动 TUI,等待退出后返回
272
+ Write-Host "正在启动 Quick-SSH TUI ..." -ForegroundColor Cyan
273
+ & "node" $Script:TUIScript
274
+ }
275
+
251
276
  # ============================================================
252
277
  # 帮助信息
253
278
  # ============================================================
@@ -257,6 +282,7 @@ function Show-QuickSSHHelp {
257
282
  Write-Host "Quick-SSH - PowerShell SSH 连接管理工具" -ForegroundColor Cyan
258
283
  Write-Host ""
259
284
  Write-Host "用法:" -ForegroundColor Yellow
285
+ Write-Host " qssh 启动 TUI 终端界面(推荐,类似 yazi 操作体验)"
260
286
  Write-Host " qssh ps [关键词] 列出所有已保存的 SSH 连接(对应 docker ps)"
261
287
  Write-Host " qssh add <别名> <用户@主机:端口> [--key <私钥路径>]" -ForegroundColor Gray
262
288
  Write-Host " 添加新 SSH 连接(端口默认 22,私钥默认 ~/.ssh/id_rsa)"
@@ -267,6 +293,7 @@ function Show-QuickSSHHelp {
267
293
  Write-Host " qssh help 显示本帮助信息"
268
294
  Write-Host ""
269
295
  Write-Host "示例:" -ForegroundColor Yellow
296
+ Write-Host " qssh # 启动 TUI 界面"
270
297
  Write-Host " qssh ps"
271
298
  Write-Host " qssh ps 生产"
272
299
  Write-Host " qssh add my-server root@192.168.1.100:22 --key D:\.ssh\id_rsa"
@@ -295,7 +322,7 @@ function global:qssh {
295
322
  Initialize-QuickSSHConfig
296
323
 
297
324
  if (-not $Command) {
298
- Show-QuickSSHHelp
325
+ Invoke-QuickSSHTUI
299
326
  return
300
327
  }
301
328
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quick-ssh-new",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "🚀 Quick-SSH - 仿 Docker 命令行风格的 PowerShell SSH 连接管理工具",
5
5
  "keywords": [
6
6
  "ssh",
@@ -31,5 +31,8 @@
31
31
  "os": [
32
32
  "win32"
33
33
  ],
34
- "private": false
34
+ "private": false,
35
+ "dependencies": {
36
+ "blessed": "^0.1.81"
37
+ }
35
38
  }
package/qssh-tui.js ADDED
@@ -0,0 +1,614 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * qssh-tui.js - Quick-SSH 终端用户界面 (TUI)
4
+ * 类似 yazi 的布局 + Vim 操作键位
5
+ *
6
+ * 布局:
7
+ * ┌─ 标题栏 ─────────────────────────────────┐
8
+ * │ 连接列表 (60%) │ 详情面板 (40%) │
9
+ * │ ● myserver │ 别名: myserver │
10
+ * │ ○ devbox │ 主机: 192.168.1.100│
11
+ * │ ... │ ... │
12
+ * ├─ 状态栏 ─────────────────────────────────┤
13
+ * │ NORMAL j/k ↑↓ ↵连接 d删除 /搜索 q退出│
14
+ * └──────────────────────────────────────────┘
15
+ *
16
+ * 依赖: blessed (npm install blessed)
17
+ * 数据: %USERPROFILE%\.quickssh\hosts.json
18
+ */
19
+
20
+ const blessed = require("blessed");
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+ const { spawn } = require("child_process");
24
+
25
+ // ============================================================
26
+ // 配置
27
+ // ============================================================
28
+
29
+ const CONFIG_DIR = path.join(process.env.USERPROFILE || "~", ".quickssh");
30
+ const CONFIG_FILE = path.join(CONFIG_DIR, "hosts.json");
31
+
32
+ // ============================================================
33
+ // 数据层
34
+ // ============================================================
35
+
36
+ function ensureConfig() {
37
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ if (!fs.existsSync(CONFIG_FILE)) fs.writeFileSync(CONFIG_FILE, "[]", "utf-8");
39
+ }
40
+
41
+ function loadHosts() {
42
+ ensureConfig();
43
+ try {
44
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8").trim();
45
+ return raw ? JSON.parse(raw) : [];
46
+ } catch { return []; }
47
+ }
48
+
49
+ function saveHosts(hosts) {
50
+ ensureConfig();
51
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(hosts, null, 4), "utf-8");
52
+ }
53
+
54
+ // ============================================================
55
+ // SSH 连接
56
+ // ============================================================
57
+
58
+ function sshConnect(host, cbReturn) {
59
+ const sshExe = "ssh.exe";
60
+ const args = [
61
+ "-i", host.key,
62
+ "-p", String(host.port),
63
+ "-o", "HostKeyAlgorithms=+ssh-rsa",
64
+ `${host.user}@${host.host}`,
65
+ ];
66
+
67
+ screen.destroy();
68
+ process.stdin.removeAllListeners("data");
69
+
70
+ process.stdout.write(`\n\x1b[32m正在连接到 '${host.alias}' (${host.user}@${host.host}:${host.port}) ...\x1b[0m\n\n`);
71
+
72
+ const child = spawn(sshExe, args, {
73
+ stdio: "inherit",
74
+ shell: true,
75
+ });
76
+
77
+ child.on("exit", (code) => {
78
+ process.stdout.write(`\n\x1b[33mSSH 会话已结束 (退出码: ${code})\x1b[0m\n`);
79
+ process.stdout.write(`\x1b[32m按 Enter 键返回 Quick-SSH TUI...\x1b[0m`);
80
+ process.stdin.setRawMode(false);
81
+ process.stdin.once("data", () => cbReturn());
82
+ });
83
+ }
84
+
85
+ // ============================================================
86
+ // TUI 组件
87
+ // ============================================================
88
+
89
+ let screen;
90
+ let headerBar;
91
+ let listBox;
92
+ let detailBox;
93
+ let statusBar;
94
+ let inputBox;
95
+ let helpBox;
96
+ let confirmBox;
97
+
98
+ let hosts = [];
99
+ let filteredHosts = [];
100
+ let filterText = "";
101
+
102
+ // 模式管理
103
+ const MODE = { NORMAL: 0, SEARCH: 1, ADD: 2, EXPORT: 3, IMPORT: 4, CONFIRM: 5, HELP: 6 };
104
+ let currentMode = MODE.NORMAL;
105
+
106
+ // ============================================================
107
+ // UI 更新函数
108
+ // ============================================================
109
+
110
+ const MODE_LABELS = {
111
+ [MODE.NORMAL]: "{green-fg}{bold} NORMAL {/bold}{/green-fg}",
112
+ [MODE.SEARCH]: "{cyan-fg}{bold} SEARCH {/bold}{/cyan-fg}",
113
+ [MODE.ADD]: "{yellow-fg}{bold} ADD {/bold}{/yellow-fg}",
114
+ [MODE.EXPORT]: "{yellow-fg}{bold} EXPORT {/bold}{/yellow-fg}",
115
+ [MODE.IMPORT]: "{yellow-fg}{bold} IMPORT {/bold}{/yellow-fg}",
116
+ [MODE.CONFIRM]: "{red-fg}{bold} CONFIRM {/bold}{/red-fg}",
117
+ [MODE.HELP]: "{white-fg}{bold} HELP {/bold}{/white-fg}",
118
+ };
119
+
120
+ const MODE_HINTS = {
121
+ [MODE.NORMAL]: " j/k ↑↓ g首 G尾 ↵连接 d删除 a添加 /搜索 e导出 i导入 r重命名 ?帮助 q退出",
122
+ [MODE.SEARCH]: " 输入关键词过滤 Enter确认 Esc取消",
123
+ [MODE.ADD]: " 格式: 别名 用户@主机:端口 [--key 路径] Enter确认 Esc取消",
124
+ [MODE.EXPORT]: " 输入导出文件路径 (默认: ~/.quickssh/export.json) Enter确认 Esc取消",
125
+ [MODE.IMPORT]: " 输入导入文件路径 Enter确认 Esc取消",
126
+ [MODE.CONFIRM]: " y确认 n取消",
127
+ [MODE.HELP]: " 按任意键返回",
128
+ };
129
+
130
+ function setMode(mode, inputValue) {
131
+ currentMode = mode;
132
+ statusBar.setContent(`${MODE_LABELS[mode] || ""} ${MODE_HINTS[mode] || ""}`);
133
+
134
+ if (mode === MODE.NORMAL || mode === MODE.HELP) {
135
+ inputBox.hide();
136
+ confirmBox.hide();
137
+ if (mode === MODE.NORMAL) listBox.focus();
138
+ }
139
+
140
+ if (mode === MODE.SEARCH || mode === MODE.ADD || mode === MODE.EXPORT || mode === MODE.IMPORT) {
141
+ inputBox.show();
142
+ inputBox.setValue(inputValue || "");
143
+ inputBox.focus();
144
+ }
145
+
146
+ if (mode === MODE.CONFIRM) {
147
+ confirmBox.show();
148
+ }
149
+
150
+ screen.render();
151
+ }
152
+
153
+ function refreshList(keepSelection) {
154
+ hosts = loadHosts();
155
+ filteredHosts = filterText
156
+ ? hosts.filter(h =>
157
+ h.alias.toLowerCase().includes(filterText.toLowerCase()) ||
158
+ h.host.toLowerCase().includes(filterText.toLowerCase()) ||
159
+ h.user.toLowerCase().includes(filterText.toLowerCase())
160
+ )
161
+ : [...hosts];
162
+
163
+ const prevIdx = listBox.selected;
164
+ listBox.setItems(filteredHosts.map(h =>
165
+ `${h.alias.padEnd(16)} ${h.user}@${h.host}:${h.port}`
166
+ ));
167
+
168
+ if (keepSelection && prevIdx < filteredHosts.length) {
169
+ listBox.select(prevIdx);
170
+ listBox.scrollTo(prevIdx);
171
+ } else if (filteredHosts.length > 0) {
172
+ listBox.select(0);
173
+ listBox.scrollTo(0);
174
+ }
175
+
176
+ updateDetail();
177
+ headerBar.setContent(
178
+ `{bold} Quick-SSH {/bold}(${filteredHosts.length}/${hosts.length})`
179
+ );
180
+ screen.render();
181
+ }
182
+
183
+ function updateDetail() {
184
+ const idx = listBox.selected;
185
+ if (idx < 0 || idx >= filteredHosts.length) {
186
+ detailBox.setContent("{center}{yellow-fg}--- 无选中连接 ---{/yellow-fg}{/center}");
187
+ screen.render();
188
+ return;
189
+ }
190
+
191
+ const h = filteredHosts[idx];
192
+ detailBox.setContent(`
193
+ {bold}{cyan-fg} 连接详情{/cyan-fg}{/bold}
194
+
195
+ {bold}别名:{/bold} ${h.alias}
196
+ {bold}主机:{/bold} ${h.host}
197
+ {bold}账号:{/bold} ${h.user}
198
+ {bold}端口:{/bold} ${h.port}
199
+ {bold}私钥:{/bold} ${h.key || "(默认)"}
200
+
201
+ {blue-fg}─────────────────────{/blue-fg}
202
+ {green-fg} Enter → 连接{/green-fg}
203
+ {red-fg} d → 删除{/red-fg}
204
+ `);
205
+ screen.render();
206
+ }
207
+
208
+ // ============================================================
209
+ // 操作函数
210
+ // ============================================================
211
+
212
+ function connectSelected() {
213
+ const idx = listBox.selected;
214
+ if (idx < 0 || idx >= filteredHosts.length) return;
215
+ sshConnect(filteredHosts[idx], startTUI);
216
+ }
217
+
218
+ function deleteSelected() {
219
+ const idx = listBox.selected;
220
+ if (idx < 0 || idx >= filteredHosts.length) return;
221
+ const h = filteredHosts[idx];
222
+ confirmBox.setContent(
223
+ `{center}{red-fg}确认删除连接 '{h.alias}'? (y/n){/red-fg}{/center}`
224
+ );
225
+ setMode(MODE.CONFIRM);
226
+ }
227
+
228
+ function confirmDelete(confirmed) {
229
+ confirmBox.hide();
230
+ if (confirmed) {
231
+ const idx = listBox.selected;
232
+ const h = filteredHosts[idx];
233
+ hosts = hosts.filter(item => item.alias !== h.alias);
234
+ saveHosts(hosts);
235
+ refreshList();
236
+ flashMessage(`已删除连接 '${h.alias}'`, "green");
237
+ }
238
+ setMode(MODE.NORMAL);
239
+ }
240
+
241
+ function flashMessage(msg, color) {
242
+ statusBar.setContent(`{${color}-fg} ${msg}{/${color}-fg}`);
243
+ screen.render();
244
+ setTimeout(() => {
245
+ if (currentMode === MODE.NORMAL) setMode(MODE.NORMAL);
246
+ }, 3000);
247
+ }
248
+
249
+ // ============================================================
250
+ // 输入处理
251
+ // ============================================================
252
+
253
+ function handleInputSubmit(value) {
254
+ const val = value.trim();
255
+
256
+ switch (currentMode) {
257
+ case MODE.SEARCH: {
258
+ filterText = val;
259
+ inputBox.hide();
260
+ refreshList();
261
+ setMode(MODE.NORMAL);
262
+ break;
263
+ }
264
+ case MODE.ADD: {
265
+ inputBox.hide();
266
+ const parts = val.split(/\s+/);
267
+ if (parts.length < 2) {
268
+ flashMessage("格式错误: 需要 别名 用户@主机:端口", "red");
269
+ setMode(MODE.NORMAL);
270
+ break;
271
+ }
272
+ const alias = parts[0];
273
+ const userAtHost = parts[1];
274
+ let keyPath = "";
275
+ const ki = parts.indexOf("--key");
276
+ if (ki >= 0 && ki + 1 < parts.length) keyPath = parts[ki + 1];
277
+
278
+ let user = "", hostname = "", port = 22;
279
+ const m1 = userAtHost.match(/^(.+)@(.+):(\d+)$/);
280
+ const m2 = userAtHost.match(/^(.+)@(.+)$/);
281
+ if (m1) { user = m1[1]; hostname = m1[2]; port = parseInt(m1[3]); }
282
+ else if (m2) { user = m2[1]; hostname = m2[2]; }
283
+ else {
284
+ flashMessage("格式无效: 请使用 用户@主机:端口", "red");
285
+ setMode(MODE.NORMAL);
286
+ break;
287
+ }
288
+
289
+ if (!keyPath) keyPath = path.join(process.env.USERPROFILE || "~", ".ssh", "id_rsa");
290
+
291
+ if (hosts.find(h => h.alias === alias)) {
292
+ flashMessage(`别名 '${alias}' 已存在`, "red");
293
+ } else {
294
+ hosts.push({ alias, host: hostname, user, port, key: keyPath });
295
+ saveHosts(hosts);
296
+ refreshList(true);
297
+ flashMessage(`已添加 '${alias}' → ${user}@${hostname}:${port}`, "green");
298
+ }
299
+ setMode(MODE.NORMAL);
300
+ break;
301
+ }
302
+ case MODE.EXPORT: {
303
+ inputBox.hide();
304
+ const fp = val || path.join(CONFIG_DIR, "export.json");
305
+ try {
306
+ fs.writeFileSync(fp, JSON.stringify(hosts, null, 4), "utf-8");
307
+ flashMessage(`已导出 ${hosts.length} 个连接到 '${fp}'`, "green");
308
+ } catch (e) {
309
+ flashMessage(`导出失败: ${e.message}`, "red");
310
+ }
311
+ setMode(MODE.NORMAL);
312
+ break;
313
+ }
314
+ case MODE.IMPORT: {
315
+ inputBox.hide();
316
+ if (!val || !fs.existsSync(val)) {
317
+ flashMessage("文件不存在", "red");
318
+ setMode(MODE.NORMAL);
319
+ break;
320
+ }
321
+ try {
322
+ const imported = JSON.parse(fs.readFileSync(val, "utf-8"));
323
+ let added = 0, skipped = 0;
324
+ for (const h of imported) {
325
+ if (!hosts.find(e => e.alias === h.alias)) { hosts.push(h); added++; }
326
+ else { skipped++; }
327
+ }
328
+ saveHosts(hosts);
329
+ refreshList();
330
+ flashMessage(`导入完成: 新增 ${added}, 跳过 ${skipped}`, "green");
331
+ } catch (e) {
332
+ flashMessage(`导入失败: ${e.message}`, "red");
333
+ }
334
+ setMode(MODE.NORMAL);
335
+ break;
336
+ }
337
+ }
338
+ }
339
+
340
+ function handleInputCancel() {
341
+ inputBox.hide();
342
+ switch (currentMode) {
343
+ case MODE.SEARCH: setMode(MODE.NORMAL); break;
344
+ case MODE.ADD: setMode(MODE.NORMAL); break;
345
+ case MODE.EXPORT: setMode(MODE.NORMAL); break;
346
+ case MODE.IMPORT: setMode(MODE.NORMAL); break;
347
+ default: setMode(MODE.NORMAL); break;
348
+ }
349
+ }
350
+
351
+ // ============================================================
352
+ // 启动 TUI
353
+ // ============================================================
354
+
355
+ function startTUI() {
356
+ screen = blessed.screen({
357
+ smartCSR: true,
358
+ title: "Quick-SSH",
359
+ cursor: { artificial: true, shape: "underline" },
360
+ dockBorders: true,
361
+ fullUnicode: true,
362
+ });
363
+
364
+ // 标题栏
365
+ headerBar = blessed.box({
366
+ top: 0, left: 0, width: "100%", height: 1,
367
+ content: "{bold} Quick-SSH {/bold}",
368
+ tags: true,
369
+ style: { fg: "white", bg: "blue" },
370
+ });
371
+
372
+ // 连接列表
373
+ listBox = blessed.list({
374
+ top: 1, left: 0, width: "60%", height: "100%-2",
375
+ label: " 连接列表 ",
376
+ border: { type: "line", fg: "cyan" },
377
+ tags: true, keys: false, vi: false, mouse: true,
378
+ scrollbar: { ch: " ", bg: "cyan" },
379
+ style: {
380
+ fg: "white", bg: "black",
381
+ selected: { fg: "white", bg: "blue", bold: true },
382
+ item: { fg: "white", bg: "black" },
383
+ },
384
+ });
385
+
386
+ // 详情面板
387
+ detailBox = blessed.box({
388
+ top: 1, right: 0, width: "40%", height: "100%-2",
389
+ label: " 详情 ",
390
+ border: { type: "line", fg: "cyan" },
391
+ tags: true, style: { fg: "white", bg: "black" },
392
+ scrollable: true, alwaysScroll: true,
393
+ });
394
+
395
+ // 状态栏
396
+ statusBar = blessed.box({
397
+ bottom: 0, left: 0, width: "100%", height: 1,
398
+ tags: true, style: { fg: "white", bg: "black" },
399
+ });
400
+
401
+ // 输入框
402
+ inputBox = blessed.textbox({
403
+ top: "50%-3", left: "25%", width: "50%", height: 3,
404
+ label: " 输入 ",
405
+ border: { type: "line", fg: "cyan" },
406
+ hidden: true, keys: true, vi: false,
407
+ style: { fg: "white", bg: "black", border: { fg: "cyan" } },
408
+ });
409
+
410
+ // 确认框
411
+ confirmBox = blessed.box({
412
+ top: "50%-3", left: "30%", width: "40%", height: 3,
413
+ border: { type: "line", fg: "red" },
414
+ tags: true, hidden: true,
415
+ style: { fg: "white", bg: "black" },
416
+ });
417
+
418
+ // 帮助弹窗
419
+ helpBox = blessed.box({
420
+ top: "5%", left: "10%", width: "80%", height: "90%",
421
+ label: " 帮助 ",
422
+ border: { type: "line", fg: "yellow" },
423
+ tags: true, hidden: true, scrollable: true, alwaysScroll: true,
424
+ keys: true, vi: false,
425
+ style: { fg: "white", bg: "black" },
426
+ content: `
427
+ {bold}{yellow-fg}Quick-SSH TUI — 快捷键帮助{/yellow-fg}{/bold}
428
+
429
+ {cyan-fg}━━━━━━━━━ 导航 ━━━━━━━━━{/cyan-fg}
430
+ {green-fg}j{/green-fg} / {green-fg}↓{/green-fg} 向下移动
431
+ {green-fg}k{/green-fg} / {green-fg}↑{/green-fg} 向上移动
432
+ {green-fg}g{/green-fg} 跳转到第一个
433
+ {green-fg}G{/green-fg} 跳转到最后一个
434
+
435
+ {cyan-fg}━━━━━━━━━ 操作 ━━━━━━━━━{/cyan-fg}
436
+ {green-fg}Enter{/green-fg} 连接选中的服务器
437
+ {green-fg}d{/green-fg} 删除选中的连接
438
+ {green-fg}a{/green-fg} 添加新连接
439
+ {green-fg}/ {/green-fg} 搜索 / 筛选
440
+ {green-fg}Esc{/green-fg} 取消 / 返回普通模式
441
+
442
+ {cyan-fg}━━━━━━━━━ 导入导出 ━━━━━━━━━{/cyan-fg}
443
+ {green-fg}e{/green-fg} 导出全部配置
444
+ {green-fg}i{/green-fg} 从 JSON 文件导入
445
+
446
+ {cyan-fg}━━━━━━━━━ 其他 ━━━━━━━━━{/cyan-fg}
447
+ {green-fg}q{/green-fg} 退出 TUI
448
+ {green-fg}?{/green-fg} 显示本帮助
449
+
450
+ {white-fg}按任意键关闭帮助{/white-fg}
451
+ `
452
+ });
453
+
454
+ screen.append(headerBar);
455
+ screen.append(listBox);
456
+ screen.append(detailBox);
457
+ screen.append(statusBar);
458
+ screen.append(inputBox);
459
+ screen.append(confirmBox);
460
+ screen.append(helpBox);
461
+
462
+ // ============================================================
463
+ // 键位绑定
464
+ // ============================================================
465
+
466
+ // ----- 全局键 -----
467
+ screen.key(["C-c"], () => { screen.destroy(); process.exit(0); });
468
+
469
+ screen.key(["escape"], () => {
470
+ if (currentMode === MODE.HELP) {
471
+ helpBox.hide();
472
+ setMode(MODE.NORMAL);
473
+ } else if (currentMode === MODE.CONFIRM) {
474
+ confirmDelete(false);
475
+ } else if (currentMode !== MODE.NORMAL) {
476
+ handleInputCancel();
477
+ }
478
+ });
479
+
480
+ // ----- NORMAL 模式 -----
481
+ screen.key(["q"], () => {
482
+ if (currentMode === MODE.NORMAL) { screen.destroy(); process.exit(0); }
483
+ });
484
+
485
+ screen.key(["/"], () => {
486
+ if (currentMode === MODE.NORMAL) setMode(MODE.SEARCH, filterText);
487
+ });
488
+
489
+ screen.key(["a"], () => {
490
+ if (currentMode === MODE.NORMAL) setMode(MODE.ADD);
491
+ });
492
+
493
+ screen.key(["d"], () => {
494
+ if (currentMode === MODE.NORMAL) deleteSelected();
495
+ });
496
+
497
+ screen.key(["y"], () => {
498
+ if (currentMode === MODE.CONFIRM) confirmDelete(true);
499
+ });
500
+
501
+ screen.key(["n"], () => {
502
+ if (currentMode === MODE.CONFIRM) confirmDelete(false);
503
+ });
504
+
505
+ screen.key(["enter"], () => {
506
+ if (currentMode === MODE.NORMAL) connectSelected();
507
+ });
508
+
509
+ screen.key(["e"], () => {
510
+ if (currentMode === MODE.NORMAL) setMode(MODE.EXPORT, path.join(CONFIG_DIR, "export.json"));
511
+ });
512
+
513
+ screen.key(["i"], () => {
514
+ if (currentMode === MODE.NORMAL) setMode(MODE.IMPORT);
515
+ });
516
+
517
+ screen.key(["?", "h"], () => {
518
+ if (currentMode === MODE.NORMAL) { helpBox.show(); helpBox.focus(); setMode(MODE.HELP); }
519
+ });
520
+
521
+ // Vim 导航键
522
+ screen.key(["j", "down"], () => {
523
+ if (currentMode !== MODE.NORMAL) return;
524
+ if (listBox.selected < filteredHosts.length - 1) {
525
+ listBox.select(listBox.selected + 1);
526
+ listBox.scrollTo(listBox.selected);
527
+ updateDetail();
528
+ screen.render();
529
+ }
530
+ });
531
+
532
+ screen.key(["k", "up"], () => {
533
+ if (currentMode !== MODE.NORMAL) return;
534
+ if (listBox.selected > 0) {
535
+ listBox.select(listBox.selected - 1);
536
+ listBox.scrollTo(listBox.selected);
537
+ updateDetail();
538
+ screen.render();
539
+ }
540
+ });
541
+
542
+ screen.key(["g"], () => {
543
+ if (currentMode !== MODE.NORMAL) return;
544
+ if (filteredHosts.length > 0) {
545
+ listBox.select(0);
546
+ listBox.scrollTo(0);
547
+ updateDetail();
548
+ screen.render();
549
+ }
550
+ });
551
+
552
+ screen.key(["G"], () => {
553
+ if (currentMode !== MODE.NORMAL) return;
554
+ if (filteredHosts.length > 0) {
555
+ listBox.select(filteredHosts.length - 1);
556
+ listBox.scrollTo(filteredHosts.length - 1);
557
+ updateDetail();
558
+ screen.render();
559
+ }
560
+ });
561
+
562
+ // ----- 输入框事件 -----
563
+ inputBox.on("submit", handleInputSubmit);
564
+ inputBox.on("cancel", handleInputCancel);
565
+
566
+ // ----- 列表事件 -----
567
+ listBox.on("select", () => {
568
+ if (currentMode === MODE.NORMAL) connectSelected();
569
+ });
570
+
571
+ listBox.on("select item", () => { updateDetail(); });
572
+
573
+ // 鼠标滚轮
574
+ listBox.on("wheeldown", () => {
575
+ if (currentMode !== MODE.NORMAL) return;
576
+ if (listBox.selected < filteredHosts.length - 1) {
577
+ listBox.select(listBox.selected + 1);
578
+ listBox.scrollTo(listBox.selected);
579
+ updateDetail();
580
+ screen.render();
581
+ }
582
+ });
583
+
584
+ listBox.on("wheelup", () => {
585
+ if (currentMode !== MODE.NORMAL) return;
586
+ if (listBox.selected > 0) {
587
+ listBox.select(listBox.selected - 1);
588
+ listBox.scrollTo(listBox.selected);
589
+ updateDetail();
590
+ screen.render();
591
+ }
592
+ });
593
+
594
+ // ----- 帮助弹窗关闭 -----
595
+ helpBox.key(["escape", "q", "enter", "space"], () => {
596
+ helpBox.hide();
597
+ setMode(MODE.NORMAL);
598
+ });
599
+
600
+ // ============================================================
601
+ // 初始化
602
+ // ============================================================
603
+
604
+ refreshList();
605
+ listBox.focus();
606
+ setMode(MODE.NORMAL);
607
+ screen.render();
608
+ }
609
+
610
+ // ============================================================
611
+ // 入口
612
+ // ============================================================
613
+
614
+ startTUI();