Files
NebulaShell/oss/tui/plugin.py

271 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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):
self.webui = webui
def set_http_api(self, http_api):
Log.info("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)
if self.http_api and self.http_api.router:
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.get("/tui/js", self._handle_tui_js)
self.http_api.router.post("/tui/interact", self._handle_tui_interact)
self.http_api.router.get("/tui/pages", self._handle_tui_pages)
Log.ok("tui", "已注册 TUI API 路由 (/tui/*)")
else:
Log.warn("tui", "警告:未找到 http-api 依赖")
self._load_default_pages()
Log.ok("tui", "TUI 插件初始化完成 - 强大的转换层已就绪")
def _load_default_pages(self):
此方法模拟访问 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):
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):
<!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):
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': break
elif char == '\x04': 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':
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):
html =
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=html
)
def _handle_tui_page(self, request):
from urllib.parse import parse_qs, urlparse
parsed = urlparse(request.path)
params = parse_qs(parsed.query)
page_path = params.get('path', ['/'])[0]
html = self._fetch_webui_page(page_path)
if html:
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 =
return Response(
status=404,
headers={"Content-Type": "text/html; charset=utf-8"},
body=error_html
)
def _handle_tui_css(self, request):
css = // 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):
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 wait_for_exit(self):
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()