import { themeManager } from './theme-manager.js'; import { TabManager } from './tab-manager.js'; import { WorkspaceManager } from './workspace-manager.js'; import { CommandPalette } from './command-palette.js'; import { SettingsModal } from './settings.js'; import { ContextMenu } from './context-menu.js'; class TerminalApp { constructor() { this.initElements(); this.initTheme(); this.initTabManager(); this.initWorkspaceManager(); this.initContextMenu(); this.initModals(); this.initSearch(); this.initWindowControls(); this.initIpcListeners(); this.initKeyboardShortcuts(); this.initStatusBar(); } initElements() { this.tabBar = document.getElementById('tab-bar'); this.viewport = document.getElementById('main-viewport'); this.newTabBtn = document.getElementById('btn-new-tab'); this.commandPaletteOverlay = document.getElementById('command-palette-overlay'); this.commandPaletteInput = document.getElementById('command-palette-input'); this.commandPaletteList = document.getElementById('command-palette-list'); this.settingsOverlay = document.getElementById('settings-overlay'); this.settingsModal = document.getElementById('settings-modal'); this.searchBar = document.getElementById('search-bar'); this.searchInput = document.getElementById('search-input'); this.searchClose = document.getElementById('search-close'); this.searchNext = document.getElementById('search-next'); this.searchPrev = document.getElementById('search-prev'); this.statusTheme = document.getElementById('status-theme'); this.statusShell = document.getElementById('status-shell'); this.statusHost = document.getElementById('status-host'); } initTheme() { themeManager.init(); if (this.statusTheme) { this.statusTheme.textContent = themeManager.getCurrentTheme().name; } themeManager.onThemeChange((theme) => { if (this.statusTheme) { this.statusTheme.textContent = theme.name; } }); } initTabManager() { 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'; } this.updateSplitButtonsState(activeTerm); }, onContextMenu: (e, term) => { this.contextMenu?.show(e, term); }, }); this.newTabBtn.addEventListener('click', () => { const activeWs = this.workspaceManager ? this.workspaceManager.getActiveWorkspace() : null; const spawnOpts = this.workspaceManager ? this.workspaceManager.getSpawnOptions(activeWs?.id) : {}; this.tabManager.createTab({ workspaceId: activeWs?.id, title: spawnOpts.title, cwd: spawnOpts.cwd, shell: spawnOpts.shell, args: spawnOpts.args, }); }); } updateSplitButtonsState(activeTerm) { const isAgent = !!( activeTerm?.isAgent || activeTerm?.element?.closest?.('.agent-panel') || activeTerm?.terminalWrapper?.closest?.('.agent-panel') ); const btnH = document.getElementById('btn-split-h'); const btnV = document.getElementById('btn-split-v'); if (btnH) { btnH.disabled = isAgent; btnH.classList.toggle('disabled', isAgent); btnH.title = isAgent ? 'Agent 面板不可分屏' : '水平分屏 (Ctrl+Shift+E)'; } if (btnV) { btnV.disabled = isAgent; btnV.classList.toggle('disabled', isAgent); btnV.title = isAgent ? 'Agent 面板不可分屏' : '垂直分屏 (Ctrl+Shift+O)'; } } initWorkspaceManager() { this.workspaceManager = new WorkspaceManager({ tabManager: this.tabManager, }); } initContextMenu() { this.contextMenu = new ContextMenu({ tabManager: this.tabManager, workspaceManager: this.workspaceManager, }); window.addEventListener('terminal-contextmenu', (e) => { this.contextMenu?.show(e.detail.event, e.detail.termView); }); } initModals() { this.commandPalette = new CommandPalette({ overlay: this.commandPaletteOverlay, input: this.commandPaletteInput, list: this.commandPaletteList, tabManager: this.tabManager, workspaceManager: this.workspaceManager, onOpenSettings: () => this.settings.open(), onToggleSearch: () => this.toggleSearch(), }); this.settings = new SettingsModal({ overlay: this.settingsOverlay, modal: this.settingsModal, tabManager: this.tabManager, }); // 快捷按钮绑定 document.getElementById('btn-palette')?.addEventListener('click', () => { this.commandPalette.toggle(); }); document.getElementById('btn-settings')?.addEventListener('click', () => { this.settings.open(); }); document.getElementById('btn-split-h')?.addEventListener('click', () => { const tab = this.tabManager.getActiveTab(); if (tab && !tab.splitTree.activeTerminal?.isAgent) { tab.splitTree.splitCurrent('horizontal'); } }); document.getElementById('btn-split-v')?.addEventListener('click', () => { const tab = this.tabManager.getActiveTab(); if (tab && !tab.splitTree.activeTerminal?.isAgent) { tab.splitTree.splitCurrent('vertical'); } }); document.getElementById('btn-file-browser')?.addEventListener('click', () => { this.toggleFileBrowser(); }); document.getElementById('btn-search')?.addEventListener('click', () => { this.toggleSearch(); }); document.getElementById('status-theme')?.addEventListener('click', () => { this.commandPalette.open(); this.commandPalette.input.value = '切换主题'; this.commandPalette.filter('切换主题'); }); } initSearch() { const doSearch = (direction = 'next') => { const term = this.tabManager.getActiveTerminal(); const query = this.searchInput.value; if (!term || !query) return; if (direction === 'next') { term.searchAddon.findNext(query, { incremental: true }); } else { term.searchAddon.findPrevious(query); } }; this.searchInput.addEventListener('input', () => doSearch('next')); this.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); doSearch(e.shiftKey ? 'prev' : 'next'); } else if (e.key === 'Escape') { e.preventDefault(); this.closeSearch(); } }); this.searchNext?.addEventListener('click', () => doSearch('next')); this.searchPrev?.addEventListener('click', () => doSearch('prev')); this.searchClose?.addEventListener('click', () => this.closeSearch()); } toggleSearch() { if (this.searchBar.classList.contains('active')) { this.closeSearch(); } else { this.searchBar.classList.add('active'); this.searchInput.value = ''; this.searchInput.focus(); } } closeSearch() { this.searchBar.classList.remove('active'); const term = this.tabManager.getActiveTerminal(); if (term) { term.searchAddon.clearDecorations(); term.focus(); } } toggleFileBrowser(initialDir = null) { const tab = this.tabManager?.getActiveTab(); if (!tab || !tab.splitTree) return null; return tab.splitTree.toggleFilePanel(initialDir); } initWindowControls() { document.getElementById('win-min')?.addEventListener('click', () => { window.terminalAPI.minimize(); }); document.getElementById('win-max')?.addEventListener('click', () => { window.terminalAPI.maximize(); }); document.getElementById('win-close')?.addEventListener('click', () => { window.terminalAPI.close(); }); // 双击标题栏空白处最大化/还原 document.getElementById('titlebar-drag-spacer')?.addEventListener('dblclick', () => { window.terminalAPI.maximize(); }); document.querySelector('.app-brand')?.addEventListener('dblclick', () => { window.terminalAPI.maximize(); }); } initIpcListeners() { // 监听 PTY 数据并派发给对应 terminal window.terminalAPI.onPtyData(({ id, data }) => { for (const tab of this.tabManager.tabs.values()) { const info = tab.splitTree.terminals.get(id); if (info) { info.termView.xterm.write(data); break; } } }); // 监听 PTY 退出事件 window.terminalAPI.onPtyExit(({ id, exitCode }) => { for (const tab of this.tabManager.tabs.values()) { if (tab.splitTree.terminals.has(id)) { tab.splitTree.closeTerminal(id); break; } } }); } initKeyboardShortcuts() { window.addEventListener( 'keydown', (e) => { // Ctrl+Shift+P 或 F1: 命令面板 if ((e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'p') || e.key === 'F1') { e.preventDefault(); e.stopPropagation(); this.commandPalette.toggle(); return; } // Ctrl+B: 切换左侧工作区侧边栏 if (e.ctrlKey && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'b') { e.preventDefault(); e.stopPropagation(); this.workspaceManager?.toggleSidebar(); return; } const isEditingText = (el) => { if (!el) return false; const tag = el.tagName?.toLowerCase(); return (tag === 'input' || tag === 'textarea') && !el.classList.contains('xterm-helper-textarea'); }; // Ctrl+Shift+C: 复制当前选中终端文本 if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'c') { if (isEditingText(e.target)) return; e.preventDefault(); e.stopPropagation(); const activeTerm = this.tabManager.getActiveTerminal(); if (activeTerm) activeTerm.copySelection(); return; } // Ctrl+Shift+V: 粘贴剪贴板文本到当前终端 if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'v') { if (isEditingText(e.target)) return; e.preventDefault(); e.stopPropagation(); const activeTerm = this.tabManager.getActiveTerminal(); if (activeTerm) activeTerm.pasteFromClipboard(); return; } // Ctrl+Shift+T: 新建当前工作区标签页 if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') { e.preventDefault(); e.stopPropagation(); const activeWs = this.workspaceManager ? this.workspaceManager.getActiveWorkspace() : null; const spawnOpts = this.workspaceManager ? this.workspaceManager.getSpawnOptions(activeWs?.id) : {}; this.tabManager.createTab({ workspaceId: activeWs?.id, title: spawnOpts.title, cwd: spawnOpts.cwd, shell: spawnOpts.shell, args: spawnOpts.args, }); return; } // Ctrl+Shift+W: 关闭窗格 if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'w') { e.preventDefault(); e.stopPropagation(); const tab = this.tabManager.getActiveTab(); if (tab && tab.splitTree.activeTerminal) { tab.splitTree.closeTerminal(tab.splitTree.activeTerminal.id); } return; } // Ctrl+Shift+E: 水平分屏 (左右) if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'e') { e.preventDefault(); e.stopPropagation(); const tab = this.tabManager.getActiveTab(); if (tab && !tab.splitTree.activeTerminal?.isAgent) { tab.splitTree.splitCurrent('horizontal'); } return; } // Ctrl+Shift+O: 垂直分屏 (上下) if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'o') { e.preventDefault(); e.stopPropagation(); const tab = this.tabManager.getActiveTab(); if (tab && !tab.splitTree.activeTerminal?.isAgent) { tab.splitTree.splitCurrent('vertical'); } return; } // Ctrl+Shift+F: 搜索 if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'f') { e.preventDefault(); e.stopPropagation(); this.toggleSearch(); return; } // Ctrl+Shift+U 或 Alt+F: 切换文件浏览面板 if ((e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'u') || (e.altKey && !e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'f')) { e.preventDefault(); e.stopPropagation(); this.toggleFileBrowser(); return; } // Ctrl+,: 设置 if (e.ctrlKey && e.key === ',') { e.preventDefault(); e.stopPropagation(); this.settings.open(); return; } // Ctrl+Tab / Ctrl+Shift+Tab: 标签页切换 if (e.ctrlKey && e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); if (e.shiftKey) { this.tabManager.prevTab(); } else { this.tabManager.nextTab(); } return; } // Alt+1 .. Alt+9: 快速直达标签页 if (e.altKey && e.key >= '1' && e.key <= '9') { e.preventDefault(); e.stopPropagation(); const index = parseInt(e.key, 10) - 1; const ids = Array.from(this.tabManager.tabs.keys()); if (ids[index]) { this.tabManager.switchTab(ids[index]); } } }, true // 启用捕获阶段,确保终端即使在聚焦/输入状态下应用级快捷键也能被优先拦截响应 ); } async initStatusBar() { try { const info = await window.terminalAPI.getSystemInfo(); if (info && this.statusHost) { this.statusHost.textContent = `${info.user}@${info.hostname}`; } } catch (e) { console.warn('Failed to load system info:', e); } } } // 页面加载就绪后启动终端 window.addEventListener('DOMContentLoaded', () => { window.terminalApp = new TerminalApp(); });