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:
187
store/@{NebulaShell}/webui/tui/README.md
Normal file
187
store/@{NebulaShell}/webui/tui/README.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# NebulaShell TUI - 终端用户界面
|
||||
|
||||
## 概述
|
||||
|
||||
TUI(Terminal 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
|
||||
0
store/@{NebulaShell}/webui/tui/__init__.py
Normal file
0
store/@{NebulaShell}/webui/tui/__init__.py
Normal file
1063
store/@{NebulaShell}/webui/tui/converter.py
Normal file
1063
store/@{NebulaShell}/webui/tui/converter.py
Normal file
File diff suppressed because it is too large
Load Diff
113
store/@{NebulaShell}/webui/tui/index.html
Normal file
113
store/@{NebulaShell}/webui/tui/index.html
Normal 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>
|
||||
378
store/@{NebulaShell}/webui/tui/main.py
Normal file
378
store/@{NebulaShell}/webui/tui/main.py
Normal 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' 退出 TUI,WebUI 仍在运行")
|
||||
|
||||
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 请求"""
|
||||
# 返回特殊标记的 HTML,TUI 会识别并转换
|
||||
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()
|
||||
28
store/@{NebulaShell}/webui/tui/manifest.json
Normal file
28
store/@{NebulaShell}/webui/tui/manifest.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user