From 470781705375202ca41296e5c988104c1298a49e Mon Sep 17 00:00:00 2001 From: zzzjy Date: Thu, 24 Sep 2026 01:48:43 +0800 Subject: [PATCH] fix: resolve new tab creation failure and shortcut passthrough - Fix pty.spawn cwd tilde expansion: ptyManager now passes resolvedCwd (expanding ~ and verifying directory existence) so child processes do not exit immediately on launch - Add pointer-events: none to SVGs inside buttons and tabs to prevent Electron window-dragging interception - Extend xterm attachCustomKeyEventHandler to allow Ctrl+Shift+T, Ctrl+Shift+W, etc. through while terminal is focused - Wrap remote SSH and serial spawns with fallback to local bash to prevent tab death loops on connection failures --- src/main/pty-manager.cjs | 19 ++++++++++++++++--- src/renderer/css/app.css | 13 ++++++++++++- src/renderer/css/tabs.css | 17 +++++++++++++++-- src/renderer/js/app.js | 1 + src/renderer/js/tab-manager.js | 10 ++++++++-- src/renderer/js/terminal-view.js | 28 +++++++++++++++++++++++++++- src/renderer/js/workspace-manager.js | 10 ++++++---- 7 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/main/pty-manager.cjs b/src/main/pty-manager.cjs index 3ebb90a..78ed165 100644 --- a/src/main/pty-manager.cjs +++ b/src/main/pty-manager.cjs @@ -16,8 +16,21 @@ class PtyManager { try { let resolvedCwd = cwd || process.env.HOME || process.cwd(); - if (typeof resolvedCwd === 'string' && resolvedCwd.startsWith('~')) { - resolvedCwd = path.join(os.homedir(), resolvedCwd.slice(1)); + if (typeof resolvedCwd === 'string') { + if (resolvedCwd === '~') { + resolvedCwd = os.homedir(); + } else if (resolvedCwd.startsWith('~/') || resolvedCwd.startsWith('~\\')) { + resolvedCwd = path.join(os.homedir(), resolvedCwd.slice(2)); + } + } + + const fs = require('fs'); + try { + if (!resolvedCwd || !fs.existsSync(resolvedCwd) || !fs.statSync(resolvedCwd).isDirectory()) { + resolvedCwd = os.homedir() || process.cwd(); + } + } catch (e) { + resolvedCwd = os.homedir() || process.cwd(); } const targetShell = shell || this.defaultShell; @@ -27,7 +40,7 @@ class PtyManager { name: 'xterm-256color', cols, rows, - cwd, + cwd: resolvedCwd, env: { ...process.env, TERM: 'xterm-256color', diff --git a/src/renderer/css/app.css b/src/renderer/css/app.css index b9857d9..04f106f 100644 --- a/src/renderer/css/app.css +++ b/src/renderer/css/app.css @@ -100,6 +100,11 @@ body { flex-shrink: 0; } +.btn-icon svg, +.btn-icon svg * { + pointer-events: none !important; +} + .btn-icon:hover { background: var(--btn-hover, rgba(255, 255, 255, 0.08)); color: var(--text-bright); @@ -115,7 +120,7 @@ body { align-items: center; gap: 2px; margin-left: 6px; - -webkit-app-region: no-drag; + -webkit-app-region: no-drag !important; } .win-btn { @@ -130,6 +135,12 @@ body { justify-content: center; cursor: pointer; transition: all 0.15s ease; + -webkit-app-region: no-drag !important; +} + +.win-btn svg, +.win-btn svg * { + pointer-events: none !important; } .win-btn:hover { diff --git a/src/renderer/css/tabs.css b/src/renderer/css/tabs.css index cd65761..9734e05 100644 --- a/src/renderer/css/tabs.css +++ b/src/renderer/css/tabs.css @@ -6,7 +6,7 @@ overflow-x: auto; scrollbar-width: none; max-width: calc(100vw - 320px); - -webkit-app-region: no-drag; + -webkit-app-region: no-drag !important; } .tab-bar::-webkit-scrollbar { @@ -30,6 +30,12 @@ white-space: nowrap; max-width: 180px; min-width: 90px; + -webkit-app-region: no-drag !important; +} + +.tab-item svg, +.tab-item svg * { + pointer-events: none !important; } .tab-item:hover { @@ -66,6 +72,8 @@ border-radius: 50%; opacity: 0; transition: all 0.12s ease; + -webkit-app-region: no-drag !important; + pointer-events: auto !important; } .tab-item:hover .tab-close, @@ -92,7 +100,12 @@ cursor: pointer; transition: all 0.15s ease; flex-shrink: 0; - -webkit-app-region: no-drag; + -webkit-app-region: no-drag !important; +} + +.new-tab-btn svg, +.new-tab-btn svg * { + pointer-events: none !important; } .new-tab-btn:hover { diff --git a/src/renderer/js/app.js b/src/renderer/js/app.js index 58ee3e2..f9dd59a 100644 --- a/src/renderer/js/app.js +++ b/src/renderer/js/app.js @@ -54,6 +54,7 @@ class TerminalApp { this.tabManager = new TabManager({ tabBar: this.tabBar, viewport: this.viewport, + getSpawnOptions: (wsId) => (this.workspaceManager ? this.workspaceManager.getSpawnOptions(wsId) : {}), onActiveChange: (activeTerm) => { if (activeTerm && this.statusShell) { this.statusShell.textContent = activeTerm.title || 'bash'; diff --git a/src/renderer/js/tab-manager.js b/src/renderer/js/tab-manager.js index 6c88b8f..789fc08 100644 --- a/src/renderer/js/tab-manager.js +++ b/src/renderer/js/tab-manager.js @@ -4,6 +4,7 @@ export class TabManager { constructor(options = {}) { this.tabBar = options.tabBar; this.viewport = options.viewport; + this.getSpawnOptions = options.getSpawnOptions || (() => ({})); this.onActiveChange = options.onActiveChange || (() => {}); this.onTabListChange = options.onTabListChange || (() => {}); @@ -49,10 +50,15 @@ export class TabManager { } createTab(options = {}) { - const title = typeof options === 'string' ? options : (options.title || 'bash'); const workspaceId = typeof options === 'object' && options.workspaceId ? options.workspaceId : this.activeWorkspaceId; + const defaultSpawn = this.getSpawnOptions(workspaceId) || {}; + const title = typeof options === 'string' ? options : (options.title || defaultSpawn.title || 'bash'); const spawnOptions = typeof options === 'object' && options.spawnOptions ? options.spawnOptions : ( - typeof options === 'object' ? { cwd: options.cwd, shell: options.shell, args: options.args } : {} + typeof options === 'object' ? { + cwd: options.cwd !== undefined ? options.cwd : defaultSpawn.cwd, + shell: options.shell !== undefined ? options.shell : defaultSpawn.shell, + args: options.args !== undefined ? options.args : defaultSpawn.args, + } : defaultSpawn ); const tabId = 'tab_' + Math.random().toString(36).substring(2, 9); diff --git a/src/renderer/js/terminal-view.js b/src/renderer/js/terminal-view.js index a6fc868..2852a21 100644 --- a/src/renderer/js/terminal-view.js +++ b/src/renderer/js/terminal-view.js @@ -75,11 +75,32 @@ export class TerminalView { console.warn('WebGL addon not supported, fallback to default Canvas renderer:', e); } - // 自定义按键过滤:允许应用级全局快捷键(如 Ctrl+B 切换侧边栏)穿透 + // 自定义按键过滤:允许应用级全局快捷键穿透终端聚焦拦截 this.xterm.attachCustomKeyEventHandler((event) => { + // Ctrl+B: 侧边栏 if (event.ctrlKey && !event.shiftKey && !event.altKey && event.key.toLowerCase() === 'b') { return false; } + // Ctrl+Shift+T / W / E / O / F / P + if (event.ctrlKey && event.shiftKey && ['t', 'w', 'e', 'o', 'f', 'p'].includes(event.key.toLowerCase())) { + return false; + } + // F1: 命令面板 + if (event.key === 'F1') { + return false; + } + // Ctrl+,: 设置 + if (event.ctrlKey && !event.shiftKey && !event.altKey && event.key === ',') { + return false; + } + // Ctrl+Tab / Ctrl+Shift+Tab + if (event.ctrlKey && event.key === 'Tab') { + return false; + } + // Alt+1 .. Alt+9 + if (event.altKey && event.key >= '1' && event.key <= '9') { + return false; + } return true; }); @@ -126,12 +147,17 @@ export class TerminalView { shell: this.shell, args: this.args, }).then((res) => { + if (res && res.error) { + this.xterm.writeln(`\r\n\x1b[31m[创建终端进程失败: ${res.error}]\x1b[0m\r\n`); + } if (res && res.shell) { if (!this.title || this.title === 'bash') { this.title = res.shell.split('/').pop() || 'bash'; this.onTitleChange(this.title); } } + }).catch((err) => { + this.xterm.writeln(`\r\n\x1b[31m[启动终端异常: ${err.message}]\x1b[0m\r\n`); }); // 聚焦 diff --git a/src/renderer/js/workspace-manager.js b/src/renderer/js/workspace-manager.js index c032652..c8d9fc4 100644 --- a/src/renderer/js/workspace-manager.js +++ b/src/renderer/js/workspace-manager.js @@ -229,9 +229,10 @@ export class WorkspaceManager { const targetHost = ws.user ? `${ws.user}@${ws.host}` : ws.host; const portArgs = ws.port && Number(ws.port) !== 22 ? ['-p', String(ws.port)] : []; const extra = ws.extraArgs ? ws.extraArgs.split(' ').filter(Boolean) : []; + const sshCmd = `ssh ${[...portArgs, ...extra, targetHost].join(' ')}`; return { - shell: 'ssh', - args: [...portArgs, ...extra, targetHost], + shell: '/bin/bash', + args: ['-c', `${sshCmd} || (echo -e "\\n\\x1b[33m[SSH 会话断开或连接失败,已保留本地交互终端]\\x1b[0m" && exec bash)`], title: `ssh: ${ws.name || targetHost}`, }; } @@ -239,9 +240,10 @@ export class WorkspaceManager { if (ws.type === WORKSPACE_TYPES.SERIAL) { const baud = String(ws.baudRate || 115200); const dev = ws.device || '/dev/ttyUSB0'; + const tool = ws.tool || 'picocom'; return { - shell: 'picocom', - args: ['-b', baud, dev], + shell: '/bin/bash', + args: ['-c', `which ${tool} >/dev/null 2>&1 && ${tool} -b ${baud} ${dev} || (echo -e "\\n\\x1b[33m[串口工具 ${tool} 未安装或 ${dev} 无法连接,已保留本地交互终端]\\x1b[0m" && exec bash)`], title: `serial: ${dev}`, }; }