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)