808 lines
30 KiB
Python
808 lines
30 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
import logging
|
||
import sys
|
||
import io
|
||
import time
|
||
from textual.app import App
|
||
from textual.containers import Container, ScrollableContainer
|
||
from textual.widgets import Static, Input, Header, Footer
|
||
from textual.reactive import reactive
|
||
from typing import List, Dict
|
||
import asyncio
|
||
from datetime import datetime
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class SystemExitGraceful(Exception):
|
||
"""优雅的系统退出异常"""
|
||
pass
|
||
|
||
class LogDisplay(Static):
|
||
"""日志显示组件 - 直接捕获所有日志输出"""
|
||
|
||
def __init__(self):
|
||
super().__init__("日志显示区域 - 等待日志输入...")
|
||
self.log_lines: List[str] = []
|
||
self.max_lines = 500
|
||
self.auto_scroll_enabled = True # 启用自动滚动
|
||
|
||
# 保存原始的logging处理器和格式器
|
||
self.original_handlers = []
|
||
self.original_formatters = {}
|
||
|
||
logger.debug("LogDisplay初始化完成")
|
||
|
||
def start_capture(self):
|
||
"""开始捕获所有日志输出"""
|
||
try:
|
||
# 获取根日志记录器
|
||
root_logger = logging.getLogger()
|
||
|
||
# 保存原始处理器和它们的格式器
|
||
self.original_handlers = root_logger.handlers.copy()
|
||
for handler in self.original_handlers:
|
||
self.original_formatters[handler] = handler.formatter
|
||
|
||
# 清除所有现有处理器
|
||
for handler in root_logger.handlers[:]:
|
||
root_logger.removeHandler(handler)
|
||
|
||
# 添加我们的自定义处理器
|
||
custom_handler = self.TUILogHandler(self)
|
||
log_level = getattr(logging, self.log_level.upper(), logging.DEBUG) if hasattr(self, log_level) else logging.DEBUG
|
||
custom_handler.setLevel(log_level) # 从配置读取日志级别
|
||
|
||
# 强制使用包含彩色级别的格式器
|
||
formatter = self.ColoredFormatter(
|
||
'%(asctime)s %(levelname_color)s %(name)s: %(message)s',
|
||
datefmt='%H:%M:%S'
|
||
)
|
||
custom_handler.setFormatter(formatter)
|
||
|
||
root_logger.addHandler(custom_handler)
|
||
|
||
# 同时重定向stdout和stderr作为备份
|
||
self.original_stdout = sys.stdout
|
||
self.original_stderr = sys.stderr
|
||
sys.stdout = self.TUIOutput(self)
|
||
sys.stderr = self.TUIOutput(self, is_error=True)
|
||
|
||
print("✅ TUI日志捕获已启动 - 捕获所有日志输出")
|
||
|
||
except Exception as e:
|
||
logger.error(f"启动日志捕获失败: {e}")
|
||
|
||
def stop_capture(self):
|
||
"""停止捕获输出"""
|
||
try:
|
||
# 恢复logging处理器
|
||
root_logger = logging.getLogger()
|
||
|
||
# 移除我们的处理器
|
||
for handler in root_logger.handlers[:]:
|
||
if hasattr(handler, 'log_display'):
|
||
root_logger.removeHandler(handler)
|
||
|
||
# 恢复原始处理器和格式器
|
||
for handler in self.original_handlers:
|
||
# 恢复格式器
|
||
if handler in self.original_formatters:
|
||
handler.setFormatter(self.original_formatters[handler])
|
||
root_logger.addHandler(handler)
|
||
|
||
# 恢复stdout和stderr
|
||
sys.stdout = self.original_stdout
|
||
sys.stderr = self.original_stderr
|
||
|
||
print("🛑 TUI日志捕获已停止")
|
||
|
||
except Exception as e:
|
||
logger.error(f"停止日志捕获失败: {e}")
|
||
|
||
def add_log_line(self, line: str):
|
||
"""添加日志行到TUI显示"""
|
||
try:
|
||
# 添加到缓冲区
|
||
self.log_lines.append(line)
|
||
if len(self.log_lines) > self.max_lines:
|
||
self.log_lines.pop(0)
|
||
|
||
# 更新显示
|
||
display_content = "\n".join(self.log_lines)
|
||
self.update(display_content)
|
||
|
||
# 自动滚动到底部
|
||
if self.auto_scroll_enabled:
|
||
self.scroll_to_bottom()
|
||
|
||
except Exception as e:
|
||
# 如果TUI更新失败,回退到原始输出
|
||
if hasattr(self, 'original_stdout'):
|
||
self.original_stdout.write(f"TUI日志显示错误: {e}\n")
|
||
|
||
def scroll_to_bottom(self):
|
||
"""滚动到底部"""
|
||
try:
|
||
# 获取父容器(ScrollableContainer)
|
||
parent = self.parent
|
||
if parent and hasattr(parent, 'scroll_end'):
|
||
parent.scroll_end()
|
||
except Exception as e:
|
||
# 忽略滚动错误,不影响主要功能
|
||
pass
|
||
|
||
def toggle_auto_scroll(self, enabled: bool = None):
|
||
"""切换自动滚动状态"""
|
||
if enabled is None:
|
||
self.auto_scroll_enabled = not self.auto_scroll_enabled
|
||
else:
|
||
self.auto_scroll_enabled = enabled
|
||
|
||
logger.debug(f"日志自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
|
||
return self.auto_scroll_enabled
|
||
|
||
class ColoredFormatter(logging.Formatter):
|
||
"""带颜色的日志格式器 - 增强版"""
|
||
|
||
# ANSI颜色代码
|
||
COLORS = {
|
||
'DEBUG': '\033[36m', # 青色 - DEBUG信息
|
||
'INFO': '\033[32m', # 绿色 - 正常信息
|
||
'WARNING': '\033[33m', # 黄色 - 警告信息
|
||
'ERROR': '\033[31m', # 红色 - 错误信息
|
||
'CRITICAL': '\033[35m', # 紫色 - 严重错误
|
||
'RESET': '\033[0m' # 重置颜色
|
||
}
|
||
|
||
# 级别显示宽度
|
||
LEVEL_WIDTH = 8
|
||
|
||
def format(self, record):
|
||
"""格式化日志记录,为级别添加颜色"""
|
||
try:
|
||
# 为级别添加颜色和固定宽度
|
||
levelname = record.levelname
|
||
if levelname in self.COLORS:
|
||
# 添加颜色并保持固定宽度
|
||
colored_level = f"{self.COLORS[levelname]}[{levelname:<{self.LEVEL_WIDTH}}]{self.COLORS['RESET']}"
|
||
record.levelname_color = colored_level
|
||
else:
|
||
record.levelname_color = f"[{levelname:<{self.LEVEL_WIDTH}}]"
|
||
|
||
# 调用父类格式化方法
|
||
formatted_message = super().format(record)
|
||
return formatted_message
|
||
|
||
except Exception:
|
||
# 如果格式化失败,返回简单格式
|
||
return f"{record.asctime} [{record.levelname}] {record.name}: {record.getMessage()}"
|
||
|
||
class TUILogHandler(logging.Handler):
|
||
"""自定义logging处理器,同时输出到终端和TUI"""
|
||
|
||
def __init__(self, log_display):
|
||
super().__init__()
|
||
self.log_display = log_display
|
||
|
||
def emit(self, record):
|
||
"""处理日志记录"""
|
||
try:
|
||
# 格式化日志记录(使用我们的格式器)
|
||
formatted_message = self.format(record)
|
||
|
||
# 输出到原始终端(通过原始处理器,但使用我们的格式器)
|
||
for original_handler in self.log_display.original_handlers:
|
||
if original_handler.level <= record.levelno:
|
||
# 临时使用我们的格式器来确保级别显示一致
|
||
original_handler.setFormatter(self.formatter)
|
||
original_handler.emit(record)
|
||
# 恢复原始格式器
|
||
original_formatter = self.log_display.original_formatters.get(original_handler)
|
||
if original_formatter:
|
||
original_handler.setFormatter(original_formatter)
|
||
|
||
# 添加到TUI显示
|
||
self.log_display.add_log_line(formatted_message)
|
||
|
||
except Exception as e:
|
||
# 如果处理失败,使用简单格式
|
||
try:
|
||
simple_message = f"{datetime.now().strftime('%H:%M:%S')} [{record.levelname:8}] {record.name}: {record.getMessage()}"
|
||
self.log_display.add_log_line(simple_message)
|
||
except:
|
||
pass
|
||
|
||
class TUIOutput(io.TextIOBase):
|
||
"""自定义输出流,捕获print等输出"""
|
||
|
||
def __init__(self, log_display, is_error=False):
|
||
self.log_display = log_display
|
||
self.is_error = is_error
|
||
self.original_stream = sys.stderr if is_error else sys.stdout
|
||
|
||
# 颜色定义
|
||
self.COLORS = {
|
||
'INFO': '\033[32m', # 绿色
|
||
'ERROR': '\033[31m', # 红色
|
||
'RESET': '\033[0m' # 重置颜色
|
||
}
|
||
|
||
def write(self, text):
|
||
"""写入文本"""
|
||
try:
|
||
# 写入到原始终端
|
||
self.original_stream.write(text)
|
||
self.original_stream.flush()
|
||
|
||
# 如果文本不是空的,添加到TUI
|
||
if text.strip():
|
||
# 添加简单的时间戳和级别
|
||
timestamp = datetime.now().strftime('%H:%M:%S')
|
||
level = "ERROR" if self.is_error else "INFO"
|
||
|
||
# 添加颜色
|
||
if level in self.COLORS:
|
||
colored_level = f"{self.COLORS[level]}[{level}]{self.COLORS['RESET']}"
|
||
else:
|
||
colored_level = f"[{level}]"
|
||
|
||
# 分割多行文本
|
||
lines = text.split('\n')
|
||
for line in lines:
|
||
if line.strip(): # 忽略空行
|
||
log_line = f"{timestamp} {colored_level} {line.strip()}"
|
||
self.log_display.add_log_line(log_line)
|
||
|
||
return len(text)
|
||
|
||
except Exception:
|
||
# 如果TUI处理失败,只输出到终端
|
||
self.original_stream.write(text)
|
||
self.original_stream.flush()
|
||
return len(text)
|
||
|
||
|
||
def flush(self):
|
||
"""刷新缓冲区"""
|
||
self.original_stream.flush()
|
||
|
||
def close(self):
|
||
"""关闭流"""
|
||
pass
|
||
|
||
class MessageDisplay(Static):
|
||
"""消息显示组件 - 增强版"""
|
||
|
||
def __init__(self):
|
||
super().__init__("消息区域")
|
||
self.current_messages: List[Dict] = []
|
||
self.max_messages = 200 # 更大的消息容量
|
||
self.auto_scroll_enabled = True # 启用自动滚动
|
||
# ANSI颜色代码
|
||
self.COLORS = {
|
||
'INFO': '\033[37m', # 黑底白字
|
||
'DEBUG': '\033[36m', # 青色 - DEBUG信息
|
||
'SUCCESS': '\033[32m', # 绿色 - 正常信息
|
||
'WARNING': '\033[33m', # 黄色 - 警告信息
|
||
'ERROR': '\033[31m', # 红色 - 错误信息
|
||
'COMMAND': '\033[40;37m', # 灰底白字 - 命令信息
|
||
'RESET': '\033[0m' # 重置颜色
|
||
}
|
||
self.message_types = {
|
||
'info': {'icon': f"{self.COLORS['INFO']}[INFO ]{self.COLORS['RESET']}", 'color': 'white'},
|
||
'success': {'icon': f"{self.COLORS['SUCCESS']}[SUCCESS]{self.COLORS['RESET']}", 'color': 'green'},
|
||
'error': {'icon': f"{self.COLORS['ERROR']}[ERROR ]{self.COLORS['RESET']}", 'color': 'red'},
|
||
'warning': {'icon': f"{self.COLORS['WARNING']}[WARNING]{self.COLORS['RESET']}", 'color': 'yellow'},
|
||
'debug': {'icon': f"{self.COLORS['DEBUG']}[DEBUG ]{self.COLORS['RESET']}", 'color': 'cyan'},
|
||
'command': {'icon': f"{self.COLORS['COMMAND']}[COMMAND]{self.COLORS['RESET']}", 'color': 'meow'}
|
||
}
|
||
logger.debug("MessageDisplay初始化完成")
|
||
|
||
def add_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
||
"""添加消息 - 支持多行消息"""
|
||
try:
|
||
# 分割多行消息为单独的消息
|
||
lines = message.strip().split('\n')
|
||
for line in lines:
|
||
if line.strip(): # 忽略空行
|
||
message_data = {
|
||
"text": line.strip(),
|
||
"type": msg_type,
|
||
"persistent": persistent,
|
||
"timestamp": asyncio.get_event_loop().time(),
|
||
"display_time": datetime.now().strftime('%H:%M:%S')
|
||
}
|
||
self.current_messages.append(message_data)
|
||
|
||
# 智能消息管理
|
||
self._manage_messages()
|
||
|
||
self._update_display()
|
||
|
||
except Exception as e:
|
||
logger.error(f"添加消息时出错: {str(e)}")
|
||
|
||
def reset_display(self):
|
||
"""重置显示状态"""
|
||
try:
|
||
# 清空所有消息
|
||
self.current_messages.clear()
|
||
# 更新显示
|
||
self.update("消息区域已重置")
|
||
# 强制刷新
|
||
self.refresh()
|
||
except Exception as e:
|
||
logger.error(f"重置消息显示时出错: {str(e)}")
|
||
|
||
def _manage_messages(self):
|
||
"""智能管理消息数量"""
|
||
try:
|
||
# 计算非持久化消息的数量
|
||
non_persistent_messages = [msg for msg in self.current_messages if not msg['persistent']]
|
||
|
||
if len(non_persistent_messages) > self.max_messages:
|
||
# 移除最旧的非持久化消息
|
||
for i, msg in enumerate(self.current_messages):
|
||
if not msg['persistent']:
|
||
self.current_messages.pop(i)
|
||
break
|
||
|
||
except Exception as e:
|
||
logger.error(f"管理消息时出错: {str(e)}")
|
||
|
||
def _update_display(self):
|
||
"""更新显示 - 带时间戳的格式化消息"""
|
||
try:
|
||
if not self.current_messages:
|
||
display_text = "📭 暂无消息"
|
||
else:
|
||
display_text = []
|
||
for msg in self.current_messages:
|
||
# 获取消息类型配置
|
||
msg_config = self.message_types.get(msg['type'], self.message_types['info'])
|
||
icon = msg_config['icon']
|
||
|
||
# 构建显示行
|
||
persistent_mark = "🔒 " if msg['persistent'] else ""
|
||
time_stamp = f"[{msg['display_time']}] " if len(self.current_messages) > 1 else ""
|
||
|
||
display_line = f"{time_stamp}{persistent_mark}{icon} {msg['text']}"
|
||
display_text.append(display_line)
|
||
|
||
display_text = "\n".join(display_text)
|
||
|
||
self.update(display_text)
|
||
|
||
# 自动滚动到底部
|
||
if self.auto_scroll_enabled:
|
||
self.scroll_to_bottom()
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新消息显示时出错: {str(e)}")
|
||
|
||
def scroll_to_bottom(self):
|
||
"""滚动到底部"""
|
||
try:
|
||
# 获取父容器(ScrollableContainer)
|
||
parent = self.parent
|
||
if parent and hasattr(parent, 'scroll_end'):
|
||
parent.scroll_end()
|
||
except Exception as e:
|
||
# 忽略滚动错误,不影响主要功能
|
||
pass
|
||
|
||
def toggle_auto_scroll(self, enabled: bool = None):
|
||
"""切换自动滚动状态"""
|
||
if enabled is None:
|
||
self.auto_scroll_enabled = not self.auto_scroll_enabled
|
||
else:
|
||
self.auto_scroll_enabled = enabled
|
||
|
||
logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
|
||
return self.auto_scroll_enabled
|
||
|
||
class TUIFramework(App):
|
||
"""TUI框架应用"""
|
||
def __init__(self, config, log_service, command_service):
|
||
super().__init__()
|
||
self.config = config
|
||
self.log_service = log_service
|
||
self.command_service = command_service
|
||
|
||
self.log_display = LogDisplay()
|
||
self.message_display = MessageDisplay()
|
||
self.command_input = None
|
||
self.CSS = self._generate_css()
|
||
|
||
|
||
def _generate_css(self):
|
||
"""根据配置动态生成CSS - 增强版"""
|
||
try:
|
||
tui_config = self.config.get('tui', {})
|
||
layout_config = tui_config.get('layout', {})
|
||
styles_config = tui_config.get('styles', {})
|
||
|
||
# 获取布局配置,使用默认值
|
||
grid_rows = layout_config.get('grid_rows', '7fr 2fr 1fr')
|
||
|
||
# 获取样式配置,使用默认值
|
||
log_area_style = styles_config.get('log_area', 'border: solid green; overflow-y: auto;')
|
||
message_area_style = styles_config.get('message_area', 'border: solid yellow; overflow-y: auto;')
|
||
input_area_style = styles_config.get('input_area', 'border: solid red;')
|
||
|
||
css = f"""
|
||
Screen {{
|
||
layout: grid;
|
||
grid-size: 1 3;
|
||
grid-rows: {grid_rows};
|
||
}}
|
||
|
||
#log-area {{
|
||
{log_area_style}
|
||
overflow-y: auto;
|
||
scrollbar-size: 1 1;
|
||
}}
|
||
|
||
#message-area {{
|
||
{message_area_style}
|
||
overflow-y: auto;
|
||
scrollbar-size: 1 1;
|
||
}}
|
||
|
||
#input-area {{
|
||
{input_area_style}
|
||
}}
|
||
|
||
/* 自定义滚动条样式 */
|
||
ScrollableContainer {{
|
||
scrollbar-color: #666 #222;
|
||
scrollbar-color-hover: #888 #333;
|
||
overflow-y: auto;
|
||
}}
|
||
|
||
/* 确保内容正确换行 */
|
||
Static {{
|
||
width: 100%;
|
||
content-align: left middle;
|
||
overflow-y: auto;
|
||
}}
|
||
"""
|
||
logger.debug(f"生成的TUI CSS:\n{css}")
|
||
return css
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成TUI CSS时出错: {str(e)}", exc_info=True)
|
||
# 返回默认CSS作为回退
|
||
return """
|
||
Screen {
|
||
layout: grid;
|
||
grid-size: 1 3;
|
||
grid-rows: 7fr 2fr 1fr;
|
||
}
|
||
|
||
#log-area {
|
||
border: solid green;
|
||
overflow-y: auto;
|
||
scrollbar-size: 1 1;
|
||
}
|
||
|
||
#message-area {
|
||
border: solid yellow;
|
||
overflow-y: auto;
|
||
scrollbar-size: 1 1;
|
||
}
|
||
|
||
#input-area {
|
||
border: solid red;
|
||
}
|
||
|
||
/* 自定义滚动条样式 */
|
||
ScrollableContainer {
|
||
scrollbar-color: #666 #222;
|
||
scrollbar-color-hover: #888 #333;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 确保内容正确换行 */
|
||
Static {
|
||
width: 100%;
|
||
content-align: left middle;
|
||
overflow-y: auto;
|
||
}
|
||
"""
|
||
|
||
|
||
def compose(self):
|
||
"""组合界面"""
|
||
yield Header()
|
||
yield ScrollableContainer(
|
||
self.log_display,
|
||
id="log-area"
|
||
)
|
||
yield ScrollableContainer(
|
||
self.message_display,
|
||
id="message-area"
|
||
)
|
||
self.command_input = Input(placeholder="输入指令...", id="command-input")
|
||
yield Container(
|
||
self.command_input,
|
||
id="input-area"
|
||
)
|
||
yield Footer()
|
||
|
||
async def on_mount(self):
|
||
"""挂载完成事件"""
|
||
try:
|
||
# 开始捕获所有输出
|
||
self.log_display.start_capture()
|
||
|
||
# 设置输入框焦点
|
||
if self.command_input:
|
||
self.command_input.focus()
|
||
|
||
# 显示欢迎消息
|
||
self.show_message("🐱 SenSu TUI 已就绪!输入 'help' 查看命令\n", "info")
|
||
print("✅ TUI已启动,开始捕获所有输出")
|
||
|
||
except Exception as e:
|
||
print(f"❌ TUI挂载时出错: {str(e)}")
|
||
|
||
async def on_input_submitted(self, event):
|
||
"""输入提交事件"""
|
||
try:
|
||
if hasattr(event, 'input') and event.input.id == "command-input":
|
||
command = event.value
|
||
event.input.value = "" # 清空输入框
|
||
|
||
if command.strip():
|
||
print(f"执行命令: {command}")
|
||
|
||
# 在消息区域显示正在处理
|
||
self.show_message(f"执行命令: {command}", "command")
|
||
|
||
# 发送到指令服务处理
|
||
result = await self.command_service.process_command(command, "tui")
|
||
|
||
# 显示命令结果
|
||
if result:
|
||
self.show_message(f"结果: {result}", "success")
|
||
else:
|
||
self.show_message("命令执行完成", "success")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 指令处理错误: {str(e)}")
|
||
self.show_message(f"指令处理错误: {str(e)}", "error")
|
||
|
||
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
||
"""显示消息"""
|
||
try:
|
||
self.message_display.add_message(message, msg_type, persistent)
|
||
except Exception as e:
|
||
print(f"❌ 显示TUI消息时出错: {str(e)}")
|
||
|
||
def clear_messages(self, clear_persistent: bool = False):
|
||
"""清空消息区域"""
|
||
try:
|
||
self.message_display.clear_messages(clear_persistent)
|
||
except Exception as e:
|
||
print(f"❌ 清空消息时出错: {str(e)}")
|
||
|
||
async def action_quit(self):
|
||
"""重写退出动作 - 最佳方案:优雅关闭"""
|
||
try:
|
||
logger.info("🐱 TUI接收到退出信号,开始关闭流程")
|
||
|
||
# 显示关闭消息
|
||
self.show_message("🐱 正在关闭框架...", "info", persistent=True)
|
||
|
||
# 停止捕获输出
|
||
self.log_display.stop_capture()
|
||
|
||
# 使用异步任务来优雅关闭,避免阻塞
|
||
asyncio.create_task(self._async_graceful_shutdown())
|
||
|
||
except Exception as e:
|
||
logger.error(f"TUI退出处理时出错: {str(e)}")
|
||
# 紧急退出
|
||
import os
|
||
os._exit(0)
|
||
|
||
async def _async_graceful_shutdown(self):
|
||
"""异步优雅关闭"""
|
||
try:
|
||
# 给一点时间显示消息
|
||
self.show_message("🐱 3...", "info", persistent=True)
|
||
await asyncio.sleep(1)
|
||
self.show_message("🐱 2..", "info", persistent=True)
|
||
await asyncio.sleep(1)
|
||
self.show_message("🐱 1.", "info", persistent=True)
|
||
await asyncio.sleep(1)
|
||
|
||
logger.info("🐱 执行异步关闭")
|
||
|
||
logger.debug("使用事件循环停止")
|
||
|
||
# 获取当前事件循环
|
||
loop = asyncio.get_event_loop()
|
||
|
||
# 停止所有运行中的任务(除了当前任务)
|
||
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task()]
|
||
|
||
if tasks:
|
||
logger.debug(f"取消 {len(tasks)} 个运行中的任务")
|
||
for task in tasks:
|
||
task.cancel()
|
||
|
||
# 等待任务取消完成
|
||
await asyncio.gather(*tasks, return_exceptions=True)
|
||
|
||
# 停止事件循环
|
||
loop.stop()
|
||
logger.info("🐱 事件循环已停止,框架关闭完成")
|
||
|
||
except Exception as e:
|
||
logger.error(f"异步关闭失败: {str(e)}")
|
||
# 最后的手段
|
||
import os
|
||
os._exit(0)
|
||
|
||
def shutdown(self):
|
||
"""关闭TUI"""
|
||
try:
|
||
# 停止捕获输出
|
||
self.log_display.stop_capture()
|
||
self.action_quit()
|
||
self.exit()
|
||
print("🛑 TUI已关闭")
|
||
except Exception as e:
|
||
print(f"❌ 关闭TUI时出错: {str(e)}")
|
||
|
||
class TuiService:
|
||
"""TUI服务"""
|
||
|
||
def __init__(self, config: Dict, log_service, command_service):
|
||
self.config = config
|
||
self.log_service = log_service
|
||
self.command_service = command_service
|
||
self.tui_app = None
|
||
self._message_queue = asyncio.Queue()
|
||
self._message_processor_task = None
|
||
|
||
async def start(self):
|
||
"""启动TUI"""
|
||
try:
|
||
if not self.config.get('tui', {}).get('enabled', True):
|
||
print("TUI已禁用")
|
||
return
|
||
|
||
print("启动TUI服务")
|
||
self.tui_app = TUIFramework(self.config, self.log_service, self.command_service)
|
||
|
||
# 设置动态标题
|
||
self._setup_title()
|
||
|
||
# 启动消息处理任务
|
||
self._message_processor_task = asyncio.create_task(self._process_message_queue())
|
||
|
||
# 在后台运行TUI
|
||
asyncio.create_task(self._run_tui())
|
||
|
||
except Exception as e:
|
||
print(f"启动TUI服务时出错: {str(e)}")
|
||
raise
|
||
|
||
def toggle_auto_scroll(self, target: str = "all", enabled: bool = None):
|
||
"""切换自动滚动状态"""
|
||
try:
|
||
if not self.tui_app:
|
||
return "❌ TUI未启动"
|
||
|
||
result = []
|
||
|
||
if target in ["all", "log"]:
|
||
log_state = self.tui_app.log_display.toggle_auto_scroll(enabled)
|
||
result.append(f"📜 日志自动滚动: {'✅ 启用' if log_state else '❌ 禁用'}")
|
||
|
||
if target in ["all", "message"]:
|
||
msg_state = self.tui_app.message_display.toggle_auto_scroll(enabled)
|
||
result.append(f"💬 消息自动滚动: {'✅ 启用' if msg_state else '❌ 禁用'}")
|
||
|
||
return "\n".join(result)
|
||
|
||
except Exception as e:
|
||
return f"❌ 切换自动滚动失败: {str(e)}"
|
||
|
||
def scroll_to_bottom(self, target: str = "all"):
|
||
"""手动滚动到底部"""
|
||
try:
|
||
if not self.tui_app:
|
||
return "❌ TUI未启动"
|
||
|
||
result = []
|
||
|
||
if target in ["all", "log"]:
|
||
self.tui_app.log_display.scroll_to_bottom()
|
||
result.append("📜 日志区域已滚动到底部")
|
||
|
||
if target in ["all", "message"]:
|
||
self.tui_app.message_display.scroll_to_bottom()
|
||
result.append("💬 消息区域已滚动到底部")
|
||
|
||
return "\n".join(result)
|
||
|
||
except Exception as e:
|
||
return f"❌ 滚动到底部失败: {str(e)}"
|
||
|
||
async def _process_message_queue(self):
|
||
"""处理消息队列,避免消息过多导致界面卡顿"""
|
||
try:
|
||
while True:
|
||
# 从队列中获取消息
|
||
message_data = await self._message_queue.get()
|
||
|
||
if message_data is None: # 停止信号
|
||
break
|
||
|
||
message, msg_type, persistent = message_data
|
||
|
||
# 显示消息
|
||
if self.tui_app:
|
||
self.tui_app.show_message(message, msg_type, persistent)
|
||
|
||
# 小延迟避免消息过快
|
||
await asyncio.sleep(0.05)
|
||
|
||
except asyncio.CancelledError:
|
||
logger.debug("消息处理任务被取消")
|
||
except Exception as e:
|
||
logger.error(f"消息处理任务出错: {str(e)}")
|
||
|
||
def _setup_title(self):
|
||
"""设置TUI标题"""
|
||
try:
|
||
framework_config = self.config.get('framework', {})
|
||
name = framework_config.get('name', 'SenSu')
|
||
version = framework_config.get('version', 'Unknown')
|
||
debug_mode = framework_config.get('debug', False)
|
||
|
||
# 构建标题
|
||
title_parts = [f"🐱 {name} Ver.{version}"]
|
||
if debug_mode:
|
||
title_parts.append("[DEBUG]")
|
||
|
||
self.tui_app.title = " ".join(title_parts)
|
||
self.tui_app.sub_title = "Based DreamSu Framework"
|
||
logger.debug(f"设置TUI标题: {self.tui_app.title}")
|
||
logger.debug(f"设置TUI副标题: {self.tui_app.sub_title}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"设置TUI标题时出错: {e}")
|
||
self.tui_app.title = "🐱 SenSu - Based DreamSu Framework" # 默认标题
|
||
|
||
async def _run_tui(self):
|
||
"""运行TUI"""
|
||
try:
|
||
await self.tui_app.run_async()
|
||
except Exception as e:
|
||
print(f"运行TUI时出错: {str(e)}")
|
||
|
||
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
||
"""显示消息"""
|
||
try:
|
||
if self.tui_app:
|
||
# 将消息放入队列,由后台任务处理
|
||
self._message_queue.put_nowait((message, msg_type, persistent))
|
||
except Exception as e:
|
||
print(f"通过TUI服务显示消息时出错: {str(e)}")
|
||
|
||
def shutdown(self):
|
||
"""关闭TUI服务"""
|
||
try:
|
||
if self.tui_app:
|
||
self.tui_app.shutdown()
|
||
print("TUI服务已关闭")
|
||
except Exception as e:
|
||
print(f"关闭TUI服务时出错: {str(e)}")
|