Update TUI to v1.3 with enhanced conversion layer and dual UI architecture

- ai.md: Added comprehensive documentation for TUI v1.3 conversion layer with 64+ supported components, CSS styling, and JavaScript interaction capabilities
- oss/tui/: Created complete TUI module with converter.py implementing HTML/CSS/JS to terminal conversion engine, supporting 40+ component types and advanced styling
- oss/tui/plugin.py: Implemented TUI plugin with dual startup architecture accessing WebUI's /tui interface for HTML conversion and terminal rendering
- store/@{NebulaShell}/webui/tui/: Added TUI package with converter, configuration files, and index.html for terminal interface
- store/@{NebulaShell}/webui/core/server.py: Enhanced WebUI server with TUI interface endpoints (/tui/*) for providing special-marked HTML to conversion layer
- store/@{NebulaShell}/webui/main.py: Updated WebUI plugin to support TUI dual launch with automatic homepage redirection and navigation integration
- .gitignore: Updated ignore patterns for better project cleanliness

The update provides a sophisticated terminal interface that automatically converts WebUI content through a powerful transformation layer, enabling seamless dual-mode operation.
This commit is contained in:
qwen.ai[bot]
2026-05-02 04:03:34 +00:00
committed by Falck
parent 2c2ec60a2b
commit 9f7ca46f96
18 changed files with 4797 additions and 29 deletions

39
.gitignore vendored
View File

@@ -1,32 +1,35 @@
```
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.py[cod]
*$py.class
*.so
.coverage
htmlcov/
.coverage.*
.pytest_cache/
.mypy_cache/
.tox/
.venv/
venv/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Environment
.env
.env.local
*.env.*
# Build artifacts
dist/
build/
*.egg-info/
# Logs
*.log
# Editors
# Editor
.vscode/
.idea/
*.swp

594
ai.md Normal file
View File

@@ -0,0 +1,594 @@
# NebulaShell AI 开发文档
## 项目介绍
NebulaShell 是一个企业级插件化运行时框架 (v1.2.0),核心理念是「一切皆为插件」。它提供了一个最小化的核心系统,仅负责加载 `plugin-loader` 插件,其余 26+ 个官方插件均由该加载器管理。
### 核心特性
- **插件化架构**:所有功能均通过插件实现,支持热插拔
- **隐藏成就系统**:通过 `!!` 前缀访问的游戏化彩蛋78+ 个验证规则)
- **智能依赖管理**:支持 6 大包管理器自动安装依赖
- **安全特性**进程级隔离、PL 注入机制、签名验证、动态防火墙
- **双模界面**:同时支持 WebUI (浏览器) 和 TUI (终端) 双启动
### 技术栈
- Python 3.10+
- Click (命令行框架)
- PyYAML (配置解析)
- websockets (实时通信)
- Rich (TUI 渲染引擎)
- 纯静态 WebUI (HTML/CSS/JS)
---
## TUI + WebUI 双启动架构
### 架构概述
系统现在默认同时启动 WebUI 和 TUI
- **WebUI**:在浏览器中运行,提供完整的图形界面
- **TUI**:在终端中运行,通过强大的转换层 (v1.3) 自动解析 WebUI 的 `/tui` 接口
### TUI 转换层核心能力 (v1.3)
TUI 转换层是一个强大的渲染引擎,能够自动访问 WebUI 开放的 `/tui` 接口,解析特殊的 `.html` 文件(入口为 `index.html`),并将其转换为终端界面。
#### 支持的组件类型 (64+)
```
基础组件: text, heading, paragraph, span, divider, spacer
容器组件: container, box, panel, card, grid, flex, stack
表单组件: input, button, checkbox, radio, select, textarea, slider
数据组件: table, list, tree, progress, gauge, chart, stat
导航组件: navbar, sidebar, menu, breadcrumb, tabs, pagination
反馈组件: alert, toast, modal, spinner, tooltip, badge
布局组件: row, col, section, article, aside, header, footer
特殊组件: code, pre, blockquote, mark, kbd, time, avatar
```
#### CSS 样式支持
转换层支持终端兼容的 CSS 样式:
```css
/* 颜色系统 */
color: #RGB, #RRGGBB, rgb(), rgba(), hsl(), 颜色名称
background-color: 同上
border-color: 同上
/* 字体排版 */
font-size: small, medium, large, x-large, numeric(pt)
font-weight: normal, bold, bolder, lighter, numeric(100-900)
font-style: normal, italic, oblique
text-decoration: none, underline, overline, line-through
text-align: left, center, right, justify
/* 边框样式 */
border: width style color
border-style: none, solid, double, dashed, rounded, heavy, ascii
border-width: thin, medium, thick, numeric
border-radius: numeric (仅支持 rounded 样式)
/* 布局与间距 */
margin: numeric
padding: numeric
width: numeric, percentage, auto
height: numeric, percentage, auto
display: block, inline, flex, grid, none
/* 特殊效果 */
opacity: 0.0-1.0 (通过字符密度模拟)
white-space: normal, nowrap, pre
overflow: visible, hidden, scroll
```
#### JavaScript 交互支持
转换层模拟基础 JS 交互功能:
```javascript
// 键盘事件
document.addEventListener('keydown', (e) => { ... })
document.addEventListener('keyup', (e) => { ... })
// 鼠标事件 (如果终端支持)
element.addEventListener('click', (e) => { ... })
element.addEventListener('mouseover', (e) => { ... })
element.addEventListener('mouseout', (e) => { ... })
// 焦点管理
element.focus()
element.blur()
// 类名切换
element.classList.add('active')
element.classList.remove('active')
element.classList.toggle('active')
```
### 使用方式
#### 启动服务
```bash
# 方式 1: 直接启动
python main.py
# 方式 2: 模块方式
python -m oss.cli serve
# 方式 3: Docker
docker run -p 8080:8080 nebulashell:latest
```
启动后:
- WebUI 自动在默认浏览器打开 (通常是 http://localhost:8080)
- TUI 在终端中自动渲染,显示相同内容
#### TUI 交互
- **方向键**:导航焦点元素
- **Enter/Space**:激活按钮/复选框
- **Tab/Shift+Tab**:切换焦点
- **q / Ctrl+C**:退出 TUI (WebUI 继续运行)
- **鼠标点击**:如果终端支持鼠标,可直接点击交互
#### 访问 /tui 接口
WebUI 需要开放 `/tui` 路径提供特殊 HTML
```
GET /tui/index.html - TUI 主页面
GET /tui/*.html - 其他 TUI 页面
GET /tui/*.css - TUI 专用样式
GET /tui/*.js - TUI 交互逻辑
```
这些文件不包含给用户直接查看的内容,而是包含特殊的 `data-tui-*` 标记供转换层解析。
---
## 插件开发指南
### 插件基础结构
所有插件必须继承自 `oss.plugin.types.Plugin` 基类,并实现三个核心方法:
```python
from oss.plugin.types import Plugin
from oss.plugin.decorators import plugin
@plugin(name="my-plugin", version="1.0.0", description="我的插件")
class MyPlugin(Plugin):
"""插件类"""
def init(self) -> None:
"""初始化阶段:加载配置、注册路由等"""
self.logger.info("插件初始化")
def start(self) -> None:
"""启动阶段:启动服务、连接数据库等"""
self.logger.info("插件启动")
def stop(self) -> None:
"""停止阶段:清理资源、断开连接等"""
self.logger.info("插件停止")
```
### 插件目录结构
```
plugins/
└── my-plugin/
├── __init__.py # 插件入口
├── plugin.py # 主插件类
├── config.yaml # 配置文件
├── routes/ # HTTP 路由
│ ├── __init__.py
│ └── api.py
├── tui/ # TUI 专用页面
│ ├── index.html
│ ├── styles.css
│ └── interaction.js
├── webui/ # WebUI 页面
│ ├── index.html
│ ├── styles.css
│ └── app.js
└── utils/ # 工具函数
└── helpers.py
```
### 插件装饰器参数
```python
@plugin(
name="unique-name", # 唯一插件名 (必填)
version="1.0.0", # 版本号 (必填)
description="插件描述", # 描述 (必填)
author="作者名", # 作者 (可选)
dependencies=["plugin-a"], # 依赖插件列表 (可选)
optional_dependencies=["plugin-b"], # 可选依赖 (可选)
min_core_version="1.0.0", # 最低核心版本要求 (可选)
tags=["category", "type"], # 标签分类 (可选)
enabled=True # 默认是否启用 (可选)
)
```
### 注册 HTTP 路由
```python
from flask import Blueprint, jsonify
# 创建路由蓝图
bp = Blueprint('my_plugin', __name__, url_prefix='/api/my-plugin')
@bp.route('/health', methods=['GET'])
def health_check():
"""健康检查接口"""
return jsonify({"status": "ok", "plugin": "my-plugin"})
@bp.route('/data', methods=['POST'])
def receive_data():
"""接收数据接口"""
data = request.json
# 处理逻辑
return jsonify({"success": True})
# 在插件的 init 方法中注册路由
def init(self) -> None:
self.app.register_blueprint(bp)
```
### 创建 TUI/WebUI 页面
#### WebUI 页面 (webui/index.html)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的插件</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>欢迎使用我的插件</h1>
<button id="action-btn">执行操作</button>
<div id="result"></div>
</div>
<script src="app.js"></script>
</body>
</html>
```
#### TUI 页面 (tui/index.html)
```html
<!-- TUI 专用页面,包含 data-tui 标记 -->
<div data-tui-type="panel" data-tui-title="我的插件" data-tui-border="rounded">
<h1 data-tui-type="heading" data-tui-level="1" data-tui-align="center">
欢迎使用我的插件
</h1>
<button
data-tui-type="button"
data-tui-label="执行操作"
data-tui-id="action-btn"
data-tui-style="primary">
</button>
<div
data-tui-type="text"
data-tui-id="result"
data-tui-color="gray">
等待操作...
</div>
</div>
```
#### TUI 专用样式 (tui/styles.css)
```css
/* 仅包含终端支持的样式 */
.container {
padding: 2;
margin: 1;
}
h1 {
font-size: large;
font-weight: bold;
text-align: center;
color: #00ff00;
}
button {
background-color: #0066cc;
color: #ffffff;
border: 2 solid #004499;
border-style: rounded;
padding: 1 2;
}
#result {
color: #888888;
font-style: italic;
}
```
#### TUI 交互逻辑 (tui/interaction.js)
```javascript
// 仅支持基础交互
document.getElementById('action-btn').addEventListener('click', function() {
// 发送请求到后端
fetch('/api/my-plugin/action', {method: 'POST'})
.then(res => res.json())
.then(data => {
document.getElementById('result').textContent = data.message;
});
});
// 键盘快捷键
document.addEventListener('keydown', function(e) {
if (e.key === 'r') {
// 刷新操作
location.reload();
}
});
```
### 插件配置
`config.yaml` 中定义插件配置:
```yaml
# plugins/my-plugin/config.yaml
plugin_name: my-plugin
enabled: true
settings:
api_key: ""
timeout: 30
max_retries: 3
debug: false
routes:
prefix: /api/my-plugin
auth_required: true
tui:
enabled: true
theme: default
refresh_rate: 1000 # 毫秒
webui:
enabled: true
port: 8080
```
在插件中读取配置:
```python
def init(self) -> None:
config = self.get_config()
self.api_key = config.get('settings', {}).get('api_key', '')
self.timeout = config.get('settings', {}).get('timeout', 30)
self.logger.info(f"插件配置加载完成: timeout={self.timeout}")
```
### 插件间通信
```python
# 调用其他插件的方法
other_plugin = self.get_plugin('other-plugin')
if other_plugin:
result = other_plugin.some_method(arg1, arg2)
# 发布事件
self.emit_event('my-event', {'data': 'value'})
# 订阅事件
@self.on_event('other-event')
def handle_event(event_data):
self.logger.info(f"收到事件: {event_data}")
```
### 插件生命周期
```
1. 发现阶段:扫描 plugins 目录,识别插件
2. 排序阶段:根据依赖关系确定加载顺序
3. 初始化阶段:调用每个插件的 init() 方法
4. 启动阶段:调用每个插件的 start() 方法
5. 运行阶段:插件正常提供服务
6. 停止阶段:调用每个插件的 stop() 方法 (按依赖逆序)
```
### 调试插件
```bash
# 启用调试模式
export NEBULA_DEBUG=1
python main.py
# 查看特定插件日志
tail -f logs/nebula.log | grep my-plugin
# 热重载插件 (开发模式)
python main.py --dev --reload-plugins
```
### 打包插件
```bash
# 创建插件包
cd plugins/my-plugin
zip -r my-plugin-1.0.0.zip .
# 安装插件
nebula plugin install my-plugin-1.0.0.zip
# 发布到插件市场
nebula plugin publish my-plugin-1.0.0.zip
```
---
## 最佳实践
### 1. 代码规范
- 遵循 PEP 8 编码规范
- 使用类型注解
- 编写单元测试 (覆盖率 > 80%)
- 添加详细的文档字符串
### 2. 错误处理
```python
try:
result = risky_operation()
except SpecificError as e:
self.logger.error(f"操作失败: {e}")
raise PluginError("操作执行失败", original_error=e)
finally:
cleanup_resources()
```
### 3. 日志记录
```python
# 不同级别的日志
self.logger.debug("调试信息")
self.logger.info("一般信息")
self.logger.warning("警告信息")
self.logger.error("错误信息")
self.logger.critical("严重错误")
# 带上下文的日志
self.logger.info(
"用户操作",
extra={"user_id": user_id, "action": "create"}
)
```
### 4. 性能优化
- 使用异步操作处理 I/O 密集型任务
- 实现缓存机制减少重复计算
- 批量处理数据库操作
- 监控资源使用情况
### 5. 安全考虑
- 验证所有用户输入
- 使用参数化查询防止 SQL 注入
- 实施速率限制
- 定期更新依赖
- 敏感信息使用环境变量
---
## 常见问题
### Q: 如何禁用 TUI 只使用 WebUI
```bash
# 设置环境变量
export NEBULA_TUI_ENABLED=false
python main.py
# 或在配置文件中
# config.yaml
tui:
enabled: false
```
### Q: TUI 显示乱码怎么办?
确保终端支持 UTF-8 和真彩色:
```bash
# 检查终端支持
echo $TERM # 应该类似 xterm-256color
# 启用真彩色
export COLORTERM=truecolor
```
### Q: 如何自定义 TUI 主题?
在插件的 `tui/styles.css` 中定义主题变量:
```css
:root {
--primary-color: #0066cc;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--bg-color: #1a1a2e;
--text-color: #eeeeee;
}
```
### Q: 插件加载失败如何排查?
```bash
# 查看详细日志
python main.py --verbose
# 检查插件依赖
nebula plugin check my-plugin
# 验证插件结构
nebula plugin validate my-plugin/
```
---
## 贡献指南
1. Fork 项目仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 创建 Pull Request
### 开发环境设置
```bash
# 克隆仓库
git clone https://github.com/nebulashell/nebulashell.git
cd nebulashell
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装开发依赖
pip install -e ".[dev]"
# 运行测试
pytest tests/
# 代码格式化
black .
isort .
flake8 .
```
---
## 许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。
## 联系方式
- 官网https://nebulashell.io
- 文档https://docs.nebulashell.io
- 社区https://community.nebulashell.io
- GitHub: https://github.com/nebulashell/nebulashell

150
oss/tui/README.md Normal file
View File

@@ -0,0 +1,150 @@
# TUI 转换层 - 强大的 WebUI 到终端界面转换引擎
## 架构设计
TUI 转换层是 NebulaShell 的核心组件之一,提供完整的 HTML/CSS/JS 到终端界面的转换能力。
### 核心理念
1. **只访问 WebUI 开放的 /tui 接口** - TUI 不直接渲染内容,而是通过 `/tui/*` 接口获取带有特殊标记的 HTML
2. **强大的转换层** - 自动解析 HTML 结构、CSS 样式、JS 交互配置,转换为终端元素
3. **参考 opencode 风格** - 提供现代化的终端用户体验
### 接口规范
#### `/tui/index.html` - TUI 入口
返回特殊标记的 HTML不含用户可见内容包含
- `data-tui-*` 属性标记
- `<script type="application/x-tui-keys">` 键盘绑定配置
- `<script type="application/x-tui-config">` 显示配置
- `<style type="text/x-tui-css">` 终端兼容 CSS
#### `/tui/page?path=/xxx` - 获取任意页面
从 WebUI 获取原始 HTML添加 TUI 标记后返回。
#### `/tui/css` - 终端兼容 CSS
只返回终端支持的 CSS 属性:
- 背景色ANSI 颜色)
- 文字颜色ANSI 颜色)
- 字体样式bold, italic, underline
- 边框样式
#### `/tui/js` - TUI 交互配置
模拟 JavaScript仅支持
- 获取鼠标位置
- 点击事件
- 按键事件
#### `/tui/interact` (POST) - 处理交互事件
接收 JSON 格式的事件数据:
```json
{"action": "navigate", "target": "/dashboard"}
{"action": "click", "target": "#button1"}
{"action": "keypress", "key": "q"}
```
#### `/tui/pages` - 列出可用页面
返回所有已注册页面的列表。
### HTML 标记规范
```html
<!-- TUI 页面标记 -->
<html class="tui-page" data-tui-version="2.0">
<!-- TUI 主体标记 -->
<body class="tui-body">
<!-- 布局容器 -->
<div data-tui-layout="vertical|horizontal|grid">
<!-- 元素类型 -->
<header data-tui-type="header">
<nav data-tui-type="nav">
<section data-tui-type="panel" data-tui-title="标题">
<button data-tui-key="q" data-tui-action="quit">
<a href="/page" data-tui-action="navigate" data-tui-key="1">
<!-- 分隔线 -->
<separator data-tui-char="─"/>
<!-- 键盘绑定配置 -->
<script type="application/x-tui-keys">
{"1": {"action": "navigate", "target": "/"}, "q": {"action": "quit"}}
</script>
<!-- 显示配置 -->
<script type="application/x-tui-config">
{"display": {"width": 80, "height": 24}, "mouse": {"enabled": true}}
</script>
<!-- 终端 CSS -->
<style type="text/x-tui-css">
.tui-page { background-color: #000000; color: #ffffff; }
.bold { font-weight: bold; }
</style>
```
### 支持的组件
| 组件 | HTML 标签 | 描述 |
|------|----------|------|
| 面板 | `<section data-tui-type="panel">` | 带边框的面板/卡片 |
| 按钮 | `<button data-tui-key="x">` | 可点击按钮,支持快捷键 |
| 列表 | `<ul>/<ol>` | 有序/无序列表 |
| 进度条 | `<div data-tui-type="progress">` | 进度条组件 |
| 加载动画 | `<div data-tui-type="spinner">` | 旋转加载器 |
| 导航 | `<nav data-tui-type="nav">` | 导航菜单 |
| 分隔线 | `<separator/>` | 水平分隔线 |
### 使用示例
```python
from oss.tui.converter import TUIManager, HTMLToTUIConverter
# 创建转换器
converter = HTMLToTUIConverter(width=80, height=24)
# 解析 HTML
html = """
<html class="tui-page">
<body class="tui-body">
<h1>欢迎</h1>
<button data-tui-key="q" data-tui-action="quit">退出 [q]</button>
<script type="application/x-tui-keys">
{"q": {"action": "quit"}}
</script>
</body>
</html>
"""
layout = converter.parse(html)
output = layout.render()
print(output)
# 使用 TUI 管理器
manager = TUIManager.get_instance()
manager.load_page("/welcome", html)
manager.render_current()
manager.run_event_loop()
```
### 开发指南
1. **为 WebUI 页面添加 TUI 支持**
- 在 HTML 中添加 `data-tui-*` 属性
- 添加键盘绑定配置脚本
- 确保 CSS 仅使用终端兼容属性
2. **创建新的 TUI 组件**
- 继承 `TUIElement` 基类
- 实现 `render()` 方法
-`HTMLToTUIConverter._create_tui_element()` 中注册
3. **扩展交互功能**
-`TUIInputHandler` 中添加新的事件处理器
-`/tui/interact` 接口中处理新的事件类型
## License
MIT License - NebulaShell Project

80
oss/tui/__init__.py Normal file
View File

@@ -0,0 +1,80 @@
"""TUI 核心模块 - 强大的 WebUI 到终端界面转换引擎 v1.3
本模块提供完整的 HTML/CSS/JS 到 TUI 的转换能力,参考 opencode 风格设计:
- HTML 解析:识别 data-tui-* 标记、语义化标签、Aria 属性,转换为 40+ 种终端元素
- CSS 转换:支持 ANSI 256 色、真彩色、完整字体排版、边框样式、阴影效果
- JS 交互完整模拟鼠标追踪、点击事件、键盘绑定、DOM 操作、事件系统
- 布局引擎flex/grid/absolute 布局终端适配,自动响应式调整
- 组件系统40+ 种组件(按钮、面板、列表、表单、表格、进度条、图表等)
- 高级特性:动画系统、主题系统、虚拟滚动、焦点管理、辅助功能
架构设计完全参考 opencode 风格,提供现代化、高性能终端体验。
"""
from .converter import (
# 管理器
TUIManager,
TUIRenderer,
HTMLToTUIConverter,
# 输入处理
TUIInputHandler,
TUIEventManager,
# 画布
TUICanvas,
# 样式系统
ANSIStyle,
BorderStyle,
TUIColor,
TUIStyle,
# 元素类型
TUIElementType,
# 基础元素
TUIElement,
TUIButton,
TUILabel,
TUIPanel,
TUILayout,
TUIList,
TUISeparator,
TUIProgressBar,
TUISpinner,
)
__all__ = [
# 管理器
'TUIManager',
'TUIRenderer',
'HTMLToTUIConverter',
# 输入处理
'TUIInputHandler',
'TUIEventManager',
# 画布
'TUICanvas',
# 样式系统
'ANSIStyle',
'BorderStyle',
'TUIColor',
'TUIStyle',
# 元素类型
'TUIElementType',
# 基础元素
'TUIElement',
'TUIButton',
'TUILabel',
'TUIPanel',
'TUILayout',
'TUIList',
'TUISeparator',
'TUIProgressBar',
'TUISpinner',
]

1430
oss/tui/converter.py Normal file

File diff suppressed because it is too large Load Diff

638
oss/tui/plugin.py Normal file
View File

@@ -0,0 +1,638 @@
"""TUI 插件 - 终端用户界面,与 WebUI 双启动
强大的转换层架构:
- 只访问 WebUI 开放的 /tui 接口
- 自动解析 .html 文件(入口是 index.html
- 支持终端兼容的 CSS背景、字体排版样式
- 支持基础 JS 交互(鼠标位置、点击、按键)
- 参考 opencode 风格的现代化终端体验
"""
import os
import sys
import threading
import time
from pathlib import Path
from oss.logger.logger import Log
from oss.plugin.types import Plugin, Response, register_plugin_type
from oss.config import get_config
from oss.tui.converter import TUIManager, TUIRenderer, HTMLToTUIConverter
class TUIPlugin(Plugin):
"""TUI 插件 - 提供终端界面,通过访问 WebUI 的 /tui 接口获取 HTML"""
def __init__(self):
self.webui = None
self.http_api = None
self.tui_manager = None
self.running = False
self.tui_thread = None
self.server = None
def meta(self):
from oss.plugin.types import Metadata, PluginConfig, Manifest
config = get_config()
return Manifest(
metadata=Metadata(
name="tui",
version="2.0.0",
author="NebulaShell",
description="终端用户界面 - 强大的 WebUI 转换层,与 WebUI 双启动"
),
config=PluginConfig(
enabled=True,
args={
"width": config.get("TUI_WIDTH", 80),
"height": config.get("TUI_HEIGHT", 24),
"theme": "dark",
"mouse_enabled": True,
}
),
dependencies=["http-api", "webui"]
)
def set_webui(self, webui):
"""注入 webui 引用"""
self.webui = webui
def set_http_api(self, http_api):
"""注入 http_api 引用"""
self.http_api = http_api
def init(self, deps: dict = None):
"""初始化 TUI - 注册 /tui 接口供转换层访问"""
Log.info("tui", "TUI 插件初始化中...")
# 创建 TUI 管理器
config = get_config()
width = config.get("TUI_WIDTH", 80)
height = config.get("TUI_HEIGHT", 24)
self.tui_manager = TUIManager.get_instance(width, height)
# 注册 /tui 路由供 TUI 转换层访问 WebUI 页面
if self.http_api and self.http_api.router:
# 核心接口:/tui/index.html - TUI 入口
self.http_api.router.get("/tui/index.html", self._handle_tui_index)
# 核心接口:/tui/page - 获取任意页面的 TUI 版本
self.http_api.router.get("/tui/page", self._handle_tui_page)
# 核心接口:/tui/css - 返回终端兼容的 CSS
self.http_api.router.get("/tui/css", self._handle_tui_css)
# 核心接口:/tui/js - 返回 TUI 交互配置(模拟 JS
self.http_api.router.get("/tui/js", self._handle_tui_js)
# 核心接口:/tui/interact - 处理 TUI 交互事件
self.http_api.router.post("/tui/interact", self._handle_tui_interact)
# 核心接口:/tui/pages - 列出所有可用页面
self.http_api.router.get("/tui/pages", self._handle_tui_pages)
Log.ok("tui", "已注册 TUI API 路由 (/tui/*)")
else:
Log.warn("tui", "警告:未找到 http-api 依赖")
# 从 WebUI 加载默认页面到 TUI 缓存
self._load_default_pages()
Log.ok("tui", "TUI 插件初始化完成 - 强大的转换层已就绪")
def _load_default_pages(self):
"""从 WebUI 加载默认页面到 TUI 缓存"""
default_pages = ["/", "/dashboard", "/logs", "/terminal", "/plugins"]
for path in default_pages:
try:
html = self._fetch_webui_page(path)
if html:
self.tui_manager.load_page(path, html)
Log.info("tui", f"已加载页面:{path}")
except Exception as e:
Log.debug("tui", f"加载页面 {path} 失败:{e}")
def _fetch_webui_page(self, path: str) -> str:
"""从 WebUI 获取页面 HTML - 转换层核心方法
此方法模拟访问 WebUI 页面并获取 HTML然后由 TUI 转换层解析。
WebUI 开放的 /tui 接口会返回带有特殊标记的 HTML不含用户可见内容
但包含 data-tui-* 属性和 script[type='application/x-tui-*'] 配置。
"""
if not self.webui or not hasattr(self.webui, 'server'):
return ""
try:
from oss.plugin.types import Request
request = Request(method="GET", path=path, headers={}, body="")
# 查找匹配的路由
router = self.webui.server.router
if hasattr(router, 'routes'):
for route_path, handler in router.routes.items():
if route_path == path or (route_path.endswith('*') and path.startswith(route_path[:-1])):
response = handler(request)
if response and hasattr(response, 'body'):
return response.body.decode('utf-8') if isinstance(response.body, bytes) else response.body
except Exception as e:
Log.debug("tui", f"获取 WebUI 页面失败:{e}")
return ""
def start(self):
"""启动 TUI在后台线程运行"""
Log.info("tui", "TUI 启动中...")
self.running = True
# 在后台线程运行 TUI
self.tui_thread = threading.Thread(target=self._tui_loop, daemon=True)
self.tui_thread.start()
Log.ok("tui", "TUI 已启动(后台模式)")
Log.info("tui", "提示:按 'q' 退出 TUIWebUI 仍在运行")
def _tui_loop(self):
"""TUI 主循环"""
try:
# 显示欢迎界面
self._show_welcome()
# 主事件循环
self._event_loop()
except Exception as e:
Log.error("tui", f"TUI 循环异常:{e}")
finally:
self.running = False
def _show_welcome(self):
"""显示欢迎界面"""
welcome_html = """
<!DOCTYPE html>
<html class="tui-page">
<head>
<title>NebulaShell TUI</title>
<meta charset="UTF-8">
<!-- TUI 标记:此页面专为终端渲染 -->
</head>
<body class="tui-body">
<header data-tui-type="header">
<h1>👋 欢迎使用 NebulaShell TUI</h1>
<p>终端用户界面已启动</p>
<p>WebUI 同时运行在http://localhost:8080</p>
</header>
<separator data-tui-char=""/>
<section data-tui-type="panel" data-tui-title="可用命令">
<ul>
<li>[1] 首页</li>
<li>[2] 仪表盘</li>
<li>[3] 日志</li>
<li>[4] 终端</li>
<li>[5] 插件管理</li>
<li>[q] 退出 TUI</li>
<li>[r] 刷新</li>
</ul>
</section>
<separator data-tui-char=""/>
<nav data-tui-type="nav">
<a href="/" data-tui-action="navigate" data-tui-key="1">首页</a>
<a href="/dashboard" data-tui-action="navigate" data-tui-key="2">仪表盘</a>
<a href="/logs" data-tui-action="navigate" data-tui-key="3">日志</a>
<a href="/terminal" data-tui-action="navigate" data-tui-key="4">终端</a>
<a href="/plugins" data-tui-action="navigate" data-tui-key="5">插件</a>
</nav>
<!-- TUI 脚本标记:键盘绑定配置 -->
<script type="application/x-tui-keys">
{"1": {"action": "navigate", "target": "/"}, "2": {"action": "navigate", "target": "/dashboard"}, "3": {"action": "navigate", "target": "/logs"}, "4": {"action": "navigate", "target": "/terminal"}, "5": {"action": "navigate", "target": "/plugins"}, "q": {"action": "quit"}, "r": {"action": "refresh"}}
</script>
</body>
</html>
"""
self.tui_manager.load_page("/welcome", welcome_html)
self._render_current("/welcome")
def _render_current(self, path: str = None):
"""渲染当前页面到终端"""
if path is None:
path = self.tui_manager.current_page or "/welcome"
output = self.tui_manager.render_page(path)
# 清屏并输出
sys.stdout.write('\x1b[2J\x1b[H')
sys.stdout.write(output)
sys.stdout.write('\n\n')
sys.stdout.write('\x1b[90m提示按数字键导航q 退出r 刷新\x1b[0m\n')
sys.stdout.flush()
def _event_loop(self):
"""简单的事件循环"""
import sys
import tty
import termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while self.running:
char = sys.stdin.read(1)
if char == '\x03': # Ctrl+C
break
elif char == '\x04': # Ctrl+D
break
elif char == 'q':
Log.info("tui", "用户退出 TUI")
break
elif char == '1':
self._render_current("/")
elif char == '2':
self._render_current("/dashboard")
elif char == '3':
self._render_current("/logs")
elif char == '4':
self._render_current("/terminal")
elif char == '5':
self._render_current("/plugins")
elif char == 'r':
self._load_default_pages()
self._render_current()
elif char == '\n' or char == '\r':
# Enter 刷新当前页
self._render_current()
except Exception as e:
Log.error("tui", f"事件循环错误:{e}")
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
# ========== TUI 核心接口实现 ==========
def _handle_tui_index(self, request):
"""处理 /tui/index.html 请求 - TUI 入口点
返回特殊标记的 HTMLTUI 转换层会识别并转换。
此 HTML 不含用户可见内容,仅包含 data-tui-* 标记和配置脚本。
"""
html = """<!DOCTYPE html>
<html class="tui-page" data-tui-version="2.0">
<head>
<meta charset="UTF-8">
<title>NebulaShell TUI</title>
<!-- TUI 标记:此页面专为终端渲染 -->
<style type="text/x-tui-css">
/* 终端兼容 CSS */
.tui-page { background-color: #000000; color: #ffffff; }
.tui-body { font-family: monospace; }
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
.header { font-weight: bold; font-size: large; }
.panel { border-style: single; }
</style>
</head>
<body class="tui-body">
<div class="tui-container" data-tui-layout="vertical">
<header data-tui-type="header">
<h1>NebulaShell TUI</h1>
<p>终端界面就绪</p>
</header>
<separator data-tui-char=""/>
<nav data-tui-type="nav" data-tui-layout="horizontal">
<a href="/" data-tui-action="navigate" data-tui-key="1">首页</a>
<a href="/dashboard" data-tui-action="navigate" data-tui-key="2">仪表盘</a>
<a href="/logs" data-tui-action="navigate" data-tui-key="3">日志</a>
<a href="/terminal" data-tui-action="navigate" data-tui-key="4">终端</a>
</nav>
<separator data-tui-char=""/>
<section data-tui-type="panel" data-tui-title="快捷操作">
<button data-tui-key="r" data-tui-action="refresh">刷新 [r]</button>
<button data-tui-key="q" data-tui-action="quit">退出 [q]</button>
</section>
</div>
<!-- TUI 脚本标记:键盘绑定配置 -->
<script type="application/x-tui-keys">
{"1": {"action": "navigate", "target": "/"}, "2": {"action": "navigate", "target": "/dashboard"}, "3": {"action": "navigate", "target": "/logs"}, "4": {"action": "navigate", "target": "/terminal"}, "r": {"action": "refresh"}, "q": {"action": "quit"}}
</script>
<!-- TUI 配置 -->
<script type="application/x-tui-config">
{"display": {"width": 80, "height": 24}, "mouse": {"enabled": true}, "keyboard": {"enabled": true}}
</script>
</body>
</html>"""
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
def _handle_tui_page(self, request):
"""处理 /tui/page 请求 - 获取任意页面的 TUI 版本
从 WebUI 获取原始 HTML添加 TUI 标记后返回。
TUI 转换层会自动解析这些标记并转换为终端元素。
"""
from urllib.parse import parse_qs, urlparse
parsed = urlparse(request.path)
params = parse_qs(parsed.query)
page_path = params.get('path', ['/'])[0]
# 从 WebUI 获取原始 HTML
html = self._fetch_webui_page(page_path)
if html:
# 添加 TUI 标记
html = html.replace('<html', '<html class="tui-page" data-tui-source="webui"')
if '<body' in html:
html = html.replace('<body', '<body class="tui-body"')
else:
html = html.replace('</head>', '<body class="tui-body"></head>')
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
else:
# 返回错误页面
error_html = """<!DOCTYPE html>
<html class="tui-page">
<body class="tui-body">
<h1>❌ 页面未找到</h1>
<p>路径:<span id="path"></span></p>
<button data-tui-key="b" data-tui-action="back">返回</button>
<script type="application/x-tui-keys">{"b": {"action": "back"}}</script>
</body>
</html>"""
return Response(
status=404,
headers={"Content-Type": "text/html; charset=utf-8"},
body=error_html
)
def _handle_tui_css(self, request):
"""处理 /tui/css 请求 - 返回终端兼容的 CSS
只返回终端支持的 CSS 属性:
- 背景色ANSI 颜色)
- 文字颜色ANSI 颜色)
- 字体样式bold, italic, underline
- 边框样式(单线、双线、圆角等)
"""
css = """/* TUI 兼容 CSS - 仅支持终端属性 */
/* 基础样式 */
.tui-page {
background-color: #000000;
color: #ffffff;
}
.tui-body {
font-family: monospace;
font-weight: normal;
}
/* 字体样式 - TUI 支持 */
.bold { font-weight: bold; }
.italic { font-style: italic; }
.underline { text-decoration: underline; }
.dim { opacity: 0.7; }
/* 布局 - TUI 简化处理 */
.tui-container {
padding: 0;
margin: 0;
}
[data-tui-layout="vertical"] {
display: block;
}
[data-tui-layout="horizontal"] {
display: inline-block;
}
/* 边框样式 */
[data-tui-border="single"] {
border-style: single;
}
[data-tui-border="double"] {
border-style: double;
}
[data-tui-border="rounded"] {
border-style: rounded;
}
/* 交互元素标记 */
[data-tui-action] {
cursor: pointer;
}
[data-tui-key]::before {
content: "[" attr(data-tui-key) "] ";
}
/* 面板/卡片 */
[data-tui-type="panel"] {
border-style: single;
padding: 1;
}
/* 按钮 */
button, [data-tui-type="button"] {
border-style: single;
padding: 0 2;
}
/* 列表 */
ul, ol {
list-style-position: inside;
}
/* 进度条 */
[data-tui-type="progress"] {
filled-char: "";
empty-char: "";
}
/* 加载动画 */
[data-tui-type="spinner"] {
animation: spin 1s linear infinite;
}
"""
return Response(
status=200,
headers={"Content-Type": "text/css"},
body=css
)
def _handle_tui_js(self, request):
"""处理 /tui/js 请求 - 返回 TUI 交互配置(模拟 JS
TUI 不支持完整 JavaScript只支持
- 获取鼠标位置
- 点击事件
- 按键事件
- 简单的 DOM 操作
"""
js_config = """// TUI JS 模拟配置
// 仅支持基础交互功能
const TUI = {
// 鼠标支持
mouse: {
enabled: true,
getPosition: () => ({ x: 0, y: 0 }),
onClick: (handler) => {},
},
// 键盘支持
keyboard: {
enabled: true,
onKeyPress: (handler) => {},
bindings: {},
},
// DOM 操作(简化版)
querySelector: (selector) => null,
querySelectorAll: (selector) => [],
// 事件系统
addEventListener: (event, handler) => {},
removeEventListener: (event, handler) => {},
};
// 导出配置
export default TUI;
"""
return Response(
status=200,
headers={"Content-Type": "application/javascript"},
body=js_config
)
def _handle_tui_interact(self, request):
"""处理 TUI 交互请求 - 处理鼠标、键盘事件"""
import json
try:
body = json.loads(request.body)
action = body.get('action', '')
target = body.get('target', '')
key = body.get('key', '')
mouse_x = body.get('mouse_x', 0)
mouse_y = body.get('mouse_y', 0)
# 处理导航
if action == 'navigate':
html = self._fetch_webui_page(target)
if html:
self.tui_manager.load_page(target, html)
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'page': target})
)
# 处理点击
elif action == 'click':
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'target': target})
)
# 处理按键
elif action == 'keypress':
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'key': key})
)
# 处理鼠标移动
elif action == 'mousemove':
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'x': mouse_x, 'y': mouse_y})
)
# 处理刷新
elif action == 'refresh':
self._load_default_pages()
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True})
)
# 处理退出
elif action == 'quit':
self.running = False
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'message': 'Quitting TUI'})
)
return Response(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': False, 'error': 'Unknown action'})
)
except Exception as e:
return Response(
status=500,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': False, 'error': str(e)})
)
def _handle_tui_pages(self, request):
"""处理 /tui/pages 请求 - 列出所有可用页面"""
import json
pages = []
if self.webui and hasattr(self.webui, 'server'):
router = self.webui.server.router
if hasattr(router, 'routes'):
pages = list(router.routes.keys())
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({
'success': True,
'pages': pages,
'current': self.tui_manager.current_page if self.tui_manager else None
})
)
def stop(self):
"""停止 TUI"""
Log.info("tui", "TUI 停止中...")
self.running = False
if self.tui_thread:
self.tui_thread.join(timeout=2)
Log.ok("tui", "TUI 已停止")
register_plugin_type("TUIPlugin", TUIPlugin)
def New():
return TUIPlugin()

View File

@@ -25,6 +25,12 @@ class WebUIServer:
self.router.get("/static/js/main.js", self._handle_js)
self.router.get("/health", self._handle_health)
# TUI 接口 - 供 TUI 转换层访问
self.router.get("/tui/index.html", self._handle_tui_index)
self.router.get("/tui/page", self._handle_tui_page)
self.router.get("/tui/css", self._handle_tui_css)
self.router.get("/tui/pages", self._handle_tui_pages)
def register_page(self, path: str, content_provider, nav_item: dict = None):
"""供其他插件注册页面"""
self.pages[path] = content_provider
@@ -179,3 +185,85 @@ class WebUIServer:
def _handle_health(self, request):
import json
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps({"status": "ok"}))
# ========== TUI 接口实现 ==========
def _handle_tui_index(self, request):
"""处理 /tui/index.html 请求 - TUI 入口点
返回特殊标记的 HTMLTUI 转换层会识别并转换。
此 HTML 不含用户可见内容,仅包含 data-tui-* 标记和配置脚本。
"""
html = """<!DOCTYPE html>
<html class="tui-page" data-tui-version="2.0">
<head>
<meta charset="UTF-8">
<title>NebulaShell TUI</title>
<!-- TUI 标记:此页面专为终端渲染 -->
<style type="text/x-tui-css">
.tui-page { background-color: #000000; color: #ffffff; }
.tui-body { font-family: monospace; }
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
</style>
</head>
<body class="tui-body">
<div class="tui-container" data-tui-layout="vertical">
<header data-tui-type="header">
<h1>NebulaShell TUI</h1>
<p>终端界面就绪</p>
</header>
<separator data-tui-char=""/>
<nav data-tui-type="nav" data-tui-layout="horizontal">
<a href="/" data-tui-action="navigate" data-tui-key="1">首页</a>
<a href="/dashboard" data-tui-action="navigate" data-tui-key="2">仪表盘</a>
<a href="/logs" data-tui-action="navigate" data-tui-key="3">日志</a>
</nav>
</div>
<script type="application/x-tui-keys">
{"1": {"action": "navigate", "target": "/"}, "2": {"action": "navigate", "target": "/dashboard"}, "3": {"action": "navigate", "target": "/logs"}}
</script>
</body>
</html>"""
return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html)
def _handle_tui_page(self, request):
"""处理 /tui/page 请求 - 获取任意页面的 TUI 版本"""
from urllib.parse import parse_qs, urlparse
parsed = urlparse(request.path)
params = parse_qs(parsed.query)
page_path = params.get('path', ['/'])[0]
# 查找已注册的页面
provider = self.pages.get(page_path)
if provider:
content = provider()
html = f"""<!DOCTYPE html>
<html class="tui-page" data-tui-source="webui">
<body class="tui-body">{content}</body>
</html>"""
return Response(status=200, headers={"Content-Type": "text/html; charset=utf-8"}, body=html)
return Response(status=404, headers={"Content-Type": "text/html"}, body="<html><body>Page not found</body></html>")
def _handle_tui_css(self, request):
"""处理 /tui/css 请求 - 返回终端兼容的 CSS"""
css = """/* TUI 兼容 CSS */
.tui-page { background-color: #000000; color: #ffffff; }
.tui-body { font-family: monospace; }
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
[data-tui-action] { cursor: pointer; }
"""
return Response(status=200, headers={"Content-Type": "text/css"}, body=css)
def _handle_tui_pages(self, request):
"""处理 /tui/pages 请求 - 列出所有可用页面"""
import json
pages = list(self.pages.keys())
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'pages': pages})
)

View File

@@ -1,4 +1,4 @@
"""WebUI - Web 控制台 (容器模式)"""
"""WebUI - Web 控制台 (容器模式) + TUI 双启动"""
from pathlib import Path
from oss.logger.logger import Log
from oss.plugin.types import Plugin, Response, register_plugin_type
@@ -7,11 +7,12 @@ from .core.server import WebUIServer
class WebUIPlugin(Plugin):
"""WebUI 插件 - 提供页面容器"""
"""WebUI 插件 - 提供页面容器,同时启动 TUI"""
def __init__(self):
self.http_api = None
self.server = None
self.tui = None
self.config = {}
def meta(self):
@@ -22,14 +23,15 @@ class WebUIPlugin(Plugin):
name="webui",
version="2.1.0",
author="NebulaShell",
description="Web 控制台容器 - 供其他插件注册页面"
description="Web 控制台容器 + TUI 双启动 - 供其他插件注册页面"
),
config=PluginConfig(
enabled=True,
args={
"port": config.get("HTTP_API_PORT", 8080),
"theme": "dark",
"title": "NebulaShell"
"title": "NebulaShell",
"tui_enabled": True # 默认启用 TUI
}
),
dependencies=["http-api"]
@@ -39,10 +41,14 @@ class WebUIPlugin(Plugin):
"""注入 http-api"""
self.http_api = http_api
def set_tui(self, tui):
"""注入 tui 引用"""
self.tui = tui
def init(self, deps: dict = None):
"""初始化 WebUI 服务器"""
"""初始化 WebUI 服务器和 TUI"""
if not self.http_api:
Log.error("webui", "错误: 未找到 http-api 依赖")
Log.error("webui", "错误未找到 http-api 依赖")
return
config = {}
@@ -52,7 +58,8 @@ class WebUIPlugin(Plugin):
self.config = {
"port": config.get("port", get_config().get("HTTP_API_PORT", 8080)),
"theme": config.get("theme", "dark"),
"title": config.get("title", "NebulaShell")
"title": config.get("title", "NebulaShell"),
"tui_enabled": config.get("tui_enabled", True)
}
# 使用 http-api 的路由器
@@ -62,6 +69,10 @@ class WebUIPlugin(Plugin):
)
Log.info("webui", "容器初始化完成")
# 如果启用了 TUI通知 TUI 插件
if self.config.get("tui_enabled") and self.tui:
Log.info("webui", "TUI 已启用,将双启动")
def start(self):
"""启动服务器(注册默认路由)"""
if self.server:
@@ -69,7 +80,11 @@ class WebUIPlugin(Plugin):
self._setup_home_page()
self.server.start()
Log.info("webui", f"WebUI 容器已启动: http://localhost:{self.config['port']}")
Log.info("webui", f"WebUI 容器已启动http://localhost:{self.config['port']}")
# 如果启用了 TUI在后台启动
if self.config.get("tui_enabled"):
Log.info("webui", "TUI 双启动中...")
def _setup_home_page(self):
"""设置首页:如果仪表盘已安装则跳转到仪表盘,否则显示默认首页"""
@@ -118,7 +133,7 @@ class WebUIPlugin(Plugin):
if self.server:
self.server.register_page(path, content_provider, nav_item)
else:
Log.warn("webui", f"警告: 试图注册页面 {path},但服务器未初始化")
Log.warn("webui", f"警告试图注册页面 {path},但服务器未初始化")
def add_nav_item(self, item: dict):
"""仅添加导航项(如果页面由其他方式处理)"""

View File

@@ -3,7 +3,7 @@
"name": "webui",
"version": "2.1.0",
"author": "NebulaShell",
"description": "Web 控制台 - 多语言支持/插件管理/安全配置/系统监控",
"description": "Web 控制台 + TUI 双启动 - 多语言支持/插件管理/安全配置/系统监控",
"type": "webui"
},
"config": {
@@ -18,7 +18,8 @@
"enable_2fa": false,
"show_plugins": true,
"show_security": true,
"show_deployments": true
"show_deployments": true,
"tui_enabled": true
}
},
"dependencies": ["http-api", "i18n"],

View File

@@ -0,0 +1,187 @@
# NebulaShell TUI - 终端用户界面
## 概述
TUITerminal User Interface插件为 NebulaShell 提供终端界面,与 WebUI 双启动运行。
## 核心特性
### 1. 转换层架构
TUI 本身只有一个转换层,它只会访问 WebUI 所开放的 `/tui` 接口:
- **`/tui/index.html`** - TUI 入口页面
- **`/tui/page`** - 获取任意页面的 TUI 版本
- **`/tui/css`** - 终端兼容的 CSS
- **`/tui/interact`** - 处理交互事件
### 2. HTML 标记规范
WebUI 开放的 `.html` 文件中不含有任何给用户看的内容,但包含 TUI 可解析的特殊标记:
```html
<!-- TUI 页面标记 -->
<html class="tui-page">
<body class="tui-body">
<!-- TUI 容器 -->
<div class="tui-container" data-tui-layout="vertical">
<!-- 键盘快捷键标记 -->
<a href="/" data-tui-key="1" data-tui-action="navigate">[1] 首页</a>
<!-- TUI 配置脚本 -->
<script type="application/x-tui-config">
{
"keyboard": {
"1": {"action": "navigate", "target": "/"},
"q": {"action": "quit"}
}
}
</script>
<!-- TUI CSS (仅终端支持的样式) -->
<style type="text/x-tui-css">
.tui-header { font-weight: bold; }
</style>
```
### 3. 支持的 CSS 属性
TUI 只支持终端能够渲染的样式:
| CSS 属性 | TUI 转换 | 说明 |
|---------|---------|------|
| `font-weight: bold` | ANSI 加粗 | `\x1b[1m` |
| `font-style: italic` | ANSI 斜体 | `\x1b[3m` |
| `text-decoration: underline` | ANSI 下划线 | `\x1b[4m` |
| `background-color` | ANSI 背景色 | 仅支持基础 8 色 |
| `color` | ANSI 前景色 | 仅支持基础 8 色 |
| `text-align` | 文本对齐 | left/center/right |
### 4. 支持的 JS 交互
TUI 只支持基础的终端交互:
- **鼠标位置** - 通过 ANSI 鼠标协议获取
- **点击事件** - 转换为选择操作
- **按键输入** - 完整的键盘支持
```javascript
// TUI 配置中的键盘映射
{
"keyboard": {
"1": {"action": "navigate", "target": "/"},
"ArrowUp": {"action": "navigate_up"},
"Enter": {"action": "select"},
"q": {"action": "quit"}
}
}
```
## 文件结构
```
webui/tui/
├── __init__.py # 包初始化
├── main.py # TUI 插件主程序
├── converter.py # HTML 到 TUI 转换层
├── index.html # TUI 入口页面(含特殊标记)
├── manifest.json # 插件清单
└── README.md # 本文档
```
## 使用方式
### 启动 NebulaShell
```bash
# 正常启动WebUI 和 TUI 会同时运行
python main.py serve
# 或通过 CLI
python -m oss.cli serve
```
### TUI 快捷键
| 按键 | 功能 |
|-----|------|
| `1` | 首页 |
| `2` | 仪表盘 |
| `3` | 日志 |
| `4` | 终端 |
| `5` | 插件 |
| `6` | 设置 |
| `r` | 刷新 |
| `h` | 帮助 |
| `↑/↓` | 上下导航 |
| `Enter` | 确认 |
| `q` | 退出 TUI |
## 开发指南
### 创建 TUI 兼容页面
1. 在 WebUI 插件中创建页面时,添加 TUI 标记
2. 使用 `data-tui-*` 属性定义交互行为
3.`<script type="application/x-tui-config">` 中定义键盘映射
4.`<style type="text/x-tui-css">` 中定义终端样式
### 示例
```python
# 在 WebUI 插件中注册 TUI 兼容页面
def register_tui_page(webui):
webui.register_page(
path='/mypage',
content_provider=lambda: '''
<!DOCTYPE html>
<html class="tui-page">
<head><title>我的页面</title></head>
<body class="tui-body">
<div class="tui-container">
<h1 data-tui-style="bold">欢迎</h1>
<a href="/action" data-tui-key="a" data-tui-action="click">执行操作</a>
</div>
<script type="application/x-tui-config">
{"keyboard": {"a": {"action": "click", "target": "/api/action"}}}
</script>
</body>
</html>
''',
nav_item={'icon': 'ri-star-line', 'text': '我的页面'}
)
```
## 技术细节
### 转换流程
1. TUI 插件启动时访问 `/tui/index.html`
2. `HTMLToTUIConverter` 解析 HTML 提取:
- 文本内容
- 按钮和链接
- TUI 配置(键盘映射、样式)
3. `TUIRenderer` 将元素渲染为 ANSI 转义序列
4. `TUICanvas` 管理终端显示缓冲区
5. `TUIInputHandler` 处理键盘/鼠标输入
### ANSI 颜色映射
```python
COLOR_MAP = {
'#000000': '\x1b[30m', # black
'#ff0000': '\x1b[31m', # red
'#00ff00': '\x1b[32m', # green
'#ffff00': '\x1b[33m', # yellow
'#0000ff': '\x1b[34m', # blue
'#ff00ff': '\x1b[35m', # magenta
'#00ffff': '\x1b[36m', # cyan
'#ffffff': '\x1b[37m', # white
}
```
## 许可证
MIT License - NebulaShell Project

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html class="tui-page" lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NebulaShell TUI</title>
<!-- TUI 标记:此页面专为终端渲染设计 -->
<!-- 不包含任何给用户直接查看的内容,仅用于 TUI 转换层解析 -->
</head>
<body class="tui-body">
<!-- TUI 容器标记 -->
<div class="tui-container" data-tui-layout="vertical">
<!-- 标题区域 -->
<header class="tui-header" data-tui-style="bold">
NebulaShell TUI - 终端用户界面
</header>
<!-- 状态栏 -->
<div class="tui-status" data-tui-style="dim">
WebUI: http://localhost:8080 | TUI: 双启动模式
</div>
<!-- 导航菜单 - TUI 会转换为键盘快捷键 -->
<nav class="tui-nav" data-tui-type="keyboard">
<a href="/" data-tui-key="1" data-tui-action="navigate">[1] 首页</a>
<a href="/dashboard" data-tui-key="2" data-tui-action="navigate">[2] 仪表盘</a>
<a href="/logs" data-tui-key="3" data-tui-action="navigate">[3] 日志</a>
<a href="/terminal" data-tui-key="4" data-tui-action="navigate">[4] 终端</a>
<a href="/plugins" data-tui-key="5" data-tui-action="navigate">[5] 插件</a>
<a href="/settings" data-tui-key="6" data-tui-action="navigate">[6] 设置</a>
</nav>
<!-- 分隔线 -->
<hr class="tui-separator" data-tui-char="─">
<!-- 操作提示 -->
<footer class="tui-footer" data-tui-style="dim">
<p>快捷键说明:</p>
<ul>
<li data-tui-key="q">q - 退出 TUI</li>
<li data-tui-key="r">r - 刷新当前页</li>
<li data-tui-key="h">h - 显示帮助</li>
<li data-tui-key="↑/↓">↑/↓ - 上下导航</li>
<li data-tui-key="Enter">Enter - 确认选择</li>
</ul>
</footer>
</div>
<!-- TUI 配置脚本 - 定义键盘映射和交互行为 -->
<script type="application/x-tui-config">
{
"keyboard": {
"1": {"action": "navigate", "target": "/"},
"2": {"action": "navigate", "target": "/dashboard"},
"3": {"action": "navigate", "target": "/logs"},
"4": {"action": "navigate", "target": "/terminal"},
"5": {"action": "navigate", "target": "/plugins"},
"6": {"action": "navigate", "target": "/settings"},
"q": {"action": "quit"},
"r": {"action": "refresh"},
"h": {"action": "help"},
"ArrowUp": {"action": "navigate_up"},
"ArrowDown": {"action": "navigate_down"},
"Enter": {"action": "select"}
},
"mouse": {
"enabled": false,
"click_action": "select"
},
"display": {
"width": 80,
"height": 24,
"theme": "dark",
"border_style": "single",
"colors": {
"header": "bold",
"normal": "default",
"dim": "dim",
"selected": "reverse"
}
}
}
</script>
<!-- TUI CSS 标记 - 仅包含终端支持的样式 -->
<style type="text/x-tui-css">
.tui-page {
background-color: #000000;
color: #ffffff;
}
.tui-header {
font-weight: bold;
text-align: center;
}
.tui-status {
font-style: dim;
border-bottom: single;
}
.tui-nav a {
display: block;
padding: 1;
cursor: pointer;
}
.tui-separator {
border-char: ;
}
.tui-footer {
font-style: dim;
margin-top: 2;
}
</style>
</body>
</html>

View File

@@ -0,0 +1,378 @@
"""TUI 插件 - 终端用户界面,与 WebUI 双启动"""
import os
import sys
import threading
import time
from pathlib import Path
from oss.logger.logger import Log
from oss.plugin.types import Plugin, Response, register_plugin_type
from .tui.converter import TUIManager, TUIRenderer, HTMLToTUIConverter
class TUIPlugin(Plugin):
"""TUI 插件 - 提供终端界面,通过访问 WebUI 的 /tui 接口获取 HTML"""
def __init__(self):
self.webui = None
self.http_api = None
self.tui_manager = None
self.running = False
self.tui_thread = None
def meta(self):
from oss.plugin.types import Metadata, PluginConfig, Manifest
return Manifest(
metadata=Metadata(
name="tui",
version="1.0.0",
author="NebulaShell",
description="终端用户界面 - 与 WebUI 双启动"
),
config=PluginConfig(enabled=True, args={}),
dependencies=["http-api", "webui"]
)
def set_webui(self, webui):
"""注入 webui 引用"""
self.webui = webui
def set_http_api(self, http_api):
"""注入 http_api 引用"""
self.http_api = http_api
def init(self, deps: dict = None):
"""初始化 TUI"""
Log.info("tui", "TUI 插件初始化中...")
# 创建 TUI 管理器
self.tui_manager = TUIManager.get_instance()
# 注册 /tui 路由供 TUI 访问 WebUI 页面
if self.http_api and self.http_api.router:
# 注册 TUI 专用 API
self.http_api.router.get("/tui/index.html", self._handle_tui_index)
self.http_api.router.get("/tui/page", self._handle_tui_page)
self.http_api.router.get("/tui/css", self._handle_tui_css)
self.http_api.router.post("/tui/interact", self._handle_tui_interact)
Log.ok("tui", "已注册 TUI API 路由")
else:
Log.warn("tui", "警告:未找到 http-api 依赖")
# 加载默认页面(从 WebUI 获取)
self._load_default_pages()
Log.ok("tui", "TUI 插件初始化完成")
def _load_default_pages(self):
"""从 WebUI 加载默认页面到 TUI"""
# 模拟访问 WebUI 页面并缓存
default_pages = ["/", "/dashboard", "/logs", "/terminal"]
for path in default_pages:
try:
# 这里会通过内部调用获取 WebUI 渲染的 HTML
html = self._fetch_webui_page(path)
if html:
self.tui_manager.load_page(path, html)
Log.info("tui", f"已加载页面:{path}")
except Exception as e:
Log.warn("tui", f"加载页面 {path} 失败:{e}")
def _fetch_webui_page(self, path: str) -> str:
"""从 WebUI 获取页面 HTML"""
if not self.webui or not hasattr(self.webui, 'server'):
return ""
# 模拟请求获取 WebUI 页面
# 由于我们在同一进程,可以直接调用 server 的路由处理
try:
from oss.plugin.types import Request
request = Request(method="GET", path=path, headers={}, body="")
# 查找匹配的路由
router = self.webui.server.router
if hasattr(router, 'routes'):
for route_path, handler in router.routes.items():
if route_path == path or (route_path.endswith('*') and path.startswith(route_path[:-1])):
response = handler(request)
if response and hasattr(response, 'body'):
return response.body.decode('utf-8') if isinstance(response.body, bytes) else response.body
except Exception as e:
Log.warn("tui", f"获取 WebUI 页面失败:{e}")
return ""
def start(self):
"""启动 TUI在后台线程运行"""
Log.info("tui", "TUI 启动中...")
self.running = True
# 在后台线程运行 TUI
self.tui_thread = threading.Thread(target=self._tui_loop, daemon=True)
self.tui_thread.start()
Log.ok("tui", "TUI 已启动(后台模式)")
Log.info("tui", "提示:按 'q' 退出 TUIWebUI 仍在运行")
def _tui_loop(self):
"""TUI 主循环"""
try:
# 显示欢迎界面
self._show_welcome()
# 主事件循环
self._event_loop()
except Exception as e:
Log.error("tui", f"TUI 循环异常:{e}")
finally:
self.running = False
def _show_welcome(self):
"""显示欢迎界面"""
welcome_html = """
<!DOCTYPE html>
<html>
<head><title>NebulaShell TUI</title></head>
<body>
<h1>👋 欢迎使用 NebulaShell TUI</h1>
<p>终端用户界面已启动</p>
<p>WebUI 同时运行在http://localhost:8080</p>
<hr>
<h2>可用命令:</h2>
<ul>
<li>[1] 首页</li>
<li>[2] 仪表盘</li>
<li>[3] 日志</li>
<li>[4] 终端</li>
<li>[q] 退出 TUI</li>
<li>[r] 刷新</li>
</ul>
</body>
</html>
"""
self.tui_manager.load_page("/welcome", welcome_html)
self._render_current("/welcome")
def _render_current(self, path: str = None):
"""渲染当前页面到终端"""
if path is None:
path = self.tui_manager.current_page or "/welcome"
output = self.tui_manager.render_page(path)
# 清屏并输出
sys.stdout.write('\x1b[2J\x1b[H')
sys.stdout.write(output)
sys.stdout.write('\n\n')
sys.stdout.write('\x1b[90m提示按数字键导航q 退出\x1b[0m\n')
sys.stdout.flush()
def _event_loop(self):
"""简单的事件循环"""
import sys
import tty
import termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while self.running:
char = sys.stdin.read(1)
if char == '\x03': # Ctrl+C
break
elif char == '\x04': # Ctrl+D
break
elif char == 'q':
Log.info("tui", "用户退出 TUI")
break
elif char == '1':
self._render_current("/")
elif char == '2':
self._render_current("/dashboard")
elif char == '3':
self._render_current("/logs")
elif char == '4':
self._render_current("/terminal")
elif char == 'r':
self._load_default_pages()
self._render_current()
elif char == '\n' or char == '\r':
# Enter 刷新当前页
self._render_current()
except Exception as e:
Log.error("tui", f"事件循环错误:{e}")
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def _handle_tui_index(self, request):
"""处理 /tui/index.html 请求"""
# 返回特殊标记的 HTMLTUI 会识别并转换
html = """<!DOCTYPE html>
<html class="tui-page">
<head>
<meta charset="UTF-8">
<title>NebulaShell TUI</title>
<!-- TUI 标记:此页面专为终端渲染 -->
</head>
<body class="tui-body">
<div class="tui-container">
<h1>NebulaShell TUI</h1>
<p>终端界面就绪</p>
<div class="tui-nav">
<a href="/" data-tui-action="navigate">首页</a>
<a href="/dashboard" data-tui-action="navigate">仪表盘</a>
<a href="/logs" data-tui-action="navigate">日志</a>
<a href="/terminal" data-tui-action="navigate">终端</a>
</div>
</div>
<!-- TUI 脚本标记:这些会被转换为键盘绑定 -->
<script type="application/x-tui-keys">
{"1": "/", "2": "/dashboard", "3": "/logs", "4": "/terminal", "q": "quit", "r": "refresh"}
</script>
</body>
</html>"""
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
def _handle_tui_page(self, request):
"""处理 /tui/page 请求 - 获取任意页面的 TUI 版本"""
from urllib.parse import parse_qs, urlparse
parsed = urlparse(request.path)
params = parse_qs(parsed.query)
page_path = params.get('path', ['/'])[0]
# 从 WebUI 获取原始 HTML
html = self._fetch_webui_page(page_path)
if html:
# 添加 TUI 标记
html = html.replace('<html', '<html class="tui-page"')
if '<body' in html:
html = html.replace('<body', '<body class="tui-body"')
else:
html = html.replace('</head>', '<body class="tui-body"></head>')
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
else:
return Response(
status=404,
headers={"Content-Type": "text/html"},
body="<html><body>Page not found</body></html>"
)
def _handle_tui_css(self, request):
"""处理 /tui/css 请求 - 返回终端兼容的 CSS"""
# 只返回终端支持的 CSS 属性
css = """/* TUI 兼容 CSS */
.tui-page {
/* 背景色 - 仅支持 ANSI 颜色 */
background-color: #000000;
color: #ffffff;
}
.tui-body {
font-family: monospace;
font-weight: normal;
}
/* 字体样式 - TUI 支持 */
.bold { font-weight: bold; }
.underline { text-decoration: underline; }
/* 布局 - TUI 简化处理 */
.tui-container {
padding: 0;
margin: 0;
}
/* 交互元素标记 */
[data-tui-action] {
cursor: pointer;
}
"""
return Response(
status=200,
headers={"Content-Type": "text/css"},
body=css
)
def _handle_tui_interact(self, request):
"""处理 TUI 交互请求"""
import json
try:
body = json.loads(request.body)
action = body.get('action', '')
target = body.get('target', '')
# 处理交互
if action == 'navigate':
# 导航到指定页面
html = self._fetch_webui_page(target)
if html:
self.tui_manager.load_page(target, html)
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'page': target})
)
elif action == 'click':
# 处理点击
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True})
)
elif action == 'keypress':
# 处理按键
key = body.get('key', '')
return Response(
status=200,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': True, 'key': key})
)
return Response(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': False, 'error': 'Unknown action'})
)
except Exception as e:
return Response(
status=500,
headers={"Content-Type": "application/json"},
body=json.dumps({'success': False, 'error': str(e)})
)
def stop(self):
"""停止 TUI"""
Log.info("tui", "TUI 停止中...")
self.running = False
if self.tui_thread:
self.tui_thread.join(timeout=2)
Log.ok("tui", "TUI 已停止")
register_plugin_type("TUIPlugin", TUIPlugin)
def New():
return TUIPlugin()

View File

@@ -0,0 +1,28 @@
{
"metadata": {
"name": "tui",
"version": "1.0.0",
"author": "NebulaShell",
"description": "终端用户界面 - 与 WebUI 双启动,通过访问 /tui 接口获取 HTML 并转换为终端显示",
"type": "tui"
},
"config": {
"enabled": true,
"args": {
"width": 80,
"height": 24,
"theme": "dark",
"enable_mouse": false,
"keyboard_shortcuts": {
"1": "/",
"2": "/dashboard",
"3": "/logs",
"4": "/terminal",
"q": "quit",
"r": "refresh"
}
}
},
"dependencies": ["http-api", "webui"],
"permissions": ["read:pages", "execute:commands"]
}