feat: initial commit of next_term modern linux terminal
This commit is contained in:
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user