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
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
+83
View File
@@ -0,0 +1,83 @@
# next_term ⚡
> 一个超高颜值、极速响应、功能丰富的现代 Linux 终端模拟器。
基于 **Electron + 原生 PTY + xterm.js (WebGL 硬件加速)** 架构打造,兼备现代桌面应用的丝滑交互与系统底层的澎湃性能。
---
## ✨ 核心特性
- 🎨 **极致现代设计**:
- 沉浸式无边框磨砂毛玻璃窗口(Acrylic / Frosted Glass)。
- 内置 5 套精品现代主题(**Tokyo Night**、**Catppuccin Mocha**、**Dracula**、**One Dark**、**Cyberpunk**),支持全局即时热切换与调色板联动。
- 状态栏与标签页平滑微动效、发光焦点与光标呼吸闪烁。
- ⚡ **高性能 GPU 渲染**:
- 挂载 `@xterm/addon-webgl` 硬件加速管线,利用 GPU 直接进行字形栅格化与批处理渲染,轻松保持 60~120 FPS 丝滑帧率。
- 📑 **多标签页与多窗格分屏**:
- **多标签页(Tabs)**:无上限新建、关闭与快捷切换,自动提取进程标题。
- **多窗格分屏(Split Panes)**:支持向右水平分屏(Split Right)与向下垂直分屏(Split Down),每个窗格独立持有 Linux PTY 会话,焦点自由切换。
- 🧭 **Command Palette 快速命令面板**:
- 按下 `Ctrl+Shift+P` 或 `F1` 唤出类似 VS Code / Sublime 的命令搜索面板,支持键盘模糊搜索主题、分屏、快捷操作。
- 🔍 **终端内搜索**:
- 按下 `Ctrl+Shift+F` 唤起悬浮搜索框,支持高亮匹配、上一个/下一个遍历与即时跳转。
- ⚙️ **丰富的偏好配置**:
- 支持调整字号(11px ~ 24px)、字体族(JetBrains Mono / Fira Code 等)、光标形状(Block / Bar / Underline)、光标闪烁,配置自动本地持久化。
---
## ⌨️ 常用快捷键
| 快捷键 | 功能说明 |
| :--- | :--- |
| `Ctrl + Shift + P` / `F1` | 呼出快速命令面板 (Command Palette) |
| `Ctrl + Shift + T` | 新建标签页 (New Tab) |
| `Ctrl + Shift + W` | 关闭当前窗格 / 标签页 (Close Pane / Tab) |
| `Ctrl + Shift + E` | 向右水平分屏 (Split Right) |
| `Ctrl + Shift + O` | 向下垂直分屏 (Split Down) |
| `Ctrl + Shift + F` | 唤出终端内快速搜索 (Search Buffer) |
| `Ctrl + Tab` / `Ctrl + Shift + Tab` | 切换到下一个 / 上一个标签页 |
| `Alt + 1` ~ `Alt + 9` | 快速直达第 1 ~ 9 个标签页 |
| `Ctrl + ,` | 打开偏好设置 (Preferences) |
---
## 🚀 启动与构建
```bash
# 启动开发终端
npm start
# 若 Node ABI 变动,重新编译底层 node-pty
npm run rebuild
```
---
## 🛠️ 架构分层
```
next_term/
├── src/
│ ├── main/
│ │ ├── index.cjs # Electron 主进程,无边框透明窗口与 IPC 路由
│ │ └── pty-manager.cjs # 基于 node-pty 的 Linux 伪终端管理与进程调度
│ ├── preload/
│ │ └── index.cjs # 安全 ContextBridge 预加载隔离脚本
│ └── renderer/
│ ├── index.html # 现代终端 UI DOM 结构
│ ├── css/
│ │ ├── variables.css # 主题调色板与设计 Token
│ │ ├── app.css # 全局视口、标题栏、控制按钮
│ │ ├── tabs.css # 标签栏微动效与样式
│ │ ├── split.css # 窗格分屏容器与焦点高亮
│ │ └── components.css # 状态栏、搜索条、命令面板与设置弹窗
│ └── js/
│ ├── app.js # 前端总控中枢
│ ├── terminal-view.js # xterm + WebGL + FitAddon 实例管理
│ ├── tab-manager.js # 标签页生命周期
│ ├── split-manager.js # 多窗格分屏树与动态尺寸自适应
│ ├── theme-manager.js # 主题系统与 xterm 颜色映射
│ ├── command-palette.js # 快速命令面板交互
│ └── settings.js # 偏好设置及本地存储
```
+626
View File
@@ -0,0 +1,626 @@
{
"name": "next_term",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "next_term",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"lucide": "^1.47.0",
"node-pty": "^1.1.0"
},
"devDependencies": {
"@electron/rebuild": "^4.2.0",
"electron": "^44.4.3"
}
},
"node_modules/@electron-internal/extract-zip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz",
"integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz",
"integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^3.0.0",
"graceful-fs": "^4.2.11",
"progress": "^2.0.3",
"semver": "^7.6.3",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"optionalDependencies": {
"undici": "^7.24.4"
}
},
"node_modules/@electron/rebuild": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz",
"integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.1.1",
"node-abi": "^4.2.0",
"node-api-version": "^0.2.1",
"node-gyp": "^12.2.0",
"read-binary-file-arch": "^1.0.6"
},
"bin": {
"electron-rebuild": "lib/cli.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
"integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/malept"
},
{
"type": "tidelift",
"url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
}
],
"license": "Apache-2.0",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/@types/node": {
"version": "24.13.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz",
"integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
"license": "MIT"
},
"node_modules/@xterm/addon-search": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0.tgz",
"integrity": "sha512-9OeuBFu0/uZJPu+9AHKY6g/w0Czyb/Ut0A5t79I4ULoU4IfU5BEpPFVGQxP4zTTMdfZEYkVIRYbHBX1xWwjeSA==",
"license": "MIT"
},
"node_modules/@xterm/addon-web-links": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
"license": "MIT"
},
"node_modules/@xterm/addon-webgl": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz",
"integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==",
"license": "MIT"
},
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/abbrev": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
"integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/electron": {
"version": "44.4.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-44.4.3.tgz",
"integrity": "sha512-LTpSFTB40qVCXIX5xMo+cgHI/Jjkbjw7VpB26PccEbroqOn72LBukeaDwPVo1fBYzSzs0c9iPuAucFCO7Tw81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-internal/extract-zip": "^1.0.1",
"@electron/get": "^5.0.0",
"@types/node": "^24.9.0"
},
"bin": {
"electron": "cli.js",
"install-electron": "install.js"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/env-paths": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/exponential-backoff": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/lucide": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/lucide/-/lucide-1.47.0.tgz",
"integrity": "sha512-gJoVJM5fvpBDGZTvhpJS9IEurz85HbD+wgOlCkXCIw35tHrqTxSdIYUN9sXvs7Aw3NCl67A+nlotNyoU4u7zOQ==",
"license": "ISC"
},
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/node-abi": {
"version": "4.35.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz",
"integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.6.3"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-api-version": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
"integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
}
},
"node_modules/node-gyp": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz",
"integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"env-paths": "^2.2.0",
"exponential-backoff": "^3.1.1",
"graceful-fs": "^4.2.6",
"nopt": "^9.0.0",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"tar": "^7.5.4",
"tinyglobby": "^0.2.12",
"undici": "^6.25.0",
"which": "^6.0.0"
},
"bin": {
"node-gyp": "bin/node-gyp.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/node-gyp/node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/node-gyp/node_modules/isexe": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=20"
}
},
"node_modules/node-gyp/node_modules/undici": {
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz",
"integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/node-gyp/node_modules/which": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^4.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.1.0"
}
},
"node_modules/nopt": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
"integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
"dev": true,
"license": "ISC",
"dependencies": {
"abbrev": "^4.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/proc-log": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
"integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"bin": {
"read-binary-file-arch": "cli.js"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/tar": {
"version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/undici": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
"integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "next_term",
"version": "1.0.0",
"description": "",
"main": "src/main/index.cjs",
"scripts": {
"start": "electron .",
"rebuild": "electron-rebuild -f -w node-pty"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"lucide": "^1.47.0",
"node-pty": "^1.1.0"
},
"devDependencies": {
"@electron/rebuild": "^4.2.0",
"electron": "^44.4.3"
}
}
+143
View File
@@ -0,0 +1,143 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const os = require('os');
const { PtyManager } = require('./pty-manager.cjs');
let mainWindow = null;
let ptyManager = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1060,
height: 680,
minWidth: 600,
minHeight: 400,
frame: false, // 无边框现代窗口
transparent: true,
backgroundColor: '#00000000', // 允许 CSS 毛玻璃及半透明背景
hasShadow: true,
titleBarStyle: 'hidden',
webPreferences: {
preload: path.join(__dirname, '../preload/index.cjs'),
nodeIntegration: false,
contextIsolation: true,
spellcheck: false,
backgroundThrottling: false,
},
});
ptyManager = new PtyManager(mainWindow);
mainWindow.webContents.on('console-message', (event, ...args) => {
const params = typeof args[0] === 'object' && args[0] !== null ? args[0] : { level: args[0], message: args[1], sourceId: args[3], line: args[2] };
if (params.message && !params.message.includes('task queue exceeded')) {
console.log(`[Renderer] ${params.message}`);
}
});
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error(`[Load Error] ${errorCode}: ${errorDescription}`);
});
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
mainWindow.on('maximize', () => {
mainWindow.webContents.send('window:maximized-change', true);
});
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send('window:maximized-change', false);
});
mainWindow.on('closed', () => {
if (ptyManager) {
ptyManager.killAll();
}
mainWindow = null;
});
}
// 窗口控制 IPC
ipcMain.on('window:minimize', () => {
if (mainWindow) mainWindow.minimize();
});
ipcMain.on('window:maximize', () => {
if (mainWindow) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.on('window:close', () => {
if (mainWindow) mainWindow.close();
});
ipcMain.handle('window:is-maximized', () => {
return mainWindow ? mainWindow.isMaximized() : false;
});
// PTY IPC
ipcMain.handle('pty:create', (event, options) => {
if (!ptyManager) return { success: false, error: 'PtyManager not ready' };
return ptyManager.create(options);
});
ipcMain.on('pty:write', (event, { id, data }) => {
if (ptyManager) ptyManager.write({ id, data });
});
ipcMain.on('pty:resize', (event, { id, cols, rows }) => {
if (ptyManager) ptyManager.resize({ id, cols, rows });
});
ipcMain.on('pty:kill', (event, { id }) => {
if (ptyManager) ptyManager.kill({ id });
});
ipcMain.handle('app:get-system-info', () => {
return {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
shell: process.env.SHELL || '/bin/bash',
user: os.userInfo().username,
hostname: os.hostname(),
};
});
// 增强 Linux GPU 稳定性
app.commandLine.appendSwitch('disable-gpu-sandbox');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.whenReady().then(() => {
createWindow();
if (process.env.TEST_EXIT) {
console.log('[Test Mode] Window created successfully, auto-exiting in 3s...');
setTimeout(() => {
app.quit();
}, 3000);
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
if (ptyManager) {
ptyManager.killAll();
}
});
+92
View File
@@ -0,0 +1,92 @@
const pty = require('node-pty');
const os = require('os');
class PtyManager {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.ptys = new Map();
this.defaultShell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : '/bin/bash');
}
create({ id, cols = 80, rows = 24, cwd = process.env.HOME || process.cwd() }) {
if (this.ptys.has(id)) {
this.kill({ id });
}
try {
const ptyProcess = pty.spawn(this.defaultShell, [], {
name: 'xterm-256color',
cols,
rows,
cwd,
env: {
...process.env,
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
},
});
ptyProcess.onData((data) => {
if (!this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('pty:data', { id, data });
}
});
ptyProcess.onExit(({ exitCode, signal }) => {
if (!this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('pty:exit', { id, exitCode, signal });
}
this.ptys.delete(id);
});
this.ptys.set(id, ptyProcess);
return { success: true, pid: ptyProcess.pid, shell: this.defaultShell };
} catch (err) {
console.error(`Failed to spawn pty [${id}]:`, err);
return { success: false, error: err.message };
}
}
write({ id, data }) {
const ptyProcess = this.ptys.get(id);
if (ptyProcess) {
ptyProcess.write(data);
}
}
resize({ id, cols, rows }) {
const ptyProcess = this.ptys.get(id);
if (ptyProcess && cols > 0 && rows > 0) {
try {
ptyProcess.resize(Math.floor(cols), Math.floor(rows));
} catch (err) {
console.warn(`Failed to resize pty [${id}]:`, err.message);
}
}
}
kill({ id }) {
const ptyProcess = this.ptys.get(id);
if (ptyProcess) {
try {
ptyProcess.kill();
} catch (err) {
console.warn(`Failed to kill pty [${id}]:`, err.message);
}
this.ptys.delete(id);
}
}
killAll() {
for (const [id, ptyProcess] of this.ptys.entries()) {
try {
ptyProcess.kill();
} catch (err) {
// ignore
}
}
this.ptys.clear();
}
}
module.exports = { PtyManager };
+35
View File
@@ -0,0 +1,35 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('terminalAPI', {
// PTY 管理
createPty: (options) => ipcRenderer.invoke('pty:create', options),
writePty: (id, data) => ipcRenderer.send('pty:write', { id, data }),
resizePty: (id, cols, rows) => ipcRenderer.send('pty:resize', { id, cols, rows }),
killPty: (id) => ipcRenderer.send('pty:kill', { id }),
// 事件监听
onPtyData: (callback) => {
const listener = (event, args) => callback(args);
ipcRenderer.on('pty:data', listener);
return () => ipcRenderer.removeListener('pty:data', listener);
},
onPtyExit: (callback) => {
const listener = (event, args) => callback(args);
ipcRenderer.on('pty:exit', listener);
return () => ipcRenderer.removeListener('pty:exit', listener);
},
// 窗口控制
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
onMaximizedChange: (callback) => {
const listener = (event, isMaximized) => callback(isMaximized);
ipcRenderer.on('window:maximized-change', listener);
return () => ipcRenderer.removeListener('window:maximized-change', listener);
},
// 系统信息
getSystemInfo: () => ipcRenderer.invoke('app:get-system-info'),
});
+139
View File
@@ -0,0 +1,139 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
font-family: var(--font-sans);
background: transparent;
color: var(--text-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-app);
backdrop-filter: blur(24px) saturate(140%);
-webkit-backdrop-filter: blur(24px) saturate(140%);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
overflow: hidden;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
}
/* 顶部无边框拖拽标题栏 */
.titlebar {
height: var(--titlebar-height);
background: var(--bg-titlebar);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag; /* 允许窗口拖拽 */
position: relative;
z-index: 100;
padding: 0 8px;
}
.titlebar-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
-webkit-app-region: no-drag;
}
.app-brand {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 700;
color: var(--accent-color);
padding: 0 8px;
letter-spacing: 0.5px;
}
.app-brand-icon {
width: 14px;
height: 14px;
}
.titlebar-actions {
display: flex;
align-items: center;
gap: 4px;
-webkit-app-region: no-drag;
}
.btn-icon {
background: transparent;
border: none;
color: var(--text-muted);
width: 28px;
height: 28px;
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-icon:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-bright);
}
.btn-icon:active {
transform: scale(0.95);
}
/* 窗口控制按钮 (最小化、最大化、关闭) */
.window-controls {
display: flex;
align-items: center;
gap: 2px;
margin-left: 6px;
-webkit-app-region: no-drag;
}
.win-btn {
background: transparent;
border: none;
color: var(--text-muted);
width: 32px;
height: 28px;
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.win-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-bright);
}
.win-btn.close:hover {
background: #f7768e;
color: #ffffff;
}
/* 主内容区 */
.main-viewport {
flex: 1;
position: relative;
overflow: hidden;
background: transparent;
}
+257
View File
@@ -0,0 +1,257 @@
/* 底部状态栏 */
.statusbar {
height: var(--statusbar-height);
background: var(--bg-titlebar);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
font-size: 11px;
color: var(--text-muted);
z-index: 50;
}
.statusbar-left,
.statusbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.statusbar-item {
display: flex;
align-items: center;
gap: 4px;
}
.statusbar-item.clickable {
cursor: pointer;
padding: 2px 6px;
border-radius: var(--border-radius-sm);
transition: all 0.15s ease;
}
.statusbar-item.clickable:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-bright);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #9ece6a;
box-shadow: 0 0 6px rgba(158, 206, 106, 0.6);
}
/* 模态框遮罩层 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(4px);
display: none;
align-items: flex-start;
justify-content: center;
padding-top: 12vh;
z-index: 1000;
animation: fadeIn 0.15s ease;
}
.modal-overlay.active {
display: flex;
}
/* 快速命令面板 (Command Palette) */
.command-palette {
width: 520px;
max-width: 90vw;
background: var(--bg-app);
border: 1px solid var(--border-active);
border-radius: var(--border-radius-md);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 20px var(--accent-glow);
overflow: hidden;
display: flex;
flex-direction: column;
}
.palette-input-wrapper {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
gap: 10px;
}
.palette-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text-bright);
font-size: 14px;
font-family: var(--font-sans);
}
.palette-list {
max-height: 280px;
overflow-y: auto;
padding: 6px;
}
.palette-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: var(--border-radius-sm);
color: var(--text-main);
font-size: 13px;
cursor: pointer;
transition: all 0.1s ease;
}
.palette-item.selected,
.palette-item:hover {
background: var(--accent-color);
color: #0f141c;
font-weight: 600;
}
.palette-item-left {
display: flex;
align-items: center;
gap: 8px;
}
.palette-shortcut {
font-size: 11px;
opacity: 0.7;
font-family: var(--font-mono);
background: rgba(0, 0, 0, 0.2);
padding: 2px 6px;
border-radius: 3px;
}
/* 设置弹窗 */
.settings-modal {
width: 560px;
max-width: 90vw;
background: var(--bg-app);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.65);
overflow: hidden;
}
.settings-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border-color);
}
.settings-header h3 {
font-size: 15px;
font-weight: 600;
color: var(--text-bright);
}
.settings-body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
max-height: 480px;
overflow-y: auto;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.setting-label {
font-size: 13px;
font-weight: 500;
color: var(--text-bright);
}
.setting-desc {
font-size: 11px;
color: var(--text-muted);
}
.setting-control select,
.setting-control input[type="text"],
.setting-control input[type="number"] {
background: var(--bg-tab-inactive);
border: 1px solid var(--border-color);
color: var(--text-bright);
padding: 6px 10px;
border-radius: var(--border-radius-sm);
font-size: 12px;
outline: none;
}
.setting-control select:focus,
.setting-control input:focus {
border-color: var(--border-active);
}
/* 终端内搜索条 */
.search-bar {
position: absolute;
top: 10px;
right: 20px;
background: var(--bg-titlebar);
border: 1px solid var(--border-active);
border-radius: var(--border-radius-md);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
padding: 4px 8px;
gap: 6px;
z-index: 200;
animation: slideDown 0.15s ease;
}
.search-bar.active {
display: flex;
}
.search-input {
background: transparent;
border: none;
outline: none;
color: var(--text-bright);
font-size: 12px;
width: 160px;
}
.search-count {
font-size: 11px;
color: var(--text-muted);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideDown {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
+83
View File
@@ -0,0 +1,83 @@
/* 分屏视图与窗格容器 */
.tab-view {
width: 100%;
height: 100%;
display: none;
position: absolute;
top: 0;
left: 0;
}
.tab-view.active {
display: flex;
}
.split-container {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.split-container.horizontal {
flex-direction: row;
}
.split-container.vertical {
flex-direction: column;
}
.split-pane {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
border: 1px solid transparent;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
min-width: 120px;
min-height: 80px;
}
.split-pane.focused {
border-color: var(--border-active);
box-shadow: inset 0 0 0 1px var(--border-active), 0 0 12px var(--accent-glow);
}
.terminal-wrapper {
width: 100%;
height: 100%;
padding: 6px 10px;
overflow: hidden;
}
.xterm {
width: 100%;
height: 100%;
padding: 0;
}
.xterm-viewport {
overflow-y: auto !important;
}
/* 分割线 */
.split-divider {
background: var(--border-color);
z-index: 10;
transition: background 0.2s ease;
}
.split-container.horizontal > .split-divider {
width: 2px;
cursor: col-resize;
}
.split-container.vertical > .split-divider {
height: 2px;
cursor: row-resize;
}
.split-divider:hover {
background: var(--accent-color);
}
+98
View File
@@ -0,0 +1,98 @@
/* 标签栏 */
.tab-bar {
display: flex;
align-items: center;
gap: 4px;
overflow-x: auto;
scrollbar-width: none;
max-width: calc(100vw - 320px);
}
.tab-bar::-webkit-scrollbar {
display: none;
}
.tab-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
height: 28px;
background: var(--bg-tab-inactive);
border-radius: var(--border-radius-sm);
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s ease;
white-space: nowrap;
max-width: 180px;
min-width: 90px;
}
.tab-item:hover {
background: var(--bg-tab-hover);
color: var(--text-main);
}
.tab-item.active {
background: var(--bg-tab-active);
color: var(--text-bright);
border-color: var(--border-active);
box-shadow: 0 0 10px var(--accent-glow);
}
.tab-icon {
width: 12px;
height: 12px;
opacity: 0.8;
}
.tab-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab-close {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
opacity: 0;
transition: all 0.12s ease;
}
.tab-item:hover .tab-close,
.tab-item.active .tab-close {
opacity: 0.6;
}
.tab-close:hover {
opacity: 1 !important;
background: rgba(255, 255, 255, 0.15);
color: #f7768e;
}
.new-tab-btn {
background: transparent;
border: none;
color: var(--text-muted);
width: 26px;
height: 26px;
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.new-tab-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--accent-color);
}
+97
View File
@@ -0,0 +1,97 @@
:root {
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'Courier New', monospace;
/* 基础设计变量 */
--bg-app: rgba(22, 22, 30, 0.88);
--bg-titlebar: rgba(16, 16, 22, 0.92);
--bg-tab-active: rgba(36, 40, 59, 0.95);
--bg-tab-inactive: rgba(26, 27, 38, 0.6);
--bg-tab-hover: rgba(34, 36, 54, 0.8);
--border-color: rgba(255, 255, 255, 0.08);
--border-active: #7aa2f7;
--text-main: #c0caf5;
--text-muted: #565f89;
--text-bright: #ffffff;
--accent-color: #7aa2f7;
--accent-glow: rgba(122, 162, 247, 0.35);
--titlebar-height: 38px;
--statusbar-height: 26px;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--terminal-font-size: 14px;
--terminal-line-height: 1.25;
}
/* Tokyo Night */
[data-theme="tokyo-night"] {
--bg-app: rgba(26, 27, 38, 0.92);
--bg-titlebar: rgba(22, 22, 30, 0.96);
--bg-tab-active: rgba(36, 40, 59, 0.98);
--bg-tab-inactive: rgba(26, 27, 38, 0.6);
--border-color: rgba(122, 162, 247, 0.15);
--border-active: #7aa2f7;
--text-main: #c0caf5;
--text-muted: #565f89;
--accent-color: #7aa2f7;
--accent-glow: rgba(122, 162, 247, 0.3);
}
/* Catppuccin Mocha */
[data-theme="catppuccin"] {
--bg-app: rgba(30, 30, 46, 0.92);
--bg-titlebar: rgba(24, 24, 37, 0.96);
--bg-tab-active: rgba(49, 50, 68, 0.98);
--bg-tab-inactive: rgba(30, 30, 46, 0.6);
--border-color: rgba(203, 166, 247, 0.15);
--border-active: #cba6f7;
--text-main: #cdd6f4;
--text-muted: #6c7086;
--accent-color: #cba6f7;
--accent-glow: rgba(203, 166, 247, 0.3);
}
/* Dracula */
[data-theme="dracula"] {
--bg-app: rgba(40, 42, 54, 0.92);
--bg-titlebar: rgba(33, 34, 44, 0.96);
--bg-tab-active: rgba(68, 71, 90, 0.98);
--bg-tab-inactive: rgba(40, 42, 54, 0.6);
--border-color: rgba(189, 147, 249, 0.15);
--border-active: #bd93f9;
--text-main: #f8f8f2;
--text-muted: #6272a4;
--accent-color: #ff79c6;
--accent-glow: rgba(255, 121, 198, 0.3);
}
/* One Dark */
[data-theme="one-dark"] {
--bg-app: rgba(40, 44, 52, 0.92);
--bg-titlebar: rgba(33, 37, 43, 0.96);
--bg-tab-active: rgba(53, 59, 69, 0.98);
--bg-tab-inactive: rgba(40, 44, 52, 0.6);
--border-color: rgba(97, 175, 239, 0.15);
--border-active: #61afef;
--text-main: #abb2bf;
--text-muted: #5c6370;
--accent-color: #61afef;
--accent-glow: rgba(97, 175, 239, 0.3);
}
/* Cyberpunk Neon */
[data-theme="cyberpunk"] {
--bg-app: rgba(13, 18, 30, 0.94);
--bg-titlebar: rgba(9, 12, 20, 0.98);
--bg-tab-active: rgba(26, 35, 58, 0.98);
--bg-tab-inactive: rgba(13, 18, 30, 0.6);
--border-color: rgba(0, 255, 204, 0.2);
--border-active: #00ffcc;
--text-main: #e2f0fb;
--text-muted: #4e6a88;
--accent-color: #ff007f;
--accent-glow: rgba(255, 0, 127, 0.4);
}
+144
View File
@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="tokyo-night">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' data:; style-src 'self' 'unsafe-inline';">
<title>next_term - 现代 Linux 高性能终端</title>
<!-- 引入 xterm 基础样式与各功能模块样式 -->
<link rel="stylesheet" href="../../node_modules/@xterm/xterm/css/xterm.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/tabs.css">
<link rel="stylesheet" href="css/split.css">
<link rel="stylesheet" href="css/components.css">
</head>
<body>
<div id="app">
<!-- 顶部现代拖拽式标题栏 -->
<header class="titlebar">
<div class="titlebar-left">
<div class="app-brand">
<svg class="app-brand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
<span>next_term</span>
</div>
<!-- 标签页栏 -->
<div class="tab-bar" id="tab-bar"></div>
<button class="new-tab-btn" id="btn-new-tab" title="新建标签页 (Ctrl+Shift+T)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<!-- 右侧快速操作按钮与窗口控制 -->
<div class="titlebar-actions">
<button class="btn-icon" id="btn-split-h" title="向右水平分屏 (Ctrl+Shift+E)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<line x1="12" y1="3" x2="12" y2="21"></line>
</svg>
</button>
<button class="btn-icon" id="btn-split-v" title="向下垂直分屏 (Ctrl+Shift+O)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<line x1="3" y1="12" x2="21" y2="12"></line>
</svg>
</button>
<button class="btn-icon" id="btn-palette" title="快速命令面板 (Ctrl+Shift+P / F1)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
</button>
<button class="btn-icon" id="btn-settings" title="外观与配置设置 (Ctrl+,)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<!-- 窗口最小化、最大化与关闭按钮 -->
<div class="window-controls">
<button class="win-btn" id="win-min" title="最小化">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<button class="win-btn" id="win-max" title="最大化">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
</button>
<button class="win-btn close" id="win-close" title="关闭">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
</header>
<!-- 终端主视口 -->
<main class="main-viewport" id="main-viewport"></main>
<!-- 底部状态栏 -->
<footer class="statusbar">
<div class="statusbar-left">
<div class="statusbar-item">
<span class="status-dot"></span>
<span id="status-host">linux-desktop</span>
</div>
<div class="statusbar-item">
<span>Shell:</span>
<strong id="status-shell" style="color: var(--accent-color);">bash</strong>
</div>
</div>
<div class="statusbar-right">
<div class="statusbar-item">
<span>编码: UTF-8</span>
</div>
<div class="statusbar-item clickable" id="status-theme" title="点击快速切换主题">
Tokyo Night
</div>
</div>
</footer>
<!-- 终端搜索悬浮条 -->
<div class="search-bar" id="search-bar">
<input type="text" class="search-input" id="search-input" placeholder="在终端中搜索...">
<button class="btn-icon" id="search-prev" title="查找上一个 (Shift+Enter)">▲</button>
<button class="btn-icon" id="search-next" title="查找下一个 (Enter)">▼</button>
<button class="btn-icon" id="search-close" title="关闭搜索 (Esc)">&times;</button>
</div>
<!-- 快速命令面板模态框 -->
<div class="modal-overlay" id="command-palette-overlay">
<div class="command-palette">
<div class="palette-input-wrapper">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" class="palette-input" id="command-palette-input" placeholder="输入操作名称或搜索快捷键 (支持上下方向键与回车)..." autocomplete="off">
</div>
<div class="palette-list" id="command-palette-list"></div>
</div>
</div>
<!-- 偏好设置模态框 -->
<div class="modal-overlay" id="settings-overlay">
<div class="settings-modal" id="settings-modal"></div>
</div>
</div>
<script type="module" src="js/app.js"></script>
</body>
</html>
+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();
});
+207
View File
@@ -0,0 +1,207 @@
import { themeManager, THEMES } from './theme-manager.js';
export class CommandPalette {
constructor(options = {}) {
this.overlay = options.overlay;
this.input = options.input;
this.list = options.list;
this.tabManager = options.tabManager;
this.onOpenSettings = options.onOpenSettings || (() => {});
this.onToggleSearch = options.onToggleSearch || (() => {});
this.selectedIndex = 0;
this.isOpen = false;
this.filteredCommands = [];
this.initCommands();
this.bindEvents();
}
initCommands() {
this.baseCommands = [
{
id: 'new-tab',
title: '新建标签页 (New Tab)',
shortcut: 'Ctrl+Shift+T',
icon: 'plus',
action: () => this.tabManager.createTab(),
},
{
id: 'split-horizontal',
title: '向右水平分屏 (Split Right)',
shortcut: 'Ctrl+Shift+E',
icon: 'columns',
action: () => {
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('horizontal');
},
},
{
id: 'split-vertical',
title: '向下垂直分屏 (Split Down)',
shortcut: 'Ctrl+Shift+O',
icon: 'rows',
action: () => {
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('vertical');
},
},
{
id: 'close-pane',
title: '关闭当前窗格/标签页 (Close Pane)',
shortcut: 'Ctrl+Shift+W',
icon: 'x',
action: () => {
const tab = this.tabManager.getActiveTab();
if (tab && tab.splitTree.activeTerminal) {
tab.splitTree.closeTerminal(tab.splitTree.activeTerminal.id);
}
},
},
{
id: 'search',
title: '终端内容快速搜索 (Search Buffer)',
shortcut: 'Ctrl+Shift+F',
icon: 'search',
action: () => this.onToggleSearch(),
},
{
id: 'clear',
title: '清空当前屏幕缓冲区 (Clear Buffer)',
shortcut: 'Ctrl+L',
icon: 'trash',
action: () => {
const term = this.tabManager.getActiveTerminal();
if (term) term.xterm.clear();
},
},
{
id: 'settings',
title: '打开终端外观与核心配置 (Settings)',
shortcut: 'Ctrl+,',
icon: 'settings',
action: () => this.onOpenSettings(),
},
];
// 添加预设主题快速切换项
for (const [key, theme] of Object.entries(THEMES)) {
this.baseCommands.push({
id: `theme-${key}`,
title: `切换主题: ${theme.name}`,
shortcut: '',
icon: 'palette',
action: () => themeManager.applyTheme(key),
});
}
this.filteredCommands = [...this.baseCommands];
}
bindEvents() {
this.input.addEventListener('input', () => {
this.filter(this.input.value.trim().toLowerCase());
});
this.input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
this.selectedIndex = (this.selectedIndex + 1) % this.filteredCommands.length;
this.renderList();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.selectedIndex = (this.selectedIndex - 1 + this.filteredCommands.length) % this.filteredCommands.length;
this.renderList();
} else if (e.key === 'Enter') {
e.preventDefault();
this.executeSelected();
} else if (e.key === 'Escape') {
e.preventDefault();
this.close();
}
});
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.close();
}
});
}
open() {
this.isOpen = true;
this.overlay.classList.add('active');
this.input.value = '';
this.selectedIndex = 0;
this.filteredCommands = [...this.baseCommands];
this.renderList();
setTimeout(() => this.input.focus(), 50);
}
close() {
this.isOpen = false;
this.overlay.classList.remove('active');
const term = this.tabManager.getActiveTerminal();
if (term) term.focus();
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
filter(query) {
if (!query) {
this.filteredCommands = [...this.baseCommands];
} else {
this.filteredCommands = this.baseCommands.filter((cmd) =>
cmd.title.toLowerCase().includes(query) || cmd.id.toLowerCase().includes(query)
);
}
this.selectedIndex = 0;
this.renderList();
}
renderList() {
this.list.innerHTML = '';
if (this.filteredCommands.length === 0) {
this.list.innerHTML = `<div style="padding: 12px; color: var(--text-muted); text-align: center; font-size: 12px;">未匹配到相关命令</div>`;
return;
}
this.filteredCommands.forEach((cmd, idx) => {
const item = document.createElement('div');
item.className = `palette-item ${idx === this.selectedIndex ? 'selected' : ''}`;
item.innerHTML = `
<div class="palette-item-left">
<span>${cmd.title}</span>
</div>
${cmd.shortcut ? `<span class="palette-shortcut">${cmd.shortcut}</span>` : ''}
`;
item.addEventListener('click', () => {
this.selectedIndex = idx;
this.executeSelected();
});
this.list.appendChild(item);
});
// 确保选中项在视口内
const selectedEl = this.list.children[this.selectedIndex];
if (selectedEl) {
selectedEl.scrollIntoView({ block: 'nearest' });
}
}
executeSelected() {
const cmd = this.filteredCommands[this.selectedIndex];
if (cmd) {
this.close();
cmd.action();
}
}
}
+166
View File
@@ -0,0 +1,166 @@
import { themeManager, THEMES } from './theme-manager.js';
export class SettingsModal {
constructor(options = {}) {
this.overlay = options.overlay;
this.modal = options.modal;
this.tabManager = options.tabManager;
this.settings = this.loadSettings();
this.initUI();
this.bindEvents();
}
loadSettings() {
const defaults = {
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
fontSize: 14,
cursorStyle: 'block',
cursorBlink: true,
theme: themeManager.currentThemeKey,
};
try {
const saved = JSON.parse(localStorage.getItem('next_term:settings') || '{}');
return { ...defaults, ...saved };
} catch {
return defaults;
}
}
saveSettings() {
localStorage.setItem('next_term:settings', JSON.stringify(this.settings));
this.applyAll();
}
initUI() {
// 渲染设置项
const themeOptions = Object.keys(THEMES)
.map((key) => `<option value="${key}" ${this.settings.theme === key ? 'selected' : ''}>${THEMES[key].name}</option>`)
.join('');
this.modal.innerHTML = `
<div class="settings-header">
<h3>终端偏好设置 (Preferences)</h3>
<button class="btn-icon close-settings">&times;</button>
</div>
<div class="settings-body">
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">色彩主题 (Color Theme)</span>
<span class="setting-desc">选择预设的高对比度高颜值现代配色方案</span>
</div>
<div class="setting-control">
<select id="setting-theme">${themeOptions}</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">字体大小 (Font Size)</span>
<span class="setting-desc">网格字符栅格化大小 (11px ~ 24px)</span>
</div>
<div class="setting-control">
<input type="number" id="setting-fontSize" min="11" max="24" value="${this.settings.fontSize}">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">字体族 (Font Family)</span>
<span class="setting-desc">优先使用的等宽编程字体</span>
</div>
<div class="setting-control">
<select id="setting-fontFamily">
<option value="'JetBrains Mono', monospace">JetBrains Mono</option>
<option value="'Fira Code', monospace">Fira Code</option>
<option value="'Cascadia Code', monospace">Cascadia Code</option>
<option value="monospace">系统默认等宽 (Monospace)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">光标样式 (Cursor Style)</span>
<span class="setting-desc">终端提示符光标形态</span>
</div>
<div class="setting-control">
<select id="setting-cursorStyle">
<option value="block" ${this.settings.cursorStyle === 'block' ? 'selected' : ''}>方块光标 (Block)</option>
<option value="bar" ${this.settings.cursorStyle === 'bar' ? 'selected' : ''}>细竖线光标 (Bar)</option>
<option value="underline" ${this.settings.cursorStyle === 'underline' ? 'selected' : ''}>下划线光标 (Underline)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">光标平滑闪烁 (Cursor Blink)</span>
<span class="setting-desc">启用后光标将周期性呼吸闪烁</span>
</div>
<div class="setting-control">
<input type="checkbox" id="setting-cursorBlink" ${this.settings.cursorBlink ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
</div>
</div>
</div>
`;
const fontSelect = this.modal.querySelector('#setting-fontFamily');
if (fontSelect) fontSelect.value = this.settings.fontFamily;
}
bindEvents() {
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) this.close();
});
this.modal.querySelector('.close-settings')?.addEventListener('click', () => {
this.close();
});
// 监听表单变更
this.modal.querySelector('#setting-theme')?.addEventListener('change', (e) => {
this.settings.theme = e.target.value;
themeManager.applyTheme(this.settings.theme);
this.saveSettings();
});
this.modal.querySelector('#setting-fontSize')?.addEventListener('change', (e) => {
this.settings.fontSize = parseInt(e.target.value, 10) || 14;
this.saveSettings();
});
this.modal.querySelector('#setting-fontFamily')?.addEventListener('change', (e) => {
this.settings.fontFamily = e.target.value;
this.saveSettings();
});
this.modal.querySelector('#setting-cursorStyle')?.addEventListener('change', (e) => {
this.settings.cursorStyle = e.target.value;
this.saveSettings();
});
this.modal.querySelector('#setting-cursorBlink')?.addEventListener('change', (e) => {
this.settings.cursorBlink = e.target.checked;
this.saveSettings();
});
}
applyAll() {
for (const tab of this.tabManager.tabs.values()) {
for (const info of tab.splitTree.terminals.values()) {
info.termView.updateSettings(this.settings);
}
}
}
open() {
this.overlay.classList.add('active');
}
close() {
this.overlay.classList.remove('active');
const term = this.tabManager.getActiveTerminal();
if (term) term.focus();
}
}
+140
View File
@@ -0,0 +1,140 @@
import { TerminalView } from './terminal-view.js';
export class TabSplitTree {
constructor(options = {}) {
this.container = options.container;
this.tabId = options.tabId;
this.onActiveTerminalChange = options.onActiveTerminalChange || (() => {});
this.onEmpty = options.onEmpty || (() => {});
this.onTitleChange = options.onTitleChange || (() => {});
this.terminals = new Map(); // id -> TerminalView
this.activeTerminal = null;
this.rootElement = document.createElement('div');
this.rootElement.className = 'split-container horizontal';
this.container.appendChild(this.rootElement);
// 创建初始窗格
this.addPane(this.rootElement);
}
addPane(parentElement, orientation = 'horizontal') {
const paneElement = document.createElement('div');
paneElement.className = 'split-pane';
parentElement.appendChild(paneElement);
const termView = new TerminalView({
container: paneElement,
onFocus: (term) => {
this.setActiveTerminal(term);
},
onTitleChange: (title) => {
if (this.activeTerminal === termView) {
this.onTitleChange(title);
}
},
onExit: () => {
this.closeTerminal(termView.id);
},
});
this.terminals.set(termView.id, {
termView,
paneElement,
parentElement,
});
paneElement.addEventListener('click', () => {
termView.focus();
});
this.setActiveTerminal(termView);
return termView;
}
splitCurrent(direction = 'horizontal') {
if (!this.activeTerminal) return;
const currentInfo = this.terminals.get(this.activeTerminal.id);
if (!currentInfo) return;
const currentPane = currentInfo.paneElement;
const parent = currentPane.parentElement;
// 创建一个新的 split-container 容器替换原有 pane,实现嵌套分屏
const splitWrapper = document.createElement('div');
splitWrapper.className = `split-container ${direction}`;
parent.replaceChild(splitWrapper, currentPane);
splitWrapper.appendChild(currentPane);
// 添加分割线
const divider = document.createElement('div');
divider.className = 'split-divider';
splitWrapper.appendChild(divider);
// 添加新窗格
const newTerm = this.addPane(splitWrapper, direction);
// 刷新自适应尺寸
setTimeout(() => {
this.fitAll();
newTerm.focus();
}, 50);
}
setActiveTerminal(termView) {
this.activeTerminal = termView;
// 清除所有聚焦样式
for (const info of this.terminals.values()) {
info.paneElement.classList.remove('focused');
}
const currentInfo = this.terminals.get(termView.id);
if (currentInfo) {
currentInfo.paneElement.classList.add('focused');
}
this.onActiveTerminalChange(termView);
this.onTitleChange(termView.title);
}
closeTerminal(termId) {
const info = this.terminals.get(termId);
if (!info) return;
info.termView.destroy();
const pane = info.paneElement;
const parent = pane.parentElement;
this.terminals.delete(termId);
pane.remove();
if (this.terminals.size === 0) {
this.onEmpty();
return;
}
// 焦点转移给另一个终端
const nextTerm = this.terminals.values().next().value.termView;
this.setActiveTerminal(nextTerm);
nextTerm.focus();
this.fitAll();
}
fitAll() {
for (const info of this.terminals.values()) {
info.termView.fit();
}
}
destroy() {
for (const info of this.terminals.values()) {
info.termView.destroy();
}
this.terminals.clear();
this.rootElement.remove();
}
}
+148
View File
@@ -0,0 +1,148 @@
import { TabSplitTree } from './split-manager.js';
export class TabManager {
constructor(options = {}) {
this.tabBar = options.tabBar;
this.viewport = options.viewport;
this.onActiveChange = options.onActiveChange || (() => {});
this.tabs = new Map(); // tabId -> { tabElement, viewElement, splitTree, title }
this.activeTabId = null;
}
createTab(title = 'bash') {
const tabId = 'tab_' + Math.random().toString(36).substring(2, 9);
// 1. 创建标签页 DOM
const tabElement = document.createElement('div');
tabElement.className = 'tab-item';
tabElement.innerHTML = `
<svg class="tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
<span class="tab-title">${title}</span>
<span class="tab-close">&times;</span>
`;
// 2. 创建视口 DOM
const viewElement = document.createElement('div');
viewElement.className = 'tab-view';
this.viewport.appendChild(viewElement);
// 3. 创建分屏树
const splitTree = new TabSplitTree({
container: viewElement,
tabId,
onActiveTerminalChange: (term) => {
if (this.activeTabId === tabId) {
this.onActiveChange(term);
}
},
onTitleChange: (newTitle) => {
this.updateTabTitle(tabId, newTitle);
},
onEmpty: () => {
this.closeTab(tabId);
},
});
// 4. 标签页点击与关闭事件
tabElement.addEventListener('click', (e) => {
if (e.target.classList.contains('tab-close')) {
this.closeTab(tabId);
} else {
this.switchTab(tabId);
}
});
this.tabBar.appendChild(tabElement);
this.tabs.set(tabId, {
tabId,
tabElement,
viewElement,
splitTree,
title,
});
this.switchTab(tabId);
return tabId;
}
switchTab(tabId) {
if (!this.tabs.has(tabId)) return;
for (const [id, tab] of this.tabs.entries()) {
const isActive = id === tabId;
tab.tabElement.classList.toggle('active', isActive);
tab.viewElement.classList.toggle('active', isActive);
if (isActive) {
this.activeTabId = tabId;
setTimeout(() => {
tab.splitTree.fitAll();
if (tab.splitTree.activeTerminal) {
tab.splitTree.activeTerminal.focus();
this.onActiveChange(tab.splitTree.activeTerminal);
}
}, 30);
}
}
}
closeTab(tabId) {
const tab = this.tabs.get(tabId);
if (!tab) return;
tab.splitTree.destroy();
tab.tabElement.remove();
tab.viewElement.remove();
this.tabs.delete(tabId);
// 如果还有其他标签页,切换到上一个或下一个
if (this.tabs.size > 0) {
const remainingIds = Array.from(this.tabs.keys());
const nextId = remainingIds[remainingIds.length - 1];
this.switchTab(nextId);
} else {
// 没有任何标签页时自动新建一个
this.createTab();
}
}
updateTabTitle(tabId, newTitle) {
const tab = this.tabs.get(tabId);
if (tab && newTitle) {
tab.title = newTitle;
const titleEl = tab.tabElement.querySelector('.tab-title');
if (titleEl) {
titleEl.textContent = newTitle;
}
}
}
getActiveTab() {
return this.tabs.get(this.activeTabId);
}
getActiveTerminal() {
const activeTab = this.getActiveTab();
return activeTab ? activeTab.splitTree.activeTerminal : null;
}
nextTab() {
const ids = Array.from(this.tabs.keys());
if (ids.length <= 1) return;
const currentIndex = ids.indexOf(this.activeTabId);
const nextIndex = (currentIndex + 1) % ids.length;
this.switchTab(ids[nextIndex]);
}
prevTab() {
const ids = Array.from(this.tabs.keys());
if (ids.length <= 1) return;
const currentIndex = ids.indexOf(this.activeTabId);
const prevIndex = (currentIndex - 1 + ids.length) % ids.length;
this.switchTab(ids[prevIndex]);
}
}
+162
View File
@@ -0,0 +1,162 @@
import { Terminal } from '../../../node_modules/@xterm/xterm/lib/xterm.mjs';
import { FitAddon } from '../../../node_modules/@xterm/addon-fit/lib/addon-fit.mjs';
import { WebglAddon } from '../../../node_modules/@xterm/addon-webgl/lib/addon-webgl.mjs';
import { WebLinksAddon } from '../../../node_modules/@xterm/addon-web-links/lib/addon-web-links.mjs';
import { SearchAddon } from '../../../node_modules/@xterm/addon-search/lib/addon-search.mjs';
import { themeManager } from './theme-manager.js';
export class TerminalView {
constructor(options = {}) {
this.id = 'term_' + Math.random().toString(36).substring(2, 9);
this.container = options.container;
this.cwd = options.cwd || '';
this.onTitleChange = options.onTitleChange || (() => {});
this.onFocus = options.onFocus || (() => {});
this.onExit = options.onExit || (() => {});
this.title = 'bash';
this.isFocused = false;
this.initTerminal();
}
initTerminal() {
const currentTheme = themeManager.getCurrentTheme();
const config = JSON.parse(localStorage.getItem('next_term:settings') || '{}');
const fontSize = config.fontSize || 14;
const fontFamily = config.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace";
const cursorStyle = config.cursorStyle || 'block';
const cursorBlink = config.cursorBlink !== false;
this.xterm = new Terminal({
cursorBlink,
cursorStyle,
fontSize,
fontFamily,
lineHeight: 1.22,
letterSpacing: 0,
scrollback: 10000,
allowTransparency: true,
theme: currentTheme.xterm,
macOptionIsMeta: true,
rightClickSelectsWord: true,
convertEol: true,
});
// 插件初始化
this.fitAddon = new FitAddon();
this.xterm.loadAddon(this.fitAddon);
this.searchAddon = new SearchAddon();
this.xterm.loadAddon(this.searchAddon);
this.webLinksAddon = new WebLinksAddon();
this.xterm.loadAddon(this.webLinksAddon);
// 挂载 DOM
this.terminalWrapper = document.createElement('div');
this.terminalWrapper.className = 'terminal-wrapper';
this.container.appendChild(this.terminalWrapper);
this.xterm.open(this.terminalWrapper);
// 启用 WebGL 硬件加速 (GPU 渲染)
try {
this.webglAddon = new WebglAddon();
this.webglAddon.onContextLoss(() => {
console.warn('WebGL context lost, disposing webgl addon');
this.webglAddon.dispose();
});
this.xterm.loadAddon(this.webglAddon);
} catch (e) {
console.warn('WebGL addon not supported, fallback to default Canvas renderer:', e);
}
// 监听输入
this.xterm.onData((data) => {
window.terminalAPI.writePty(this.id, data);
});
// 监听标题变更 (OSC 0/2)
this.xterm.onTitleChange((title) => {
if (title) {
this.title = title;
this.onTitleChange(title);
}
});
// 监听终端聚焦
this.xterm.textarea?.addEventListener('focus', () => {
this.isFocused = true;
this.onFocus(this);
});
// 主题监听
this.unsubscribeTheme = themeManager.onThemeChange((theme) => {
this.xterm.options.theme = theme.xterm;
});
// 自适应尺寸监听
this.resizeObserver = new ResizeObserver(() => {
this.fit();
});
this.resizeObserver.observe(this.terminalWrapper);
// 启动 PTY
this.fit();
const cols = this.xterm.cols || 80;
const rows = this.xterm.rows || 24;
window.terminalAPI.createPty({
id: this.id,
cols,
rows,
cwd: this.cwd,
}).then((res) => {
if (res && res.shell) {
this.title = res.shell.split('/').pop() || 'bash';
this.onTitleChange(this.title);
}
});
// 聚焦
setTimeout(() => this.focus(), 100);
}
fit() {
if (!this.terminalWrapper || this.terminalWrapper.clientWidth === 0) return;
try {
this.fitAddon.fit();
if (this.xterm.cols && this.xterm.rows) {
window.terminalAPI.resizePty(this.id, this.xterm.cols, this.xterm.rows);
}
} catch (err) {
// 容器不可见时可能会静默捕获
}
}
focus() {
this.xterm.focus();
this.isFocused = true;
this.onFocus(this);
}
updateSettings(settings) {
if (settings.fontSize) this.xterm.options.fontSize = settings.fontSize;
if (settings.fontFamily) this.xterm.options.fontFamily = settings.fontFamily;
if (settings.cursorStyle) this.xterm.options.cursorStyle = settings.cursorStyle;
if (settings.cursorBlink !== undefined) this.xterm.options.cursorBlink = settings.cursorBlink;
this.fit();
}
destroy() {
if (this.unsubscribeTheme) this.unsubscribeTheme();
if (this.resizeObserver) this.resizeObserver.disconnect();
window.terminalAPI.killPty(this.id);
this.xterm.dispose();
if (this.terminalWrapper && this.terminalWrapper.parentNode) {
this.terminalWrapper.parentNode.removeChild(this.terminalWrapper);
}
}
}
+179
View File
@@ -0,0 +1,179 @@
// 现代高颜值终端主题库
export const THEMES = {
'tokyo-night': {
name: 'Tokyo Night',
themeName: 'tokyo-night',
xterm: {
background: '#1a1b26',
foreground: '#c0caf5',
cursor: '#c0caf5',
cursorAccent: '#1a1b26',
selectionBackground: '#33467c',
selectionForeground: '#ffffff',
black: '#15161e',
red: '#f7768e',
green: '#9ece6a',
yellow: '#e0af68',
blue: '#7aa2f7',
magenta: '#bb9af7',
cyan: '#7dcfff',
white: '#a9b1d6',
brightBlack: '#414868',
brightRed: '#f7768e',
brightGreen: '#9ece6a',
brightYellow: '#e0af68',
brightBlue: '#7aa2f7',
brightMagenta: '#bb9af7',
brightCyan: '#7dcfff',
brightWhite: '#c0caf5',
},
},
'catppuccin': {
name: 'Catppuccin Mocha',
themeName: 'catppuccin',
xterm: {
background: '#1e1e2e',
foreground: '#cdd6f4',
cursor: '#f5e0dc',
cursorAccent: '#1e1e2e',
selectionBackground: '#585b70',
selectionForeground: '#cdd6f4',
black: '#45475a',
red: '#f38ba8',
green: '#a6e3a1',
yellow: '#f9e2af',
blue: '#89b4fa',
magenta: '#f5c2e7',
cyan: '#94e2d5',
white: '#bac2de',
brightBlack: '#585b70',
brightRed: '#f38ba8',
brightGreen: '#a6e3a1',
brightYellow: '#f9e2af',
brightBlue: '#89b4fa',
brightMagenta: '#f5c2e7',
brightCyan: '#94e2d5',
brightWhite: '#a6adc8',
},
},
'dracula': {
name: 'Dracula Dark',
themeName: 'dracula',
xterm: {
background: '#282a36',
foreground: '#f8f8f2',
cursor: '#f8f8f2',
cursorAccent: '#282a36',
selectionBackground: '#44475a',
selectionForeground: '#ffffff',
black: '#21222c',
red: '#ff5555',
green: '#50fa7b',
yellow: '#f1fa8c',
blue: '#bd93f9',
magenta: '#ff79c6',
cyan: '#8be9fd',
white: '#f8f8f2',
brightBlack: '#6272a4',
brightRed: '#ff6e6e',
brightGreen: '#69ff94',
brightYellow: '#ffffa5',
brightBlue: '#d6acff',
brightMagenta: '#ff92df',
brightCyan: '#a4ffff',
brightWhite: '#ffffff',
},
},
'one-dark': {
name: 'One Dark Pro',
themeName: 'one-dark',
xterm: {
background: '#282c34',
foreground: '#abb2bf',
cursor: '#528bff',
cursorAccent: '#282c34',
selectionBackground: '#3e4451',
selectionForeground: '#ffffff',
black: '#1e2127',
red: '#e06c75',
green: '#98c379',
yellow: '#d19a66',
blue: '#61afef',
magenta: '#c678dd',
cyan: '#56b6c2',
white: '#abb2bf',
brightBlack: '#5c6370',
brightRed: '#be5046',
brightGreen: '#98c379',
brightYellow: '#d19a66',
brightBlue: '#61afef',
brightMagenta: '#c678dd',
brightCyan: '#56b6c2',
brightWhite: '#ffffff',
},
},
'cyberpunk': {
name: 'Cyberpunk Neon',
themeName: 'cyberpunk',
xterm: {
background: '#0d121e',
foreground: '#e2f0fb',
cursor: '#00ffcc',
cursorAccent: '#0d121e',
selectionBackground: '#ff007f',
selectionForeground: '#ffffff',
black: '#1b2234',
red: '#ff0055',
green: '#00ff9f',
yellow: '#ffe600',
blue: '#00b8ff',
magenta: '#ff007f',
cyan: '#00ffcc',
white: '#e2f0fb',
brightBlack: '#38435d',
brightRed: '#ff3377',
brightGreen: '#33ffb2',
brightYellow: '#ffeb33',
brightBlue: '#33c6ff',
brightMagenta: '#ff3399',
brightCyan: '#33ffd6',
brightWhite: '#ffffff',
},
},
};
class ThemeManager {
constructor() {
this.currentThemeKey = localStorage.getItem('next_term:theme') || 'tokyo-night';
this.listeners = new Set();
}
init() {
this.applyTheme(this.currentThemeKey);
}
getCurrentTheme() {
return THEMES[this.currentThemeKey] || THEMES['tokyo-night'];
}
applyTheme(themeKey) {
if (!THEMES[themeKey]) themeKey = 'tokyo-night';
this.currentThemeKey = themeKey;
localStorage.setItem('next_term:theme', themeKey);
const theme = THEMES[themeKey];
document.documentElement.setAttribute('data-theme', theme.themeName);
// 通知所有监听者(xterm 实例)
for (const callback of this.listeners) {
callback(theme);
}
}
onThemeChange(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
}
export const themeManager = new ThemeManager();