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

332 lines
10 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 asyncio
import inspect
from collections.abc import Awaitable, Callable
from typing import Any
from textual.app import SuspendNotSupported
from cli.tui.actions import (
TUI_ACTION_EXECUTION_SERVICE,
TUI_ACTION_PRESENTATION_SERVICE,
TuiActionResult,
TuiActionSpec,
)
from cli.tui.copy import TUI_COPY
from cli.tui.screens.confirm import ActionConfirmScreen
from cli.tui.screens.search import SearchInputScreen
from cli.tui.search import PageSearchContext
from cli.tui.widgets import WorkspaceSidebar
class TuiScreenInteractionService:
"""
TUI 屏幕交互服务。
该服务负责搜索弹窗、搜索状态写回、动作确认弹窗和动作执行通知等
屏幕级交互流程,减少浏览页与详情页之间的重复控制逻辑。
"""
@staticmethod
def open_search_dialog(
screen: Any,
search_context: PageSearchContext | None,
callback: Callable[[str | None], None],
) -> None:
"""
打开搜索输入弹窗。
:param screen: 当前屏幕对象
:param search_context: 搜索上下文
:param callback: 提交搜索词后的回调
:return: None
"""
if search_context is None:
return
screen.app.push_screen(
SearchInputScreen(
'页内搜索',
search_context.placeholder,
search_context.query,
search_context.suggestions,
),
callback=callback,
)
@staticmethod
def remember_query_and_refresh(screen: Any, active_view: str, query: str | None) -> None:
"""
记录当前页面搜索词并刷新页面。
:param screen: 当前屏幕对象
:param active_view: 当前页面视图标识
:param query: 搜索词
:return: None
"""
if query is None:
return
remember_query = getattr(screen.app, 'remember_browser_query', None)
if callable(remember_query):
remember_query(active_view, query)
screen.app.action_refresh_current_view()
def clear_query_and_refresh(self, screen: Any, active_view: str, current_query: str) -> None:
"""
清空当前页面搜索词并刷新页面。
:param screen: 当前屏幕对象
:param active_view: 当前页面视图标识
:param current_query: 当前搜索词
:return: None
"""
if not current_query:
return
self.remember_query_and_refresh(screen, active_view, '')
@staticmethod
def open_action_confirm(
screen: Any,
action: TuiActionSpec | None,
callback: Callable[[bool | None, TuiActionSpec], None],
) -> None:
"""
打开动作确认弹窗。
:param screen: 当前屏幕对象
:param action: 待执行动作
:param callback: 确认回调
:return: None
"""
if action is None:
screen.notify(
TUI_COPY.build_action_unavailable_message(),
title=TUI_COPY.build_action_notification_title(),
severity='warning',
)
return
screen.app.push_screen(
ActionConfirmScreen(action.preview_title, action.preview_lines, action.label),
callback=lambda confirmed, action=action: callback(confirmed, action),
)
@staticmethod
def schedule_action_task(
existing_task: asyncio.Task[None] | None,
action: TuiActionSpec,
execute_callback: Callable[[TuiActionSpec], Awaitable[None]],
) -> asyncio.Task[None]:
"""
安排动作执行任务,并在必要时取消旧任务。
:param existing_task: 现有动作任务
:param action: 待执行动作
:param execute_callback: 动作执行协程工厂
:return: 新建任务
"""
if existing_task is not None and not existing_task.done():
existing_task.cancel()
return asyncio.create_task(execute_callback(action))
def confirm_and_schedule_action(
self,
screen: Any,
action: TuiActionSpec | None,
existing_task: asyncio.Task[None] | None,
execute_callback: Callable[[TuiActionSpec], Awaitable[None]],
) -> asyncio.Task[None] | None:
"""
统一处理动作确认结果,并在确认后调度后台执行任务。
:param screen: 当前屏幕对象
:param action: 待执行动作
:param existing_task: 当前已有动作任务
:param execute_callback: 动作执行协程工厂
:return: 新建任务或原任务
"""
if action is None:
self.open_action_confirm(screen, action, lambda confirmed, action_spec: None)
return existing_task
scheduled_task = existing_task
def _handle_confirmed(confirmed: bool | None, action_spec: TuiActionSpec) -> None:
nonlocal scheduled_task
if not confirmed:
return
scheduled_task = self.schedule_action_task(
scheduled_task,
action_spec,
execute_callback,
)
self.open_action_confirm(screen, action, _handle_confirmed)
return scheduled_task
async def execute_action(
self,
screen: Any,
action: TuiActionSpec,
env: str,
active_view: str,
*,
on_result: Callable[[TuiActionResult, list[str]], Any] | None = None,
) -> TuiActionResult:
"""
执行屏幕动作并统一处理通知、反馈持久化与刷新。
:param screen: 当前屏幕对象
:param action: 动作定义
:param env: 当前运行环境
:param active_view: 当前页面视图标识
:param on_result: 动作结果回调
:return: 动作结果
"""
screen.notify(
TUI_COPY.build_action_running_message(action.label),
title=TUI_COPY.build_action_notification_title(),
)
if action.execution_mode == 'external':
try:
with screen.app.suspend():
result = TUI_ACTION_EXECUTION_SERVICE.execute_external(action)
except SuspendNotSupported:
result = TuiActionResult(
spec=action,
external_exit_code=1,
external_message='当前终端不支持挂起 TUI无法打开外部交互向导',
)
else:
result = await asyncio.to_thread(TUI_ACTION_EXECUTION_SERVICE.execute, action, env)
feedback_lines = TUI_ACTION_EXECUTION_SERVICE.build_result_lines(result)
remember_feedback = getattr(screen.app, 'remember_action_feedback', None)
if callable(remember_feedback):
remember_feedback(active_view, feedback_lines)
if on_result is not None:
maybe_result = on_result(result, feedback_lines)
if inspect.isawaitable(maybe_result):
await maybe_result
screen.notify(
TUI_ACTION_PRESENTATION_SERVICE.build_action_result_message(result),
title=TUI_COPY.build_action_notification_title(),
severity='information' if result.ok else 'error',
)
if result.ok and action.refresh_view:
screen.app.action_refresh_current_view()
return result
async def execute_action_with_feedback(
self,
screen: Any,
action: TuiActionSpec,
env: str,
active_view: str,
feedback_callback: Callable[[TuiActionResult, list[str]], Any],
) -> TuiActionResult:
"""
执行动作并将统一反馈回调注入结果收口流程。
:param screen: 当前屏幕对象
:param action: 动作定义
:param env: 当前运行环境
:param active_view: 当前页面视图标识
:param feedback_callback: 反馈写回回调
:return: 动作结果
"""
return await self.execute_action(
screen,
action,
env,
active_view,
on_result=feedback_callback,
)
class ScreenInteractionActionsMixin:
"""
屏幕交互动作混入。
该混入负责把搜索、动作确认和侧栏切屏这类重复桥接 action 收口为
通用 Screen 方法,具体页面只需实现对应的私有协作方法。
"""
def _open_search(self) -> None:
"""
打开当前页面搜索输入弹窗。
:return: None
"""
def _clear_search(self) -> None:
"""
清空当前页面搜索词。
:return: None
"""
def _open_action_confirm(self, slot: str) -> None:
"""
打开指定槽位动作的确认弹窗。
:param slot: 动作槽位
:return: None
"""
def _open_sidebar_item(self, event: WorkspaceSidebar.Highlighted | WorkspaceSidebar.Selected) -> None:
"""
根据侧栏事件打开对应视图。
:param event: 侧栏事件
:return: None
"""
def action_open_search(self) -> None:
"""
打开当前页面搜索输入。
:return: None
"""
self._open_search()
def action_clear_search(self) -> None:
"""
清空当前页面搜索词。
:return: None
"""
self._clear_search()
def action_trigger_primary_action(self) -> None:
"""
触发当前页面主动作。
:return: None
"""
self._open_action_confirm('primary')
def action_trigger_secondary_action(self) -> None:
"""
触发当前页面次动作。
:return: None
"""
self._open_action_confirm('secondary')
def action_trigger_global_action(self) -> None:
"""
触发当前页面全局动作。
:return: None
"""
self._open_action_confirm('global')
def action_trigger_utility_action(self) -> None:
"""
触发当前页面工具动作。
:return: None
"""
self._open_action_confirm('utility')
TUI_SCREEN_INTERACTION_SERVICE = TuiScreenInteractionService()