ShiErFeng/shierfeng-fastapi-backend/cli/tui/app.py
2026-07-24 09:43:10 +08:00

899 lines
24 KiB
Python

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from textual.app import App
from textual.css.query import NoMatches
from cli.tui.adapters import (
TUI_SNAPSHOT_COLLECTOR_REGISTRY,
BrowserPageSnapshot,
DashboardSnapshot,
DetailPageSnapshot,
)
from cli.tui.copy import TUI_COPY
from cli.tui.keymaps import TUI_KEYMAP_REGISTRY
from cli.tui.screens import BrowserScreen, DashboardScreen, DetailScreen
from cli.tui.widgets import NavigationItem, WorkspaceSidebar
SnapshotCollector = Callable[['ShierfengTuiApp'], BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot]
@dataclass(frozen=True)
class TuiViewSpec:
"""
TUI 视图规格定义。
:param view_key: 视图标识
:param include_query: 是否注入搜索词
:param include_filter: 是否注入筛选键
"""
view_key: str
include_query: bool = False
include_filter: bool = False
@dataclass(frozen=True)
class TuiViewDefinition:
"""
TUI 视图定义。
:param view_key: 视图标识
:param collector: 页面快照采集器
"""
view_key: str
collector: SnapshotCollector
@dataclass(frozen=True)
class TuiViewRegistry:
"""
TUI 视图注册表。
该对象集中维护视图打开、刷新与顺序导航所需的元数据,
避免 `ShierfengTuiApp` 内继续手工维护多份视图映射。
:param definitions: 视图定义映射
:param navigation_items: 导航项列表
"""
definitions: dict[str, TuiViewDefinition]
navigation_items: list[NavigationItem]
@staticmethod
def normalize_view_key(view_key: str) -> str:
"""
规范化视图标识。
:param view_key: 原始视图标识
:return: 规范化后的视图标识
"""
return str(view_key).strip().lower()
def resolve_view_key(self, view_key: str) -> str:
"""
解析可用视图标识,不存在时回退到 dashboard。
:param view_key: 原始视图标识
:return: 可用视图标识
"""
normalized_view_key = self.normalize_view_key(view_key)
if normalized_view_key in self.definitions:
return normalized_view_key
return 'dashboard'
def collect_snapshot(
self, app: 'ShierfengTuiApp', view_key: str
) -> BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot:
"""
采集指定视图的页面快照。
:param app: 当前 TUI 应用实例
:param view_key: 目标视图标识
:return: 页面快照
"""
resolved_view_key = self.resolve_view_key(view_key)
return self.definitions[resolved_view_key].collector(app)
def get_navigation_index(self, view_key: str) -> int:
"""
获取视图在导航列表中的索引。
:param view_key: 目标视图标识
:return: 导航索引
"""
resolved_view_key = self.resolve_view_key(view_key)
return next(
(index for index, item in enumerate(self.navigation_items) if item.view_key == resolved_view_key),
0,
)
def get_relative_view_key(self, view_key: str, offset: int) -> str:
"""
获取相对当前视图偏移后的目标视图标识。
:param view_key: 当前视图标识
:param offset: 偏移量
:return: 目标视图标识
"""
current_index = self.get_navigation_index(view_key)
target_index = (current_index + offset) % len(self.navigation_items)
return self.navigation_items[target_index].view_key
_TUI_VIEW_SPECS: tuple[TuiViewSpec, ...] = (
TuiViewSpec('dashboard'),
TuiViewSpec('app', include_query=True),
TuiViewSpec('ops', include_query=True),
TuiViewSpec('database', include_query=True),
TuiViewSpec('cache', include_query=True),
TuiViewSpec('jobs', include_query=True, include_filter=True),
TuiViewSpec('gen', include_query=True),
TuiViewSpec('configs', include_query=True, include_filter=True),
TuiViewSpec('crypto', include_query=True),
)
_TUI_APP_CSS = """
Screen {
layout: vertical;
background: #04080f;
color: #d8f7ff;
scrollbar-size-horizontal: 1;
scrollbar-size-vertical: 1;
scrollbar-background: #06111b;
scrollbar-background-hover: #0a1723;
scrollbar-background-active: #0d1f2d;
scrollbar-color: #1f6f8d;
scrollbar-color-hover: #38d8ff;
scrollbar-color-active: #6df7ff;
}
Widget {
scrollbar-size-horizontal: 1;
scrollbar-size-vertical: 1;
scrollbar-background: #06111b;
scrollbar-background-hover: #0a1723;
scrollbar-background-active: #0d1f2d;
scrollbar-color: #1f6f8d;
scrollbar-color-hover: #38d8ff;
scrollbar-color-active: #6df7ff;
}
#workspace-shell {
height: 1fr;
background: #050c15;
}
#workspace-main {
padding: 1 2 1 1;
background: #050c15;
border: round #18425d;
margin: 1 1 1 0;
overflow-x: auto;
overflow-y: auto;
scrollbar-size-horizontal: 1;
scrollbar-size-vertical: 1;
scrollbar-background: #06111b;
scrollbar-background-hover: #0a1723;
scrollbar-background-active: #0d1f2d;
scrollbar-color: #1f6f8d;
scrollbar-color-hover: #38d8ff;
scrollbar-color-active: #6df7ff;
}
#workspace-main:focus {
border: heavy #6df7ff;
background: #071320;
}
#detail-body {
height: auto;
margin-top: 1;
border: double #18425d;
background: #06111b;
padding: 1;
overflow-x: auto;
overflow-y: auto;
}
#browser-body {
height: auto;
margin-top: 1;
border: double #18425d;
background: #06111b;
padding: 1;
overflow-x: auto;
overflow-y: auto;
}
#browser-filter-bar {
border: round #163c57;
padding: 1 2;
margin-bottom: 1;
background: #07131f;
color: #d9fbff;
}
#browser-detail-pane {
width: 1fr;
min-width: 48;
border: round #163c57;
padding: 1;
background: #07131f;
overflow-x: auto;
overflow-y: auto;
}
#browser-detail-pane.is-ok {
border: round #22c983;
}
#browser-detail-pane.is-fail {
border: round #ff6b7a;
background: #190d12;
}
#browser-detail-pane.is-warn {
border: round #f2b84b;
background: #171108;
}
#browser-detail-pane.is-info {
border: round #38d8ff;
}
#browser-section-body {
height: auto;
border-top: solid #163c57;
padding-top: 1;
background: #06111b;
overflow-x: auto;
overflow-y: auto;
}
#dashboard-metrics {
grid-size: 4;
grid-gutter: 1 2;
grid-columns: 1fr 1fr 1fr 1fr;
height: auto;
margin-top: 1;
margin-bottom: 1;
padding: 1 0;
}
#dashboard-grid, #detail-grid {
grid-size: 2;
grid-gutter: 1 2;
grid-columns: 1fr 1fr;
height: auto;
margin-top: 1;
}
#detail-grid {
margin-bottom: 1;
}
Footer {
background: #060d16;
color: #b6efff;
border-top: heavy #163c57;
}
FooterKey {
background: #0b2130;
color: #ecfcff;
}
"""
@dataclass(frozen=True)
class TuiBindingSpec:
"""
TUI 快捷键绑定定义。
:param key: 快捷键
:param action: 动作名称
:param label: 展示标签
"""
key: str
action: str
label: str
class TuiBindingBuilder:
"""
TUI 快捷键绑定构建器。
该对象负责统一装配应用级固定绑定和视图跳转绑定,避免 `ShierfengTuiApp`
继续手工维护重复的绑定元组列表。
"""
def build_base_bindings(self) -> list[TuiBindingSpec]:
"""
构建应用级固定绑定。
:return: 绑定定义列表
"""
return [
TuiBindingSpec('q', 'quit', TUI_COPY.build_app_binding_label('quit')),
TuiBindingSpec('s', 'focus_sidebar', TUI_COPY.build_app_binding_label('sidebar')),
TuiBindingSpec('[', 'show_previous_view', TUI_COPY.build_app_binding_label('previous')),
TuiBindingSpec(']', 'show_next_view', TUI_COPY.build_app_binding_label('next')),
TuiBindingSpec('r', 'refresh_current_view', TUI_COPY.build_app_binding_label('refresh')),
]
def build_navigation_bindings(self) -> list[TuiBindingSpec]:
"""
构建页面导航绑定。
:return: 绑定定义列表
"""
return [
TuiBindingSpec(
TUI_KEYMAP_REGISTRY.get_navigation_shortcut(spec.view_key),
f'show_{spec.view_key}',
TUI_COPY.render_view_label(spec.view_key),
)
for spec in _TUI_VIEW_SPECS
]
def build(self) -> list[tuple[str, str, str]]:
"""
构建 Textual 所需的绑定元组列表。
:return: 绑定元组列表
"""
return [
(binding.key, binding.action, binding.label)
for binding in [*self.build_base_bindings(), *self.build_navigation_bindings()]
]
TUI_APP_BINDINGS = TuiBindingBuilder().build()
@dataclass(frozen=True)
class TuiViewRegistryBuilder:
"""
TUI 视图注册表构建器。
该对象负责根据统一规格构建导航项和快照采集定义,避免模块内
继续维护重复的 `lambda collect(...)` 配置块。
"""
def build_navigation_items(self) -> list[NavigationItem]:
"""
构建工作台导航项列表。
:return: 导航项列表
"""
return [
NavigationItem(
view_key,
TUI_COPY.render_view_label(view_key),
shortcut,
TUI_COPY.render_navigation_description(view_key),
)
for view_key, shortcut in TUI_KEYMAP_REGISTRY.navigation_shortcuts.items()
]
def build_collector(self, spec: TuiViewSpec) -> SnapshotCollector:
"""
根据视图规格构建页面快照采集器。
:param spec: 视图规格
:return: 页面快照采集器
"""
def collect(app: 'ShierfengTuiApp') -> BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot:
collect_kwargs: dict[str, str] = {}
if spec.include_filter:
collect_kwargs['filter_key'] = app.get_browser_filter(spec.view_key)
if spec.include_query:
collect_kwargs['query'] = app.get_browser_query(spec.view_key)
return TUI_SNAPSHOT_COLLECTOR_REGISTRY.collect(spec.view_key, app.env, **collect_kwargs)
return collect
def build_definitions(self) -> dict[str, TuiViewDefinition]:
"""
构建视图定义映射。
:return: 视图定义映射
"""
return {spec.view_key: TuiViewDefinition(spec.view_key, self.build_collector(spec)) for spec in _TUI_VIEW_SPECS}
def build(self) -> TuiViewRegistry:
"""
构建 TUI 视图注册表。
:return: 视图注册表
"""
return TuiViewRegistry(
definitions=self.build_definitions(),
navigation_items=self.build_navigation_items(),
)
@dataclass
class TuiViewStateStore:
"""
TUI 视图状态存储。
该对象集中维护页面级动作反馈、浏览页筛选键和搜索词,避免
`ShierfengTuiApp` 本体继续直接操作多份同类字典状态。
:param action_feedback_by_view: 动作反馈映射
:param browser_filter_by_view: 浏览页筛选映射
:param browser_query_by_view: 浏览页搜索词映射
"""
action_feedback_by_view: dict[str, list[str]]
browser_filter_by_view: dict[str, str]
browser_query_by_view: dict[str, str]
@classmethod
def create_default(cls) -> 'TuiViewStateStore':
"""
创建默认视图状态存储。
:return: 视图状态存储
"""
return cls(
action_feedback_by_view={},
browser_filter_by_view={'jobs': 'all', 'configs': 'all'},
browser_query_by_view={},
)
def remember_action_feedback(self, view_key: str, lines: list[str]) -> None:
"""
记录指定页面最近一次动作反馈。
:param view_key: 页面视图标识
:param lines: 反馈文本行
:return: None
"""
self.action_feedback_by_view[view_key] = list(lines)
def get_action_feedback_lines(self, view_key: str) -> list[str]:
"""
读取指定页面最近一次动作反馈。
:param view_key: 页面视图标识
:return: 反馈文本行
"""
return list(self.action_feedback_by_view.get(view_key, []))
def remember_browser_filter(self, view_key: str, filter_key: str) -> None:
"""
记录指定浏览页当前筛选键。
:param view_key: 页面视图标识
:param filter_key: 当前筛选键
:return: None
"""
self.browser_filter_by_view[view_key] = filter_key
def get_browser_filter(self, view_key: str, default: str = 'all') -> str:
"""
读取指定浏览页当前筛选键。
:param view_key: 页面视图标识
:param default: 默认筛选键
:return: 当前筛选键
"""
return str(self.browser_filter_by_view.get(view_key, default) or default)
def remember_browser_query(self, view_key: str, query: str) -> None:
"""
记录指定浏览页当前搜索词。
:param view_key: 页面视图标识
:param query: 当前搜索词
:return: None
"""
normalized_query = str(query or '').strip()
if normalized_query:
self.browser_query_by_view[view_key] = normalized_query
return
self.browser_query_by_view.pop(view_key, None)
def get_browser_query(self, view_key: str, default: str = '') -> str:
"""
读取指定浏览页当前搜索词。
:param view_key: 页面视图标识
:param default: 默认搜索词
:return: 当前搜索词
"""
return str(self.browser_query_by_view.get(view_key, default) or default)
TUI_VIEW_REGISTRY = TuiViewRegistryBuilder().build()
NAVIGATION_ITEMS = TUI_VIEW_REGISTRY.navigation_items
@dataclass(frozen=True)
class TuiScreenFactory:
"""
TUI 工作台页面工厂。
该对象负责把页面快照转换为具体 screen 实例,避免 `ShierfengTuiApp`
同时承担状态协调和 screen 构建职责。
"""
@staticmethod
def build_refresh_timestamp() -> str:
"""
构建当前页面刷新时间文本。
:return: 格式化后的刷新时间
"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def build(
self,
*,
snapshot: BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot,
env: str,
active_view: str,
navigation_items: list[NavigationItem],
action_feedback_lines: list[str],
) -> BrowserScreen | DashboardScreen | DetailScreen:
"""
根据快照类型构建对应的工作台页面。
:param snapshot: 页面快照
:param env: 当前运行环境
:param active_view: 当前激活视图
:param navigation_items: 导航项列表
:param action_feedback_lines: 当前视图动作反馈
:return: 对应的页面对象
"""
refreshed_at = self.build_refresh_timestamp()
if isinstance(snapshot, DashboardSnapshot):
return DashboardScreen(
snapshot,
env=env,
active_view=active_view,
navigation_items=navigation_items,
refreshed_at=refreshed_at,
)
if isinstance(snapshot, BrowserPageSnapshot):
return BrowserScreen(
snapshot,
env=env,
active_view=active_view,
navigation_items=navigation_items,
refreshed_at=refreshed_at,
action_feedback_lines=action_feedback_lines,
)
return DetailScreen(
snapshot,
env=env,
active_view=active_view,
navigation_items=navigation_items,
refreshed_at=refreshed_at,
)
@dataclass
class TuiScreenNavigator:
"""
TUI 工作台页面导航器。
该对象负责处理首屏展示与后续切屏差异,让应用壳更专注于状态切换。
:param app: 当前 TUI 应用实例
:param initialized: 工作台屏幕是否已初始化
"""
app: 'ShierfengTuiApp'
initialized: bool = False
def show(self, screen: BrowserScreen | DashboardScreen | DetailScreen) -> None:
"""
显示工作台页面,并兼容首屏初始化与后续切屏。
Textual 在默认根屏阶段直接调用 `switch_screen()` 会触发内部回调栈异常,
因此首次展示页面需要使用 `push_screen()` 建立活动屏,后续再改用
`switch_screen()` 做同层页面替换。
:param screen: 待显示的页面对象
:return: None
"""
if not self.initialized:
self.app.push_screen(screen)
self.initialized = True
return
self.app.switch_screen(screen)
@dataclass(frozen=True)
class TuiViewOpeningCoordinator:
"""
TUI 视图打开协调器。
该对象负责串联视图解析、快照采集、screen 构建与切屏展示,
让 `ShierfengTuiApp` 更专注于状态暴露与 Textual action 桥接。
:param view_registry: 视图注册表
:param screen_factory: 工作台页面工厂
:param navigation_items: 导航项列表
"""
view_registry: TuiViewRegistry
screen_factory: TuiScreenFactory
navigation_items: list[NavigationItem]
def show(self, app: 'ShierfengTuiApp', view_key: str) -> None:
"""
切换并展示指定视图。
:param app: 当前 TUI 应用实例
:param view_key: 目标视图标识
:return: None
"""
resolved_view_key = self.view_registry.resolve_view_key(view_key)
app.current_view = resolved_view_key
snapshot = self.view_registry.collect_snapshot(app, resolved_view_key)
screen = self.screen_factory.build(
snapshot=snapshot,
env=app.env,
active_view=app.current_view,
navigation_items=self.navigation_items,
action_feedback_lines=app.get_action_feedback_lines(app.current_view),
)
app.screen_navigator.show(screen)
class ShierfengTuiApp(App[None]):
"""
ShiErFeng CLI Textual 应用。
:param env: 当前运行环境
"""
CSS = _TUI_APP_CSS
BINDINGS = TUI_APP_BINDINGS
def __init__(self, env: str) -> None:
"""
初始化 TUI 应用实例。
:param env: 当前运行环境
:return: None
"""
self.env = env
self.current_view = 'dashboard'
self.view_state_store = TuiViewStateStore.create_default()
self.screen_factory = TuiScreenFactory()
self.screen_navigator = TuiScreenNavigator(self)
self.view_opening_coordinator = TuiViewOpeningCoordinator(
view_registry=TUI_VIEW_REGISTRY,
screen_factory=self.screen_factory,
navigation_items=NAVIGATION_ITEMS,
)
super().__init__()
def show_view(self, view_key: str) -> None:
"""
切换并展示指定视图。
:param view_key: 目标视图标识
:return: None
"""
self.view_opening_coordinator.show(self, view_key)
def remember_action_feedback(self, view_key: str, lines: list[str]) -> None:
"""
记录指定页面最近一次动作反馈。
:param view_key: 页面视图标识
:param lines: 反馈文本行
:return: None
"""
self.view_state_store.remember_action_feedback(view_key, lines)
def get_action_feedback_lines(self, view_key: str) -> list[str]:
"""
读取指定页面最近一次动作反馈。
:param view_key: 页面视图标识
:return: 反馈文本行
"""
return self.view_state_store.get_action_feedback_lines(view_key)
def remember_browser_filter(self, view_key: str, filter_key: str) -> None:
"""
记录指定浏览页当前筛选键。
:param view_key: 页面视图标识
:param filter_key: 当前筛选键
:return: None
"""
self.view_state_store.remember_browser_filter(view_key, filter_key)
def get_browser_filter(self, view_key: str, default: str = 'all') -> str:
"""
读取指定浏览页当前筛选键。
:param view_key: 页面视图标识
:param default: 默认筛选键
:return: 当前筛选键
"""
return self.view_state_store.get_browser_filter(view_key, default)
def remember_browser_query(self, view_key: str, query: str) -> None:
"""
记录指定浏览页当前搜索词。
:param view_key: 页面视图标识
:param query: 当前搜索词
:return: None
"""
self.view_state_store.remember_browser_query(view_key, query)
def get_browser_query(self, view_key: str, default: str = '') -> str:
"""
读取指定浏览页当前搜索词。
:param view_key: 页面视图标识
:param default: 默认搜索词
:return: 当前搜索词
"""
return self.view_state_store.get_browser_query(view_key, default)
def on_mount(self) -> None:
"""
在应用启动时载入首页。
:return: None
"""
self.show_view('dashboard')
def open_view(self, view_key: str) -> None:
"""
按视图标识打开对应页面。
:param view_key: 视图标识
:return: None
"""
self.show_view(view_key)
def action_focus_sidebar(self) -> None:
"""
将焦点切换到左侧导航栏。
:return: None
"""
try:
self.screen.query_one(WorkspaceSidebar).focus()
except NoMatches:
return
def action_show_previous_view(self) -> None:
"""
切换到前一个工作台视图。
:return: None
"""
self.open_view(TUI_VIEW_REGISTRY.get_relative_view_key(self.current_view, -1))
def action_show_next_view(self) -> None:
"""
切换到后一个工作台视图。
:return: None
"""
self.open_view(TUI_VIEW_REGISTRY.get_relative_view_key(self.current_view, 1))
def action_refresh_current_view(self) -> None:
"""
刷新当前视图数据。
:return: None
"""
self.show_view(self.current_view)
def action_show_dashboard(self) -> None:
"""
切换到首页视图。
:return: None
"""
self.open_view('dashboard')
def action_show_app(self) -> None:
"""
切换到应用详情视图。
:return: None
"""
self.open_view('app')
def action_show_ops(self) -> None:
"""
切换到运维详情视图。
:return: None
"""
self.open_view('ops')
def action_show_database(self) -> None:
"""
切换到数据库详情视图。
:return: None
"""
self.open_view('database')
def action_show_cache(self) -> None:
"""
切换到缓存浏览视图。
:return: None
"""
self.open_view('cache')
def action_show_jobs(self) -> None:
"""
切换到任务浏览视图。
:return: None
"""
self.open_view('jobs')
def action_show_gen(self) -> None:
"""
切换到代码生成浏览视图。
:return: None
"""
self.open_view('gen')
def action_show_configs(self) -> None:
"""
切换到配置浏览视图。
:return: None
"""
self.open_view('configs')
def action_show_crypto(self) -> None:
"""
切换到传输加密详情视图。
:return: None
"""
self.open_view('crypto')
@dataclass(frozen=True)
class TuiAppRunner:
"""
TUI 应用启动器。
该对象负责按环境创建并运行 Textual 应用实例,使命令入口不再依赖
模块级启动函数。
:param app_factory: TUI 应用工厂
"""
app_factory: Callable[[str], ShierfengTuiApp]
def run(self, env: str) -> None:
"""
启动 TUI 应用。
:param env: 当前运行环境
:return: None
"""
self.app_factory(env).run()
TUI_APP_RUNNER = TuiAppRunner(ShierfengTuiApp)