feat: initial commit of next_term modern linux terminal

This commit is contained in:
2026-09-22 00:47:00 +08:00
commit c58c319805
20 changed files with 3120 additions and 0 deletions
+291
View File
@@ -0,0 +1,291 @@
import { themeManager } from './theme-manager.js';
import { TabManager } from './tab-manager.js';
import { CommandPalette } from './command-palette.js';
import { SettingsModal } from './settings.js';
class TerminalApp {
constructor() {
this.initElements();
this.initTheme();
this.initTabManager();
this.initModals();
this.initSearch();
this.initWindowControls();
this.initIpcListeners();
this.initKeyboardShortcuts();
this.initStatusBar();
// 创建初始标签页
this.tabManager.createTab();
}
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,
onActiveChange: (activeTerm) => {
if (activeTerm && this.statusShell) {
this.statusShell.textContent = activeTerm.title || 'bash';
}
},
});
this.newTabBtn.addEventListener('click', () => {
this.tabManager.createTab();
});
}
initModals() {
this.commandPalette = new CommandPalette({
overlay: this.commandPaletteOverlay,
input: this.commandPaletteInput,
list: this.commandPaletteList,
tabManager: this.tabManager,
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.splitCurrent('horizontal');
});
document.getElementById('btn-split-v')?.addEventListener('click', () => {
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('vertical');
});
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();
}
}
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();
});
}
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();
this.commandPalette.toggle();
return;
}
// Ctrl+Shift+T: 新建标签
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
this.tabManager.createTab();
return;
}
// Ctrl+Shift+W: 关闭窗格
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'w') {
e.preventDefault();
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();
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('horizontal');
return;
}
// Ctrl+Shift+O: 垂直分屏 (上下)
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'o') {
e.preventDefault();
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('vertical');
return;
}
// Ctrl+Shift+F: 搜索
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'f') {
e.preventDefault();
this.toggleSearch();
return;
}
// Ctrl+,: 设置
if (e.ctrlKey && e.key === ',') {
e.preventDefault();
this.settings.open();
return;
}
// Ctrl+Tab / Ctrl+Shift+Tab: 标签页切换
if (e.ctrlKey && e.key === 'Tab') {
e.preventDefault();
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();
const index = parseInt(e.key, 10) - 1;
const ids = Array.from(this.tabManager.tabs.keys());
if (ids[index]) {
this.tabManager.switchTab(ids[index]);
}
}
});
}
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', () => {
new TerminalApp();
});