TUI 启动、引导和终端所有权
这一阶段是 Codex 终端界面的开场和接管现场。lib.rs 先读命令和配置,串起登录、恢复会话、本地模型选择、钩子信任、更新和启动提醒。新手引导负责欢迎、登录、信任目录,迁移页负责旧配置和模型切换。随后 app 和 ChatWidget 搭好聊天主界面,回放历史、显示状态和宠物。tui、绘图器、终端探测、键盘模式、暂停恢复、通知和标题模块,则负责安全占用终端,最后再干净还回去。
启动入口与引导决策
这些文件涵盖顶层 TUI 入口,以及主交互式应用完全接管前的早期启动提示和预检决策。
tui/src/lib.rs源码 ↗
可以把这个文件理解成“开店前的总管”。用户输入 codex 后,它先看命令行参数,找到配置目录,加载配置,判断要连本机内置服务、本机常驻服务,还是远程服务;再准备数据库、日志、登录限制、开源模型、本地或远程工作目录等。之后它初始化终端界面,必要时显示更新提示、登录引导、信任目录提示,还能根据参数恢复旧会话、分叉旧会话,或打开选择器。最后它把所有准备好的零件交给 App::run,让真正的 TUI 主界面开始跑。这个文件还特别小心地恢复终端状态:如果程序崩了或提前退出,也尽量把终端从特殊显示模式切回正常模式,避免用户的命令行被弄乱。
start_embedded_app_server232–258 ↗
async fn start_embedded_app_server(
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
strict_config: b
作用:启动一个嵌在当前进程里的 app server。app server 可以理解成后台大脑,TUI 通过它读写会话、发请求、处理配置。
数据流:进去的是启动路径、配置、命令行覆盖项、云配置、日志和数据库等材料;它把这些原样整理后交给更通用的 start_embedded_app_server_with;出来的是一个可直接在本进程通信的客户端。
调用关系:start_app_server 在决定使用内置服务时会调用它;测试里的 start_test_embedded_app_server 也用它搭一个临时后台。它本身把具体启动动作委托给 start_embedded_app_server_with。
调用图:调用 1 个内部函数(start_embedded_app_server_with);被 2 处调用(start_app_server, start_test_embedded_app_server)。
AppServerTarget::uses_remote_workspace268–270 ↗
fn uses_remote_workspace(&self) -> bool
作用:判断当前 app server 目标是不是远程工作区。这个判断会影响路径、配置和会话过滤方式。
数据流:进去的是一个目标类型;它检查这个目标是不是 Remote;出来一个真假值,不改动任何东西。
调用关系:thread_params_mode、config_cwd_for_app_server_target、run_ratatui_app 和 should_load_configured_environments 都靠它判断“这次是在本地干活还是远端干活”。
调用图:被 4 处调用(thread_params_mode, config_cwd_for_app_server_target, run_ratatui_app, should_load_configured_environments);外部调用 1 个(matches!)。
AppServerTarget::thread_params_mode272–278 ↗
fn thread_params_mode(&self) -> ThreadParamsMode
作用:把 app server 的目标类型转换成启动线程时要用的模式。简单说,就是告诉后台“按远程规则还是本地规则创建会话”。
数据流:进去的是 app server 目标;它先问 uses_remote_workspace;如果是远程就产出 Remote 模式,否则产出 Embedded 模式。
调用关系:run_ratatui_app 和 start_app_server_for_picker 创建 AppServerSession 时会用它,保证会话参数和实际连接方式一致。
调用图:调用 1 个内部函数(uses_remote_workspace);被 2 处调用(run_ratatui_app, start_app_server_for_picker)。
init_state_db_for_app_server_target281–298 ↗
async fn init_state_db_for_app_server_target(
config: &Config,
app_server_target: &AppServerTarget,
) -> std::io::Result<Option<StateDbHandle>>
作用:按当前连接方式准备状态数据库。状态数据库用来记录会话、日志、索引等本地状态。
数据流:进去的是配置和 app server 目标;如果是内置服务,它尝试初始化数据库,失败时包装成带数据库路径的启动错误;如果是远程或本地守护进程,它只尝试拿已有数据库句柄;出来是可选的数据库句柄。
调用关系:run_main 启动时会调用它;选择器和多项测试也调用它。它把真正的数据库动作交给 state_db::try_init 或 state_db::get_state_db。
调用图:调用 2 个内部函数(get_state_db, try_init);被 5 处调用(run_main, start_embedded_app_server_for_picker, embedded_state_db_corruption_preserves_failed_database_for_cli_recovery, embedded_state_db_failure_is_typed_for_cli_recovery, start_test_embedded_app_server)。
remove_legacy_tui_log_file301–305 ↗
fn remove_legacy_tui_log_file(codex_home: &Path)
作用:删除旧版本留下的 TUI 日志文件,防止一个共享追加日志无限变大。
数据流:进去的是 Codex 主目录;它拼出旧日志路径并尝试删除;没有返回有用结果,失败也不报错,因为这只是尽力清理。
调用关系:run_main 加载配置后会调用它;对应测试确认旧日志会被删除。
调用图:被 2 处调用(run_main, startup_removes_legacy_tui_log_file);外部调用 2 个(join, remove_file)。
remote_addr_has_explicit_port307–334 ↗
fn remote_addr_has_explicit_port(addr: &str, parsed: &Url) -> bool
作用:检查远程地址里是否真的写了端口号。这里很严格,因为连接远程服务必须明确端口,避免猜错。
数据流:进去的是原始地址字符串和已经解析好的 URL;它查看 URL 端口,也会识别 ws 的 80、wss 的 443 这种显式默认端口;出来是真假值。
调用关系:resolve_remote_addr 用它判断一个 websocket 地址是否合法。它不负责解析完整地址,只负责“端口有没有明说”这一件事。
调用图:被 1 处调用(resolve_remote_addr);外部调用 4 个(host_str, port, scheme, format!)。
websocket_url_supports_auth_token336–344 ↗
fn websocket_url_supports_auth_token(parsed: &Url) -> bool
作用:判断某个 websocket 地址能不能安全地带认证 token。认证 token 就像临时通行证,不能随便发到不安全地址。
数据流:进去的是解析后的 URL;它允许安全的 wss,也允许本机回环地址上的 ws;出来是真假值。
调用关系:它服务于远程地址认证判断,主要被 remote_addr_supports_auth_token 间接使用来决定是否能给连接加 token。
调用图:外部调用 2 个(host, scheme)。
resolve_remote_addr346–383 ↗
fn resolve_remote_addr(addr: &str) -> color_eyre::Result<RemoteAppServerEndpoint>
作用:把用户写的远程地址字符串变成程序内部能连接的 endpoint。endpoint 就是“后台服务在哪里”的标准说法。
数据流:进去的是类似 ws://host:port、wss://host:port、unix:// 或 unix://PATH 的字符串;它解析、校验端口和路径,必要时补出默认 Unix socket 路径;出来是 websocket 或 Unix socket endpoint,格式不对就报错。
调用关系:命令行层会用它把用户参数变成可连接目标;它内部调用 remote_addr_has_explicit_port、路径解析和 URL 解析;测试覆盖了合法和非法地址。
调用图:调用 2 个内部函数(remote_addr_has_explicit_port, relative_to_current_dir);被 1 处调用(resolve_remote_addr_rejects_invalid_remote_addresses);外部调用 5 个(parse, app_server_control_socket_path, find_codex_home, bail!, matches!)。
remote_addr_supports_auth_token385–392 ↗
fn remote_addr_supports_auth_token(endpoint: &RemoteAppServerEndpoint) -> bool
作用:判断一个远程 endpoint 是否适合附带认证 token。这样可以避免把通行证发到不该发的地方。
数据流:进去的是 endpoint;如果是 websocket,它重新解析 URL 并检查安全规则;如果是 Unix socket,直接认为不支持;出来是真假值。
调用关系:它是连接远程服务前的安全辅助判断,依赖 websocket_url_supports_auth_token 的规则。
调用图:外部调用 1 个(parse)。
connect_remote_app_server394–408 ↗
async fn connect_remote_app_server(
endpoint: RemoteAppServerEndpoint,
) -> color_eyre::Result<AppServerClient>
作用:连接已经存在的远程 app server,而不是在当前进程里启动一个新的。
数据流:进去的是远程 endpoint;它带上客户端名字、版本、通道容量等信息发起连接;出来是统一包装后的远程客户端,连接失败就带上下文报错。
调用关系:start_app_server 在目标是远程或本地守护进程时调用它。它把实际网络或 socket 连接交给 RemoteAppServerClient::connect。
调用图:调用 1 个内部函数(connect);被 1 处调用(start_app_server);外部调用 3 个(new, Remote, env!)。
maybe_probe_default_daemon_socket440–442 ↗
async fn maybe_probe_default_daemon_socket(_codex_home: &Path) -> Option<AbsolutePathBuf>
作用:尝试快速探测默认的本地守护进程 socket 是否可用。守护进程就是已经在后台跑着的 app server。
数据流:进去的是 Codex 主目录;它算出默认 socket 路径,先看文件是否存在,再在很短超时时间内试连;成功返回 socket 路径,失败或超时返回空。
调用关系:run_main 在没有显式远程地址、且当前启动参数可复用时调用它,用来决定是否复用本地后台服务。非 Unix 平台上这个函数等价于总是返回空。
调用图:调用 1 个内部函数(connect);被 1 处调用(run_main);外部调用 3 个(app_server_control_socket_path, timeout, debug!)。
start_app_server445–477 ↗
async fn start_app_server(
target: &AppServerTarget,
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
作用:根据目标类型统一启动或连接 app server。调用方不用关心具体是内置、守护进程还是远程。
数据流:进去的是目标、配置、覆盖项、云配置、日志、数据库和环境管理器;它按目标分支:内置就启动,远程或本地守护进程就连接;出来是统一的 AppServerClient。
调用关系:run_ratatui_app 和 start_app_server_for_picker 都通过它拿后台连接。它把内置情况交给 start_embedded_app_server,远程情况交给 connect_remote_app_server。
调用图:调用 2 个内部函数(connect_remote_app_server, start_embedded_app_server);被 2 处调用(run_ratatui_app, start_app_server_for_picker)。
start_app_server_for_picker479–503 ↗
async fn start_app_server_for_picker(
config: &Config,
target: &AppServerTarget,
state_db: Option<StateDbHandle>,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<
作用:专门为会话选择器启动一个 app server 会话。会话选择器用来列出、恢复或分叉旧聊天。
数据流:进去的是配置、目标、可选状态数据库和环境管理器;它用一组默认启动参数启动 app server,再包装成 AppServerSession;出来的是可供选择器调用的会话对象。
调用关系:测试辅助函数 start_embedded_app_server_for_picker 会调用它;它内部走 start_app_server,再用 thread_params_mode 决定会话参数模式。
调用图:调用 5 个内部函数(default, new, thread_params_mode, new, start_app_server);被 1 处调用(start_embedded_app_server_for_picker);外部调用 4 个(new, clone, default, default)。
start_embedded_app_server_for_picker506–517 ↗
async fn start_embedded_app_server_for_picker(
config: &Config,
) -> color_eyre::Result<AppServerSession>
作用:测试中使用的快捷函数,用内置 app server 启动一个选择器用会话。
数据流:进去的是配置;它先初始化内置服务需要的状态数据库,再用测试环境管理器启动选择器会话;出来是 AppServerSession。
调用关系:这是测试专用路径,组合了 init_state_db_for_app_server_target 和 start_app_server_for_picker,方便测试不必重复写启动步骤。
调用图:调用 3 个内部函数(default_for_tests, init_state_db_for_app_server_target, start_app_server_for_picker);外部调用 1 个(new)。
start_embedded_app_server_with520–571 ↗
async fn start_embedded_app_server_with(
arg0_paths: Arg0DispatchPaths,
config: Config,
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
strict_conf
作用:内置 app server 的通用启动器,允许把“真正启动客户端”的函数作为参数传进来。这样正式代码和测试都能复用同一套组装逻辑。
数据流:进去的是配置、覆盖项、日志、数据库、环境管理器和一个启动函数;它把配置警告整理成通知,组装完整启动参数,再调用传入的启动函数;出来是内置客户端,失败时加上“启动内置服务失败”的说明。
调用关系:start_embedded_app_server 正式使用它;测试 embedded_app_server_start_failure_is_returned 用自定义失败启动函数验证错误信息。
调用图:被 2 处调用(start_embedded_app_server, embedded_app_server_start_failure_is_returned);外部调用 5 个(new, new, env!, from_value, json!)。
shutdown_app_server_if_present573–579 ↗
async fn shutdown_app_server_if_present(app_server: Option<AppServerSession>)
作用:如果手上还有临时 app server 会话,就把它关掉。这样退出选择器或引导流程时不会留下后台任务。
数据流:进去的是可选 app server 会话;有值就调用关闭,失败只写警告;没有值就什么也不做。
调用关系:run_ratatui_app 在用户退出、找不到会话等早退路径里调用它,做收尾清理。
调用图:被 1 处调用(run_ratatui_app);外部调用 1 个(warn!)。
session_target_from_app_server_thread581–598 ↗
fn session_target_from_app_server_thread(
thread: AppServerThread,
) -> Option<resume_picker::SessionTarget>
作用:把 app server 返回的线程信息转换成 TUI 恢复会话时需要的目标对象。
数据流:进去的是 app server 的 thread;它解析 thread id,保留会话文件路径;解析成功返回 SessionTarget,失败写警告并返回空。
调用关系:按名称查找、按 ID 查找、查最新会话时都会用它把后台结果变成选择器能理解的格式。
调用图:调用 1 个内部函数(from_string);被 2 处调用(lookup_session_target_by_name_with_app_server, lookup_session_target_with_app_server);外部调用 1 个(warn!)。
resume_source_kinds600–609 ↗
fn resume_source_kinds(include_non_interactive: bool) -> Vec<ThreadSourceKind>
作用:决定恢复会话时要查哪些来源的线程。来源可以理解成会话最初是从 CLI、VS Code、执行器等哪里来的。
数据流:进去的是是否包含非交互会话的开关;它默认放入 CLI 和 VS Code,需要时再加入 Exec 和 AppServer;出来是来源列表。
调用关系:latest_session_lookup_params 用它生成查询参数,保证“恢复最近会话”时查的范围符合用户选项。
调用图:被 1 处调用(latest_session_lookup_params);外部调用 1 个(vec!)。
lookup_session_target_by_name_with_app_server611–644 ↗
async fn lookup_session_target_by_name_with_app_server(
app_server: &mut AppServerSession,
name: &str,
) -> color_eyre::Result<Option<resume_picker::SessionTarget>>
作用:按会话名称从 app server 查找可恢复的会话。用户传的不是 UUID 时,就会走这种标题搜索。
数据流:进去的是 app server 会话和名称;它分页请求线程列表,每页最多 100 条,找标题完全相同的线程;找到就转成 SessionTarget,翻完还没有就返回空。
调用关系:lookup_session_target_with_app_server 在判断输入不是 UUID 后调用它;测试确认它会使用后台标题搜索。
调用图:调用 2 个内部函数(thread_list, session_target_from_app_server_thread);被 2 处调用(lookup_session_target_with_app_server, lookup_session_target_by_name_uses_backend_title_search);外部调用 1 个(vec!)。
lookup_session_target_with_app_server646–679 ↗
async fn lookup_session_target_with_app_server(
app_server: &mut AppServerSession,
id_or_name: &str,
) -> color_eyre::Result<Option<resume_picker::SessionTarget>>
作用:按用户输入的“会话 ID 或名称”查找会话。用户不需要区分自己输入的是 UUID 还是标题。
数据流:进去的是 app server 会话和字符串;如果像 UUID,就按 ID 读取线程;否则按名称搜索;成功返回 SessionTarget,失败或找不到返回空并写警告。
调用关系:run_ratatui_app 在 --resume <id> 和 --fork <id> 时调用它;它会把名称搜索交给 lookup_session_target_by_name_with_app_server。
调用图:调用 4 个内部函数(from_string, thread_read, lookup_session_target_by_name_with_app_server, session_target_from_app_server_thread);被 1 处调用(run_ratatui_app);外部调用 2 个(parse_str, warn!)。
lookup_latest_session_target_with_app_server681–712 ↗
async fn lookup_latest_session_target_with_app_server(
app_server: &mut AppServerSession,
config: &Config,
cwd_filter: Option<&Path>,
include_non_interactive: bool,
) -> color_eyre::Re
作用:查找最近一次可恢复或可分叉的会话。它还会处理老会话没有进数据库的情况。
数据流:进去的是 app server、配置、可选当前目录过滤、是否包含非交互会话;它先只查状态数据库,再允许扫描并修复旧记录;找到存在的本地路径或远程目标就返回,否则返回空。
调用关系:run_ratatui_app 在 --resume --last 和 --fork --last 时使用它;测试验证它会按目录过滤,也会在数据库缺记录时回退扫描。
调用图:调用 3 个内部函数(thread_list, uses_remote_workspace, latest_session_lookup_params);被 3 处调用(run_ratatui_app, fork_last_filters_latest_session_by_cwd_unless_show_all, latest_session_lookup_falls_back_for_rollout_missing_from_state_db)。
latest_session_lookup_params720–747 ↗
fn latest_session_lookup_params(
uses_remote_workspace: bool,
config: &Config,
cwd_filter: Option<&Path>,
include_non_interactive: bool,
lookup_mode: LatestSessionLookupMode,
) ->
作用:生成“查最近会话”的请求参数。它把远程、本地、目录过滤、来源过滤这些规则集中到一个地方。
数据流:进去的是是否远程、配置、可选目录、是否包含非交互来源和查询模式;它生成限制 1 条、按更新时间排序、必要时带模型和目录过滤的参数;出来是 ThreadListParams。
调用关系:lookup_latest_session_target_with_app_server 用它实际发请求;多项测试检查本地、远程、目录和来源过滤是否正确。
调用图:调用 1 个内部函数(resume_source_kinds);被 6 处调用(lookup_latest_session_target_with_app_server, latest_session_lookup_params_can_include_non_interactive_sources, latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions, latest_session_lookup_params_keep_local_filters_for_embedded_sessions, latest_session_lookup_params_keep_local_filters_for_local_daemon_sessions, latest_session_lookup_params_omit_local_filters_for_remote_sessions);外部调用 1 个(vec!)。
config_cwd_for_app_server_target749–769 ↗
fn config_cwd_for_app_server_target(
cwd: Option<&Path>,
app_server_target: &AppServerTarget,
environment_manager: &EnvironmentManager,
) -> std::io::Result<Option<AbsolutePathBuf>>
作用:决定加载配置时该不该带当前工作目录。远程工作区的路径可能不是本机路径,所以不能随便在本机校验。
数据流:进去的是可选 cwd、app server 目标和环境管理器;远程目标或远程执行环境直接返回空;本地则把 cwd 规范化成绝对路径;路径不存在会报错。
调用关系:run_main 加载配置前调用它;多项测试覆盖远程跳过、本地规范化、缺失路径报错等情况。
调用图:调用 4 个内部函数(default_environment, uses_remote_workspace, current_dir, from_absolute_path);被 6 处调用(run_main, config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd, config_cwd_for_app_server_target_canonicalizes_local_daemon_cli_cwd, config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd, config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server, config_cwd_for_app_server_target_omits_cwd_for_remote_sessions);外部调用 1 个(canonicalize_existing_preserving_symlinks)。
should_load_configured_environments771–776 ↗
fn should_load_configured_environments(
loader_overrides: &LoaderOverrides,
app_server_target: &AppServerTarget,
) -> bool
作用:判断是否应该读取用户配置里的执行环境。远程工作区不能套用本地配置环境。
数据流:进去的是加载覆盖项和 app server 目标;如果没有忽略用户配置且不是远程工作区,就返回真;否则返回假。
调用关系:run_main 用它决定 EnvironmentManager 是从 Codex 主目录加载,还是只从当前环境变量创建。
调用图:调用 1 个内部函数(uses_remote_workspace);被 1 处调用(run_main)。
latest_session_cwd_filter778–793 ↗
fn latest_session_cwd_filter(
uses_remote_workspace: bool,
remote_cwd_override: Option<&'a Path>,
config: &'a Config,
show_all: bool,
) -> Option<&'a Path>
作用:决定“最近会话”查询要不要限定当前目录。这样 --last 默认更符合“当前项目”的直觉。
数据流:进去的是是否远程、远程 cwd 覆盖、本地配置和是否显示全部;如果显示全部就不加过滤;远程用远程 cwd;本地用配置里的 cwd;出来是可选路径。
调用关系:run_ratatui_app 在 resume/fork latest 路径调用它;测试验证 show-all、本地和远程三种情况。
调用图:被 3 处调用(run_ratatui_app, fork_last_filters_latest_session_by_cwd_unless_show_all, latest_session_cwd_filter_respects_scope_options)。
app_server_target_for_launch795–811 ↗
fn app_server_target_for_launch(
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
default_daemon_socket: Option<AbsolutePathBuf>,
can_reuse_implicit_local_daemon: bool,
) -> AppS
作用:决定这次启动应该用显式远程服务、默认本地守护进程,还是内置服务。
数据流:进去的是显式远程 endpoint、探测到的默认 socket、是否允许复用本地守护进程;显式远程优先,其次可复用时用默认 socket,否则用内置;出来是 AppServerTarget。
调用关系:run_main 在探测守护进程后调用它;测试覆盖显式远程优先、默认 socket 使用、不能复用时回退内置。
调用图:被 4 处调用(run_main, app_server_target_for_launch_prefers_explicit_remote_endpoint, app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable, app_server_target_for_launch_uses_local_daemon_for_default_socket)。
loader_overrides_are_default813–829 ↗
fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool
作用:判断配置加载覆盖项是不是完全默认。只有默认时,才比较安全地复用已有本地守护进程。
数据流:进去的是 LoaderOverrides;它逐项检查用户配置路径、系统配置、管理配置、忽略开关等是否都没改;出来是真假值。
调用关系:can_reuse_implicit_local_daemon 调用它,作为判断守护进程能不能复用的一部分。
调用图:被 1 处调用(can_reuse_implicit_local_daemon)。
can_reuse_implicit_local_daemon831–842 ↗
fn can_reuse_implicit_local_daemon(
cli_kv_overrides: &[(String, toml::Value)],
loader_overrides: &LoaderOverrides,
strict_config: bool,
has_non_replayable_launch_overrides: bool,
) ->
作用:判断这次启动能不能复用已经跑着的本地守护进程。因为守护进程没法完全吸收这次命令行的所有临时配置。
数据流:进去的是命令行配置覆盖、加载覆盖、严格配置开关和不可重放覆盖标记;它要求这些都接近默认;出来是真假值。
调用关系:run_main 用它决定要不要探测默认 daemon socket;它内部依赖 loader_overrides_are_default。
调用图:调用 1 个内部函数(loader_overrides_are_default);被 1 处调用(run_main)。
run_main844–1262 ↗
async fn run_main(
mut cli: Cli,
arg0_paths: Arg0DispatchPaths,
loader_overrides: LoaderOverrides,
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
) -> std::io::Result<AppEx
作用:这是 TUI 库层面的主启动流程。它把命令行参数、配置、认证、日志、数据库、后台服务选择等都准备好,然后进入真正的终端界面。
数据流:进去的是 CLI 参数、可执行文件路径、配置加载覆盖和可选远程 endpoint;它解析覆盖项,找 Codex 主目录,选择 app server 目标,加载配置和云配置,处理 OSS 模型、登录限制、日志、遥测和状态数据库;最后调用 run_ratatui_app,出来是应用退出信息或 IO 错误。
调用关系:二进制入口通常会调用它。它是启动链的第一大段,负责“上桌前备菜”,最后把运行 UI 的工作交给 run_ratatui_app。
调用图:调用 21 个内部函数(default, resolve_oss_provider, resolve_profile_v2_config_path, from_codex_home, from_env, from_optional_paths, new, originator, set_default_client_residency_requirement, add_dir_warning_message (+11 more));外部调用 25 个(default, try_from_default_env, new, other, migrate_personality_if_needed, cloud_config_bundle_loader_for_storage, enforce_login_restrictions, record_process_start_once, sqlite_telemetry_recorder, install_process_db_telemetry (+15 more))。
run_ratatui_app1265–1795 ↗
async fn run_ratatui_app(
cli: Cli,
arg0_paths: Arg0DispatchPaths,
loader_overrides: LoaderOverrides,
strict_config: bool,
app_server_target: AppServerTarget,
remote_cwd_overri
作用:启动并运行真正的终端用户界面。ratatui 是一个 Rust 终端 UI 库,可以把纯文本终端画成类似应用窗口的界面。
数据流:进去的是已经解析好的 CLI、配置、后台目标、数据库、日志、云配置等;它安装错误报告,初始化终端,启动 app server,跑更新提示、登录/信任引导、会话恢复或分叉选择,最终调用 App::run;退出前恢复终端并记录会话结束。
调用关系:run_main 准备好外部条件后调用它。它内部会调用大量小函数,比如 start_app_server、get_login_status、determine_alt_screen_mode,最后把主循环交给 App::run。
调用图:调用 29 个内部函数(set_default_client_residency_requirement, thread_params_mode, uses_remote_workspace, new, new, write_config_batch, determine_alt_screen_mode, get_login_status, latest_session_cwd_filter, load_config_or_exit (+15 more));被 1 处调用(run_main);外部调用 26 个(new, now, auth_keyring_backend_kind, clone, clone, run, cloud_config_bundle_loader_for_storage, find_codex_home, install, clone (+15 more))。
restore1801–1807 ↗
fn restore()
作用:把终端从 TUI 的特殊模式恢复到普通命令行模式。失败时会告诉用户可以运行 reset 或重启终端。
数据流:没有输入;它调用终端恢复函数;如果失败就往标准错误输出提示。
调用关系:TerminalRestoreGuard::restore_silently 用它做兜底恢复,尤其适合出错或崩溃后的清理路径。
调用图:调用 1 个内部函数(restore_after_exit);被 1 处调用(restore_silently);外部调用 1 个(eprintln!)。
TerminalRestoreGuard::new1814–1816 ↗
fn new() -> Self
作用:创建一个终端恢复守卫。守卫就像“离开时自动关灯”的便签,防止忘记恢复终端。
数据流:没有输入;它创建一个 active=true 的守卫;出来是 TerminalRestoreGuard。
调用关系:run_ratatui_app 初始化终端后创建它;之后无论正常退出还是提前返回,守卫都能帮助恢复终端。
调用图:被 1 处调用(run_ratatui_app)。
TerminalRestoreGuard::restore1819–1825 ↗
fn restore(&mut self) -> color_eyre::Result<()>
作用:主动恢复终端,并把守卫标记为已经用过。这个版本会把错误返回给调用方。
数据流:进去的是可变守卫;如果还 active,就调用终端恢复函数并设为 inactive;出来是成功或错误。
调用关系:更新提示选择“执行更新”时会需要先干净恢复终端再退出。它和静默版本共同防止重复恢复。
调用图:调用 1 个内部函数(restore_after_exit)。
TerminalRestoreGuard::restore_silently1827–1832 ↗
TerminalRestoreGuard::drop1836–1838 ↗
fn drop(&mut self)
作用:守卫被销毁时自动恢复终端。这是防止异常路径漏清理的最后一道保险。
数据流:进去的是即将销毁的守卫;它调用 restore_silently;没有返回值。
调用关系:Rust 在变量离开作用域时自动调用它,所以即使 run_ratatui_app 中途返回,也会尽量恢复终端。
调用图:调用 1 个内部函数(restore_silently)。
determine_alt_screen_mode1848–1854 ↗
fn determine_alt_screen_mode(no_alt_screen: bool, tui_alternate_screen: AltScreenMode) -> bool
作用:决定是否使用终端的备用屏幕。备用屏幕像临时白板,退出后能保留原来的命令行滚动内容。
数据流:进去的是 --no-alt-screen 开关和配置里的备用屏幕模式;命令行禁用优先,否则只要配置不是 Never 就启用;出来是真假值。
调用关系:run_ratatui_app 在启动主界面前调用它;测试确认 auto、always、never 和命令行禁用的行为。
调用图:被 1 处调用(run_ratatui_app)。
get_login_status1865–1880 ↗
async fn get_login_status(
app_server: &mut AppServerSession,
config: &Config,
) -> color_eyre::Result<LoginStatus>
作用:轻量读取当前登录状态。它避免做完整 bootstrap,因为完整流程可能触发模型列表请求和限流等待。
数据流:进去的是 app server 会话和配置;如果模型提供商不需要 OpenAI 登录,直接返回未认证;否则读取账户信息,区分 API key、ChatGPT 或无登录;出来是 LoginStatus。
调用关系:run_ratatui_app 用它决定是否显示登录引导。它把实际账户读取交给 app_server.read_account。
调用图:调用 1 个内部函数(read_account);被 1 处调用(run_ratatui_app);外部调用 1 个(AuthMode)。
load_config_or_exit1882–1898 ↗
async fn load_config_or_exit(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
cloud_config_bundle: CloudConfigBundleLoader,
作用:加载完整配置;如果失败,就打印错误并直接退出进程。它用于启动阶段的硬性配置读取。
数据流:进去的是命令行覆盖、程序覆盖、加载覆盖、云配置和严格配置开关;它把工作交给带 fallback cwd 的版本,fallback 为空;出来是 Config,失败不会返回。
调用关系:run_main 和 run_ratatui_app 多次调用它,尤其在登录、信任、恢复会话后需要重新加载配置时。
调用图:调用 1 个内部函数(load_config_or_exit_with_fallback_cwd);被 2 处调用(run_main, run_ratatui_app)。
load_config_or_exit_with_fallback_cwd1900–1925 ↗
async fn load_config_or_exit_with_fallback_cwd(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
cloud_config_bundle: CloudC
作用:加载完整配置,并允许给恢复/分叉会话提供一个备用工作目录。失败时直接结束程序。
数据流:进去的是各种覆盖项、云配置、严格模式和可选 fallback cwd;它用 ConfigBuilder 一步步设置后构建配置;成功返回配置,失败打印错误并 exit(1)。
调用关系:load_config_or_exit 包装它;run_ratatui_app 在恢复或分叉旧会话、需要换 cwd 时直接调用它。
调用图:被 2 处调用(load_config_or_exit, run_ratatui_app);外部调用 3 个(default, eprintln!, exit)。
load_bootstrap_config_or_exit1928–1965 ↗
async fn load_bootstrap_config_or_exit(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_kv_overrides: Vec<(String, codex_config::TomlValue)>,
loader_overrides: LoaderOverrides,
作用:加载较早期、较轻量的 config.toml 结果,用来决定后续云配置、认证存储和 OSS 提供商等启动信息。
数据流:进去的是 Codex 主目录、可选 cwd、命令行覆盖、加载覆盖、严格配置和云配置加载器;它读取配置层栈;成功返回 ConfigTomlLoadResult,失败打印带来源位置的配置错误并退出。
调用关系:run_main 在完整配置前调用它;这让启动流程能先知道云配置和认证相关设置。
调用图:调用 1 个内部函数(load_config_toml_with_layer_stack);被 1 处调用(run_main);外部调用 2 个(eprintln!, exit)。
should_show_trust_screen1968–1970 ↗
fn should_show_trust_screen(config: &Config) -> bool
作用:判断是否要显示“是否信任当前目录”的提示。信任决定会影响工具执行时的安全限制。
数据流:进去的是配置;它查看当前项目是否已经有 trust_level;没有就返回真,有就返回假。
调用关系:run_ratatui_app 用它决定是否进入引导流程;多项测试验证未决定、已不信任等情况。
调用图:被 4 处调用(run_ratatui_app, untrusted_project_skips_trust_prompt, windows_shows_trust_prompt_with_sandbox, windows_shows_trust_prompt_without_sandbox)。
should_show_onboarding1972–1982 ↗
fn should_show_onboarding(
login_status: LoginStatus,
config: &Config,
show_trust_screen: bool,
) -> bool
作用:判断是否要显示首次引导流程。只要需要目录信任确认,或需要登录,就应该显示。
数据流:进去的是登录状态、配置和是否显示信任页;信任页需要时直接返回真,否则再看登录页是否需要;出来是真假值。
调用关系:run_ratatui_app 用它决定是否调用 onboarding 应用;它把登录判断交给 should_show_login_screen。
调用图:调用 1 个内部函数(should_show_login_screen);被 1 处调用(run_ratatui_app)。
should_show_login_screen1984–1992 ↗
fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool
作用:判断是否要显示登录页。只有需要 OpenAI 类认证的模型提供商,并且当前未登录,才显示。
数据流:进去的是登录状态和配置;它先看模型提供商是否要求 OpenAI 登录,再看状态是不是未认证;出来是真假值。
调用关系:run_ratatui_app 和 should_show_onboarding 都会用它来决定引导页面组成。
调用图:被 2 处调用(run_ratatui_app, should_show_onboarding)。
tests::build_config2009–2014 ↗
tests::write_session_rollout2016–2093 ↗
fn write_session_rollout(
codex_home: &Path,
filename_ts: &str,
meta_rfc3339: &str,
preview: &str,
model_provider: &str,
cwd: &Path,
) -> color_eyre
作用:测试用的小工具,手写一个假的会话记录文件。rollout 文件可以理解成保存聊天历史的一行行 JSON 日志。
数据流:进去的是 Codex home、时间戳、预览文字、模型提供商和 cwd;它创建会话目录和 JSONL 文件,写入元信息和用户消息,并设置修改时间;出来是新建的线程 ID。
调用关系:恢复最近会话相关测试用它制造旧会话,之后再让 app server 搜索这些会话。
调用图:调用 1 个内部函数(from_string);外部调用 12 个(default, join, to_path_buf, new_v4, parse_from_rfc3339, format!, json!, to_value, new, new (+2 more))。
tests::startup_removes_legacy_tui_log_file2096–2107 ↗
fn startup_removes_legacy_tui_log_file() -> std::io::Result<()>
作用:验证启动清理会删除旧 TUI 日志文件。
数据流:它创建临时日志文件,调用 remove_legacy_tui_log_file,再断言文件不存在;测试通过说明清理逻辑有效。
调用关系:直接覆盖 remove_legacy_tui_log_file 的行为,防止以后改动导致旧日志继续累积。
调用图:调用 1 个内部函数(remove_legacy_tui_log_file);外部调用 4 个(new, assert!, create_dir_all, write)。
tests::start_test_embedded_app_server2109–2127 ↗
async fn start_test_embedded_app_server(
config: Config,
) -> color_eyre::Result<InProcessAppServerClient>
作用:测试用快捷启动内置 app server。
数据流:进去的是配置;它初始化状态数据库,用测试环境管理器启动内置 app server;出来是 InProcessAppServerClient。
调用关系:多个需要真实后台能力的测试调用它,比如线程启动、会话查找、最近会话扫描。
调用图:调用 5 个内部函数(default, default_for_tests, new, init_state_db_for_app_server_target, start_embedded_app_server);外部调用 4 个(new, new, default, default)。
tests::alternate_screen_auto_uses_alt_screen2130–2147 ↗
fn alternate_screen_auto_uses_alt_screen()
作用:验证备用屏幕模式的选择规则。
数据流:它用不同的命令行开关和配置值调用 determine_alt_screen_mode,再断言结果符合预期。
调用关系:保护终端显示模式逻辑,避免未来改动让 auto、always、never 行为反了。
调用图:外部调用 1 个(assert!)。
tests::session_target_display_label_falls_back_to_thread_id2150–2158 ↗
fn session_target_display_label_falls_back_to_thread_id()
作用:验证没有会话路径时,显示标签会退回到线程 ID。
数据流:它创建一个只有 thread id、没有路径的 SessionTarget,读取显示标签,并断言里面是 thread id。
调用关系:保障恢复选择器在缺少文件路径时仍然能给用户一个可识别的名字。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::resolve_remote_addr_accepts_websocket_url2161–2169 ↗
fn resolve_remote_addr_accepts_websocket_url()
作用:验证普通 websocket 远程地址会被接受。
数据流:它输入 ws://127.0.0.1:4500,检查解析结果是否是带规范化斜杠的 websocket endpoint。
调用关系:覆盖 resolve_remote_addr 的合法 ws 地址路径。
调用图:外部调用 1 个(assert_eq!)。
tests::resolve_remote_addr_accepts_secure_websocket_url2172–2180 ↗
fn resolve_remote_addr_accepts_secure_websocket_url()
作用:验证安全 websocket 地址会被接受。
数据流:它输入 wss://example.com:443,检查解析结果是否规范化为 websocket endpoint。
调用关系:覆盖 resolve_remote_addr 对 wss 和显式默认端口的处理。
调用图:外部调用 1 个(assert_eq!)。
tests::resolve_remote_addr_accepts_default_socket2183–2192 ↗
fn resolve_remote_addr_accepts_default_socket() -> color_eyre::Result<()>
作用:验证 unix:// 会表示默认本地 socket。
数据流:它找到 Codex home,解析 unix://,再断言结果等于默认控制 socket 路径。
调用关系:覆盖 resolve_remote_addr 的默认 Unix socket 分支。
调用图:外部调用 2 个(assert_eq!, find_codex_home)。
tests::resolve_remote_addr_accepts_relative_socket_path2195–2203 ↗
fn resolve_remote_addr_accepts_relative_socket_path() -> color_eyre::Result<()>
作用:验证相对 Unix socket 路径会被接受。
数据流:它输入 unix://codex.sock,解析后断言路径被转成相对当前目录的绝对路径表示。
调用关系:覆盖 resolve_remote_addr 对用户自定义相对 socket 路径的处理。
调用图:外部调用 1 个(assert_eq!)。
tests::resolve_remote_addr_accepts_absolute_socket_path2206–2216 ↗
fn resolve_remote_addr_accepts_absolute_socket_path() -> color_eyre::Result<()>
作用:验证绝对 Unix socket 路径会被接受。
数据流:它在临时目录拼出 socket 路径,解析 unix://绝对路径,断言结果等于该绝对路径。
调用关系:覆盖 resolve_remote_addr 对绝对 socket 路径的处理。
调用图:外部调用 2 个(new, assert_eq!)。
tests::resolve_remote_addr_rejects_invalid_remote_addresses2219–2231 ↗
fn resolve_remote_addr_rejects_invalid_remote_addresses()
作用:验证不合规远程地址会被拒绝。
数据流:它遍历没有端口、缺少协议、错误协议等地址,调用 resolve_remote_addr,断言错误信息提示支持的格式。
调用关系:保护远程地址解析的严格性,直接测试 resolve_remote_addr。
调用图:调用 1 个内部函数(resolve_remote_addr);外部调用 1 个(assert!)。
tests::default_daemon_auto_connect_skips_missing_socket2234–2242 ↗
tests::default_daemon_auto_connect_probes_socket_only2246–2258 ↗
async fn default_daemon_auto_connect_probes_socket_only() -> color_eyre::Result<()>
作用:在 Unix 上验证存在且可连接的默认 socket 会被探测到。
数据流:它创建默认 socket 路径并绑定一个 Unix listener,然后调用探测函数,断言返回该路径。
调用关系:覆盖 maybe_probe_default_daemon_socket 的成功路径。
调用图:调用 1 个内部函数(bind);外部调用 4 个(new, assert_eq!, app_server_control_socket_path, create_dir_all)。
tests::app_server_target_for_launch_uses_local_daemon_for_default_socket2261–2279 ↗
fn app_server_target_for_launch_uses_local_daemon_for_default_socket() -> color_eyre::Result<()>
作用:验证允许复用时,默认 socket 会让启动目标变成本地守护进程。
数据流:它传入默认 socket、没有显式远程 endpoint、允许复用;断言结果是 LocalDaemon,且不算远程工作区。
调用关系:直接测试 app_server_target_for_launch 和相关目标判断。
调用图:调用 2 个内部函数(app_server_target_for_launch, relative_to_current_dir);外部调用 2 个(assert!, assert_eq!)。
tests::app_server_target_for_launch_prefers_explicit_remote_endpoint2282–2301 ↗
fn app_server_target_for_launch_prefers_explicit_remote_endpoint() -> color_eyre::Result<()>
作用:验证显式远程地址优先级最高。
数据流:它同时给显式 endpoint 和默认 socket,调用目标选择函数,断言结果是显式 Remote。
调用关系:保护 app_server_target_for_launch 的优先级规则。
调用图:调用 2 个内部函数(app_server_target_for_launch, relative_to_current_dir);外部调用 2 个(assert!, assert_eq!)。
tests::app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable2304–2315 ↗
fn app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable() -> color_eyre::Result<()>
作用:验证当前启动配置不能复用时,即使有默认 socket 也不用本地守护进程。
数据流:它传入 socket 但把允许复用设为假,断言目标回到 Embedded。
调用关系:覆盖 app_server_target_for_launch 的安全回退逻辑。
调用图:调用 2 个内部函数(app_server_target_for_launch, relative_to_current_dir);外部调用 1 个(assert_eq!)。
tests::can_reuse_implicit_local_daemon_requires_default_launch_config2318–2354 ↗
tests::should_load_configured_environments_for_local_daemon2357–2369 ↗
fn should_load_configured_environments_for_local_daemon() -> color_eyre::Result<()>
作用:验证本地守护进程仍然应该加载配置里的执行环境。
数据流:它构造 LocalDaemon 目标和默认加载覆盖,调用 should_load_configured_environments,断言返回真。
调用关系:保护本地 daemon 与远程 workspace 的区分逻辑。
调用图:调用 1 个内部函数(relative_to_current_dir);外部调用 1 个(assert!)。
tests::latest_session_lookup_params_keep_local_filters_for_embedded_sessions2372–2405 ↗
async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions() -> std::io::Result<()>
作用:验证内置本地会话查询会保留模型和目录过滤。
数据流:它构造本地配置和 cwd,生成最近会话查询参数,断言模型提供商、cwd 和只查数据库标记正确。
调用关系:直接覆盖 latest_session_lookup_params 的本地内置分支。
调用图:调用 1 个内部函数(latest_session_lookup_params);外部调用 4 个(new, assert!, assert_eq!, build_config)。
tests::latest_session_lookup_params_keep_local_filters_for_local_daemon_sessions2408–2433 ↗
async fn latest_session_lookup_params_keep_local_filters_for_local_daemon_sessions() -> color_eyre::Result<()>
作用:验证本地守护进程会话查询也保留本地过滤条件。
数据流:它构造 LocalDaemon 目标和 cwd,生成查询参数,断言模型和 cwd 过滤存在。
调用关系:防止把本地 daemon 错当远程 workspace,测试 latest_session_lookup_params。
调用图:调用 2 个内部函数(latest_session_lookup_params, relative_to_current_dir);外部调用 3 个(new, assert_eq!, build_config)。
tests::latest_session_lookup_params_omit_local_filters_for_remote_sessions2436–2452 ↗
async fn latest_session_lookup_params_omit_local_filters_for_remote_sessions() -> std::io::Result<()>
作用:验证远程会话查询不会带本地模型和本地目录过滤。
数据流:它用远程标记生成查询参数,断言模型提供商和 cwd 都为空。
调用关系:覆盖 latest_session_lookup_params 的远程分支,避免拿本机路径过滤远端会话。
调用图:调用 1 个内部函数(latest_session_lookup_params);外部调用 3 个(new, assert_eq!, build_config)。
tests::latest_session_lookup_params_can_include_non_interactive_sources2455–2478 ↗
async fn latest_session_lookup_params_can_include_non_interactive_sources() -> std::io::Result<()>
作用:验证用户要求时,最近会话查询会包含非交互来源。
数据流:它开启 include_non_interactive,生成查询参数,断言来源列表包含 CLI、VS Code、Exec 和 AppServer。
调用关系:测试 latest_session_lookup_params 通过 resume_source_kinds 扩展来源列表。
调用图:调用 1 个内部函数(latest_session_lookup_params);外部调用 3 个(new, assert_eq!, build_config)。
tests::latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions2481–2501 ↗
async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions() -> std::io::Result<()>
作用:验证远程会话如果明确给了远程 cwd,也会带上这个过滤条件。
数据流:它用远程标记和 repo/on/server 路径生成查询参数,断言 cwd 过滤被保留,模型过滤为空。
调用关系:覆盖远程 workspace 下由用户或参数提供 cwd 的特殊情况。
调用图:调用 1 个内部函数(latest_session_lookup_params);外部调用 4 个(new, new, assert_eq!, build_config)。
tests::latest_session_cwd_filter_respects_scope_options2504–2528 ↗
async fn latest_session_cwd_filter_respects_scope_options() -> std::io::Result<()>
作用:验证最近会话 cwd 过滤会遵守本地、远程和 show-all 选项。
数据流:它分别测试本地默认、显示全部、远程覆盖三种输入,断言返回的路径或空值正确。
调用关系:直接测试 latest_session_cwd_filter,保证 resume/fork latest 的范围符合用户预期。
调用图:调用 1 个内部函数(latest_session_cwd_filter);外部调用 4 个(new, new, assert_eq!, build_config)。
tests::fork_last_filters_latest_session_by_cwd_unless_show_all2531–2599 ↗
async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre::Result<()>
作用:验证 fork --last 默认按当前目录找最近会话,但 show-all 会跨目录找。
数据流:它写入两个不同目录、不同时间的假会话,启动内置 app server,然后分别按当前目录和全局查询,断言返回不同的线程 ID。
调用关系:组合测试 latest_session_cwd_filter、lookup_latest_session_target_with_app_server 和 app server 的会话搜索能力。
调用图:调用 3 个内部函数(new, latest_session_cwd_filter, lookup_latest_session_target_with_app_server);外部调用 8 个(default, new, InProcess, assert_eq!, default, create_dir_all, start_test_embedded_app_server, write_session_rollout)。
tests::latest_session_lookup_falls_back_for_rollout_missing_from_state_db2602–2644 ↗
async fn latest_session_lookup_falls_back_for_rollout_missing_from_state_db() -> color_eyre::Result<()>
作用:验证最近会话如果没进状态数据库,也能通过扫描旧会话文件找回来。
数据流:它先启动 app server,再模拟老写入者创建 rollout 文件,调用最近会话查找,断言能找到该线程。
调用关系:覆盖 lookup_latest_session_target_with_app_server 的“先查数据库,再扫描修复”回退流程。
调用图:调用 2 个内部函数(new, lookup_latest_session_target_with_app_server);外部调用 8 个(default, new, InProcess, assert_eq!, default, create_dir_all, start_test_embedded_app_server, write_session_rollout)。
tests::config_cwd_for_app_server_target_omits_cwd_for_remote_sessions2647–2666 ↗
async fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()>
作用:验证远程 app server 目标不会用本机 cwd 加载配置。
数据流:它传入一个可能不存在的本机路径和远程目标,调用 config_cwd_for_app_server_target,断言返回空。
调用关系:保护远程 workspace 不被本地路径检查误伤。
调用图:调用 3 个内部函数(default_for_tests, config_cwd_for_app_server_target, relative_to_current_dir);外部调用 3 个(new, assert_eq!, cfg!)。
tests::config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd2669–2685 ↗
async fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()>
作用:验证内置本地目标会把 CLI cwd 规范化。
数据流:它传入临时目录和 Embedded 目标,调用函数,断言结果等于系统规范化后的绝对路径。
调用关系:覆盖 config_cwd_for_app_server_target 的本地内置正常路径。
调用图:调用 2 个内部函数(default_for_tests, config_cwd_for_app_server_target);外部调用 2 个(new, assert_eq!)。
tests::config_cwd_for_app_server_target_canonicalizes_local_daemon_cli_cwd2688–2708 ↗
async fn config_cwd_for_app_server_target_canonicalizes_local_daemon_cli_cwd() -> std::io::Result<()>
作用:验证本地守护进程目标也会规范化 CLI cwd。
数据流:它传入临时目录和 LocalDaemon 目标,断言返回规范化绝对路径。
调用关系:证明本地 daemon 和内置目标一样使用本机 cwd 规则。
调用图:调用 3 个内部函数(default_for_tests, config_cwd_for_app_server_target, relative_to_current_dir);外部调用 2 个(new, assert_eq!)。
tests::config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd2711–2723 ↗
async fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd() -> std::io::Result<()>
作用:验证本地内置目标遇到不存在的 cwd 会报错。
数据流:它构造一个不存在路径,调用 config_cwd_for_app_server_target,断言错误类型是 NotFound。
调用关系:防止配置加载默默接受无效本地目录。
调用图:调用 2 个内部函数(default_for_tests, config_cwd_for_app_server_target);外部调用 2 个(new, assert_eq!)。
tests::config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server2726–2748 ↗
async fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server() -> std::io::Result<()>
作用:验证即使 app server 是内置的,只要执行环境是远程,也不使用本机 cwd。
数据流:它创建一个测试用远程执行环境管理器,传入本机不存在路径,调用函数,断言返回空。
调用关系:覆盖 config_cwd_for_app_server_target 对远程执行环境的判断。
调用图:调用 3 个内部函数(create_for_tests, new, config_cwd_for_app_server_target);外部调用 4 个(new, assert_eq!, cfg!, current_exe)。
tests::windows_shows_trust_prompt_without_sandbox2752–2764 ↗
async fn windows_shows_trust_prompt_without_sandbox() -> std::io::Result<()>
作用:验证项目信任状态未决定时会显示信任提示,即使 Windows 沙盒关闭。
数据流:它构造配置,把项目 trust_level 设为空并关闭 Windows 沙盒,调用 should_show_trust_screen,断言返回真。
调用关系:覆盖信任提示的基础规则。
调用图:调用 1 个内部函数(should_show_trust_screen);外部调用 3 个(new, assert!, build_config)。
tests::embedded_app_server_supports_thread_start_rpc2767–2785 ↗
async fn embedded_app_server_supports_thread_start_rpc() -> color_eyre::Result<()>
作用:验证内置 app server 能处理启动线程的 RPC 请求。RPC 是远程过程调用,可以理解成向后台发一个结构化命令。
数据流:它启动测试内置 app server,发送 thread/start 请求,断言返回的 thread id 不为空,然后关闭服务。
调用关系:测试 start_test_embedded_app_server 启动出的后台确实能工作。
调用图:外部调用 6 个(new, Integer, default, assert!, build_config, start_test_embedded_app_server)。
tests::lookup_session_target_by_name_uses_backend_title_search2788–2850 ↗
async fn lookup_session_target_by_name_uses_backend_title_search() -> color_eyre::Result<()>
作用:验证按名称查会话会使用后台的标题搜索能力。
数据流:它创建会话文件和状态数据库记录,标题设为 saved-session,启动 app server 后按名称查找,断言返回路径和线程 ID 正确。
调用关系:直接测试 lookup_session_target_by_name_with_app_server,也验证 app server 查询和状态数据库配合正常。
调用图:调用 5 个内部函数(new, new, init, new, lookup_session_target_by_name_with_app_server);外部调用 12 个(pin, new, InProcess, assert_eq!, parse_from_rfc3339, format!, from_value, json!, create_dir_all, write (+2 more))。
tests::embedded_app_server_start_failure_is_returned2853–2881 ↗
async fn embedded_app_server_start_failure_is_returned() -> color_eyre::Result<()>
作用:验证内置 app server 启动失败时,错误会被带着上下文返回。
数据流:它调用 start_embedded_app_server_with,传入一个总是返回 boom 的启动函数;断言最终错误包含“failed to start embedded app server”。
调用关系:保护 start_embedded_app_server_with 的错误包装逻辑,让用户看到更有用的错误。
调用图:调用 4 个内部函数(default, default_for_tests, new, start_embedded_app_server_with);外部调用 8 个(new, new, new, default, assert!, default, panic!, build_config)。
tests::embedded_state_db_failure_is_typed_for_cli_recovery2884–2912 ↗
async fn embedded_state_db_failure_is_typed_for_cli_recovery() -> color_eyre::Result<()>
作用:验证内置状态数据库启动失败时,会保留专门的错误类型,方便 CLI 做恢复提示。
数据流:它让 sqlite_home 被一个普通文件占用,调用 init_state_db_for_app_server_target,断言错误里能取出 LocalStateDbStartupError,且路径和详情正确。
调用关系:覆盖内置数据库初始化失败路径,确保错误信息不被普通字符串吞掉。
调用图:调用 1 个内部函数(init_state_db_for_app_server_target);外部调用 6 个(new, assert!, assert_eq!, panic!, write, build_config)。
tests::embedded_state_db_corruption_preserves_failed_database_for_cli_recovery2915–2942 ↗
async fn embedded_state_db_corruption_preserves_failed_database_for_cli_recovery() -> color_eyre::Result<()>
作用:验证数据库损坏时,错误会指出真正坏掉的数据库文件。
数据流:它写入一个假的 logs 数据库文件,触发初始化失败,然后断言错误里的数据库路径和 SQLite 损坏细节正确。
调用关系:测试 init_state_db_for_app_server_target 对数据库腐坏错误的包装质量。
调用图:调用 1 个内部函数(init_state_db_for_app_server_target);外部调用 8 个(new, assert!, assert_eq!, logs_db_path, panic!, create_dir_all, write, build_config)。
tests::windows_shows_trust_prompt_with_sandbox2946–2965 ↗
async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()>
作用:验证项目信任未决定时,即使 Windows 沙盒开启也会显示信任提示。
数据流:它构造配置,把 trust_level 设为空并开启沙盒,调用 should_show_trust_screen,断言应显示提示。
调用关系:补充覆盖 Windows 沙盒相关配置下的信任提示逻辑。
调用图:调用 1 个内部函数(should_show_trust_screen);外部调用 4 个(new, assert!, cfg!, build_config)。
tests::untrusted_project_skips_trust_prompt2967–2981 ↗
async fn untrusted_project_skips_trust_prompt() -> std::io::Result<()>
作用:验证已经明确标记为不信任的项目,不会再弹信任选择提示。
数据流:它把项目 trust_level 设为 Untrusted,调用 should_show_trust_screen,断言返回假。
调用关系:保护信任提示只在“未决定”时出现,而不是每次都打扰用户。
调用图:调用 1 个内部函数(should_show_trust_screen);外部调用 3 个(new, assert!, build_config)。
tests::config_rebuild_changes_trust_defaults_with_cwd2984–3035 ↗
async fn config_rebuild_changes_trust_defaults_with_cwd() -> std::io::Result<()>
作用:验证换 cwd 后重新构建配置,会让项目级信任默认值跟着变化。
数据流:它写入两个项目的配置,一个 trusted、一个 untrusted,然后分别以两个 cwd 构建配置,断言审批策略不同。
调用关系:覆盖配置重载和项目路径选择的交互,解释为什么恢复/分叉会话后可能要重新加载配置。
调用图:调用 1 个内部函数(without_managed_config_for_tests);外部调用 7 个(default, new, assert_eq!, default, format!, create_dir_all, write)。
tests::theme_warning_uses_final_config3050–3079 ↗
async fn theme_warning_uses_final_config() -> std::io::Result<()>
作用:验证语法高亮主题警告使用最终配置,而不是启动时的旧配置。
数据流:它先构造初始配置,再模拟最终配置里有非法主题名,调用主题名校验,断言警告进入最终配置并包含该主题名。
调用关系:对应 run_ratatui_app 中“所有配置重载之后再设置主题”的重要顺序,防止恢复会话时主题错乱。
调用图:外部调用 4 个(new, assert!, assert_eq!, build_config)。
tui/src/oss_selection.rs源码 ↗
用户想用本机的开源 AI 服务时,程序需要知道该连 LM Studio 还是 Ollama。这个文件就像一个开机向导:先用网络请求试探两个默认端口,判断服务是“运行中”“没运行”还是“不确定”。如果只有一个服务在跑,它直接选好,省得打扰用户;如果两边都在跑,或者都没跑,就进入一个全屏终端小界面。界面里会显示服务状态、可选按钮和按键提示。用户可以用左右键或 Ctrl+H/Ctrl+L 移动,用回车确认,也可以按对应字母快速选择。它还负责进入和退出终端的特殊显示模式,避免选择界面把正常终端弄乱。
OssSelectionWidget::new110–162 ↗
fn new(lmstudio_status: ProviderStatus, ollama_status: ProviderStatus) -> io::Result<Self>
作用:创建一个选择界面用的小组件。它把 LM Studio 和 Ollama 的运行状态整理成用户能看懂的提示文字和按钮。
数据流:进去的是两个服务的状态:LM Studio 的状态和 Ollama 的状态。它把这些状态变成带颜色符号的文字,比如绿色圆点表示正在运行,再准备好默认选中的选项。出来的是一个可显示、可接收按键的 OssSelectionWidget。
调用关系:select_oss_provider 在需要弹出选择界面时会调用它;测试 tests::ctrl_h_l_move_provider_selection 也会调用它来造一个假界面。它内部会把状态交给 get_status_symbol_and_color,换成适合显示的符号和颜色。
调用图:调用 1 个内部函数(get_status_symbol_and_color);被 2 处调用(select_oss_provider, ctrl_h_l_move_provider_selection);外部调用 3 个(from, new, vec!)。
OssSelectionWidget::get_confirmation_prompt_height164–167 ↗
fn get_confirmation_prompt_height(&self, width: u16) -> u16
作用:计算上方说明文字在指定宽度下要占几行。终端宽度不同,文字换行也不同,所以需要动态算高度。
数据流:进去的是当前可用宽度。它询问提示段落在这个宽度下会分成多少行。出来的是一个行数,用来决定界面怎么分区。
调用关系:desired_height 用它告诉外部这个组件大概要多高;render_ref 用它把提示区和按钮区切开,避免文字和按钮挤在一起。
调用图:被 2 处调用(desired_height, render_ref);外部调用 1 个(line_count)。
OssSelectionWidget::handle_key_event174–183 ↗
fn handle_key_event(&mut self, key: KeyEvent) -> Option<String>
作用:接收用户按下的键,并决定这个键会不会改变选择或完成选择。外部只要把按键交给它,就能拿到最终选择结果。
数据流:进去的是一个键盘事件。它只处理真正按下去的事件,然后交给 handle_select_key 判断具体动作。如果用户已经选完,出来的是所选服务的字符串;如果还没选完,就出来空值。
调用关系:select_oss_provider 的事件循环读到键盘事件后会调用它。它自己不判断所有按键细节,而是把细活交给 handle_select_key。
调用图:调用 1 个内部函数(handle_select_key)。
OssSelectionWidget::normalize_keycode188–193 ↗
fn normalize_keycode(code: KeyCode) -> KeyCode
作用:把按键变成方便比较的形式,主要是把字母统一转成小写。这样用户按 L 或 l 都能被当成同一个选择。
数据流:进去的是一个按键代码。若它是字母,就转成小写字母;如果是方向键、回车之类,就原样保留。出来的是标准化后的按键代码。
调用关系:handle_select_key 在检查快捷键时会用它,避免因为大小写不同导致快捷键失效。
调用图:外部调用 1 个(Char)。
OssSelectionWidget::handle_select_key195–235 ↗
fn handle_select_key(&mut self, key_event: KeyEvent)
作用:真正解释每个按键的含义:左右移动、回车确认、Esc 默认选择、Ctrl+C 取消、字母快捷选择。
数据流:进去的是一个键盘事件。它根据按键修改当前选中的按钮,或者调用 send_decision 写入最终结果。出来没有直接返回值,但会改变组件内部的 selected_option、selection 和 done。
调用关系:handle_key_event 会把按键交给它。它在需要结束选择时调用 send_decision;在比较字母快捷键时调用 normalize_keycode。
调用图:调用 1 个内部函数(send_decision);被 1 处调用(handle_key_event);外部调用 1 个(normalize_keycode)。
OssSelectionWidget::send_decision237–240 ↗
fn send_decision(&mut self, selection: String)
作用:把用户的最终决定写进组件,并标记这个选择界面已经可以关闭了。
数据流:进去的是一个选择结果字符串,比如某个服务的编号,或者取消用的特殊字符串。它把这个字符串保存到 selection,并把 done 改成 true。出来没有返回值,但组件状态从“还在选择”变成“已完成”。
调用关系:handle_select_key 在用户确认、取消或按快捷键选中服务时调用它。之后 handle_key_event 就能把这个结果交还给外层流程。
调用图:被 1 处调用(handle_select_key)。
OssSelectionWidget::is_complete244–246 ↗
fn is_complete(&self) -> bool
作用:告诉别人这个选择界面是不是已经完成了。外层界面可以用它判断是否该把这个小组件拿掉。
数据流:它不需要额外输入,只读取组件里的 done 标记。done 是 true 就返回 true,否则返回 false。它不改动任何数据。
调用关系:它是给外部流程查询状态用的小入口;虽然这个文件里的主循环主要通过 handle_key_event 拿结果,但这个方法让组件也能被别的界面队列按“完成状态”管理。
OssSelectionWidget::desired_height248–250 ↗
fn desired_height(&self, width: u16) -> u16
作用:估算整个选择组件在当前宽度下需要多少行,方便外层布局预留空间。
数据流:进去的是可用宽度。它先用 get_confirmation_prompt_height 算提示文字高度,再加上选项按钮需要的高度。出来的是总高度。
调用关系:它依赖 get_confirmation_prompt_height 做文字换行计算。这个方法适合被更大的终端界面在排版时调用。
调用图:调用 1 个内部函数(get_confirmation_prompt_height)。
OssSelectionWidget::render_ref254–300 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把选择界面画到终端屏幕上。它负责把提示文字、按钮、当前选中高亮和说明文字摆到正确位置。
数据流:进去的是一块屏幕区域和一块绘图缓冲区。它先算提示文字高度,再把区域切成提示区、按钮区和说明区,最后把每一行文字和按钮样式写进缓冲区。出来没有普通返回值,但屏幕缓冲区已经变成可显示的界面内容。
调用关系:select_oss_provider 在终端每次重画时会触发它。它会调用 get_confirmation_prompt_height 来保证布局不会错位,并使用终端 UI 库提供的布局和渲染工具完成绘制。
调用图:调用 1 个内部函数(get_confirmation_prompt_height);外部调用 9 个(Length, Min, default, horizontal, vertical, from, new, clone, new)。
get_status_symbol_and_color303–309 ↗
fn get_status_symbol_and_color(status: &ProviderStatus) -> (&'static str, Color)
作用:把服务状态翻译成用户一眼能看懂的符号和颜色。比如运行中显示绿色实心点,没运行显示红色空心点。
数据流:进去的是 ProviderStatus,也就是服务状态。它匹配状态后,出来一个符号和一个颜色。它不改动外部数据。
调用关系:OssSelectionWidget::new 在生成状态列表时调用它,让界面里的服务状态不只是文字,而是带有直观视觉提示。
调用图:被 1 处调用(new)。
select_oss_provider316–370 ↗
async fn select_oss_provider() -> io::Result<OssProviderSelection>
作用:这是整个选择流程的主入口。它决定是自动选择本地服务,还是打开终端选择界面让用户手动选。
数据流:它先调用 check_lmstudio_status 和 check_ollama_status 读取两个本地端口的状态。如果只有一个服务运行,就直接返回对应服务,并标记不是手动选择;否则创建 OssSelectionWidget,切换终端到原始输入模式和备用屏幕,循环绘制界面、读取按键,直到拿到用户选择后返回结果。
调用关系:run_main 会在启动流程里调用它。它把探测服务的活交给 check_lmstudio_status 和 check_ollama_status,把界面创建交给 OssSelectionWidget::new,把按键解释交给 widget 的 handle_key_event。
调用图:调用 3 个内部函数(new, check_lmstudio_status, check_ollama_status);被 1 处调用(run_main);外部调用 7 个(new, disable_raw_mode, enable_raw_mode, read, execute!, stdout, new)。
check_lmstudio_status372–378 ↗
async fn check_lmstudio_status() -> ProviderStatus
作用:检查 LM Studio 的默认本地端口,看它像不像正在运行。
数据流:它不需要外部输入,使用预设的 LM Studio 默认端口。它调用 check_port_status 发起一次本机 HTTP 请求,再把结果翻译成 Running、NotRunning 或 Unknown。出来的是 LM Studio 的状态。
调用关系:select_oss_provider 在决定是否自动选择前会调用它。它把真正访问端口的工作交给 check_port_status。
调用图:调用 1 个内部函数(check_port_status);被 1 处调用(select_oss_provider)。
check_ollama_status380–386 ↗
async fn check_ollama_status() -> ProviderStatus
作用:检查 Ollama 的默认本地端口,看它像不像正在运行。
数据流:它不需要外部输入,使用预设的 Ollama 默认端口。它调用 check_port_status 去访问本机服务,再把成功、失败或异常翻译成对应状态。出来的是 Ollama 的状态。
调用关系:select_oss_provider 在启动选择流程前会调用它。它和 check_lmstudio_status 用的是同一个底层端口检查函数 check_port_status。
调用图:调用 1 个内部函数(check_port_status);被 1 处调用(select_oss_provider)。
check_port_status388–400 ↗
async fn check_port_status(port: u16) -> io::Result<bool>
作用:尝试访问本机某个端口,判断那里有没有一个能正常响应的 HTTP 服务。这里的 HTTP 可以理解成浏览器和本地服务说话的常见方式。
数据流:进去的是端口号。它创建一个带 2 秒超时的网络客户端,访问 http://localhost:端口。如果收到成功状态码,就返回 true;如果连不上,就返回 false;如果客户端创建失败,则返回错误。
调用关系:check_lmstudio_status 和 check_ollama_status 都调用它。它是这两个具体服务检查函数共用的底层探针。
调用图:被 2 处调用(check_lmstudio_status, check_ollama_status);外部调用 3 个(from_secs, builder, format!)。
tests::ctrl_h_l_move_provider_selection407–416 ↗
fn ctrl_h_l_move_provider_selection()
作用:这是一个自动测试,用来确认 Ctrl+L 能向右切换选项,Ctrl+H 能向左切换选项。
数据流:它先创建一个状态未知的选择组件,确认默认选中第一个选项。然后模拟按下 Ctrl+L,检查选中项变成第二个;再模拟 Ctrl+H,检查又回到第一个。出来没有业务结果,测试通过就说明快捷键行为没坏。
调用关系:它直接调用 OssSelectionWidget::new 和 handle_key_event。这个测试保护的是启动选择界面的键盘操作,尤其是某些终端会把方向类操作映射成 Ctrl+H/Ctrl+L 的情况。
调用图:调用 1 个内部函数(new);外部调用 3 个(Char, new, assert_eq!)。
tui/src/startup_hooks_review.rs源码 ↗
可以把这个文件看成启动时的“安全门卫”。程序启动后,它会向后台服务要一份当前目录的钩子清单。钩子就是某些事件发生时自动执行的小脚本或命令;因为它们可能绕过沙箱运行,所以不能随便放行。这个文件先判断是否真的需要审查:如果用户选择了跳过信任检查,或者没有新钩子、改过的钩子,就直接继续。否则它会画出一个全屏选择框,给用户三个选择:查看钩子详情、全部信任后继续、或者不信任也继续。如果用户选择全部信任,它会把需要审查的钩子的 key 和当前哈希值写回配置;如果写失败,就把错误显示在同一个界面里,让用户重新选择。文件里还带了测试,用假钩子检查“是否需要审查”和界面文字是否渲染正确。
load_startup_hooks_review_entry50–67 ↗
async fn load_startup_hooks_review_entry(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
) -> HooksListEntry
作用:启动时向后台服务读取当前目录的钩子列表,准备后面判断是否要弹出审查界面。如果读取失败,它不会让程序启动失败,而是给出一个空列表继续走。
数据流:进去的是后台请求句柄和当前工作目录 → 它调用 fetch_hooks_list 拉取钩子清单,再从结果里挑出当前目录对应的那一项 → 出来的是 HooksListEntry,也就是这个目录下的钩子、警告和错误;如果拉取失败,就记录警告并返回一个空的 HooksListEntry。
调用关系:这是启动审查流程的前置取数步骤。它把数据准备好后,通常会交给 maybe_run_startup_hooks_review 去决定要不要真的展示审查界面。
调用图:调用 2 个内部函数(fetch_hooks_list, hooks_list_entry_for_cwd);外部调用 3 个(clone, new, warn!)。
maybe_run_startup_hooks_review69–81 ↗
async fn maybe_run_startup_hooks_review(
app_server: &mut AppServerSession,
tui: &mut Tui,
config: &Config,
bypass_hook_trust: bool,
entry: HooksListEntry,
) -> Result<StartupHooks
作用:决定启动时要不要进入钩子审查界面。它像一个分叉路口:没风险就放行,有需要确认的钩子才打开交互界面。
数据流:进去的是后台会话、终端界面、配置、是否跳过信任检查的开关,以及钩子列表 → 它先用 review_is_needed 判断是否需要审查 → 如果不需要,返回 Continue;如果需要,就调用 run_startup_hooks_review_app 让用户选择,最后返回用户选择带来的结果。
调用关系:它由 run_ratatui_app 在启动过程中调用,是外层主程序和这个钩子审查小流程之间的入口。它自己不画界面,只负责决定是否把事情交给 run_startup_hooks_review_app。
调用图:调用 2 个内部函数(review_is_needed, run_startup_hooks_review_app);被 1 处调用(run_ratatui_app)。
run_startup_hooks_review_app83–173 ↗
async fn run_startup_hooks_review_app(
app_server: &mut AppServerSession,
tui: &mut Tui,
config: &Config,
entry: HooksListEntry,
) -> Result<StartupHooksReviewOutcome>
作用:真正运行启动时的钩子审查小界面,读取按键,处理用户选择,并在需要时把“信任这些钩子”的决定写入后台。
数据流:进去的是后台会话、TUI 终端界面、配置和钩子列表 → 它从配置生成按键表,创建选择视图并画出来,然后循环读取键盘、重绘和窗口大小变化事件 → 如果用户选查看钩子,返回 OpenHooksBrowser;如果选不信任继续,返回 Continue;如果选全部信任,它把需要审查的钩子整理成 HookTrustUpdate 写给后台,成功就 Continue,失败就把错误放回界面显示。
调用关系:它由 maybe_run_startup_hooks_review 调用,是这个文件的核心流程。它依赖 selection_view 生成界面,draw_view 画界面,selected_choice 翻译用户选择,并在“全部信任”时调用 write_hook_trusts 和后台沟通。
调用图:调用 7 个内部函数(new, request_handle, write_hook_trusts, from_config, draw_view, selected_choice, selection_view);被 1 处调用(maybe_run_startup_hooks_review);外部调用 4 个(event_stream, matches!, pin!, OpenHooksBrowser)。
selected_choice175–185 ↗
fn selected_choice(view: &mut ListSelectionView) -> Option<StartupHooksReviewSelection>
作用:把列表控件里的选中行,翻译成这个文件能理解的业务选择,比如“查看钩子”或“全部信任”。
数据流:进去的是一个可修改的列表选择视图 → 它先看选择是否已经完成;没完成就返回 None → 完成后取出最后选中的行号,把第 0 行变成 ReviewHooks,第 1 行变成 TrustAllAndContinue,第 2 行或没有行号变成 ContinueWithoutTrusting。
调用关系:它在 run_startup_hooks_review_app 的按键循环里使用。列表控件只知道用户点了第几项,这个函数负责把行号变成后续流程能执行的动作。
调用图:调用 2 个内部函数(is_complete, take_last_selected_index);被 1 处调用(run_startup_hooks_review_app)。
selection_view187–199 ↗
fn selection_view(
entry: &HooksListEntry,
trust_all_error: Option<&str>,
trusting_all: bool,
app_event_tx: AppEventSender,
keymap: &RuntimeKeymap,
) -> ListSelectionView
作用:创建启动钩子审查用的选择列表界面。调用者给它钩子信息、错误信息和按键设置,它返回一个可以显示和接收按键的列表控件。
数据流:进去的是钩子列表、可能存在的信任失败错误、是否正在信任全部钩子的状态、事件发送器和按键表 → 它先调用 selection_view_params 组装标题、提示和三个选项 → 再创建 ListSelectionView → 出来的是可渲染、可处理键盘选择的界面对象。
调用关系:run_startup_hooks_review_app 用它来生成初始界面、信任中的界面和出错后的界面。测试 renders_prompt 和 renders_prompt_with_trust_error 也用它来检查界面长什么样。
调用图:调用 2 个内部函数(new, selection_view_params);被 3 处调用(run_startup_hooks_review_app, renders_prompt, renders_prompt_with_trust_error)。
selection_view_params202–235 ↗
fn selection_view_params(
entry: &HooksListEntry,
trust_all_error: Option<&str>,
trusting_all: bool,
keymap: &RuntimeKeymap,
) -> SelectionViewParams
作用:准备选择界面的所有文字和选项,比如标题、数量提示、底部快捷键提示,以及三条可选操作。
数据流:进去的是钩子列表、错误文字、是否正在信任全部钩子、按键表 → 它统计需要审查的钩子数量,拼出标题和说明;如果有错误就显示红色错误,如果正在信任就显示等待文字 → 最后输出 SelectionViewParams,里面包含页眉、页脚提示和三个选择项。
调用关系:它只被 selection_view 调用,属于界面组装的材料准备层。它还会调用 review_needed_count 算数量,并调用 selection_item 创建每个选项。
调用图:调用 3 个内部函数(standard_popup_hint_line_for_keymap, new, review_needed_count);被 1 处调用(selection_view);外部调用 6 个(new, default, from, new, format!, vec!)。
review_needed_count237–243 ↗
fn review_needed_count(entry: &HooksListEntry) -> usize
作用:数一数当前目录里有多少钩子需要用户重新确认信任。
数据流:进去的是 HooksListEntry,也就是某个目录的钩子清单 → 它逐个检查钩子是否满足 hook_needs_review → 出来的是需要审查的数量。
调用关系:review_is_needed 用它判断是否要弹窗;selection_view_params 用它显示“有几个钩子是新的或改过的”。
调用图:被 2 处调用(review_is_needed, selection_view_params)。
review_is_needed245–247 ↗
fn review_is_needed(bypass_hook_trust: bool, entry: &HooksListEntry) -> bool
作用:判断这次启动到底要不要拦一下,让用户审查钩子。
数据流:进去的是是否跳过钩子信任检查的开关,以及钩子清单 → 如果跳过开关打开,直接认为不需要审查;否则调用 review_needed_count,看需要审查的数量是否大于 0 → 出来的是 true 或 false。
调用关系:maybe_run_startup_hooks_review 在进入界面前先调用它。测试也直接检查它,确保跳过开关和未信任钩子的判断都正确。
调用图:调用 1 个内部函数(review_needed_count);被 1 处调用(maybe_run_startup_hooks_review)。
selection_item249–256 ↗
fn selection_item(name: &str, is_disabled: bool) -> SelectionItem
作用:创建一条选择项,比如“Review hooks”或“Trust all and continue”。它把重复的选项配置集中起来,避免每条选项都手写一遍。
数据流:进去的是选项名字和是否禁用 → 它生成 SelectionItem,把名字写进去,设置选中后关闭选择流程,并按传入值决定是否可点 → 出来的是一条列表选项。
调用关系:selection_view_params 用它生成三个按钮式的选项。当正在“全部信任”时,这些选项会被禁用,避免用户在后台写入时乱点。
调用图:外部调用 1 个(default)。
draw_view258–271 ↗
fn draw_view(tui: &mut Tui, view: &ListSelectionView) -> Result<()>
作用:把启动钩子审查界面画到终端上。它负责清屏、计算合适高度,然后把选择框渲染出来。
数据流:进去的是 TUI 终端对象和当前选择视图 → 它调用 tui.draw 开始一帧绘制,先用 Clear 清掉区域,再按终端宽高算出视图区域,最后渲染 StandaloneSelectionView → 成功时没有额外结果,只是终端画面被更新;失败则返回错误。
调用关系:run_startup_hooks_review_app 在初次显示、按键后需要刷新、窗口变化或后台写信任状态前后都会调用它。StandaloneSelectionView::render_ref 是它真正交给 ratatui 绘制时用到的适配层。
调用图:被 1 处调用(run_startup_hooks_review_app);外部调用 1 个(draw)。
StandaloneSelectionView::render_ref278–280 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把 ListSelectionView 接到 ratatui 的绘制接口上,让它能作为一个独立小窗口被渲染。
数据流:进去的是要绘制的区域和屏幕缓冲区 → 它把这些直接交给内部的 ListSelectionView.render → 出来没有普通返回值,但缓冲区里被写入了界面文字和样式。
调用关系:draw_view 创建 StandaloneSelectionView 后,ratatui 会调用这个 render_ref。它本身不决定显示什么,只是把绘制请求转交给真正的列表视图。
调用图:调用 1 个内部函数(render)。
tests::hook304–322 ↗
fn hook(key: &str, trust_status: HookTrustStatus) -> HookMetadata
作用:测试用的小工厂函数,用来快速造一个假的钩子。这样测试不用每次都手写一大堆钩子字段。
数据流:进去的是钩子的 key 和信任状态 → 它填入固定的事件名、命令、来源路径、哈希值等测试数据 → 出来的是一个 HookMetadata 测试对象。
调用关系:tests::entry 调用它来组成一份包含未信任和已修改钩子的假清单,供后面的判断和渲染测试使用。
调用图:外部调用 2 个(test_path_buf, format!)。
tests::entry324–334 ↗
fn entry() -> HooksListEntry
作用:生成一份测试用的当前目录钩子清单,里面包含两个需要审查的钩子。
数据流:进去没有参数 → 它创建 cwd 为 /tmp 的 HooksListEntry,并放入一个未信任钩子和一个已修改钩子 → 出来的是可供测试使用的 HooksListEntry。
调用关系:多个测试会调用它:判断是否需要审查时用它,渲染提示界面时也用它。
调用图:外部调用 3 个(new, test_path_buf, vec!)。
tests::render_lines336–358 ↗
fn render_lines(view: &crate::bottom_pane::ListSelectionView, width: u16) -> String
作用:把一个列表选择界面渲染成普通字符串,方便快照测试比较画面内容。
数据流:进去的是 ListSelectionView 和宽度 → 它根据宽度计算高度,创建一块空的终端缓冲区,把视图画进去,再逐行读出字符并去掉行尾空格 → 出来的是多行字符串。
调用关系:renders_prompt 和 renders_prompt_with_trust_error 用它把界面变成文本,再交给 assert_snapshot! 和保存的快照比较。
调用图:调用 2 个内部函数(desired_height, render);外部调用 2 个(empty, new)。
tests::bypass_hook_trust_suppresses_startup_review361–363 ↗
fn bypass_hook_trust_suppresses_startup_review()
作用:验证如果用户明确选择跳过钩子信任检查,即使有需要审查的钩子,也不会弹出启动审查。
数据流:进去没有参数 → 它创建测试钩子清单,调用 review_is_needed,并把 bypass_hook_trust 设为 true → 期望结果是 false,也就是不需要审查。
调用关系:这是对 review_is_needed 的单元测试,确保启动流程尊重“跳过检查”这个开关。
调用图:外部调用 1 个(assert!)。
tests::untrusted_hooks_need_review_without_bypass366–368 ↗
fn untrusted_hooks_need_review_without_bypass()
作用:验证没有跳过检查时,未信任或已修改的钩子确实会触发启动审查。
数据流:进去没有参数 → 它创建测试钩子清单,调用 review_is_needed,并把 bypass_hook_trust 设为 false → 期望结果是 true,也就是需要审查。
调用关系:这是对 review_is_needed 的另一条核心规则测试,防止安全提示被意外绕过。
调用图:外部调用 1 个(assert!)。
tests::renders_prompt371–386 ↗
fn renders_prompt()
作用:检查正常情况下的启动钩子审查提示界面是否长得符合预期。
数据流:进去没有参数 → 它创建事件通道、默认按键表和测试钩子清单,再调用 selection_view 生成界面 → 然后用 render_lines 渲染成文字,并用快照断言比较结果。
调用关系:它测试 selection_view 到 selection_view_params 这一整段界面组装逻辑,确保标题、数量提示和三个选项不会被无意改坏。
调用图:调用 3 个内部函数(new, defaults, selection_view);外部调用 2 个(assert_snapshot!, entry)。
tests::renders_prompt_with_trust_error389–406 ↗
fn renders_prompt_with_trust_error()
作用:检查“全部信任”写入失败时,错误信息会正确显示在启动审查界面里。
数据流:进去没有参数 → 它创建默认按键表和测试钩子清单,并传入一段假的失败信息给 selection_view → 再把界面渲染成文字,用快照断言确认错误提示出现在预期位置。
调用关系:它覆盖 run_startup_hooks_review_app 出错后会重建的那种界面,确保用户看得到失败原因,而不是只觉得按钮没反应。
调用图:调用 3 个内部函数(new, defaults, selection_view);外部调用 2 个(assert_snapshot!, entry)。
tui/src/app/startup_prompts.rs源码 ↗
程序启动时,很多事情需要先跟用户说清楚,但又不能把主界面流程搅乱。这个文件就像“开门前的提示牌管理员”:发现技能文件 SKILL.md 读不了,就往历史区塞警告;发现某些项目本地配置因为项目还没被信任而停用,也提醒用户;发现系统沙箱工具 bubblewrap 可能有问题,也给出警告。它还处理模型迁移提示:判断当前模型是否应该建议换到新模型,用户接受后就更新配置并保存选择,拒绝后也记住“已经问过”。另外,它会控制新模型可用性的小提示不要无限弹出。里面的 SkillLoadWarningState 像一个记事本,记住当前正在报错的技能,避免同一个错误反复刷屏;但错误消失后再出现,会重新提醒。
SkillLoadWarningState::clear22–24 ↗
fn clear(&mut self)
作用:清空已经记住的技能加载警告。有人会在想要“重新开始计算哪些错误算新错误”时用它。
数据流:进去的是这个状态对象本身,里面保存着一批正在报错的技能路径和错误信息;它把这批记录全部删掉;出来没有返回值,但状态变成空的,之后同样的错误会被当成新错误。
调用关系:它是 SkillLoadWarningState 这本“错误记事本”的重置按钮。测试里会调用它,确认清空后同一个技能错误可以再次被提示。
SkillLoadWarningState::newly_active_errors26–44 ↗
fn newly_active_errors(&mut self, errors: &[SkillErrorInfo]) -> Vec<SkillErrorInfo>
作用:从当前发现的技能错误里挑出“刚刚新出现”的那些,避免同一个还没消失的错误反复提示用户。
数据流:进去的是一组当前扫描到的 SkillErrorInfo,也就是技能文件路径加错误消息;它先拿走旧的活跃错误记录,再逐个生成“路径+消息”的钥匙,对比哪些以前没有出现过;出来是一组新错误,同时把内部状态更新为这次扫描后仍然存在的错误。
调用关系:它通常在重新扫描技能后使用,先过滤出值得提醒的新错误,再交给 emit_skill_load_warnings 去显示。它内部用集合去重,像门卫只放新来的访客进去。
emit_skill_load_warnings47–66 ↗
fn emit_skill_load_warnings(app_event_tx: &AppEventSender, errors: &[SkillErrorInfo])
作用:把技能加载失败的信息变成用户能看到的警告消息,插进 TUI 的历史区域。TUI 是文字界面,类似终端里的应用界面。
数据流:进去的是应用事件发送器和一批技能错误;如果没有错误就什么也不做;如果有错误,它先发送一条总警告说明跳过了多少个技能,再为每个错误发送一条包含文件路径和原因的警告。出来没有直接返回值,但界面事件队列里多了这些警告卡片。
调用关系:它不自己画界面,而是把 AppEvent::InsertHistoryCell 事件交给应用主流程。测试辅助函数 tests::render_skill_load_warning_cells 会调用它,把发出的警告渲染成文字来检查。
调用图:调用 1 个内部函数(send);被 1 处调用(render_skill_load_warning_cells);外部调用 6 个(new, is_empty, len, InsertHistoryCell, new_warning_event, format!)。
emit_project_config_warnings68–105 ↗
fn emit_project_config_warnings(app_event_tx: &AppEventSender, config: &Config)
作用:提醒用户:某些项目里的本地配置、钩子和执行策略因为项目还没被信任,所以暂时不会生效。这样用户不会误以为配置已经被采用。
数据流:进去的是事件发送器和完整配置;它查看配置层列表,找出来源是项目目录、并且带有禁用原因的层;然后拼出一段包含目录和原因的说明;如果确实有被禁用的目录,就发送一条历史区警告。
调用关系:它在启动阶段检查配置加载结果,把“配置为什么没生效”这件事告诉主界面。它只负责发提醒,不负责决定项目是否可信。
调用图:调用 1 个内部函数(send);外部调用 6 个(new, new, InsertHistoryCell, concat!, format!, new_warning_event)。
emit_system_bwrap_warning107–117 ↗
fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config)
作用:检查系统沙箱 bubblewrap 相关风险,并在需要时提醒用户。沙箱可以理解为给程序加一个隔离房间,限制它能碰什么。
数据流:进去的是事件发送器和配置;它根据配置里的权限档案询问沙箱模块是否有警告消息;如果没有就结束,如果有就把这条消息做成历史区警告发出去。
调用关系:它把底层沙箱检查结果转成 TUI 可见的提示。真正判断风险的是 codex_sandboxing::system_bwrap_warning,这里只负责把结果送进界面。
调用图:调用 1 个内部函数(send);外部调用 4 个(new, InsertHistoryCell, system_bwrap_warning, new_warning_event)。
should_show_model_migration_prompt119–157 ↗
fn should_show_model_migration_prompt(
current_model: &str,
target_model: &str,
seen_migrations: &BTreeMap<String, String>,
available_models: &[ModelPreset],
) -> bool
作用:判断这次启动时要不要弹出“建议你从当前模型迁移到目标模型”的提示。它避免没必要、重复或不可用的迁移提示打扰用户。
数据流:进去的是当前模型名、目标模型名、已经看过的迁移记录、可用模型列表;它依次检查目标是否和当前相同、是否已经问过、目标是否能在选择器里显示、当前或目标是否确实属于升级关系;出来是 true 或 false,表示该不该问用户。
调用关系:handle_model_migration_prompt_if_needed 会先调用它做门槛判断。只有它说“应该显示”,后面才会组装提示文案并真正弹窗。
调用图:被 1 处调用(handle_model_migration_prompt_if_needed);外部调用 1 个(iter)。
target_preset_for_upgrade172–179 ↗
fn target_preset_for_upgrade(
available_models: &'a [ModelPreset],
target_model: &str,
) -> Option<&'a ModelPreset>
作用:在可用模型列表里找到目标升级模型的详细资料。它确保目标模型不只是名字存在,而且能展示给用户选择。
数据流:进去的是可用模型列表和目标模型名;它逐个查找模型名匹配、并且允许出现在选择器里的模型;出来是这个模型的配置资料,找不到就返回空。
调用关系:handle_model_migration_prompt_if_needed 用它拿目标模型的显示名、描述和默认推理强度。找不到目标资料时,迁移提示就不会继续。
调用图:被 1 处调用(handle_model_migration_prompt_if_needed);外部调用 1 个(iter)。
apply_accepted_model_migration181–203 ↗
fn apply_accepted_model_migration(
config: &mut Config,
app_event_tx: &AppEventSender,
from_model: String,
target_model: String,
target_default_effort: ReasoningEffortConfig,
)
作用:当用户接受模型迁移时,把新模型真正应用到当前配置和界面状态里,并安排持久化保存。
数据流:进去的是可修改配置、事件发送器、原模型名、目标模型名、目标默认推理强度;它先发送“用户已确认迁移”的保存事件,再更新内存里的 config.model 和 config.model_reasoning_effort,然后发送更新界面模型、更新推理强度、保存模型选择等事件;出来没有返回值,但配置和事件队列都被改了。
调用关系:它只在 handle_model_migration_prompt_if_needed 收到 Accepted,也就是用户点了接受之后调用。它把用户选择转化为实际的配置变化。
调用图:调用 1 个内部函数(send);被 1 处调用(handle_model_migration_prompt_if_needed);外部调用 3 个(clone, UpdateModel, UpdateReasoningEffort)。
select_model_availability_nux213–229 ↗
fn select_model_availability_nux(
available_models: &[ModelPreset],
nux_config: &ModelAvailabilityNuxConfig,
) -> Option<StartupTooltipOverride>
作用:从模型列表里挑一个可以显示的新手提示。NUX 可以理解为“新功能小提示”,这里用于告诉用户某个模型现在可用了。
数据流:进去的是可用模型列表和已经显示过多少次的记录;它寻找带有可用性提示文案的模型,并检查这个模型提示显示次数是否少于上限;出来是要显示的模型名和提示文字,或者没有可显示内容。
调用关系:prepare_startup_tooltip_override 会调用它来决定启动时是否覆盖默认提示文字。它负责挑选,后者负责计数和保存。
调用图:被 1 处调用(prepare_startup_tooltip_override);外部调用 1 个(iter)。
prepare_startup_tooltip_override231–268 ↗
async fn prepare_startup_tooltip_override(
config: &mut Config,
available_models: &[ModelPreset],
is_first_run: bool,
) -> Option<String>
作用:准备启动时的小提示文案,并把“这个提示又显示了一次”的次数保存下来。它防止同一个新模型提示一直骚扰用户。
数据流:进去的是可修改配置、模型列表、是否首次运行;如果是首次运行或用户关闭了提示,就返回空;否则调用 select_model_availability_nux 找提示,增加该模型的显示次数,尝试写回配置文件;成功后更新内存配置并返回提示文案,保存失败时记录错误但仍返回文案。
调用关系:它在启动准备阶段运行,接在模型目录和配置已经可用之后。它把 select_model_availability_nux 的选择结果落到配置里,使用 ConfigEditsBuilder 做持久化。
调用图:调用 1 个内部函数(select_model_availability_nux);外部调用 2 个(for_config, error!)。
handle_model_migration_prompt_if_needed270–355 ↗
async fn handle_model_migration_prompt_if_needed(
tui: &mut tui::Tui,
config: &mut Config,
model: &str,
app_event_tx: &AppEventSender,
available_models: &[ModelPreset],
) -> Option
作用:完整处理“是否建议用户换模型”的启动弹窗流程。它从判断、组文案、显示弹窗,到根据用户选择更新配置或退出程序,都串起来。
数据流:进去的是 TUI、可修改配置、当前模型名、事件发送器、可用模型列表;它先找当前模型是否有升级目标,再检查提示是否隐藏、是否应该显示、目标模型是否可用;然后准备显示名、说明和提示文案,运行迁移弹窗;用户接受就调用 apply_accepted_model_migration,拒绝就记住已问过,选择退出就返回 AppExitInfo;正常不退出时返回空。
调用关系:这是模型迁移提示的总调度员。它调用 migration_prompt_hidden、should_show_model_migration_prompt、target_preset_for_upgrade 做判断,调用 run_model_migration_prompt 询问用户,接受后把工作交给 apply_accepted_model_migration。
调用图:调用 5 个内部函数(apply_accepted_model_migration, migration_prompt_hidden, should_show_model_migration_prompt, target_preset_for_upgrade, send);外部调用 2 个(iter, default)。
normalize_harness_overrides_for_cwd356–371 ↗
fn normalize_harness_overrides_for_cwd(
mut overrides: ConfigOverrides,
base_cwd: &AbsolutePathBuf,
) -> Result<ConfigOverrides>
作用:把测试或运行外壳里额外允许写入的目录,按照当前工作目录转成正确路径。这样相对路径不会因为启动位置不同而指错地方。
数据流:进去的是配置覆盖项和一个基准当前目录;如果没有额外可写目录,就原样返回;如果有,它把每个目录都拼到基准目录下面,形成标准化后的路径列表;出来是更新后的 ConfigOverrides。
调用关系:这是启动前的小修正工具。测试 tests::normalize_harness_overrides_resolves_relative_add_dirs 会验证相对目录会被正确拼到 base_cwd 后面。
调用图:调用 1 个内部函数(join);被 1 处调用(normalize_harness_overrides_resolves_relative_add_dirs);外部调用 1 个(with_capacity)。
tests::normalize_harness_overrides_resolves_relative_add_dirs384–400 ↗
fn normalize_harness_overrides_resolves_relative_add_dirs() -> Result<()>
作用:测试 normalize_harness_overrides_for_cwd 是否真的把相对可写目录变成基于当前目录的路径。
数据流:进去没有业务输入;它创建临时目录,构造一个包含 rel 相对路径的覆盖配置,调用被测函数;最后比较输出路径是否等于 base/rel。
调用关系:它直接覆盖 normalize_harness_overrides_for_cwd 的核心行为,防止以后改代码时把相对路径处理弄坏。
调用图:调用 1 个内部函数(normalize_harness_overrides_for_cwd);外部调用 5 个(default, assert_eq!, create_dir_all, tempdir, vec!)。
tests::skill_error402–407 ↗
fn skill_error(path: &str, message: &str) -> SkillErrorInfo
作用:给测试快速造一个技能错误对象,避免每个测试都重复写路径和消息的包装代码。
数据流:进去的是路径字符串和错误消息字符串;它把路径转成 PathBuf,把消息转成 String;出来是 SkillErrorInfo。
调用关系:多个 SkillLoadWarningState 相关测试都会用它造样本错误。它只是测试里的小工厂函数。
调用图:外部调用 1 个(from)。
tests::render_line_text409–414 ↗
fn render_line_text(line: &Line<'static>) -> String
作用:把界面渲染出来的一行文字拆掉格式,只留下纯文本,方便测试比较。
数据流:进去的是 ratatui 的 Line,也就是一行可能带样式的终端文字;它把这一行里的多个 span 文本拼起来;出来是一整行普通字符串。
调用关系:tests::render_skill_load_warning_cells 用它把警告卡片的显示结果转成可断言的纯文本。
tests::render_skill_load_warning_cells416–431 ↗
fn render_skill_load_warning_cells(errors: &[SkillErrorInfo]) -> String
作用:在测试里模拟发送技能加载警告,并把最终会显示的文字收集出来。
数据流:进去的是一批技能错误;它创建一个事件通道,调用 emit_skill_load_warnings 发送警告事件,然后从通道里读出历史卡片,渲染成宽度 120 的文本行;出来是用换行拼好的警告文本。
调用关系:它是测试 emit_skill_load_warnings 显示效果的桥梁。tests::repeated_active_skill_load_warning_renders_once 会用它确认重复错误不会重复显示。
调用图:调用 2 个内部函数(emit_skill_load_warnings, new);外部调用 2 个(new, unbounded_channel)。
tests::skill_load_warning_state_suppresses_repeated_active_errors434–446 ↗
fn skill_load_warning_state_suppresses_repeated_active_errors()
作用:测试同一个技能错误连续存在时,只会第一次被认为是新错误。
数据流:进去没有外部输入;它创建一个默认状态和一个错误,第一次调用 newly_active_errors 期望得到该错误,第二次用同样错误调用期望得到空列表;出来是测试通过或失败。
调用关系:它验证 SkillLoadWarningState::newly_active_errors 的“别重复打扰用户”规则。
调用图:外部调用 3 个(assert_eq!, default, skill_error)。
tests::skill_load_warning_state_reemits_after_error_clears449–462 ↗
fn skill_load_warning_state_reemits_after_error_clears()
作用:测试一个错误消失后又出现时,会再次提醒用户。
数据流:进去没有外部输入;它先让错误出现一次,再传入空列表表示错误已经清除,最后再次传入同一错误;期望最后一次又返回这个错误;出来是测试通过或失败。
调用关系:它验证 SkillLoadWarningState 不只是永久拉黑某个错误,而是只抑制“持续存在”的重复错误。
调用图:外部调用 3 个(assert_eq!, default, skill_error)。
tests::skill_load_warning_state_displays_new_message_for_active_path465–478 ↗
fn skill_load_warning_state_displays_new_message_for_active_path()
作用:测试同一个技能文件如果错误消息变了,也应该当成新错误显示。
数据流:进去没有外部输入;它创建同一路径但不同消息的两个错误,先输入第一个,再输入第二个;期望两次都能返回对应错误;出来是测试通过或失败。
调用关系:它验证 SkillLoadWarningState 的判断钥匙包含路径和消息,而不是只看路径。这样用户能看到错误原因的变化。
调用图:外部调用 3 个(assert_eq!, default, skill_error)。
tests::skill_load_warning_state_clear_allows_active_error_again481–500 ↗
fn skill_load_warning_state_clear_allows_active_error_again()
作用:测试手动清空状态后,同一个还在的错误也会重新被当成新错误。
数据流:进去没有外部输入;它先确认同一错误第二次会被抑制,然后调用 clear,再输入同一错误;期望这次重新返回错误;出来是测试通过或失败。
调用关系:它覆盖 SkillLoadWarningState::clear 和 newly_active_errors 的配合,确保重置按钮真的有效。
调用图:外部调用 3 个(assert_eq!, default, skill_error)。
tests::repeated_active_skill_load_warning_renders_once503–522 ↗
fn repeated_active_skill_load_warning_renders_once()
作用:测试从状态过滤到界面渲染的整体效果:重复活跃的技能错误只显示一次警告。
数据流:进去没有外部输入;它创建状态和错误,连续两次获取新错误,再分别渲染警告文本,过滤掉空输出后拼起来;最后用快照断言结果只包含一次总警告和一次具体错误。
调用关系:它把 SkillLoadWarningState::newly_active_errors、render_skill_load_warning_cells 和 emit_skill_load_warnings 串起来测,确认用户最终看到的界面不会重复刷同一条错误。
调用图:外部调用 5 个(assert_snapshot!, from_ref, default, render_skill_load_warning_cells, skill_error)。
tui/src/tooltips.rs源码 ↗
Codex 启动时,如果总是空着,会错过告诉用户新功能、活动或使用技巧的机会;如果乱推,又可能把不适合当前用户的内容显示出来。这个文件就像一个“开场提示抽签箱”。它先准备本地提示列表,过滤掉不适合当前操作系统的内容,再加入实验功能提示。真正取提示时,它会优先看有没有远程公告:远程公告是从 GitHub 下载的一份 TOML 文本,TOML 可以理解成一种人类容易读的配置表。公告会按日期、版本、套餐、操作系统筛选,最后一个匹配的生效。如果没有公告,再按用户套餐决定是显示付费用户推广、免费/Go 套餐提示,还是普通提示。文件末尾的测试确保随机选择可复现、付费提示分配正确、公告配置解析不会把过期、写错或不匹配的公告误显示出来。
experimental_tooltips44–49 ↗
fn experimental_tooltips() -> Vec<&'static str>
作用:收集实验功能自带的提示语。这样新功能在实验阶段也能通过启动提示告诉用户,而不用手写进固定提示文件。
数据流:进去的是全局功能清单 FEATURES → 它逐个查看每个功能有没有“实验公告” → 出来的是一组可以加入随机提示池的文字。
调用关系:它在提示池初始化时被 ALL_TOOLTIPS 使用,把实验功能提示和普通提示合在一起。之后 get_tooltip 间接从这个合并后的池子里抽提示。
get_tooltip52–88 ↗
fn get_tooltip(plan: Option<PlanType>, fast_mode_enabled: bool) -> Option<String>
作用:这是外部最常用的入口:给定用户套餐和是否已开启 fast 模式,返回一条适合启动时显示的提示。它负责把远程公告、套餐推广和普通随机提示排好优先级。
数据流:进去的是用户套餐 plan 和 fast_mode_enabled → 它先尝试拿远程公告;如果没有,再用随机数决定大概率走套餐相关提示,小概率走普通提示池 → 出来是 Some(提示文字) 或没有可用提示时的 None。
调用关系:启动界面需要提示时会调用它。它先把活交给 announcement::fetch_announcement_tip;如果没拿到公告,再按情况调用 pick_paid_tooltip 或 pick_tooltip。
调用图:调用 2 个内部函数(pick_paid_tooltip, pick_tooltip);外部调用 3 个(fetch_announcement_tip, matches!, rng)。
paid_app_tooltip90–96 ↗
fn paid_app_tooltip() -> Option<&'static str>
作用:判断当前系统能不能显示 Codex App 的推广提示。因为 App 推广只适合 macOS 和 Windows,Linux 上不会返回这条。
数据流:进去的是编译时确定的当前操作系统信息 → 它判断是不是 macOS 或 Windows → 出来是 App 推广文案,或者 None。
调用关系:pick_paid_tooltip 会用它给付费用户挑 App 推广。相关测试也会调用它,确认付费提示池里包含的内容符合预期。
调用图:被 3 处调用(pick_paid_tooltip, paid_tooltip_pool_rotates_between_promos, paid_tooltip_pool_skips_fast_when_fast_mode_is_enabled)。
pick_paid_tooltip102–111 ↗
fn pick_paid_tooltip(
rng: &mut R,
fast_mode_enabled: bool,
) -> Option<&'static str>
作用:专门给付费用户挑启动推广提示。它在 Codex App 和 /fast 模式推广之间做选择,但如果用户已经开了 fast,就不再提醒 fast。
数据流:进去的是随机数生成器和 fast_mode_enabled → 如果已开 fast,或随机结果偏向 App,就尝试返回 App 提示;否则返回 fast 提示 → 出来是一条付费用户提示,或在当前系统没有 App 提示时可能为 None。
调用关系:get_tooltip 在识别到付费或团队/商业类套餐时调用它。它内部会调用 paid_app_tooltip,测试会反复用不同随机种子确认它确实能轮换并跳过不该出现的 fast 提示。
调用图:调用 1 个内部函数(paid_app_tooltip);被 3 处调用(get_tooltip, paid_tooltip_pool_rotates_between_promos, paid_tooltip_pool_skips_fast_when_fast_mode_is_enabled);外部调用 1 个(random_bool)。
pick_tooltip113–121 ↗
fn pick_tooltip(rng: &mut R) -> Option<&'static str>
作用:从普通提示池里随机抽一条。它是兜底方案,用来在没有更高优先级公告或推广时给用户一点使用建议。
数据流:进去的是随机数生成器 → 它先看总提示池是否为空;不为空就随机挑一个下标 → 出来是一条提示文字,或者提示池为空时返回 None。
调用关系:get_tooltip 在最后兜底时调用它。测试用固定随机种子调用它,确认同一个种子能抽到同样结果,避免随机逻辑变得不可预测。
调用图:被 2 处调用(get_tooltip, random_tooltip_is_reproducible_with_seed);外部调用 1 个(random_range)。
announcement::prewarm139–141 ↗
fn prewarm()
作用:提前在后台准备远程公告缓存。这样真正需要显示提示时,不必卡住启动界面等网络请求。
数据流:进去没有参数 → 它启动一个后台线程,让公告缓存开始初始化 → 出来没有直接结果,只是让全局缓存有机会提前填好。
调用关系:它通常会在启动早期被调用。它把慢活交给后台线程,后面 announcement::fetch_announcement_tip 只读取已经准备好的结果,不会主动等待很久。
调用图:外部调用 1 个(spawn)。
announcement::fetch_announcement_tip144–150 ↗
fn fetch_announcement_tip(plan: Option<PlanType>) -> Option<String>
作用:读取已经预热好的远程公告,并根据当前用户套餐筛出真正该显示的那条。它不会为了公告而阻塞等待下载完成。
数据流:进去的是用户套餐 plan → 它查看全局缓存里是否已有下载好的公告文本;有的话解析并筛选,没有或不匹配就返回 None → 出来是一条公告文字或 None。
调用关系:get_tooltip 会最先调用它,因为远程公告优先级最高。它依赖预热阶段放进 ANNOUNCEMENT_TIP 的原始文本,并把筛选工作交给 announcement::parse_announcement_tip_toml。
announcement::TargetOs::current190–199 ↗
fn current() -> Self
作用:把当前运行的操作系统归成公告配置能理解的几类:Linux、macOS 或 Windows。这样公告可以只投给某些系统用户。
数据流:进去没有运行时参数 → 它根据编译目标判断系统 → 出来是一个 TargetOs 枚举值,也就是“当前系统标签”。
调用关系:它用于生成 CURRENT_OS 常量,announcement::parse_announcement_tip_toml 会用这个常量判断公告的 target_oses 是否匹配当前机器。
调用图:外部调用 1 个(cfg!)。
announcement::init_announcement_tip_in_thread202–207 ↗
fn init_announcement_tip_in_thread() -> Option<String>
作用:在单独线程里初始化公告内容,并把线程执行结果安全地取回来。它的作用是隔离网络下载这类可能慢或失败的工作。
数据流:进去没有参数 → 它再启动一个线程执行真正的阻塞下载,然后等待线程结束 → 出来是下载到的公告文本,失败时是 None。
调用关系:announcement::prewarm 启动的后台任务会通过缓存初始化调用它。它再把真正联网的部分交给 announcement::blocking_init_announcement_tip。
调用图:外部调用 1 个(spawn)。
announcement::blocking_init_announcement_tip209–221 ↗
fn blocking_init_announcement_tip() -> Option<String>
作用:真正去网络上下载公告配置文本。它设置了短超时,并关闭系统代理检测,避免因为系统网络配置问题拖慢或弄崩启动。
数据流:进去没有参数 → 它创建一个不使用代理的 HTTP 客户端,访问固定的 GitHub 原始文件地址,最多等 2 秒 → 出来是下载到的文本;建客户端、请求、状态码或读文本失败都会返回 None。
调用关系:announcement::init_announcement_tip_in_thread 把联网工作交给它。它只负责拿原始文本,不判断这条公告该不该显示,后续筛选由 announcement::parse_announcement_tip_toml 完成。
调用图:外部调用 2 个(from_millis, builder)。
announcement::parse_announcement_tip_toml223–256 ↗
fn parse_announcement_tip_toml(
text: &str,
plan: Option<PlanType>,
) -> Option<String>
作用:把远程公告配置文本解析成一条当前用户能看的公告。它会过滤掉日期不对、版本不对、App 不对、套餐不对、系统不对的公告。
数据流:进去的是 TOML 文本和用户套餐 → 它先把文本解析成公告列表,再逐条转成干净的 AnnouncementTip,按当前日期、CLI 版本、目标 app、套餐和系统检查 → 出来是最后一条匹配公告的文字,或者 None。
调用关系:announcement::fetch_announcement_tip 会调用它来处理缓存里的原始文本。它把单条公告的清洗交给 announcement::AnnouncementTip::from_raw,再调用 version_matches 和 date_matches 做筛选。
announcement::AnnouncementTip::from_raw259–301 ↗
fn from_raw(raw: AnnouncementTipRaw) -> Option<Self>
作用:把刚从 TOML 读出来的“原始公告”变成程序可以安全使用的公告对象。它会顺手拒绝空内容、坏日期、坏正则表达式和写错的套餐/系统名字。
数据流:进去的是 AnnouncementTipRaw,也就是还没校验的公告字段 → 它清理内容、解析日期、编译版本匹配用的正则表达式,并检查目标套餐和系统是否合法 → 出来是干净的 AnnouncementTip,或者发现问题时返回 None。
调用关系:announcement::parse_announcement_tip_toml 在扫描公告列表时会调用它。它相当于门卫,先把明显坏掉的公告挡在筛选流程之外。
调用图:外部调用 2 个(parse_from_str, new)。
announcement::AnnouncementTip::version_matches303–307 ↗
fn version_matches(&self, version: &str) -> bool
作用:判断一条公告是否适合当前 CLI 版本。正则表达式可以理解成一种“文字匹配规则”,用来描述哪些版本号算匹配。
数据流:进去的是公告自身和一个版本号字符串 → 如果公告没有写版本规则,就认为所有版本都匹配;如果写了,就用正则表达式检查版本号 → 出来是 true 或 false。
调用关系:announcement::parse_announcement_tip_toml 在决定公告能否显示时调用它。它只管版本这一关,日期等其它条件由别的函数处理。
announcement::AnnouncementTip::date_matches309–321 ↗
fn date_matches(&self, today: NaiveDate) -> bool
作用:判断一条公告今天是否还在有效期内。它支持开始日期和结束日期,开始日期包含当天,结束日期不包含当天。
数据流:进去的是公告自身和今天的日期 → 它检查今天是否早于 from_date,或是否已经到达/超过 to_date → 出来是 true 或 false。
调用关系:announcement::parse_announcement_tip_toml 在筛公告时调用它。它和 version_matches 一起组成公告显示前的主要检查。
tests::random_tooltip_returns_some_tip_when_available333–336 ↗
fn random_tooltip_returns_some_tip_when_available()
作用:测试普通随机提示池在有内容时能返回提示。它防止提示文件或过滤逻辑被改坏后,启动提示永远为空。
数据流:进去的是固定种子的随机数生成器 → 它调用 pick_tooltip 抽一次 → 用断言确认结果不是 None。
调用关系:这是测试阶段运行的函数。它直接验证 pick_tooltip 的基本可用性。
调用图:外部调用 2 个(seed_from_u64, assert!)。
tests::random_tooltip_is_reproducible_with_seed339–347 ↗
fn random_tooltip_is_reproducible_with_seed()
作用:测试同一个随机种子会抽到同一条普通提示。这样测试不会因为随机性每次结果不同而变得不稳定。
数据流:进去的是相同的种子值 7 → 它创建两次同样的随机数生成器并分别调用 pick_tooltip → 用断言确认两次结果相等。
调用关系:它在测试中调用 pick_tooltip,证明随机选择虽然随机,但在固定种子下可复现。
调用图:调用 1 个内部函数(pick_tooltip);外部调用 2 个(seed_from_u64, assert_eq!)。
tests::paid_tooltip_pool_rotates_between_promos350–361 ↗
fn paid_tooltip_pool_rotates_between_promos()
作用:测试付费用户的提示池会在 App 推广和 fast 推广之间轮换。它防止某个推广永远抽不到。
数据流:进去的是一批不同随机种子 → 它多次调用 pick_paid_tooltip,并把见过的结果放进集合 → 最后确认集合正好等于预期的两类提示。
调用关系:它调用 pick_paid_tooltip 和 paid_app_tooltip,专门检查付费提示选择逻辑没有偏离设计。
调用图:调用 2 个内部函数(paid_app_tooltip, pick_paid_tooltip);外部调用 4 个(seed_from_u64, assert_eq!, from, new)。
tests::paid_tooltip_pool_skips_fast_when_fast_mode_is_enabled364–374 ↗
fn paid_tooltip_pool_skips_fast_when_fast_mode_is_enabled()
作用:测试用户已经开启 fast 模式时,不再显示 fast 模式推广。否则用户会看到已经做过的事情还被反复推荐。
数据流:进去的是多组随机种子,并把 fast_mode_enabled 设为 true → 它多次调用 pick_paid_tooltip 收集结果 → 确认只出现 App 提示,并且不包含 FAST_TOOLTIP。
调用关系:它调用 pick_paid_tooltip 和 paid_app_tooltip,覆盖 get_tooltip 会依赖的一条重要分支。
调用图:调用 2 个内部函数(paid_app_tooltip, pick_paid_tooltip);外部调用 5 个(seed_from_u64, assert!, assert_eq!, from, new)。
tests::announcement_tip_toml_picks_last_matching377–417 ↗
fn announcement_tip_toml_picks_last_matching()
作用:测试公告配置里如果有多条都匹配,最后一条匹配的公告会被显示。这样运营或维护者可以用后面的配置覆盖前面的配置。
数据流:进去的是两段手写 TOML 公告文本 → 它解析这些文本 → 用断言确认返回的是“latest match”而不是更早的匹配项或过期项。
调用关系:它验证 announcement::parse_announcement_tip_toml 的选择规则,尤其是“最后匹配者胜出”这件事。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_picks_no_match420–437 ↗
fn announcement_tip_toml_picks_no_match()
作用:测试没有任何公告符合当前条件时会返回 None。它防止过期公告、版本不匹配公告或别的 App 的公告误显示出来。
数据流:进去的是一段包含过期、版本不符、target_app 不符公告的 TOML → 它执行解析筛选 → 断言结果是 None。
调用关系:它围绕 announcement::parse_announcement_tip_toml 做反向验证,确认筛选条件真的会挡住不该显示的内容。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_bad_deserialization440–448 ↗
fn announcement_tip_toml_bad_deserialization()
作用:测试公告字段类型写错时不会崩溃,也不会显示错误公告。比如 content 应该是文字,却写成数字。
数据流:进去的是一段格式不合要求的 TOML → 解析阶段失败 → 断言最终返回 None。
调用关系:它验证 announcement::parse_announcement_tip_toml 面对坏配置时的安全行为。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_parse_comments451–476 ↗
fn announcement_tip_toml_parse_comments()
作用:测试公告配置里可以带注释,并且注释不会影响解析。这样远程配置文件能写说明,方便人维护。
数据流:进去的是带很多注释和两条公告的 TOML → 它解析并按版本等条件筛选 → 断言返回符合条件的测试公告。
调用关系:它验证 announcement::parse_announcement_tip_toml 能处理真实配置文件常见的注释写法。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_matches_target_plan_type479–509 ↗
fn announcement_tip_toml_matches_target_plan_type()
作用:测试公告能按用户套餐精准匹配。不同套餐的人可能看到不同公告,没指定套餐的公告则作为通用公告。
数据流:进去的是包含通用、pro/enterprise、free 三类公告的 TOML,以及不同 plan 参数 → 它分别解析筛选 → 断言 Pro、Free、Plus、无套餐时返回各自正确的文字。
调用关系:它验证 announcement::parse_announcement_tip_toml 对 target_plan_types 的处理,保证 get_tooltip 传入套餐后能得到合适公告。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_rejects_unknown_target_plan_type512–526 ↗
fn announcement_tip_toml_rejects_unknown_target_plan_type()
作用:测试配置里写错套餐名时,这条公告会被丢弃。这样一个拼写错误不会让错误公告误伤用户。
数据流:进去的是一段含通用公告和拼错套餐公告的 TOML → 解析时拼错项会变成 Unknown 并被拒绝 → 断言最终只返回通用公告。
调用关系:它覆盖 announcement::AnnouncementTip::from_raw 里拒绝 Unknown 套餐的保护逻辑,并通过 announcement::parse_announcement_tip_toml 观察结果。
调用图:外部调用 1 个(assert_eq!)。
tests::announcement_tip_toml_matches_target_os529–555 ↗
fn announcement_tip_toml_matches_target_os()
作用:测试公告能按当前操作系统匹配。macOS、Windows、Linux 用户应该只看到发给自己系统的公告。
数据流:进去的是分别面向 linux、macos、windows 的公告 TOML → 测试先根据当前编译系统算出期望值,再解析筛选 → 断言返回当前系统对应的公告。
调用关系:它验证 announcement::TargetOs::current 产生的系统标签能被 announcement::parse_announcement_tip_toml 正确使用。
调用图:外部调用 2 个(assert_eq!, cfg!)。
tests::announcement_tip_toml_rejects_unknown_target_os558–572 ↗
fn announcement_tip_toml_rejects_unknown_target_os()
作用:测试配置里写错操作系统名时,这条公告会被丢弃。这样错误的系统标签不会造成奇怪的匹配结果。
数据流:进去的是一段含通用公告和拼错系统公告的 TOML → 拼错系统会被识别为 Unknown 并拒绝 → 断言最终返回通用公告。
调用关系:它覆盖 announcement::AnnouncementTip::from_raw 里拒绝 Unknown 操作系统的保护逻辑,并确认解析流程会跳过坏公告。
调用图:外部调用 1 个(assert_eq!)。
tui/src/update_prompt.rs源码 ↗
这个文件只在正式发布版里启用,调试版不会弹更新提示。它先问更新模块:现在是不是有比当前程序更新的版本;再问更新动作模块:这台机器上应该用什么命令来更新。如果两件事都成立,就在终端界面里画出一个小窗口,显示当前版本、新版本、发布说明链接,以及三个选项。用户可以用上下键或 j/k 移动高亮,用回车确认,也可以按 Ctrl+C、Ctrl+D 或 Esc 当作“先不更新”。确认后,它把结果交回给主程序:如果选“现在更新”,主程序会去跑更新命令;如果选“不再提醒这个版本”,它会把这个选择保存下来。可以把它想成软件启动时的门口告示牌:只在需要时出现,问清楚用户怎么处理,然后让程序继续往下走。
run_update_prompt_if_needed37–86 ↗
async fn run_update_prompt_if_needed(
tui: &mut Tui,
config: &Config,
) -> Result<UpdatePromptOutcome>
作用:这是整个更新提示的入口。主界面启动时调用它,用来决定要不要弹出更新窗口,并根据用户选择返回“继续运行”或“执行更新”。
数据流:进去的是终端界面对象和配置。它先从配置和更新记录里查有没有可提示的新版本,再查可用的更新命令;如果没有,就直接返回继续。若需要提示,它创建提示屏幕、画到终端上、不断读取按键和重绘事件,直到用户做出选择。最后输出一个结果:继续运行、运行更新,或者保存“这个版本别再提醒”后继续运行。
调用关系:它由 run_ratatui_app 在程序启动流程中调用。它把检查新版本的活交给 get_upgrade_version_for_popup,把判断更新命令的活交给 get_update_action,把界面状态交给 UpdatePromptScreen::new 和后续的按键处理;用户选完后,它可能调用 dismiss_version 保存跳过记录。
调用图:调用 3 个内部函数(get_update_action, new, get_upgrade_version_for_popup);被 1 处调用(run_ratatui_app);外部调用 7 个(draw, event_stream, frame_requester, pin!, error!, RunUpdate, dismiss_version)。
UpdatePromptScreen::new105–118 ↗
fn new(
request_frame: FrameRequester,
latest_version: String,
update_action: UpdateAction,
) -> Self
作用:创建一个更新提示窗口的内部状态。它记住新版本号、当前版本号、更新方式,以及默认高亮的选项。
数据流:进去的是请求重画用的工具、新版本号和更新动作。它读取编译时写进程序里的当前版本号,把默认高亮设为“现在更新”,并且把“用户最终选择”先设为空。出来的是一个可以被绘制、可以响应按键的提示屏幕对象。
调用关系:run_update_prompt_if_needed 在真正弹窗前会调用它。测试里的 tests::new_prompt 也用它造一个假的提示窗口,方便检查画面和按键行为。
调用图:被 2 处调用(run_update_prompt_if_needed, new_prompt);外部调用 1 个(env!)。
UpdatePromptScreen::handle_key120–140 ↗
fn handle_key(&mut self, key_event: KeyEvent)
作用:处理用户按下的键。它把键盘动作翻译成“移动选项”或“确认某个选择”。
数据流:进去的是一次按键事件。它先忽略按键释放事件;如果是 Ctrl+C 或 Ctrl+D,就当作跳过更新;如果是上下键或 j/k,就移动高亮;如果是数字键、回车或 Esc,就记录用户选择。结果是屏幕内部的高亮位置或最终选择被改变,并可能请求界面重画。
调用关系:run_update_prompt_if_needed 在事件循环里收到 TuiEvent::Key 后调用它。它自己不直接画界面,而是把移动交给 set_highlight,把确认交给 select,并用 UpdateSelection::next 和 UpdateSelection::prev 算出下一个选项。
调用图:调用 4 个内部函数(select, set_highlight, next, prev);外部调用 1 个(matches!)。
UpdatePromptScreen::set_highlight142–147 ↗
fn set_highlight(&mut self, highlight: UpdateSelection)
作用:移动当前高亮的选项。只有高亮真的变了,它才通知终端重新画一帧。
数据流:进去的是新的候选高亮项。它和当前高亮项比较;如果不同,就更新内部字段,并通过 request_frame 请求重画。出来没有单独返回值,但屏幕状态会改变。
调用关系:它只由 UpdatePromptScreen::handle_key 调用,用在用户按上下键或 j/k 浏览选项时。它把“状态变了需要刷新画面”这件事交给 schedule_frame。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_key)。
UpdatePromptScreen::select149–153 ↗
fn select(&mut self, selection: UpdateSelection)
作用:记录用户最终选了哪一项。选完后,这个提示窗口就可以结束了。
数据流:进去的是用户选择的选项。它把高亮项也改成这个选项,再把 selection 设为有值,表示已经选择完成,并请求重画一次。之后 is_done 会返回 true。
调用关系:它由 UpdatePromptScreen::handle_key 在用户按回车、数字键、Esc、Ctrl+C 或 Ctrl+D 时调用。run_update_prompt_if_needed 之后会通过 selection 读取这个结果,决定下一步怎么做。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_key)。
UpdatePromptScreen::is_done155–157 ↗
fn is_done(&self) -> bool
作用:告诉外层流程:用户是不是已经做出选择了。它是事件循环能不能结束的判断条件。
数据流:它读取屏幕内部的 selection 字段。如果里面已经有选择,就返回 true;如果还是空,就返回 false。它不改动任何状态。
调用关系:run_update_prompt_if_needed 在等待用户操作的循环里反复检查它。测试也用它确认按键之后提示窗口确实进入了完成状态。
UpdatePromptScreen::selection159–161 ↗
fn selection(&self) -> Option<UpdateSelection>
作用:取出用户最后选的更新处理方式。外层流程靠它知道该更新、跳过,还是不再提醒当前版本。
数据流:它读取屏幕内部保存的 selection。进去不需要额外参数,出来是一个可能为空的选择值;为空表示还没选,非空表示用户已经决定了。
调用关系:run_update_prompt_if_needed 在提示结束后调用它,并把选择翻译成 UpdatePromptOutcome。多个测试也用它检查按键结果是否符合预期。
UpdatePromptScreen::latest_version163–165 ↗
fn latest_version(&self) -> &str
作用:返回提示窗口里保存的新版本号。主要用于用户选择“不再提醒这个版本”时,告诉更新模块要屏蔽哪个版本。
数据流:它读取 latest_version 字符串,并以只读文本的形式交出去。它不会复制业务状态,也不会修改屏幕。
调用关系:run_update_prompt_if_needed 在用户选择 DontRemind 后调用它,然后把这个版本号交给 updates::dismiss_version 保存。
UpdateSelection::next169–175 ↗
fn next(self) -> Self
作用:算出当前选项往下走一格会到哪一项。走到最后一项后会绕回第一项。
数据流:进去的是当前选项。它按照“现在更新 → 跳过 → 跳过直到下个版本 → 现在更新”的顺序返回下一个选项。它不改动外部状态。
调用关系:UpdatePromptScreen::handle_key 在用户按下方向键或 j/k 中的“向下”时调用它,然后把结果交给 set_highlight 更新高亮。
调用图:被 1 处调用(handle_key)。
UpdateSelection::prev177–183 ↗
fn prev(self) -> Self
作用:算出当前选项往上走一格会到哪一项。走到第一项前面会绕到最后一项。
数据流:进去的是当前选项。它按照反方向返回上一个选项,例如从“现在更新”往上会到“跳过直到下个版本”。它不改动外部状态。
调用关系:UpdatePromptScreen::handle_key 在用户按上方向键或 k 时调用它,然后把结果交给 set_highlight。
调用图:被 1 处调用(handle_key)。
UpdatePromptScreen::render_ref187–240 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把更新提示窗口画到终端屏幕上。它负责用户实际看到的文字、链接、选项和高亮效果。
数据流:进去的是可绘制区域和屏幕缓冲区。它先清空这块区域,再组装一列文字:标题、新旧版本、发布说明链接、三个可选按钮、回车提示。它会根据当前 highlighted 字段决定哪一行显示为选中。最后还把发布说明链接标记成终端可识别的超链接。出来没有普通返回值,但缓冲区里的画面内容被改好了。
调用关系:run_update_prompt_if_needed 在首次显示和收到重绘事件时,通过终端绘制流程间接调用它。它会向 update_action 要更新命令文本,向 selection_option_row 要每个选项行的样式,并调用 mark_underlined_hyperlink 让链接更像真正可点的链接。
调用图:调用 5 个内部函数(tlbr, new, selection_option_row, mark_underlined_hyperlink, command_str);外部调用 3 个(from, format!, vec!)。
tests::new_prompt253–259 ↗
fn new_prompt() -> UpdatePromptScreen
作用:给测试用的快捷造屏幕函数。它创建一个固定的新版本号和固定更新方式的提示窗口,避免每个测试重复写准备代码。
数据流:它不接收参数。它用测试专用的 FrameRequester、版本号 9.9.9 和 npm 全局更新动作创建 UpdatePromptScreen。出来的是一个可供测试操作的提示窗口。
调用关系:下面所有更新提示相关测试都会调用它。它内部调用 UpdatePromptScreen::new,把正式构造逻辑也一起覆盖到测试里。
调用图:调用 2 个内部函数(test_dummy, new)。
tests::update_prompt_snapshot262–269 ↗
fn update_prompt_snapshot()
作用:检查更新提示窗口画出来的样子有没有意外变化。快照测试就像给界面拍一张标准照片,以后每次测试都拿来对比。
数据流:它先用 tests::new_prompt 创建屏幕,再用一个假的 80×12 终端把界面画出来,最后把画面和保存的快照比较。结果是测试通过或失败,不会影响真实程序。
调用关系:它通过终端绘制流程间接覆盖 UpdatePromptScreen::render_ref。若有人改了提示文案、布局或高亮样式,这个测试通常会提醒维护者确认改动是否有意。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_snapshot!, new, new_prompt)。
tests::update_prompt_confirm_selects_update272–277 ↗
fn update_prompt_confirm_selects_update()
作用:确认默认状态下直接按回车会选择“现在更新”。这是最关键的快捷路径。
数据流:它创建一个提示窗口,模拟用户按下回车,然后检查窗口已经结束,并且 selection 是 UpdateNow。测试只读写这个假的屏幕对象,不接触真实终端。
调用关系:它调用 tests::new_prompt 准备对象,再通过 UpdatePromptScreen::handle_key 走正式按键逻辑,最后用 is_done 和 selection 验证结果。
调用图:外部调用 4 个(new, assert!, assert_eq!, new_prompt)。
tests::update_prompt_dismiss_option_leaves_prompt_in_normal_state280–286 ↗
fn update_prompt_dismiss_option_leaves_prompt_in_normal_state()
作用:确认用户移动到“跳过”再回车时,结果是暂时不更新,而不是误触更新。
数据流:它创建提示窗口,模拟按一次向下键,把高亮从“现在更新”移到“跳过”,再模拟回车。最后检查提示已结束,选择值是 NotNow。
调用关系:它覆盖 handle_key、UpdateSelection::next、set_highlight 和 select 配合工作的路径,确保普通跳过选项可用。
调用图:外部调用 4 个(new, assert!, assert_eq!, new_prompt)。
tests::update_prompt_dont_remind_selects_dismissal289–296 ↗
fn update_prompt_dont_remind_selects_dismissal()
作用:确认用户选择第三项时,会得到“不再提醒这个版本”的结果。
数据流:它创建提示窗口,模拟按两次向下键到第三项,再按回车确认。最后检查窗口结束,并且 selection 是 DontRemind。
调用关系:它测试的是 handle_key 连续导航和确认的组合行为。这个结果在真实流程里会让 run_update_prompt_if_needed 去保存版本屏蔽记录。
调用图:外部调用 4 个(new, assert!, assert_eq!, new_prompt)。
tests::update_prompt_ctrl_c_skips_update299–304 ↗
fn update_prompt_ctrl_c_skips_update()
作用:确认按 Ctrl+C 不会让程序真的选择更新,而是安全地跳过提示。这样用户习惯性取消时不会发生意外。
数据流:它创建提示窗口,模拟 Ctrl+C 按键。按键处理后,测试检查提示已结束,并且选择是 NotNow。
调用关系:它直接覆盖 handle_key 里处理 Ctrl+C 的特殊分支。真实运行时,这条路径会让 run_update_prompt_if_needed 返回继续运行。
调用图:外部调用 5 个(Char, new, assert!, assert_eq!, new_prompt)。
tui/src/cwd_prompt.rs源码 ↗
恢复或分叉会话时,会遇到一个很实际的问题:会话里记录的目录,和用户现在站着的目录,可能不是同一个。这个文件就像在开车前问一句“你要从老位置出发,还是从当前位置出发?”。它定义了两个动作:恢复和分叉;也定义了两个选择:用会话目录,或用当前目录。run_cwd_selection_prompt 会画出提示框,监听键盘输入,并在用户按回车、数字键、方向键或退出键后给出结果。CwdPromptScreen 保存屏幕状态,比如当前高亮哪一项、是否已经选择、是否要退出。渲染部分负责把说明文字、两个选项和按键提示画到终端上。测试则检查画面长什么样,以及默认选择、切换选择、Ctrl-C 退出这些行为是否正确。
CwdPromptAction::verb33–38 ↗
fn verb(self) -> &'static str
作用:把动作类型变成界面上能直接显示的英文动词,比如“resume”或“fork”。这样同一个提示框可以复用在恢复会话和分叉会话两种场景。
数据流:输入是一个动作值,比如恢复或分叉 → 函数按这个值挑出对应的短词 → 输出一段固定文字,用来放进提示标题里。
调用关系:它不自己决定什么时候显示,只在画界面时被 CwdPromptScreen::render_ref 调用,帮渲染函数把动作说成人能读懂的话。
调用图:被 1 处调用(render_ref)。
CwdPromptAction::past_participle40–45 ↗
fn past_participle(self) -> &'static str
作用:把动作类型变成过去分词形式,比如“resumed”或“forked”。它用于解释“会话目录”是什么意思。
数据流:输入是当前提示对应的动作 → 函数选择合适的过去式说法 → 输出一段说明文字里的词,让句子读起来正确。
调用关系:它在 CwdPromptScreen::render_ref 画说明文字时被调用,和 verb 一起让同一套界面适配不同动作。
调用图:被 1 处调用(render_ref)。
CwdSelection::next61–66 ↗
fn next(self) -> Self
作用:在两个目录选项之间切到下一个。因为这里只有两项,所以再往下就会绕回另一项。
数据流:输入是当前高亮的选项 → 如果现在是当前目录,就切到会话目录;如果现在是会话目录,就切到当前目录 → 输出新的高亮选项。
调用关系:当用户按向下键或 j 时,CwdPromptScreen::handle_key 会调用它,然后再交给 set_highlight 更新屏幕状态。
调用图:被 1 处调用(handle_key)。
CwdSelection::prev68–73 ↗
fn prev(self) -> Self
作用:在两个目录选项之间切到上一个。由于只有两个选项,它和“下一个”一样,都是在两项之间来回切换。
数据流:输入是当前高亮的选项 → 函数算出另一个选项 → 输出新的高亮选项。
调用关系:当用户按向上键或 k 时,CwdPromptScreen::handle_key 会调用它,再把结果交给 set_highlight。
调用图:被 1 处调用(handle_key)。
run_cwd_selection_prompt76–118 ↗
async fn run_cwd_selection_prompt(
tui: &mut Tui,
action: CwdPromptAction,
current_cwd: &Path,
session_cwd: &Path,
) -> Result<CwdPromptOutcome>
作用:这是外部真正会调用的入口:弹出工作目录选择提示,等用户做决定,然后返回选择结果或退出结果。
数据流:输入是终端界面对象、这次是恢复还是分叉、当前目录、会话记录目录 → 它创建提示屏幕,先画一次,然后不断读取键盘、重画或响应窗口变化 → 输出是“用户选了哪个目录”或“用户退出了”,同时会驱动终端画面更新。
调用关系:它由 resolve_cwd_for_resume_or_fork 在需要决定工作目录时调用。它先用 CwdPromptScreen::new 建状态,再通过 TUI 的事件流接收按键,把按键交给 CwdPromptScreen::handle_key,需要画面刷新时调用终端绘制。
调用图:调用 1 个内部函数(new);被 1 处调用(resolve_cwd_for_resume_or_fork);外部调用 6 个(display, draw, event_stream, frame_requester, pin!, Selection)。
CwdPromptScreen::new131–146 ↗
fn new(
request_frame: FrameRequester,
action: CwdPromptAction,
current_cwd: String,
session_cwd: String,
) -> Self
作用:创建一个新的提示框状态。它把动作、两个目录文字和默认高亮项都准备好。
数据流:输入是刷新画面的请求器、动作类型、当前目录字符串、会话目录字符串 → 它把这些装进一个屏幕状态对象,并默认高亮“会话目录” → 输出一个可以处理按键和绘制界面的 CwdPromptScreen。
调用关系:run_cwd_selection_prompt 在真正弹窗前调用它。测试里的 tests::new_prompt 和 tests::cwd_prompt_fork_snapshot 也会调用它,用来搭出可检查的假界面。
调用图:被 3 处调用(run_cwd_selection_prompt, cwd_prompt_fork_snapshot, new_prompt)。
CwdPromptScreen::handle_key148–169 ↗
fn handle_key(&mut self, key_event: KeyEvent)
作用:处理用户按下的键。它决定按键是移动高亮、确认选择,还是退出提示。
数据流:输入是一次键盘事件 → 它先忽略按键释放事件,再识别 Ctrl-C、Ctrl-D、方向键、j/k、数字键、回车和 Esc → 结果是更新高亮、写入最终选择、标记退出,并在状态变化后请求重画界面。
调用关系:主循环 run_cwd_selection_prompt 收到键盘事件后会把事件交给它。它内部会调用 CwdSelection::next、CwdSelection::prev 算高亮位置,再调用 set_highlight 或 select 改状态。
调用图:调用 5 个内部函数(select, set_highlight, next, prev, schedule_frame);外部调用 1 个(matches!)。
CwdPromptScreen::set_highlight171–176 ↗
fn set_highlight(&mut self, highlight: CwdSelection)
作用:改变当前高亮的选项,也就是光标停在哪一行。只有真的变了,才通知界面重画。
数据流:输入是新的高亮选项 → 它和旧高亮比较,如果不同就保存新值 → 输出没有返回值,但会改变屏幕状态,并安排下一帧刷新。
调用关系:它由 CwdPromptScreen::handle_key 在用户上下移动时调用。它把“按键含义”落成具体状态变化,并通过 schedule_frame 告诉界面该重画了。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_key)。
CwdPromptScreen::select178–182 ↗
fn select(&mut self, selection: CwdSelection)
作用:确认用户最终选择了哪个目录。它也会把高亮同步到所选项,让界面状态保持一致。
数据流:输入是用户选中的目录类型 → 它把高亮改到该项,并把选择结果存起来 → 输出没有返回值,但屏幕会进入“已经完成”的状态,并请求重画。
调用关系:它由 CwdPromptScreen::handle_key 在用户按数字键、回车或 Esc 时调用。之后主循环会通过 is_done 发现已经选完,然后结束提示。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_key)。
CwdPromptScreen::is_done184–186 ↗
fn is_done(&self) -> bool
作用:告诉外部这个提示是否已经结束。结束原因可能是用户选好了,也可能是用户要求退出。
数据流:输入是当前屏幕状态 → 它检查退出标记和选择结果是否存在 → 输出一个真假值:真表示提示可以收场了。
调用关系:run_cwd_selection_prompt 的等待循环会反复用它判断要不要继续收键盘事件。测试也用它确认 Ctrl-C 会让提示结束。
CwdPromptScreen::selection188–190 ↗
fn selection(&self) -> Option<CwdSelection>
作用:取出用户当前已经确认的选择。如果还没选,返回空。
数据流:输入是屏幕里保存的状态 → 它读取选择字段 → 输出可能是会话目录、当前目录,也可能什么都没有。
调用关系:run_cwd_selection_prompt 在提示结束后用它生成最终结果。测试也用它检查回车、切换后回车、Ctrl-C 的结果是否符合预期。
CwdPromptScreen::render_ref194–247 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把提示框实际画到终端上,包括标题、说明文字、两个目录选项和回车提示。
数据流:输入是要画的区域和终端缓冲区(一块准备写字的屏幕内存),以及屏幕状态里的动作、目录和高亮信息 → 它先清空区域,再组装多行文字和选项行 → 输出是写入缓冲区的界面内容,用户下一次刷新时就能看到。
调用关系:终端绘制时会调用它。它会向 CwdPromptAction::verb 和 CwdPromptAction::past_participle 要合适的动作文字,并用 selection_option_row 生成带高亮效果的选项行。
调用图:调用 5 个内部函数(past_participle, verb, tlbr, new, selection_option_row);外部调用 3 个(from, format!, vec!)。
tests::new_prompt259–266 ↗
fn new_prompt() -> CwdPromptScreen
作用:给测试快速造一个标准的提示框。这样每个测试不用重复写同样的假目录和假刷新器。
数据流:输入没有外部参数 → 它准备一个测试用刷新请求器,以及固定的当前目录和会话目录 → 输出一个默认用于“恢复会话”的 CwdPromptScreen。
调用关系:多个测试会调用它来拿到同样的起点。它内部调用 CwdPromptScreen::new,把测试准备工作集中在一个地方。
调用图:调用 2 个内部函数(new, test_dummy)。
tests::cwd_prompt_snapshot269–277 ↗
fn cwd_prompt_snapshot()
作用:检查恢复会话时,这个提示框画出来的样子有没有意外变化。快照测试就像给界面拍一张标准照片,以后每次对比。
数据流:输入是测试里造出的提示屏幕和一个假的 80x14 终端 → 它把界面画到假终端上 → 输出是和保存的快照做比较;如果画面变了,测试会失败或要求更新快照。
调用关系:测试运行器会执行它。它通过 tests::new_prompt 创建屏幕,再触发终端绘制,间接覆盖 CwdPromptScreen::render_ref 的显示效果。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_snapshot!, new, new_prompt)。
tests::cwd_prompt_fork_snapshot280–293 ↗
fn cwd_prompt_fork_snapshot()
作用:检查分叉会话时提示框的文字和画面是否正确。它专门确认“fork”场景不会误显示成“resume”。
数据流:输入是测试中手工创建的分叉提示屏幕和假终端 → 它渲染界面 → 输出是与名为分叉弹窗的快照比较结果。
调用关系:测试运行器会执行它。它直接调用 CwdPromptScreen::new 创建分叉场景,再通过绘制流程检查 render_ref 使用动作文字的行为。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 2 个(assert_snapshot!, new)。
tests::cwd_prompt_selects_session_by_default296–300 ↗
fn cwd_prompt_selects_session_by_default()
作用:确认用户直接按回车时,默认选择的是会话目录。这很重要,因为界面一开始高亮的就是会话目录。
数据流:输入是一个新建的提示屏幕 → 测试模拟用户按下回车 → 输出是断言结果:选择应当是 CwdSelection::Session。
调用关系:测试运行器会执行它。它用 tests::new_prompt 建屏幕,再通过 CwdPromptScreen::handle_key 走真实按键逻辑,最后用 selection 查看结果。
调用图:外部调用 3 个(new, assert_eq!, new_prompt)。
tests::cwd_prompt_can_select_current303–308 ↗
fn cwd_prompt_can_select_current()
作用:确认用户可以从默认的会话目录切到当前目录并选择它。也就是验证向下移动和回车确认能配合工作。
数据流:输入是一个新建的提示屏幕 → 测试先模拟按向下键,再模拟按回车 → 输出是断言结果:最终选择应当是 CwdSelection::Current。
调用关系:测试运行器会执行它。它通过 handle_key 触发和真实用户一样的流程,间接检查 next、set_highlight、select 这些步骤是否串起来了。
调用图:外部调用 3 个(new, assert_eq!, new_prompt)。
tests::cwd_prompt_ctrl_c_exits_instead_of_selecting311–316 ↗
fn cwd_prompt_ctrl_c_exits_instead_of_selecting()
作用:确认 Ctrl-C 会退出提示,而不是偷偷选中某个目录。这样用户取消操作时不会产生误选择。
数据流:输入是一个新建的提示屏幕 → 测试模拟按下 Ctrl-C → 输出是两个检查:没有选择结果,并且提示已经结束。
调用关系:测试运行器会执行它。它调用 handle_key 走取消路径,再用 selection 和 is_done 验证屏幕状态。
调用图:外部调用 5 个(Char, new, assert!, assert_eq!, new_prompt)。
引导与迁移流程
这些文件实现首次运行的引导体验,以及启动期间显示的相关交互式迁移/导入提示。
tui/src/onboarding/mod.rs源码 ↗
这个文件本身不画界面,也不处理登录。它更像一个文件夹门口的目录牌:新手引导里有登录 auth、密钥 keys、引导界面 onboarding_screen、信任目录 trust_directory、欢迎页 welcome 这些部分。没有它,外面的代码就很难用统一的方式找到这些模块,编译器也不会自动把这些源文件纳入这个模块。它还把 auth 里的 mark_underlined_hyperlink 和 mark_url_hyperlink 重新导出出来。意思是:别的代码只要从 onboarding 这一层拿“把文字标成链接”的工具就行,不必直接钻到 auth 文件里。这让内部结构以后调整时,外部调用可以少受影响。
tui/src/onboarding/keys.rs源码 ↗
这个文件像一张“新手阶段的遥控器按键表”。在用户还没有配置 Codex 的快捷键之前, onboarding(首次引导流程)不能依赖用户设置,只能用这里写死的按键。它把常见动作和键盘按键对应起来:比如向上可以按方向键上,也可以按 k;向下可以按方向键下,也可以按 j;选择第一个选项可以按 1 或 y;取消用 Esc;退出可以按 q、Ctrl+C 或 Ctrl+D。这里还包含一个切换动画的快捷键,支持 Ctrl+. 以及 Ctrl+Shift+.。文件本身没有函数,只定义了一组常量。其他引导界面代码会读取这些常量,用来判断用户按下的键是什么意思,也用来显示提示。这样做的好处是:新手引导的按键集中在一个地方,既稳定,又不受用户配置缺失的影响。
tui/src/onboarding/onboarding_screen.rs源码 ↗
新用户打开程序时,不能一下子进主界面:可能要先看欢迎页、登录账号、确认是否信任当前项目目录。这个文件就像前台接待员,按顺序带用户走完这些步骤。它维护一个步骤列表,只显示已经完成的步骤和当前正在做的步骤;键盘输入会先经过这里,再转给真正活跃的那一页。它还处理一些跨步骤的安全规则,比如用户正在输入 API key(一串登录密钥)时,如果输入框里已经有字母,普通的 q 会当作文本,不会误触退出;但 Ctrl/Alt 这类明确的快捷键仍然可以退出。运行时它还监听终端事件和应用服务器事件,登录成功、账号更新、窗口重绘、目录信任保存失败等情况都会在这里汇合。简单说,它不是某个单独页面,而是整个引导流程的交通警察。
KeyboardHandler::handle_paste62–62 ↗
fn handle_paste(&mut self, _pasted: String)
作用:这是键盘处理接口里给“粘贴文本”准备的默认做法。默认什么都不做,表示不是每个页面都需要处理粘贴。
数据流:进去的是一段粘贴进来的文字 → 默认实现直接忽略它 → 没有返回结果,也不改任何状态。
调用关系:具体页面如果需要粘贴,比如 API key 输入框,可以自己覆盖这个方法;不需要粘贴的页面就沿用这个空实现。
OnboardingScreen::new105–169 ↗
async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self
作用:创建整个引导界面,并按配置决定要放哪些步骤。比如是否需要登录页、是否需要信任目录页、默认高亮哪种登录方式。
数据流:进去的是终端对象和一组引导参数,包括登录状态、配置、服务器请求句柄、是否显示某些页面 → 它创建欢迎页,必要时创建登录页和目录信任页,还会尝试找出当前项目的 Git 根目录作为信任目标 → 出来的是一个准备好运行的 OnboardingScreen。
调用关系:run_onboarding_app 在引导开始时调用它。它会用 Tui 提供的 frame_requester 来请求刷新画面,也会调用 resolve_root_git_project_for_trust 来确定应该信任哪个目录。
调用图:调用 2 个内部函数(new, level_from_config);被 1 处调用(run_onboarding_app);外部调用 11 个(new, new, new, resolve_root_git_project_for_trust, frame_requester, Auth, TrustDirectory, Welcome, matches!, new (+1 more))。
OnboardingScreen::current_steps_mut171–184 ↗
fn current_steps_mut(&mut self) -> Vec<&mut Step>
作用:找出当前应该参与交互的步骤,并返回可修改版本。它会包括已经完成的可见步骤,以及第一个正在进行的步骤。
数据流:进去的是当前引导界面的步骤列表 → 它从头扫描,跳过隐藏步骤,收集完成步骤,遇到第一个正在进行的步骤后停止 → 出来的是这些步骤的可修改引用。
调用关系:按键和粘贴处理会用它找到真正该接收输入的当前步骤。这样后面的未开始步骤不会提前响应用户操作。
调用图:被 2 处调用(handle_key_event, handle_paste);外部调用 1 个(new)。
OnboardingScreen::current_steps186–199 ↗
fn current_steps(&self) -> Vec<&Step>
作用:找出当前应该显示的步骤,但只读不修改。它用于画界面和判断当前可见步骤的状态。
数据流:进去的是当前步骤列表 → 它跳过隐藏步骤,保留已完成步骤和第一个正在进行的步骤 → 出来的是这些步骤的只读引用。
调用关系:render_ref 用它决定画哪些内容;should_suppress_animations 也用它判断当前页面是否需要暂停动画。
调用图:被 2 处调用(render_ref, should_suppress_animations);外部调用 1 个(new)。
OnboardingScreen::should_suppress_animations201–208 ↗
fn should_suppress_animations(&self) -> bool
作用:判断整个引导界面现在要不要暂停动画。主要是为了用户复制登录信息时,画面不要一直重绘打断终端选择。
数据流:进去的是当前界面状态 → 它查看当前可见步骤里是否有登录页要求暂停动画 → 出来是 true 或 false,表示是否压住动画。
调用关系:render_ref 在每次画界面前调用它,然后把这个决定传给欢迎页和登录页。
调用图:调用 1 个内部函数(current_steps);被 1 处调用(render_ref)。
OnboardingScreen::is_auth_in_progress210–214 ↗
fn is_auth_in_progress(&self) -> bool
作用:判断登录步骤现在是不是正在进行。这个信息用于决定用户按退出键时,要不要取消正在进行的登录尝试。
数据流:进去的是步骤列表 → 它查找是否存在登录步骤,并且这个登录步骤状态是进行中 → 出来是一个布尔值。
调用关系:handle_key_event 在处理退出快捷键时调用它。如果登录正在进行,就先取消登录,再把整个引导标记为结束并要求退出程序。
调用图:被 1 处调用(handle_key_event)。
OnboardingScreen::is_done216–222 ↗
fn is_done(&self) -> bool
作用:告诉外层循环:引导流程是不是已经结束。结束可能是用户主动退出,也可能是所有需要做的步骤都完成了。
数据流:进去的是 OnboardingScreen 当前状态 → 它先看 is_done 标记,再看是否还有任何步骤处于进行中 → 出来是 true 或 false。
调用关系:run_onboarding_app 的主循环靠它决定是否继续监听按键、重绘和服务器消息。
OnboardingScreen::should_exit224–226 ↗
fn should_exit(&self) -> bool
作用:告诉调用者,引导结束后是不是应该直接退出整个程序。比如用户在未登录状态下取消登录,就可能需要退出。
数据流:进去的是当前 should_exit 字段 → 直接把这个布尔值返回 → 不修改任何东西。
调用关系:run_onboarding_app 在结束时把这个值放进 OnboardingResult,交给更外层的程序决定下一步。
OnboardingScreen::cancel_auth_if_active228–234 ↗
fn cancel_auth_if_active(&self)
作用:如果登录页正在做登录尝试,就让它取消。这样用户按退出时,不会留下一个后台还在跑的登录流程。
数据流:进去的是当前所有步骤 → 它逐个检查,找到登录步骤后调用登录控件的取消方法 → 没有返回值,但可能让登录尝试停止。
调用关系:handle_key_event 在确认用户真的要退出,并且登录正在进行时调用它。
调用图:被 1 处调用(handle_key_event)。
OnboardingScreen::auth_widget_mut236–241 ↗
fn auth_widget_mut(&mut self) -> Option<&mut AuthModeWidget>
作用:从步骤列表里找出登录控件,并返回可修改引用。服务器发来的登录结果需要交给它处理。
数据流:进去的是步骤列表 → 它查找 Auth 类型的步骤 → 找到就返回登录控件,找不到就返回空。
调用关系:handle_app_server_notification 用它拿到登录页,然后把账号登录完成或账号更新的通知转交过去。
调用图:被 1 处调用(handle_app_server_notification)。
OnboardingScreen::handle_app_server_notification243–257 ↗
fn handle_app_server_notification(&mut self, notification: ServerNotification)
作用:处理应用服务器发来的通知,尤其是登录完成和账号信息更新。它把这些消息转发给登录页。
数据流:进去的是一条服务器通知 → 它判断通知类型,如果是账号登录完成或账号更新,就找到登录控件并调用对应处理方法;其他通知忽略 → 可能改变登录页状态。
调用关系:run_onboarding_app 在监听到 AppServerEvent::ServerNotification 时调用它。它内部依赖 auth_widget_mut 找到真正能处理登录消息的页面。
调用图:调用 1 个内部函数(auth_widget_mut)。
OnboardingScreen::api_key_entry_context259–273 ↗
fn api_key_entry_context(&self) -> ApiKeyEntryContext
作用:查看当前是否正在输入 API key,以及输入框里是否已经有文字。这个信息用来避免把用户正在输入的 q 误当成退出键。
数据流:进去的是步骤列表 → 它找到登录步骤,询问 API key 输入状态和是否有文本 → 出来是 ApiKeyEntryContext;如果没有登录页,就返回默认的“未输入”状态。
调用关系:handle_key_event 在判断退出快捷键前调用它,再交给 suppress_quit_while_typing_api_key 做最终判断。
调用图:被 1 处调用(handle_key_event)。
OnboardingScreen::handle_key_event284–323 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:这是整个引导界面的按键总入口。它决定一个按键是退出、输入文字,还是交给当前步骤处理。
数据流:进去的是一个键盘事件 → 它先忽略按下以外的事件,再判断是否是退出键;如果用户正在输入非空 API key,普通字符退出键会被保护起来;如果确认退出,就设置结束状态,必要时取消登录;否则把按键交给欢迎页和当前活跃步骤 → 最后请求终端重画。
调用关系:TUI 主循环收到按键后调用它。它会用 api_key_entry_context、suppress_quit_while_typing_api_key、is_auth_in_progress、cancel_auth_if_active 和 current_steps_mut 来完成判断和分发。
调用图:调用 6 个内部函数(api_key_entry_context, cancel_auth_if_active, current_steps_mut, is_auth_in_progress, suppress_quit_while_typing_api_key, schedule_frame);外部调用 1 个(matches!)。
OnboardingScreen::handle_paste325–334 ↗
fn handle_paste(&mut self, pasted: String)
作用:处理用户往终端里粘贴文字。它只把非空粘贴内容交给当前活跃步骤。
数据流:进去的是粘贴文本 → 如果文本为空就直接返回;否则找到当前活跃步骤,把文本交给它 → 然后请求刷新画面。
调用关系:run_onboarding_app 收到 TuiEvent::Paste 时会调用它。它通过 current_steps_mut 找到当前真正应该接收粘贴的页面。
调用图:调用 2 个内部函数(current_steps_mut, schedule_frame)。
suppress_quit_while_typing_api_key343–353 ↗
fn suppress_quit_while_typing_api_key(
key_event: KeyEvent,
api_key_entry_context: ApiKeyEntryContext,
) -> bool
作用:判断某个退出快捷键此刻应不应该被当作普通输入。它专门保护 API key 输入框,防止用户输入 q 时误退出。
数据流:进去的是一个按键事件和 API key 输入状态 → 它检查是否正在输入、输入框是否已有文字、按键是否是普通字符、是否没有 Ctrl/Alt 修饰键 → 出来是 true 或 false,true 表示这次不要退出,要当文字处理。
调用关系:OnboardingScreen::handle_key_event 用它做退出保护。测试函数也直接调用它,验证空输入、已有输入、Ctrl 键、非 API key 状态这些边界情况。
调用图:被 5 处调用(handle_key_event, does_not_suppress_control_quit_key_during_api_key_entry, does_not_suppress_printable_quit_key_when_api_key_input_is_empty, does_not_suppress_when_not_in_api_key_entry, suppresses_printable_quit_key_during_api_key_entry);外部调用 1 个(matches!)。
OnboardingScreen::render_ref356–427 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把当前引导界面画到终端屏幕上。它按从上到下的顺序画欢迎、登录、信任目录等当前可见步骤。
数据流:进去的是屏幕区域和绘图缓冲区 → 它先判断是否要暂停动画,清空区域,再为每个当前可见步骤估算需要的高度,最后把步骤逐个画到正确位置 → 输出是更新后的终端缓冲区。
调用关系:TUI draw 调用它来重绘引导页。它会调用 current_steps 和 should_suppress_animations,并把真正的绘制工作交给每个 Step 自己的 render_ref。
调用图:调用 2 个内部函数(current_steps, should_suppress_animations);外部调用 2 个(empty, new)。
Step::handle_key_event431–437 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:把按键交给具体的步骤控件。Step 是一个包装盒,里面可能是欢迎页、登录页或信任目录页。
数据流:进去的是一个按键事件和当前 Step → 它看 Step 里面实际是哪种页面,然后调用对应页面的按键处理方法 → 可能改变那个页面的状态。
调用关系:OnboardingScreen::handle_key_event 找到活跃步骤后,通过这个方法把按键转交给真正的页面。
Step::handle_paste439–445 ↗
fn handle_paste(&mut self, pasted: String)
作用:把粘贴文本交给支持粘贴的具体步骤。欢迎页不需要粘贴,所以会忽略。
数据流:进去的是粘贴文本和当前 Step → 如果是登录页或信任目录页,就转给对应控件;如果是欢迎页,就不做事 → 可能改变输入框或选择状态。
调用关系:OnboardingScreen::handle_paste 找到活跃步骤后调用它。它负责把统一的粘贴事件分发到具体页面。
Step::get_step_state449–455 ↗
fn get_step_state(&self) -> StepState
作用:询问某个步骤现在是隐藏、进行中,还是已完成。外层流程靠这个判断该显示什么、下一步走到哪里。
数据流:进去的是一个 Step → 它查看里面的具体页面,并调用该页面自己的状态方法 → 出来是 StepState。
调用关系:current_steps、current_steps_mut、is_done 等流程判断都依赖它。它让不同页面可以用同一种方式告诉外层自己的进度。
Step::render_ref459–471 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把某个具体步骤画出来。它只是转发绘图请求,真正画法由欢迎页、登录页或信任目录页自己决定。
数据流:进去的是绘图区域、缓冲区和当前 Step → 它判断 Step 类型并调用对应控件的 render_ref → 缓冲区被写入该步骤的画面内容。
调用关系:OnboardingScreen::render_ref 在逐步绘制页面时调用它。这个方法让外层不用关心每种页面的内部画法。
run_onboarding_app474–575 ↗
async fn run_onboarding_app(
args: OnboardingScreenArgs,
mut app_server: Option<&mut AppServerSession>,
tui: &mut Tui,
) -> Result<OnboardingResult>
作用:运行完整的引导流程。它创建引导界面,进入事件循环,同时听终端输入和服务器消息,直到引导结束。
数据流:进去的是引导参数、可选的应用服务器会话和终端对象 → 它创建 OnboardingScreen,先画一次界面,然后循环处理按键、粘贴、重绘、窗口变化、服务器通知和断线;如果用户选择信任目录,还会尝试保存这个选择 → 出来是 OnboardingResult,说明目录信任是否保存成功,以及是否应退出程序。
调用关系:更外层的 run_ratatui_app 会调用它。它是这个文件的运行核心,连接 TUI 事件流、AppServerSession、OnboardingScreen 和 persist_selected_trust。
调用图:调用 1 个内部函数(new);被 1 处调用(run_ratatui_app);外部调用 4 个(draw, event_stream, pin!, select!)。
persist_selected_trust577–622 ↗
async fn persist_selected_trust(
onboarding_screen: &mut OnboardingScreen,
request_handle: Option<AppServerRequestHandle>,
) -> bool
作用:把用户选择“信任当前项目目录”的决定保存下来。这样下次进入这个项目时,程序知道用户已经确认过。
数据流:进去的是引导界面和可选的服务器请求句柄 → 它查找信任目录步骤,确认用户选择了 Trust,然后通过 write_trusted_project 写入配置;如果失败,就格式化错误信息,清掉选择,并把错误显示回信任目录页 → 出来是 true 或 false,表示是否保存成功。
调用关系:run_onboarding_app 在按键处理后调用它,避免用户点了信任但结果没落盘。测试 trust_persistence_failure_keeps_trust_step_in_progress 也调用它验证失败时页面不会假装完成。
调用图:调用 2 个内部函数(format_config_error, write_trusted_project);被 1 处调用(trust_persistence_failure_keeps_trust_step_in_progress);外部调用 3 个(eyre!, format!, error!)。
tests::suppresses_printable_quit_key_during_api_key_entry642–651 ↗
fn suppresses_printable_quit_key_during_api_key_entry()
作用:测试用户正在输入非空 API key 时,普通的 q 会被保护,不会触发退出。
数据流:进去的是模拟的 q 按键和“API key 输入中且已有文字”的状态 → 调用 suppress_quit_while_typing_api_key → 断言结果是 true。
调用关系:它直接验证退出保护函数最关键的场景:用户已经开始输入密钥时,普通字符不能把程序关掉。
调用图:调用 1 个内部函数(suppress_quit_while_typing_api_key);外部调用 3 个(Char, new, assert!)。
tests::does_not_suppress_printable_quit_key_when_api_key_input_is_empty654–663 ↗
fn does_not_suppress_printable_quit_key_when_api_key_input_is_empty()
作用:测试 API key 输入框为空时,普通 q 仍然可以作为退出键使用。
数据流:进去的是模拟的 q 按键和“API key 输入中但没有文字”的状态 → 调用 suppress_quit_while_typing_api_key → 断言结果是 false。
调用关系:它保证保护规则不会太过头。空输入框时,用户按 q 退出仍然方便。
调用图:调用 1 个内部函数(suppress_quit_while_typing_api_key);外部调用 3 个(Char, new, assert!)。
tests::does_not_suppress_control_quit_key_during_api_key_entry666–675 ↗
fn does_not_suppress_control_quit_key_during_api_key_entry()
作用:测试带 Ctrl 的退出快捷键在 API key 输入中不会被拦住。Ctrl/Alt 这类组合键被视为明确的退出指令。
数据流:进去的是模拟的 Ctrl+x 按键和“API key 输入中且已有文字”的状态 → 调用 suppress_quit_while_typing_api_key → 断言结果是 false。
调用关系:它验证紧急退出通道仍然可用,配合 handle_key_event 的退出判断保证用户不会被困在输入框里。
调用图:调用 1 个内部函数(suppress_quit_while_typing_api_key);外部调用 3 个(Char, new, assert!)。
tests::does_not_suppress_when_not_in_api_key_entry678–687 ↗
fn does_not_suppress_when_not_in_api_key_entry()
作用:测试不在 API key 输入模式时,不会启用这条特殊保护规则。
数据流:进去的是模拟普通字符按键和“没有处于 API key 输入”的状态 → 调用 suppress_quit_while_typing_api_key → 断言结果是 false。
调用关系:它保证 suppress_quit_while_typing_api_key 只影响 API key 输入场景,不会改变其他页面的退出行为。
调用图:调用 1 个内部函数(suppress_quit_while_typing_api_key);外部调用 3 个(Char, new, assert!)。
tests::trust_persistence_failure_keeps_trust_step_in_progress690–721 ↗
async fn trust_persistence_failure_keeps_trust_step_in_progress()
作用:测试保存信任目录失败时,信任步骤会留在进行中,并显示错误,而不是错误地继续往下走。
数据流:进去的是一个手工构造的引导界面,其中用户已经选择信任目录,但没有可用的服务器请求句柄 → 调用 persist_selected_trust → 得到 false,并检查选择被清空、步骤仍是进行中、错误信息包含服务器不可用。
调用关系:它保护 persist_selected_trust 的失败路径。这个行为很重要,因为目录信任如果没真正保存成功,界面必须让用户知道并继续停在这一步。
调用图:调用 2 个内部函数(persist_selected_trust, test_dummy);外部调用 4 个(assert!, assert_eq!, panic!, vec!)。
tui/src/onboarding/welcome.rs源码 ↗
这个文件定义了 WelcomeWidget,也就是欢迎页这个小界面零件。它会先清空自己要画的区域,再决定要不要播放背景动画:如果动画开关关了、被临时压制了,或者终端窗口太小,就只显示欢迎文字;如果空间够,就把当前 ASCII 动画帧放在欢迎文字上方。它还会响应一个固定快捷键,用来随机切换欢迎动画版本。这里的 Cell 可以理解成“一个小盒子”,让界面在只拿到共享引用时也能记住布局区域、动画是否暂停。最后,它还告诉引导流程:如果用户已经登录,欢迎步骤可以隐藏;如果没登录,欢迎步骤本身也算完成,不会卡住后面的步骤。
WelcomeWidget::handle_key_event39–47 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:这个函数在欢迎页收到键盘按键时运行。它只关心一个事:当动画功能开启,并且用户按下切换动画的快捷键时,换一个随机的欢迎动画样式。
数据流:进去的是一个按键事件,以及组件里保存的动画开关和当前动画 → 它先检查动画有没有启用,再确认这次是不是“按下”动作并且匹配切换动画快捷键 → 如果匹配,就记录一条提示日志,并让动画对象随机挑一个新版本;没有返回值,只改变当前动画的样子。
调用关系:它是欢迎页接入键盘输入的入口,由引导屏幕的键盘分发机制在用户按键时调用。真正挑动画的活交给 animation.pick_random_variant,自己只负责判断这个按键该不该触发。
调用图:调用 1 个内部函数(pick_random_variant);外部调用 1 个(warn!)。
WelcomeWidget::new51–63 ↗
fn new(
is_logged_in: bool,
request_frame: FrameRequester,
animations_enabled: bool,
) -> Self
作用:这个函数创建一个新的欢迎页组件。调用者会告诉它用户是否已登录、怎么请求界面重画,以及动画要不要启用。
数据流:进去的是登录状态、一个用于请求重新绘制界面的 FrameRequester、动画总开关 → 它创建一个 AsciiAnimation,把动画压制状态设为 false,把已知布局区域设为空 → 出来的是一个可以被渲染、可以响应按键的 WelcomeWidget。
调用关系:这是外部组装欢迎页时最常用的构造函数,也被测试用来搭出真实组件。它把动画创建工作交给 AsciiAnimation::new,把后续绘制交给 render_ref,把按键响应交给 handle_key_event。
调用图:调用 1 个内部函数(new);被 3 处调用(new, welcome_renders_animation_on_first_draw, welcome_skips_animation_below_height_breakpoint);外部调用 1 个(new)。
WelcomeWidget::update_layout_area65–67 ↗
fn update_layout_area(&self, area: Rect)
作用:这个函数记录欢迎页实际可用的布局区域。它的用处是让渲染时知道窗口到底有多大,从而决定动画会不会被画出来。
数据流:进去的是一个 Rect,也就是终端界面中的一块矩形区域 → 它把这个区域存进组件内部的小盒子里 → 出来没有直接结果,但之后 render_ref 会读取这个区域来判断空间是否足够。
调用关系:它通常由外层布局代码在算好屏幕区域后调用。它不画界面,只给 render_ref 提供判断依据,避免动画在太小的区域里被截断。
WelcomeWidget::set_animations_suppressed69–71 ↗
fn set_animations_suppressed(&self, suppressed: bool)
作用:这个函数临时暂停或恢复欢迎页动画。它适合在某些情况下让界面安静下来,比如别的层级正在展示内容时不希望背景继续动。
数据流:进去的是一个布尔值:true 表示压制动画,false 表示允许动画 → 它把这个状态保存到组件内部 → 后续 render_ref 会根据这个状态决定是否安排下一帧、是否显示动画。
调用关系:它由外部流程控制欢迎页动画的开关细节。它不直接碰动画帧,而是给 render_ref 和 handle_key_event 之外的显示逻辑一个“先别动”的信号。
WelcomeWidget::render_ref75–104 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:这个函数真正把欢迎页画到终端缓冲区里。它负责清屏、安排动画下一帧、选择要显示的文字行,并把这些内容写进界面。
数据流:进去的是要绘制的区域 area 和一个 Buffer,Buffer 可以理解成终端屏幕的草稿纸 → 它先清掉该区域;如果动画启用且没被压制,就安排下一帧;再根据记录的布局区域判断尺寸够不够;够大就加入当前动画帧,不够就跳过动画;最后加入“Welcome to Codex...”文字并写入 Buffer → 出来没有返回值,但屏幕草稿纸被改成欢迎页内容。
调用关系:它是 ratatui 这个终端界面库在需要画欢迎页时调用的核心函数。它向 animation.current_frame 要当前画面,向 animation.schedule_next_frame 安排之后重画,然后用 Paragraph 把结果交给 ratatui 画出来。
调用图:调用 2 个内部函数(current_frame, schedule_next_frame);外部调用 4 个(from, new, new, vec!)。
WelcomeWidget::get_step_state108–113 ↗
fn get_step_state(&self) -> StepState
作用:这个函数告诉新手引导流程,欢迎页这个步骤现在算什么状态。它让引导流程知道是否应该显示这个欢迎步骤。
数据流:进去的是组件内部保存的 is_logged_in 登录状态 → 如果用户已登录,它返回 Hidden,表示这个步骤隐藏;如果未登录,它返回 Complete,表示欢迎步骤不阻塞流程 → 它不改动任何数据。
调用关系:它由引导屏幕的步骤管理逻辑调用,用来决定欢迎页在整套 onboarding 流程里的位置。它不依赖渲染,也不触发动画,只提供状态判断。
tests::row_containing129–137 ↗
fn row_containing(buf: &Buffer, needle: &str) -> Option<u16>
作用:这是测试用的小帮手,用来在终端缓冲区里找某段文字出现在哪一行。它让测试不用关心每个字符的精确位置,只检查关键文字的大概行号。
数据流:进去的是一个 Buffer 和要寻找的字符串 needle → 它从上到下扫描每一行,把这一行的所有格子拼成普通字符串,再看里面有没有 needle → 找到就返回行号,找不到就返回 None。
调用关系:它只在本文件测试里使用。welcome_renders_animation_on_first_draw 和 welcome_skips_animation_below_height_breakpoint 会调用它来确认“Welcome”被画在预期位置。
tests::welcome_renders_animation_on_first_draw140–153 ↗
fn welcome_renders_animation_on_first_draw()
作用:这个测试确认:当窗口够大、动画也开启时,欢迎页第一次绘制就会把动画画出来。它防止以后有人改代码时不小心让欢迎页只剩文字。
数据流:进去没有外部输入 → 测试创建一个启用动画的 WelcomeWidget,准备一个足够大的空 Buffer,先数出当前动画有多少行,再执行绘制 → 最后查找“Welcome”所在行,确认它在动画之后,也就是动画确实占了上方空间。
调用关系:它调用 WelcomeWidget::new 创建组件,调用 render 触发 WelcomeWidget::render_ref,再用 tests::row_containing 检查结果。这个测试覆盖的是正常大窗口下的渲染路径。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(empty, new, assert_eq!, row_containing)。
tests::welcome_skips_animation_below_height_breakpoint156–168 ↗
fn welcome_skips_animation_below_height_breakpoint()
作用:这个测试确认:当窗口高度不够时,欢迎页会跳过动画,直接显示欢迎文字。这样可以避免字符画被截断,界面看起来乱七八糟。
数据流:进去没有外部输入 → 测试创建一个启用动画的 WelcomeWidget,但给它一个比最低动画高度少一行的 Buffer → 绘制后查找“Welcome”,确认它出现在第一行,说明动画没有被画在前面。
调用关系:它和上一个渲染测试形成对照:同样通过 WelcomeWidget::new 建组件、通过 render_ref 间接绘制、通过 row_containing 查结果,但重点检查小窗口分支。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(empty, new, assert_eq!, row_containing)。
tests::ctrl_dot_changes_animation_variant171–192 ↗
fn ctrl_dot_changes_animation_variant()
作用:这个测试确认 Ctrl+. 这个快捷键真的会切换欢迎动画版本。它防止键盘快捷键失效后用户无法手动换动画。
数据流:进去没有外部输入 → 测试用两个固定动画版本手工创建 WelcomeWidget,记录按键前的当前帧 → 构造一个 Ctrl+. 按键事件并交给 handle_key_event → 再读取当前帧,确认前后不一样。
调用关系:它直接测试 WelcomeWidget::handle_key_event 的关键行为。动画对象由 AsciiAnimation::with_variants 准备,这样测试能明确知道切换前后应该不同。
调用图:调用 2 个内部函数(with_variants, test_dummy);外部调用 4 个(new, Char, new, assert_ne!)。
tests::ctrl_shift_dot_changes_animation_variant195–219 ↗
fn ctrl_shift_dot_changes_animation_variant()
作用:这个测试确认 Ctrl+Shift+. 也能切换欢迎动画版本。它照顾不同终端上修饰键上报方式不完全一样的情况。
数据流:进去没有外部输入 → 测试准备两个固定动画版本,记录当前帧 → 构造一个带 Control 和 Shift 的点号按键事件,交给 handle_key_event → 再比较当前帧,确认动画已经换成另一个版本。
调用关系:它补充验证快捷键兼容性,仍然围绕 WelcomeWidget::handle_key_event 展开。和 ctrl_dot_changes_animation_variant 一起保证两种终端按键写法都能触发同一件事。
调用图:调用 2 个内部函数(with_variants, test_dummy);外部调用 4 个(new, Char, new, assert_ne!)。
tui/src/onboarding/auth.rs源码 ↗
这个文件像一个登录柜台。用户来到 Codex 的首次设置流程时,它先展示几种登录方式,然后根据用户按键切换高亮、开始登录、取消登录、输入 API key,或者显示成功提示。它自己不决定整个引导流程什么时候结束,只告诉外层“我这一步还在进行”或“已经完成”。里面最核心的是 AuthModeWidget,它保存当前登录状态、错误信息、当前选中的选项,以及和后台应用服务器通信的句柄。ChatGPT 登录会向后台申请登录链接,必要时自动打开浏览器;设备码登录交给子模块处理;API key 登录会读环境变量预填、接收键盘输入,再发给后台保存。它还特别处理了终端里的可点击链接,因为长链接换行后普通终端可能识别失败,所以会给链接字符加上 OSC 8 超链接标记。
mark_url_hyperlink64–66 ↗
fn mark_url_hyperlink(buf: &mut Buffer, area: Rect, url: &str)
作用:把终端画面里看起来像链接的那一段文字,标成真正可点击的超链接。这样即使登录网址太长换行了,用户也能点击完整链接。
数据流:进去的是一个终端绘图缓冲区、要检查的屏幕区域、以及 URL → 它把工作转交给通用的 terminal_hyperlinks 工具,给青色且带下划线的格子包上 OSC 8 超链接标记 → 出来没有新值,但缓冲区里的对应字符被改成可点击链接。
调用关系:登录浏览器页面渲染完 URL 后会调用它;相关测试也直接调用它,确认换行链接能完整标记,并且恶意控制字符会被清理。
调用图:调用 1 个内部函数(mark_url_hyperlink);被 3 处调用(render_continue_in_browser, mark_url_hyperlink_sanitizes_control_chars, mark_url_hyperlink_wraps_cyan_underlined_cells)。
mark_underlined_hyperlink69–71 ↗
fn mark_underlined_hyperlink(buf: &mut Buffer, area: Rect, url: &str)
作用:把指定区域里所有带下划线的文字都标成同一个可点击链接。它是给其他界面复用的小包装函数。
数据流:进去的是缓冲区、区域和 URL → 它调用通用超链接工具,寻找带下划线的字符并加上终端超链接标记 → 缓冲区被原地修改,没有返回值。
调用关系:这个文件里只提供这个入口,没有在本文件内直接使用;它把具体活儿交给 crate::terminal_hyperlinks。
调用图:调用 1 个内部函数(mark_underlined_hyperlink)。
onboarding_request_id97–99 ↗
fn onboarding_request_id() -> codex_app_server_protocol::RequestId
作用:给引导登录请求生成一个新的请求编号。请求编号就像快递单号,后台靠它区分不同请求。
数据流:进去不需要参数 → 它生成一个 UUID(一串几乎不会重复的随机编号),再包装成应用服务器协议需要的 RequestId → 返回这个请求编号。
调用关系:取消登录、保存 API key、开始 ChatGPT 登录时都会用它,保证每次发给后台的请求都有独立身份。
调用图:被 3 处调用(save_api_key, start_chatgpt_login, cancel_login_attempt);外部调用 2 个(new_v4, String)。
cancel_login_attempt101–113 ↗
async fn cancel_login_attempt(
request_handle: &AppServerRequestHandle,
login_id: String,
)
作用:告诉后台取消某一次正在进行的登录。用户按取消时,不只是界面退回去,也要让后台别继续等这次登录。
数据流:进去的是后台请求句柄和 login_id → 它生成请求编号,组装取消登录请求并异步发送 → 不返回有用结果,失败也被忽略,因为取消只是尽力而为。
调用关系:AuthModeWidget::cancel_active_attempt 会在浏览器登录或设备码登录中调用它,并放到 tokio 异步任务里执行,避免卡住界面。
调用图:调用 1 个内部函数(onboarding_request_id);被 1 处调用(cancel_active_attempt)。
ContinueWithDeviceCodeState::pending137–144 ↗
fn pending(request_id: String) -> Self
作用:创建一个“设备码登录还在申请中”的状态。此时只有请求编号,还没有给用户看的网址和验证码。
数据流:进去的是 request_id → 它把 login_id、verification_url、user_code 都设为空 → 返回一个表示等待后台回复的设备码状态。
调用关系:设备码登录子模块启动时会用它;测试也用它确认申请设备码期间动画应该被压住。
调用图:被 4 处调用(start_headless_chatgpt_login, device_code_attempt_matches_only_for_matching_request_id, pending_device_code_state, auth_widget_suppresses_animations_while_requesting_device_code)。
ContinueWithDeviceCodeState::ready146–158 ↗
fn ready(
request_id: String,
login_id: String,
verification_url: String,
user_code: String,
) -> Self
作用:创建一个“设备码已经拿到,可以给用户看了”的状态。用户这时可以去网页输入一次性代码完成登录。
数据流:进去的是请求编号、登录编号、验证网址、用户代码 → 它把这些都存进状态对象 → 返回一个完整可展示的设备码登录状态。
调用关系:设备码登录子模块收到后台结果后会用它;取消登录、成功回调和多个测试都依赖这个状态里的 login_id 和验证码信息。
调用图:被 4 处调用(start_headless_chatgpt_login, auth_widget_suppresses_animations_when_device_code_is_visible, cancel_active_attempt_notifies_device_code_login, device_code_login_completion_advances_to_success_message)。
ContinueWithDeviceCodeState::login_id160–162 ↗
fn login_id(&self) -> Option<&str>
作用:安全地取出这次设备码登录的 login_id。它可能还没准备好,所以返回的是“可能有,也可能没有”。
数据流:进去的是当前设备码状态 → 它查看内部 login_id 字段,并把 String 转成只读字符串引用 → 返回 Option<&str>,不修改状态。
调用关系:取消登录和登录完成通知匹配时会用它,确保只取消或完成当前这一次登录。
ContinueWithDeviceCodeState::is_showing_copyable_auth164–172 ↗
fn is_showing_copyable_auth(&self) -> bool
作用:判断设备码登录画面现在是不是已经有可复制的网址和验证码。这样界面才知道能不能显示复制提示。
数据流:进去的是当前设备码状态 → 它检查验证网址和用户代码是否都存在且不是空字符串 → 返回 true 或 false,不改任何数据。
调用关系:设备码登录渲染函数会调用它,决定是否展示可复制的认证信息。
调用图:被 1 处调用(render_device_code_login)。
AuthModeWidget::handle_key_event176–218 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:处理登录界面的按键。用户按上下、数字、确认、取消,都会从这里进入。
数据流:进去的是一个键盘事件 → 它先让 API key 输入框处理字符类按键;如果不是输入框事件,再处理移动、选择、确认、取消 → 输出没有返回值,但会改变高亮项、登录状态、错误信息,并可能触发重新绘制。
调用关系:这是 onboarding 屏幕交给 AuthModeWidget 的键盘入口;它会把具体动作分给 move_highlight、select_option_by_index、handle_sign_in_option 或 cancel_active_attempt。
调用图:调用 5 个内部函数(cancel_active_attempt, handle_api_key_entry_key_event, handle_sign_in_option, move_highlight, select_option_by_index);外部调用 1 个(info!)。
AuthModeWidget::handle_paste220–222 ↗
fn handle_paste(&mut self, pasted: String)
作用:处理用户粘贴内容,主要用于一次性粘贴 API key。
数据流:进去的是粘贴出来的字符串 → 它交给 API key 输入模式的粘贴处理函数 → 可能把文本追加到输入框,并请求刷新界面。
调用关系:外层键盘系统检测到粘贴时调用它;真正判断当前是不是 API key 输入状态的活儿交给 handle_api_key_entry_paste。
调用图:调用 1 个内部函数(handle_api_key_entry_paste)。
AuthModeWidget::set_animations_suppressed240–242 ↗
fn set_animations_suppressed(&self, suppressed: bool)
作用:手动设置是否暂时压住动画。这里的动画是终端里的闪光文字效果,不影响登录本身。
数据流:进去的是 suppressed 布尔值 → 它写入内部 Cell(一种可在不可变引用下改的小格子)→ 之后渲染时会按这个值决定是否播放动画。
调用关系:这是给外层或相关流程控制动画用的开关;should_suppress_animations 则会根据登录状态给出自动判断。
AuthModeWidget::should_suppress_animations244–249 ↗
fn should_suppress_animations(&self) -> bool
作用:判断当前登录状态下是不是应该停掉动画。正在等浏览器或设备码时,过多动画可能干扰用户复制链接和代码。
数据流:进去的是当前控件 → 它读取 sign_in_state,并检查是否处在浏览器登录或设备码登录状态 → 返回 true 或 false,不修改数据。
调用关系:测试会验证设备码相关状态会返回 true;外层可以据此决定是否压制动画。
调用图:外部调用 1 个(matches!)。
AuthModeWidget::cancel_active_attempt251–275 ↗
fn cancel_active_attempt(&self)
作用:取消当前正在进行的 ChatGPT 登录尝试,并把界面退回选择登录方式。它避免用户退出界面后后台还傻等着。
数据流:进去的是当前控件状态 → 它查看是不是浏览器登录或设备码登录;如果有 login_id,就异步通知后台取消;然后状态改回 PickMode,清掉错误并请求刷新 → 没有返回值,但界面和后台尝试都会被清理。
调用关系:用户按取消键时由 handle_key_event 调用;它会把后台取消请求交给 cancel_login_attempt,并用 schedule_frame 让界面立刻更新。
调用图:调用 3 个内部函数(set_error, cancel_login_attempt, schedule_frame);被 1 处调用(handle_key_event);外部调用 2 个(clone, spawn)。
AuthModeWidget::set_error277–279 ↗
fn set_error(&self, message: Option<String>)
作用:设置或清除登录界面上的错误提示。比如 API key 为空、后台返回失败、登录方式被禁用。
数据流:进去的是 Option<String>,有值表示错误文字,没有值表示清空 → 它写入共享的 error 锁里 → 后续渲染函数会读到并显示或隐藏错误。
调用关系:许多流程都会调用它:开始登录前清错,失败时写错,取消时清错;render_pick_mode 和 render_api_key_entry 会通过 error_message 读出来。
调用图:被 9 处调用(cancel_active_attempt, disallow_api_login, handle_api_key_entry_key_event, handle_api_key_entry_paste, on_account_login_completed, save_api_key, start_api_key_entry, start_chatgpt_login, start_device_code_login)。
AuthModeWidget::error_message281–283 ↗
fn error_message(&self) -> Option<String>
作用:取出当前要显示的错误文字。它给渲染代码用,避免渲染代码直接碰内部锁。
数据流:进去的是当前控件 → 它读取 error 里的 Option<String> 并复制一份 → 返回当前错误,或返回 None。
调用关系:选择登录方式画面和 API key 输入画面会调用它,把错误显示给用户。
调用图:被 2 处调用(render_api_key_entry, render_pick_mode)。
AuthModeWidget::is_api_key_entry_active286–290 ↗
fn is_api_key_entry_active(&self) -> bool
作用:告诉外界当前是不是正在输入 API key。外层可以据此调整光标、提示或快捷键行为。
数据流:进去的是当前控件 → 它读取登录状态,检查是否是 ApiKeyEntry → 返回 true 或 false,不修改状态。
调用关系:它是一个状态查询接口;不主动调用别的业务函数,供外层界面判断当前模式。
AuthModeWidget::api_key_entry_has_text293–297 ↗
fn api_key_entry_has_text(&self) -> bool
作用:告诉外界 API key 输入框里现在有没有内容。比如可以用来决定是否显示清空提示。
数据流:进去的是当前控件 → 它读取 ApiKeyEntry 状态里的 value,并检查是否为空 → 返回 true 或 false,不修改内容。
调用关系:这是给外层 UI 或快捷键逻辑用的轻量查询函数。
AuthModeWidget::confirm_binding299–301 ↗
fn confirm_binding(&self) -> KeyBinding
作用:取出“确认”这个动作当前显示用的按键。这样画面提示和真实按键配置能保持一致。
数据流:进去的是当前控件 → 它从 onboarding 的确认键列表里取第一个 → 返回一个 KeyBinding。
调用关系:多个渲染函数会用它显示“Press Enter to continue/save”一类提示。
AuthModeWidget::cancel_binding303–305 ↗
fn cancel_binding(&self) -> KeyBinding
作用:取出“取消”这个动作当前显示用的按键。用户看到的提示和实际可按的键来自同一份配置。
数据流:进去的是当前控件 → 它从取消键列表里取第一个 → 返回一个 KeyBinding。
调用关系:浏览器登录页和 API key 输入页会用它显示如何取消或返回。
AuthModeWidget::is_api_login_allowed307–309 ↗
fn is_api_login_allowed(&self) -> bool
作用:判断当前工作区是否允许用 API key 登录。有些环境会强制只能用 ChatGPT 登录。
数据流:进去的是当前控件配置 → 它查看 forced_login_method 是否是 Chatgpt → 返回 false 表示 API key 被禁用,否则返回 true。
调用关系:展示选项、处理选择、开始或保存 API key 前都会调用它,防止禁用的登录方式被绕过。
调用图:被 6 处调用(displayed_sign_in_options, handle_sign_in_option, render_pick_mode, save_api_key, selectable_sign_in_options, start_api_key_entry);外部调用 1 个(matches!)。
AuthModeWidget::is_chatgpt_login_allowed311–313 ↗
fn is_chatgpt_login_allowed(&self) -> bool
作用:判断当前工作区是否允许 ChatGPT 登录。有些环境会强制只能用 API key。
数据流:进去的是当前控件配置 → 它查看 forced_login_method 是否是 Api → 返回 false 表示 ChatGPT 类登录被禁用,否则返回 true。
调用关系:选项列表、选中处理和渲染说明都会用它,让界面和实际规则一致。
调用图:被 4 处调用(displayed_sign_in_options, handle_sign_in_option, render_pick_mode, selectable_sign_in_options);外部调用 1 个(matches!)。
AuthModeWidget::displayed_sign_in_options315–324 ↗
fn displayed_sign_in_options(&self) -> Vec<SignInOption>
作用:算出界面上要显示哪些登录选项。它关心“给用户看什么”。
数据流:进去的是当前配置 → 它默认放入 ChatGPT 选项,再按允许规则加入设备码和 API key → 返回一个按显示顺序排列的选项列表。
调用关系:render_pick_mode 用它画菜单;select_option_by_index 用它让数字键对应屏幕上的第几个选项。
调用图:调用 2 个内部函数(is_api_login_allowed, is_chatgpt_login_allowed);被 2 处调用(render_pick_mode, select_option_by_index);外部调用 1 个(vec!)。
AuthModeWidget::selectable_sign_in_options326–336 ↗
fn selectable_sign_in_options(&self) -> Vec<SignInOption>
作用:算出用户实际能用上下键选到哪些登录选项。它关心“哪些能被选中”。
数据流:进去的是当前配置 → 它只把允许的登录方式放进列表 → 返回可选择的选项列表。
调用关系:move_highlight 调用它来跳过不可选项,避免高亮停在被禁用的登录方式上。
调用图:调用 2 个内部函数(is_api_login_allowed, is_chatgpt_login_allowed);被 1 处调用(move_highlight);外部调用 1 个(new)。
AuthModeWidget::move_highlight338–351 ↗
fn move_highlight(&mut self, delta: isize)
作用:把菜单高亮项向上或向下移动一格。到头后会绕回另一头,像循环菜单。
数据流:进去的是 delta,通常是 -1 或 1 → 它取出可选择选项,找到当前选项位置,再用循环取模算出下一个位置 → 修改 highlighted_mode,不直接启动登录。
调用关系:handle_key_event 在用户按上下键时调用它;它依赖 selectable_sign_in_options 来尊重登录方式限制。
调用图:调用 1 个内部函数(selectable_sign_in_options);被 1 处调用(handle_key_event)。
AuthModeWidget::select_option_by_index353–358 ↗
fn select_option_by_index(&mut self, index: usize)
作用:根据数字键选择屏幕上的第几个登录方式。比如按 1、2、3 时会走这里。
数据流:进去的是从 0 开始的 index → 它取当前显示的选项列表,找到对应项 → 如果存在,就交给 handle_sign_in_option 执行选择;不存在则什么都不做。
调用关系:handle_key_event 在用户按快捷数字键时调用它;它随后把真正的登录动作交给 handle_sign_in_option。
调用图:调用 2 个内部函数(displayed_sign_in_options, handle_sign_in_option);被 1 处调用(handle_key_event)。
AuthModeWidget::handle_sign_in_option360–380 ↗
fn handle_sign_in_option(&mut self, option: SignInOption)
作用:执行用户选中的登录方式。它是从“菜单选择”走向“开始某种登录流程”的分岔口。
数据流:进去的是 SignInOption → 它检查这种方式是否被允许;ChatGPT 会启动浏览器登录,DeviceCode 会启动设备码登录,ApiKey 会进入输入框或显示禁用错误 → 改变登录状态并可能发起后台请求。
调用关系:确认键和数字选择都会走到这里;它把具体工作分给 start_chatgpt_login、start_device_code_login、start_api_key_entry 或 disallow_api_login。
调用图:调用 6 个内部函数(disallow_api_login, is_api_login_allowed, is_chatgpt_login_allowed, start_api_key_entry, start_chatgpt_login, start_device_code_login);被 2 处调用(handle_key_event, select_option_by_index)。
AuthModeWidget::disallow_api_login382–387 ↗
fn disallow_api_login(&mut self)
作用:在 API key 登录被禁用时,给用户一个明确提示,并退回可用的登录选项。
数据流:进去的是当前控件 → 它把高亮改回 ChatGPT,写入“API key login is disabled.”错误,把状态设为 PickMode,再请求刷新 → 用户会看到禁用提示。
调用关系:选择、进入、保存 API key 前发现被禁用时都会调用它,确保界面和策略都不允许继续。
调用图:调用 2 个内部函数(set_error, schedule_frame);被 3 处调用(handle_sign_in_option, save_api_key, start_api_key_entry)。
AuthModeWidget::render_pick_mode389–489 ↗
fn render_pick_mode(&self, area: Rect, buf: &mut Buffer)
作用:画出“请选择登录方式”的页面。用户最先看到的登录菜单就是它生成的。
数据流:进去的是屏幕区域和绘图缓冲区 → 它根据允许规则拼出说明文字、登录选项、高亮样式、按键提示和错误信息 → 把这些文字写进缓冲区显示出来。
调用关系:render_ref 发现当前状态是 PickMode 时调用它;它会询问 displayed_sign_in_options、is_api_login_allowed、is_chatgpt_login_allowed 和 error_message。
调用图:调用 4 个内部函数(displayed_sign_in_options, error_message, is_api_login_allowed, is_chatgpt_login_allowed);被 1 处调用(render_ref);外部调用 3 个(from, new, vec!)。
AuthModeWidget::render_continue_in_browser491–544 ↗
fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer)
作用:画出“请到浏览器完成登录”的页面。它会显示登录链接和取消提示。
数据流:进去的是屏幕区域和缓冲区 → 它读取当前浏览器登录状态,显示等待文字;如果有 auth_url,就显示 URL,并把 URL 标成可点击超链接;动画开启时还会安排下一帧 → 缓冲区变成浏览器登录提示页。
调用关系:render_ref 在 ChatGptContinueInBrowser 状态下调用它;它用 shimmer_text 做动效,用 mark_url_hyperlink 修复终端长链接点击问题。
调用图:调用 3 个内部函数(shimmer_text, mark_url_hyperlink, schedule_frame_in);被 1 处调用(render_ref);外部调用 4 个(from, new, from_millis, vec!)。
AuthModeWidget::render_chatgpt_success_message546–591 ↗
fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer)
作用:画出 ChatGPT 登录成功后的安全提醒页。它不是简单说成功,还提醒用户 Codex 可能运行命令、写代码,要自己审查。
数据流:进去的是屏幕区域和缓冲区 → 它组织成功标记、使用说明、安全文档链接、训练数据偏好链接和继续按键提示 → 写入缓冲区。
调用关系:render_ref 在 ChatGptSuccessMessage 状态调用它;用户再按确认时,handle_key_event 会把状态推进到 ChatGptSuccess。
调用图:被 1 处调用(render_ref);外部调用 2 个(new, vec!)。
AuthModeWidget::render_chatgpt_success593–603 ↗
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer)
作用:画出简短的 ChatGPT 已登录状态。这个状态表示认证步骤已经可以算完成。
数据流:进去的是屏幕区域和缓冲区 → 它写入一行绿色成功提示 → 缓冲区显示“已用 ChatGPT 账号登录”。
调用关系:render_ref 在 ChatGptSuccess 状态调用它;get_step_state 会把这个状态报告为 Complete。
调用图:被 1 处调用(render_ref);外部调用 2 个(new, vec!)。
AuthModeWidget::render_api_key_configured605–615 ↗
fn render_api_key_configured(&self, area: Rect, buf: &mut Buffer)
作用:画出 API key 已保存成功的页面。用户能知道之后会按 API key 的用量计费方式工作。
数据流:进去的是屏幕区域和缓冲区 → 它写入绿色成功提示和简短说明 → 缓冲区显示 API key 已配置。
调用关系:render_ref 在 ApiKeyConfigured 状态调用它;get_step_state 会把这个状态报告为 Complete。
调用图:被 1 处调用(render_ref);外部调用 2 个(new, vec!)。
AuthModeWidget::render_api_key_entry617–682 ↗
fn render_api_key_entry(&self, area: Rect, buf: &mut Buffer, state: &ApiKeyInputState)
作用:画出 API key 输入页面,包括说明、输入框、保存/返回提示和错误信息。
数据流:进去的是屏幕区域、缓冲区和当前输入状态 → 它把区域分成说明、输入框、底部提示三块;如果从环境变量预填了 key,会额外说明;输入框为空时显示占位文字,否则显示当前内容 → 写入完整输入界面。
调用关系:render_ref 在 ApiKeyEntry 状态调用它;它用 error_message 显示校验或保存失败原因。
调用图:调用 1 个内部函数(error_message);被 1 处调用(render_ref);外部调用 8 个(default, Length, Min, vertical, from, new, default, vec!)。
AuthModeWidget::handle_api_key_entry_key_event684–744 ↗
fn handle_api_key_entry_key_event(&mut self, key_event: &KeyEvent) -> bool
作用:在 API key 输入模式下处理键盘:输入字符、退格、保存、取消。它会拦住这些键,避免被菜单逻辑误处理。
数据流:进去的是键盘事件 → 它先确认当前确实是 ApiKeyEntry;取消会回菜单,确认会校验并准备保存,退格和普通字符会修改输入内容;需要保存时调用 save_api_key → 返回 true 表示事件已处理,false 表示不是输入模式。
调用关系:handle_key_event 最先调用它;如果它处理了事件,后面的菜单上下移动和确认逻辑就不会再运行。
调用图:调用 3 个内部函数(save_api_key, set_error, schedule_frame);被 1 处调用(handle_key_event)。
AuthModeWidget::handle_api_key_entry_paste746–768 ↗
fn handle_api_key_entry_paste(&mut self, pasted: String) -> bool
作用:在 API key 输入模式下处理粘贴。这样用户可以直接粘贴整串 key,不必一个字符一个字符输入。
数据流:进去的是粘贴文本 → 它去掉前后空白;如果当前是 API key 输入模式,就替换环境变量预填内容或追加到已有内容,清掉错误并请求刷新 → 返回是否真的处理了这次粘贴。
调用关系:handle_paste 会把所有粘贴交给它;它只在 ApiKeyEntry 状态工作。
调用图:调用 2 个内部函数(set_error, schedule_frame);被 1 处调用(handle_paste)。
AuthModeWidget::start_api_key_entry770–798 ↗
fn start_api_key_entry(&mut self)
作用:进入 API key 输入流程。它还会尝试读取 OPENAI_API_KEY 环境变量来帮用户预填。
数据流:进去的是当前控件 → 它先检查 API key 登录是否允许;允许时清错,读取环境变量,如果已经在输入状态且为空就填入,否则创建新的 ApiKeyEntry 状态 → 请求刷新界面。
调用关系:用户选择 API key 登录时由 handle_sign_in_option 调用;如果被策略禁用,会转去 disallow_api_login。
调用图:调用 4 个内部函数(disallow_api_login, is_api_login_allowed, set_error, schedule_frame);被 1 处调用(handle_sign_in_option);外部调用 2 个(read_openai_api_key_from_env, ApiKeyEntry)。
AuthModeWidget::save_api_key800–844 ↗
fn save_api_key(&mut self, api_key: String)
作用:把用户输入的 API key 发给后台保存。保存成功后,这个认证步骤就完成了。
数据流:进去的是 API key 字符串 → 它检查是否允许 API 登录,清错,然后异步向后台发送 LoginAccount::ApiKey 请求;成功则状态变 ApiKeyConfigured,失败则恢复输入状态并显示错误 → 会请求界面刷新。
调用关系:handle_api_key_entry_key_event 在用户按确认且内容不空时调用它;它用 onboarding_request_id 给后台请求编号,并把网络等待放进 tokio 任务。
调用图:调用 5 个内部函数(disallow_api_login, is_api_login_allowed, set_error, onboarding_request_id, schedule_frame);被 1 处调用(handle_api_key_entry_key_event);外部调用 5 个(clone, format!, spawn, ApiKeyEntry, clone)。
AuthModeWidget::handle_existing_chatgpt_login846–857 ↗
fn handle_existing_chatgpt_login(&mut self) -> bool
作用:检查用户是不是其实已经有 ChatGPT 登录了。如果已经登录,就不重复打开新登录流程。
数据流:进去的是当前控件 → 它查看 login_status 是否表示已有 ChatGPT 类账号 → 如果是,就把状态设为 ChatGptSuccess、请求刷新并返回 true;否则返回 false。
调用关系:start_chatgpt_login 和 start_device_code_login 开始前都会先调用它,避免多余登录。
调用图:调用 1 个内部函数(schedule_frame);被 2 处调用(start_chatgpt_login, start_device_code_login);外部调用 1 个(matches!)。
AuthModeWidget::start_chatgpt_login860–904 ↗
fn start_chatgpt_login(&mut self)
作用:启动标准 ChatGPT 登录流程,通常会让用户去浏览器完成认证。
数据流:进去的是当前控件 → 它先检查是否已经登录;没有的话清错,异步向后台申请 ChatGPT 登录;成功拿到 login_id 和 auth_url 后尝试打开浏览器,并把状态改成等待浏览器完成;失败则回菜单并显示错误 → 界面会刷新。
调用关系:handle_sign_in_option 在用户选择 ChatGPT 时调用它;它用 maybe_open_auth_url_in_browser 尝试开浏览器,用 onboarding_request_id 标记后台请求。
调用图:调用 4 个内部函数(handle_existing_chatgpt_login, set_error, maybe_open_auth_url_in_browser, onboarding_request_id);被 1 处调用(handle_sign_in_option);外部调用 5 个(clone, format!, spawn, ChatGptContinueInBrowser, clone)。
AuthModeWidget::start_device_code_login906–913 ↗
fn start_device_code_login(&mut self)
作用:启动设备码登录流程,适合远程机器、无浏览器环境,用户可以在另一台设备上输入一次性代码。
数据流:进去的是当前控件 → 它先检查是否已经有 ChatGPT 登录;没有的话清错,然后把流程交给 headless_chatgpt_login 子模块 → 后续状态会进入设备码申请或展示。
调用关系:handle_sign_in_option 在用户选择 Device Code 时调用它;具体申请设备码和渲染细节不在本文件,而在 headless_chatgpt_login。
调用图:调用 3 个内部函数(handle_existing_chatgpt_login, set_error, start_headless_chatgpt_login);被 1 处调用(handle_sign_in_option)。
AuthModeWidget::on_account_login_completed915–943 ↗
fn on_account_login_completed(
&mut self,
notification: AccountLoginCompletedNotification,
)
作用:处理后台发来的“某次登录完成了”通知。它只响应当前这次登录,避免别的旧登录通知误改界面。
数据流:进去的是完成通知,里面可能有 login_id、成功标记和错误 → 它先用 login_id 对比当前浏览器登录或设备码登录;匹配后,成功则进入成功提醒页,失败则回菜单并显示错误 → 请求刷新界面。
调用关系:后台账户登录完成事件到达时由外层调用;它依赖当前状态里的 login_id 来确认通知属于本控件正在等待的那次登录。
调用图:调用 2 个内部函数(set_error, schedule_frame);外部调用 1 个(matches!)。
AuthModeWidget::on_account_updated945–950 ↗
fn on_account_updated(&mut self, notification: AccountUpdatedNotification)
作用:处理后台发来的账户状态更新。它让控件知道当前到底是已登录还是未登录。
数据流:进去的是 AccountUpdatedNotification → 它取出通知里的 auth_mode;有则转成 LoginStatus::AuthMode,没有则设为 NotAuthenticated → 更新控件里的 login_status。
调用关系:外层收到账号变更通知时会调用它;之后 handle_existing_chatgpt_login 会根据这个状态决定是否跳过新登录。
AuthModeWidget::get_step_state954–964 ↗
fn get_step_state(&self) -> StepState
作用:告诉外层引导流程:认证这一步是还没完,还是已经完成。
数据流:进去的是当前控件 → 它读取 sign_in_state;选择中、输入中、等待登录、成功提醒页都算 InProgress,真正 ChatGPT 成功或 API key 配好才算 Complete → 返回 StepState。
调用关系:外层 onboarding screen 通过 StepStateProvider 调用它,用来决定能不能进入下一步。
AuthModeWidget::render_ref968–993 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:认证控件的总渲染入口。它根据当前状态,把画面分派给对应的具体渲染函数。
数据流:进去的是屏幕区域和缓冲区 → 它读取 sign_in_state,然后选择画菜单、浏览器等待、设备码、成功提醒、成功摘要、API key 输入或 API key 成功页 → 缓冲区被写成当前该显示的页面。
调用关系:ratatui 终端 UI 框架绘制这个控件时调用它;设备码页面会转给 headless_chatgpt_login::render_device_code_login。
调用图:调用 7 个内部函数(render_api_key_configured, render_api_key_entry, render_chatgpt_success, render_chatgpt_success_message, render_continue_in_browser, render_pick_mode, render_device_code_login)。
maybe_open_auth_url_in_browser996–1004 ↗
fn maybe_open_auth_url_in_browser(request_handle: &AppServerRequestHandle, url: &str)
作用:在合适的情况下自动打开浏览器里的登录链接。它只对进程内后台句柄这样做,避免在不合适的连接方式下乱开浏览器。
数据流:进去的是后台请求句柄和 URL → 它先判断句柄是不是 InProcess;不是就直接返回;是的话调用系统浏览器打开 URL,失败只写警告日志 → 没有返回值。
调用关系:start_chatgpt_login 拿到 auth_url 后调用它;如果打不开,界面仍然会显示链接让用户手动打开。
调用图:被 1 处调用(start_chatgpt_login);外部调用 3 个(matches!, warn!, open)。
tests::widget_forced_chatgpt1023–1075 ↗
async fn widget_forced_chatgpt() -> (AuthModeWidget, TempDir)
作用:搭一个测试用的 AuthModeWidget,并把配置设成“强制 ChatGPT 登录”。这样测试可以验证 API key 被禁用时的行为。
数据流:进去不需要参数 → 它创建临时目录、测试配置、进程内应用服务器客户端和 AuthModeWidget → 返回控件和临时目录,供异步测试使用。
调用关系:本文件多个测试都先调用它,获得一套隔离的登录控件和后台环境。
调用图:调用 5 个内部函数(start, default, default_for_tests, new, test_dummy);外部调用 12 个(new, default, new, new, new, InProcess, default, cloud_config_bundle_loader_for_storage, default, from_value (+2 more))。
tests::api_key_flow_disabled_when_chatgpt_forced1078–1091 ↗
async fn api_key_flow_disabled_when_chatgpt_forced()
作用:测试当工作区强制 ChatGPT 登录时,进入 API key 输入流程会被挡住。
数据流:进去是测试运行环境 → 它创建强制 ChatGPT 的控件,调用 start_api_key_entry → 断言错误提示是 API key 被禁用,状态仍然回到 PickMode。
调用关系:它验证 start_api_key_entry 会走 disallow_api_login,而不是偷偷允许 API key。
调用图:外部调用 3 个(assert!, assert_eq!, widget_forced_chatgpt)。
tests::saving_api_key_is_blocked_when_chatgpt_forced1094–1108 ↗
async fn saving_api_key_is_blocked_when_chatgpt_forced()
作用:测试即使直接调用保存 API key,在强制 ChatGPT 的配置下也不能保存。
数据流:进去是测试运行环境 → 它创建控件,调用 save_api_key("sk-test") → 断言显示禁用错误、状态是 PickMode、登录状态仍未认证。
调用关系:它防止有人绕过输入界面直接调用保存函数;验证 save_api_key 自己也会检查策略。
调用图:外部调用 3 个(assert!, assert_eq!, widget_forced_chatgpt)。
tests::existing_non_oauth_chatgpt_login_counts_as_signed_in1111–1127 ↗
async fn existing_non_oauth_chatgpt_login_counts_as_signed_in()
作用:测试已有的非 OAuth ChatGPT 登录方式也算“已经登录”。OAuth 可以理解成网页授权,这里确认其他令牌方式也被认可。
数据流:进去是测试运行环境 → 它分别设置两种已有 ChatGPT 认证状态,调用 handle_existing_chatgpt_login → 断言返回 true,状态变成 ChatGptSuccess。
调用关系:它覆盖 start_chatgpt_login 和 start_device_code_login 依赖的提前跳过逻辑。
调用图:外部调用 4 个(assert!, assert_eq!, AuthMode, widget_forced_chatgpt)。
tests::cancel_active_attempt_resets_browser_login_state1130–1146 ↗
async fn cancel_active_attempt_resets_browser_login_state()
作用:测试取消浏览器登录时,界面会清掉错误并回到选择登录方式。
数据流:进去是测试运行环境 → 它把控件放进 ChatGptContinueInBrowser 状态并设置错误,再调用 cancel_active_attempt → 断言错误清空,状态回 PickMode。
调用关系:它验证用户按取消键时 cancel_active_attempt 对浏览器登录状态的本地清理。
调用图:外部调用 4 个(assert!, assert_eq!, ChatGptContinueInBrowser, widget_forced_chatgpt)。
tests::cancel_active_attempt_notifies_device_code_login1149–1167 ↗
async fn cancel_active_attempt_notifies_device_code_login()
作用:测试取消设备码登录时,控件同样会退回菜单并清错。
数据流:进去是测试运行环境 → 它创建 ready 的设备码状态,设置错误,调用 cancel_active_attempt → 断言错误清空,状态回 PickMode。
调用关系:它覆盖 cancel_active_attempt 对 ChatGptDeviceCode 状态的处理,并间接使用 ContinueWithDeviceCodeState::ready。
调用图:调用 1 个内部函数(ready);外部调用 4 个(assert!, assert_eq!, ChatGptDeviceCode, widget_forced_chatgpt)。
tests::collect_osc8_chars1171–1186 ↗
fn collect_osc8_chars(buf: &Buffer, area: Rect, url: &str) -> String
作用:测试辅助函数:从缓冲区里找出被 OSC 8 超链接包住的字符。它帮助测试确认哪些字真的变成可点击链接了。
数据流:进去的是缓冲区、区域和 URL → 它逐格扫描,找形如“打开超链接 + 字符 + 关闭超链接”的内容 → 返回这些内部字符拼起来的字符串。
调用关系:链接相关测试会调用它,对 mark_url_hyperlink 和浏览器登录页面的渲染结果做断言。
tests::continue_in_browser_renders_osc8_hyperlink1189–1207 ↗
fn continue_in_browser_renders_osc8_hyperlink()
作用:测试浏览器登录页面里的长 URL 即使换行,也会完整带上 OSC 8 超链接标记。
数据流:进去是测试运行环境 → 它创建控件,设置浏览器登录状态,在很窄的缓冲区里渲染,让 URL 换行 → 用 collect_osc8_chars 取回被标记字符,并断言等于完整 URL。
调用关系:它验证 render_continue_in_browser 会调用 mark_url_hyperlink,解决终端自动识别长链接不可靠的问题。
调用图:外部调用 7 个(empty, new, assert_eq!, new, ChatGptContinueInBrowser, collect_osc8_chars, widget_forced_chatgpt)。
tests::auth_widget_suppresses_animations_when_device_code_is_visible1210–1222 ↗
fn auth_widget_suppresses_animations_when_device_code_is_visible()
作用:测试设备码已经显示时,控件认为应该压制动画。
数据流:进去是测试运行环境 → 它把控件设为 ready 的设备码状态 → 调用 should_suppress_animations,并断言结果为 true。
调用关系:它验证设备码页面不会被动画干扰,尤其是用户需要复制网址和验证码时。
调用图:调用 1 个内部函数(ready);外部调用 4 个(assert_eq!, new, ChatGptDeviceCode, widget_forced_chatgpt)。
tests::auth_widget_suppresses_animations_while_requesting_device_code1225–1233 ↗
fn auth_widget_suppresses_animations_while_requesting_device_code()
作用:测试设备码还在申请中时,也应该压制动画。
数据流:进去是测试运行环境 → 它把控件设为 pending 的设备码状态 → 调用 should_suppress_animations,并断言为 true。
调用关系:它覆盖 ContinueWithDeviceCodeState::pending 对动画判断的影响。
调用图:调用 1 个内部函数(pending);外部调用 4 个(assert_eq!, new, ChatGptDeviceCode, widget_forced_chatgpt)。
tests::device_code_login_completion_advances_to_success_message1236–1256 ↗
async fn device_code_login_completion_advances_to_success_message()
作用:测试设备码登录成功通知到达后,界面会进入 ChatGPT 成功提醒页。
数据流:进去是测试运行环境 → 它把控件设为某个 login_id 的设备码登录状态,再发送同一个 login_id 的成功通知 → 断言状态变成 ChatGptSuccessMessage。
调用关系:它验证 on_account_login_completed 会正确匹配设备码登录,并推动成功流程。
调用图:调用 1 个内部函数(ready);外部调用 3 个(assert!, ChatGptDeviceCode, widget_forced_chatgpt)。
tests::mark_url_hyperlink_wraps_cyan_underlined_cells1259–1282 ↗
fn mark_url_hyperlink_wraps_cyan_underlined_cells()
作用:测试只有青色并带下划线的字符会被标成 URL 超链接,普通字符不会被碰。
数据流:进去是测试运行环境 → 它手工在缓冲区写入几个青色下划线字符和一个普通 X,调用 mark_url_hyperlink → 断言链接字符被收集到,普通 X 保持原样。
调用关系:它直接验证本文件的 mark_url_hyperlink 包装函数对终端缓冲区的效果。
调用图:调用 1 个内部函数(mark_url_hyperlink);外部调用 4 个(empty, new, assert_eq!, collect_osc8_chars)。
tests::mark_url_hyperlink_sanitizes_control_chars1285–1311 ↗
fn mark_url_hyperlink_sanitizes_control_chars()
作用:测试 URL 里如果混入终端控制字符,会被清理掉,不能破坏 OSC 8 超链接格式。
数据流:进去是测试运行环境 → 它准备一个带 ESC 和 BEL 控制字符的恶意 URL,调用 mark_url_hyperlink → 断言结果包含清理后的 URL,且不包含原始危险控制序列。
调用关系:它验证链接标记工具的安全性,防止登录链接文字被用来注入终端控制序列。
调用图:调用 1 个内部函数(mark_url_hyperlink);外部调用 3 个(empty, new, assert!)。
tui/src/onboarding/trust_directory.rs源码 ↗
这个文件像一个进门前的安全确认牌。程序发现你正在某个目录里工作时,会先问你:你信不信这个目录里的内容?这很重要,因为项目本地可能带有配置、钩子脚本、执行策略等东西;如果目录不可信,随便加载就可能带来“提示注入”(恶意内容诱导 AI 做不该做的事)之类风险。TrustDirectoryWidget 保存当前目录、真正要信任的目标目录、当前高亮的是“继续”还是“退出”、是否已经选择、是否要退出,以及要显示的错误信息。它会把这些状态画成终端里的文字界面;用户按上下键移动选择,按确认键执行选择。如果当前目录只是 Git 仓库的子目录,它还会提醒用户:信任会作用到仓库根目录,而不是只作用到眼前这个小目录。
TrustDirectoryWidget::render_ref41–125 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:把“是否信任这个目录”的问题画到终端屏幕上。用户看到的说明文字、警告、两个选项、错误提示和按键提示,都是这里拼出来的。
数据流:进去的是可绘制区域和屏幕缓冲区,同时读取这个控件里的当前目录、信任目标、当前高亮选项、错误信息等状态;它把这些信息组织成一列文字和段落,必要时加上颜色、缩进和自动换行;最后把结果写进终端缓冲区,让界面显示出来。
调用关系:它是这个控件的“画面输出”环节,由终端 UI 框架在需要刷新屏幕时调用。它会借助 ColumnRenderable 这样的排版工具、selection_option_row 这样的选项行工具,以及 Insets 这样的缩进设置,把状态变成用户能看的界面。
调用图:调用 3 个内部函数(tlbr, new, selection_option_row);外部调用 4 个(from, new, format!, vec!)。
TrustDirectoryWidget::handle_key_event129–151 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:处理用户按键。它决定按上、下、确认、取消、退出等键时,界面应该只是移动光标,还是正式选择信任或退出。
数据流:进去的是一次键盘事件;它先忽略“按键松开”事件,避免一次按键被处理两遍;然后根据按键类型修改 highlighted、selection 或 should_quit。结果是控件状态发生变化,比如高亮到“退出”,或者记录用户选择“信任”。
调用关系:它是这个控件的“输入入口”。 onboarding 页面收到键盘事件后会交给它;当用户选择继续时,它把活交给 TrustDirectoryWidget::handle_trust;当用户选择退出、取消或按退出键时,它把活交给 TrustDirectoryWidget::handle_quit。
调用图:调用 2 个内部函数(handle_quit, handle_trust)。
TrustDirectoryWidget::get_step_state155–161 ↗
fn get_step_state(&self) -> StepState
作用:告诉外层 onboarding 流程:这一步完成了没有。只要用户已经选了信任,或者已经决定退出,这一步就算结束。
数据流:进去的是当前控件状态;它查看 selection 有没有值,或者 should_quit 是否为真;出来的是 StepState::Complete 或 StepState::InProgress,也就是“完成”或“还在进行中”。
调用关系:它给 onboarding 流程做进度判断。外层步骤管理器会用它决定是否可以离开这个信任确认页面,进入下一步,或者开始退出流程。
TrustDirectoryWidget::handle_trust165–169 ↗
fn handle_trust(&mut self)
作用:执行“用户选择信任目录”的状态更新。它不直接写配置,只是在这个界面里记录用户已经选择继续。
数据流:进去的是当前控件的可修改状态;它把高亮项设为 Trust,清掉之前的错误信息,并把 selection 设成 Trust;出来的是一个已经标记为“用户同意信任”的控件状态。
调用关系:它只由 TrustDirectoryWidget::handle_key_event 在用户按下对应快捷键或确认“继续”时调用。之后外层流程会看到 selection 已经有值,并通过 TrustDirectoryWidget::get_step_state 判断这一步完成。
调用图:被 1 处调用(handle_key_event)。
TrustDirectoryWidget::handle_quit171–174 ↗
fn handle_quit(&mut self)
作用:执行“用户不信任并退出”的状态更新。它把界面状态改成退出意图,让外层程序知道不要继续往下走。
数据流:进去的是当前控件的可修改状态;它把高亮项设为 Quit,并把 should_quit 设成 true;出来的是一个明确表示“用户要退出”的控件状态。
调用关系:它由 TrustDirectoryWidget::handle_key_event 在用户选择“No, quit”、按退出键或取消键时调用。之后外层流程可以通过 TrustDirectoryWidget::should_quit 或 TrustDirectoryWidget::get_step_state 发现该结束了。
调用图:被 1 处调用(handle_key_event)。
TrustDirectoryWidget::should_quit176–178 ↗
fn should_quit(&self) -> bool
作用:给外层代码一个简单答案:用户是不是已经要求退出。它是读取退出标记的小窗口。
数据流:进去的是当前控件状态;它读取 should_quit 这个布尔值;出来的是 true 或 false,不改动任何东西。
调用关系:外层 onboarding 或应用主流程在决定是否关闭程序时会用它。它不调用别的函数,只负责把内部退出状态安全地暴露出来。
tests::widget194–204 ↗
fn widget(error: Option<String>) -> TrustDirectoryWidget
作用:给测试快速造一个 TrustDirectoryWidget。这样每个测试不用重复写一大段初始化代码。
数据流:进去的是一个可选错误信息;它填好固定的当前目录、信任目标、默认高亮项、退出标记等字段;出来的是一个可用于测试的控件实例。
调用关系:它是测试里的小帮手,被渲染快照测试调用。测试用它造出“正常无错误”和“带错误提示”两种界面状态。
调用图:外部调用 1 个(from)。
tests::release_event_does_not_change_selection207–228 ↗
fn release_event_does_not_change_selection()
作用:验证松开按键不会被当成真正操作。这样可以避免用户按一次回车,程序因为同时收到按下和松开而误触发两次。
数据流:进去的是一个初始高亮在 Quit 的测试控件;测试先送入一个回车的 Release 事件,确认 selection 仍然为空;再送入真正的回车按下事件,确认 should_quit 变成 true。
调用关系:它直接测试 TrustDirectoryWidget::handle_key_event 的一个重要防误触行为。这个测试保证键盘事件处理只响应有效的按下动作,而不是响应松开动作。
调用图:外部调用 4 个(new, from, assert!, assert_eq!)。
tests::renders_snapshot_for_git_repo231–241 ↗
fn renders_snapshot_for_git_repo()
作用:检查正常情况下的信任目录界面长什么样。它用快照测试,也就是把终端画面和之前保存的标准画面做对比。
数据流:进去的是一个无错误信息的测试控件和一个固定大小的虚拟终端;测试让控件渲染到终端里;出来的是一次快照断言,通过就说明画面没有意外变化。
调用关系:它调用 tests::widget 生成控件,再间接测试 TrustDirectoryWidget::render_ref 的输出。这个测试保护的是用户看到的文字、排版和选项显示。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_snapshot!, new, widget)。
tests::renders_snapshot_for_trust_error244–257 ↗
fn renders_snapshot_for_trust_error()
作用:检查出现信任失败错误时,界面是否正确显示错误信息。这样可以保证用户不会只看到界面卡住,却不知道哪里出了问题。
数据流:进去的是一段模拟的错误文字;测试把它放进测试控件,再渲染到固定大小的虚拟终端;出来的是快照对比结果,用来确认错误提示的颜色、换行和位置没有乱掉。
调用关系:它也通过 tests::widget 生成控件,并覆盖 TrustDirectoryWidget::render_ref 的错误分支。它和正常渲染测试一起,保证这个 onboarding 页面在成功和失败场景下都可读。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_snapshot!, new, widget)。
tui/src/external_agent_config_migration.rs源码 ↗
当系统发现可以从外部代理工具导入配置时,不能直接悄悄改用户环境,所以需要这个交互界面。它像一个收银台确认单:先给用户看汇总,用户可以点“导入选中的”、取消,或者进入“自定义”逐项勾选。文件里保存了界面的当前状态,比如正在看汇总还是自定义、光标在哪、哪些项目被勾上、滚动到哪里。键盘输入会改变这些状态,比如上下移动、空格勾选、Esc 取消、数字快捷选择按钮。它还把导入项整理成适合显示的文字,并把太长的插件列表折叠成简短摘要。真正画界面的细节放在同目录的 render 模块里;这个文件主要决定“界面该显示什么内容”和“用户按键后该发生什么”。
run_external_agent_config_migration_prompt77–115 ↗
async fn run_external_agent_config_migration_prompt(
tui: &mut Tui,
items: &[ExternalAgentConfigMigrationItem],
selected_items: &[ExternalAgentConfigMigrationItem],
error: Option<&str>
作用:启动并运行整个导入确认界面,直到用户选择继续导入或取消。调用方只要把候选导入项交进来,就能拿到最后结果。
数据流:进去的是终端界面对象、所有可导入项目、默认选中的项目,以及可选错误信息 → 它创建屏幕状态,先画一次界面,然后不断读取键盘、重绘、窗口变化等事件 → 出来的是用户最终选择:继续并带上选中的项目,或跳过。
调用关系:它由 handle_external_agent_config_migration_prompt 调用,是这个提示框的外层运行循环。它把具体状态变化交给 ExternalAgentConfigMigrationScreen::handle_key,把绘制交给 Tui 的 draw 和 render 模块。
调用图:调用 1 个内部函数(new);被 1 处调用(handle_external_agent_config_migration_prompt);外部调用 4 个(draw, event_stream, frame_requester, pin!)。
ExternalAgentConfigMigrationScreen::proceed_enabled132–134 ↗
fn proceed_enabled(&self) -> bool
作用:判断现在能不能点“导入选中的”。如果一个项目都没选,就不应该让用户直接继续。
数据流:进去的是当前屏幕状态 → 它数一数已勾选项目数量 → 返回 true 或 false,不改动界面。
调用关系:available_actions 会用它决定动作菜单里是否出现 Proceed。它依赖 selected_count 来得到数量。
调用图:调用 1 个内部函数(selected_count);被 1 处调用(available_actions)。
ExternalAgentConfigMigrationScreen::first_available_action136–141 ↗
fn first_available_action(&self) -> ActionMenuOption
作用:找出当前菜单里第一个可用按钮,用来给光标一个安全的位置。
数据流:进去的是当前界面状态 → 它生成当前可用按钮列表并取第一个 → 返回一个按钮;如果列表意外为空,就退回到 Back。
调用关系:normalize_highlighted_action、move_down 和 back_to_summary 会用它,避免光标停在一个当前不存在的按钮上。
调用图:调用 1 个内部函数(available_actions);被 3 处调用(back_to_summary, move_down, normalize_highlighted_action)。
ExternalAgentConfigMigrationScreen::last_available_action143–148 ↗
fn last_available_action(&self) -> ActionMenuOption
作用:找出当前菜单里最后一个可用按钮,方便用户从列表顶部往上时绕到末尾。
数据流:进去的是当前界面状态 → 它查看当前可用按钮列表并取最后一个 → 返回一个按钮;如果没有按钮,就用 Back 兜底。
调用关系:move_up 会用它处理上下循环导航,特别是从项目列表切到动作按钮时。
调用图:调用 1 个内部函数(available_actions);被 1 处调用(move_up)。
ExternalAgentConfigMigrationScreen::previous_available_action150–158 ↗
fn previous_available_action(&self, action: ActionMenuOption) -> Option<ActionMenuOption>
作用:在当前可用按钮列表里,找到某个按钮前面的按钮。它让菜单可以按上键移动。
数据流:进去的是当前状态和当前按钮 → 它生成可用按钮列表,找到当前按钮的位置,再取前一个 → 返回前一个按钮,找不到就返回空。
调用关系:move_up 调用它来移动动作区的高亮。如果没有前一个,move_up 会自己决定是否循环或切换焦点。
调用图:调用 1 个内部函数(available_actions);被 1 处调用(move_up)。
ExternalAgentConfigMigrationScreen::next_available_action160–167 ↗
fn next_available_action(&self, action: ActionMenuOption) -> Option<ActionMenuOption>
作用:在当前可用按钮列表里,找到某个按钮后面的按钮。它让菜单可以按下键移动。
数据流:进去的是当前状态和当前按钮 → 它生成可用按钮列表,定位当前按钮,再取后一个 → 返回后一个按钮,找不到就返回空。
调用关系:move_down 调用它来移动动作区的高亮。如果没有后一个,move_down 会负责绕回或切到项目列表。
调用图:调用 1 个内部函数(available_actions);被 1 处调用(move_down)。
ExternalAgentConfigMigrationScreen::available_actions169–181 ↗
fn available_actions(&self) -> Vec<ActionMenuOption>
作用:根据当前页面和选择情况,算出现在应该显示哪些按钮。比如没有选中项目时,就不显示“导入选中的”。
数据流:进去的是当前屏幕状态 → 它看当前是汇总页还是自定义页,并检查是否有已选项目 → 返回一个按钮列表,不改状态。
调用关系:这是按钮导航的中心依据。first_available_action、last_available_action、previous_available_action、next_available_action、normalize_highlighted_action 和 select_numbered_action 都依赖它。
调用图:调用 1 个内部函数(proceed_enabled);被 6 处调用(first_available_action, last_available_action, next_available_action, normalize_highlighted_action, previous_available_action, select_numbered_action);外部调用 2 个(new, vec!)。
ExternalAgentConfigMigrationScreen::normalize_highlighted_action183–187 ↗
fn normalize_highlighted_action(&mut self)
作用:修正当前高亮按钮,防止它指向一个已经不可用的按钮。比如用户取消所有勾选后,“导入选中的”就不能再高亮。
数据流:进去的是可变的屏幕状态 → 它检查当前高亮按钮是否还在可用按钮列表里 → 如果不在,就把高亮改成第一个可用按钮。
调用关系:set_all_enabled 和 toggle_selected_item 改变勾选状态后会调用它,保证后续按回车不会触发一个不存在的动作。
调用图:调用 2 个内部函数(available_actions, first_available_action);被 2 处调用(set_all_enabled, toggle_selected_item)。
ExternalAgentConfigMigrationScreen::display_description189–260 ↗
fn display_description(item: &ExternalAgentConfigMigrationItem) -> String
作用:把服务端给的导入说明改成更适合用户阅读的文字。它会把“Migrate”改成“Import”,还会把路径显示得更友好。
数据流:进去的是一个导入项目 → 它读取项目描述、当前项目目录和插件详情,必要时重写路径和补充插件数量 → 返回一段用于界面展示的说明文字,不修改项目。
调用关系:自定义列表构建时会用它生成每一项下面的说明。它还借助 display_path_for 把路径变成相对当前项目更好懂的形式。
调用图:外部调用 1 个(format!)。
ExternalAgentConfigMigrationScreen::new262–293 ↗
fn new(
request_frame: FrameRequester,
items: &[ExternalAgentConfigMigrationItem],
selected_items: &[ExternalAgentConfigMigrationItem],
error: Option<String>,
) ->
作用:创建一个新的导入确认屏幕,并把初始选中项、分组、焦点和默认按钮都准备好。
数据流:进去的是重绘请求器、所有候选项目、默认选中的项目、错误信息 → 它把项目包装成带勾选状态的列表,并按模型分组 → 出来是一个可交互的屏幕对象。
调用关系:run_external_agent_config_migration_prompt 用它开始一次真实提示;测试也大量用它搭出初始界面。它调用 external_agent_config_migration_groups 来生成汇总页分组。
调用图:调用 1 个内部函数(external_agent_config_migration_groups);被 12 处调用(run_external_agent_config_migration_prompt, control_exit_shortcuts_cancel_prompt, customize_action_snapshot, customize_snapshot, empty_selection_enter_opens_customize_instead_of_proceeding, escape_skips_prompt, numeric_shortcuts_choose_actions, numeric_shortcuts_follow_visible_actions_when_proceed_is_disabled, proceed_returns_selected_items, prompt_snapshot (+2 more));外部调用 1 个(iter)。
ExternalAgentConfigMigrationScreen::plugin_detail_lines295–327 ↗
ExternalAgentConfigMigrationScreen::is_done329–331 ↗
fn is_done(&self) -> bool
作用:告诉外层循环这个提示框是不是已经结束。结束后就不用继续等键盘事件了。
数据流:进去的是屏幕状态 → 它读取 done 标记 → 返回 true 或 false,不改状态。
调用关系:run_external_agent_config_migration_prompt 在事件循环里反复检查它,直到用户继续或取消。
ExternalAgentConfigMigrationScreen::outcome333–335 ↗
fn outcome(&self) -> ExternalAgentConfigMigrationOutcome
作用:取出用户最后的选择结果。结果可能是继续导入哪些项目,也可能是跳过。
数据流:进去的是屏幕状态 → 它复制当前 outcome → 返回给调用方,不改变原状态。
调用关系:run_external_agent_config_migration_prompt 在界面结束后调用它,把最终决定交回上层流程。
调用图:外部调用 1 个(clone)。
ExternalAgentConfigMigrationScreen::finish_with337–341 ↗
fn finish_with(&mut self, outcome: ExternalAgentConfigMigrationOutcome)
作用:用某个结果结束提示框,并安排再画一帧。它是所有“完成”动作的统一出口。
数据流:进去的是可变屏幕状态和最终结果 → 它保存结果,把 done 设为 true,并请求界面刷新 → 之后外层循环会停止。
调用关系:proceed 和 skip 都调用它。这样继续和取消虽然结果不同,但结束界面的收尾动作保持一致。
调用图:调用 1 个内部函数(schedule_frame);被 2 处调用(proceed, skip)。
ExternalAgentConfigMigrationScreen::proceed343–346 ↗
fn proceed(&mut self)
作用:确认继续导入当前已勾选的项目。用户按“导入选中的”时会走这里。
数据流:进去的是当前屏幕状态 → 它收集所有已勾选项目 → 把结果设置为 Proceed,并结束界面。
调用关系:confirm_selection 在当前高亮为 Proceed 时调用它。它内部先用 selected_items,再交给 finish_with 收尾。
调用图:调用 2 个内部函数(finish_with, selected_items);被 1 处调用(confirm_selection);外部调用 1 个(Proceed)。
ExternalAgentConfigMigrationScreen::skip348–350 ↗
fn skip(&mut self)
作用:取消这次导入提示。用户按取消、Esc、Ctrl-C 或 Ctrl-D 时都会走这里。
数据流:进去的是当前屏幕状态 → 它把最终结果设为 Skip → 标记界面结束并请求刷新。
调用关系:confirm_selection 和 handle_key 都可能调用它。它通过 finish_with 统一结束流程。
调用图:调用 1 个内部函数(finish_with);被 2 处调用(confirm_selection, handle_key)。
ExternalAgentConfigMigrationScreen::selected_items352–358 ↗
fn selected_items(&self) -> Vec<ExternalAgentConfigMigrationItem>
作用:拿到当前所有被勾选的导入项目。继续导入时需要这份清单。
数据流:进去的是屏幕里的项目列表 → 它过滤出 enabled 为真的项目并复制原始项目数据 → 返回新的项目数组,不改状态。
调用关系:proceed 调用它来生成最终 Proceed 结果。
调用图:被 1 处调用(proceed)。
ExternalAgentConfigMigrationScreen::selected_count360–362 ↗
fn selected_count(&self) -> usize
作用:数一数现在选中了几个项目。它用于判断是否允许继续。
数据流:进去的是项目列表 → 它统计 enabled 为真的项目数量 → 返回数字,不改状态。
调用关系:proceed_enabled 调用它,然后 available_actions 再根据结果决定是否显示继续按钮。
调用图:被 1 处调用(proceed_enabled)。
ExternalAgentConfigMigrationScreen::group_selection_marker364–378 ↗
fn group_selection_marker(
&self,
group: &ExternalAgentConfigMigrationGroupModel,
) -> &'static str
作用:给汇总页的每个分组生成勾选标记。全选显示 x,没选显示空,部分选显示横杠。
数据流:进去的是一个分组和当前项目状态 → 它检查这个分组里有多少项目已勾选 → 返回一个字符标记,不改状态。
调用关系:build_summary_render_lines 用它把内部勾选情况变成用户能看懂的方括号标记。
ExternalAgentConfigMigrationScreen::set_all_enabled380–387 ↗
fn set_all_enabled(&mut self, enabled: bool)
作用:一键全选或全不选所有导入项。自定义页里按 a 或 n 会用到。
数据流:进去的是目标勾选状态 true 或 false → 它把每个项目都改成这个状态,清掉错误提示,修正高亮按钮,并请求重绘 → 界面马上反映新的选择。
调用关系:handle_key 根据用户按键调用它。它会调用 normalize_highlighted_action,防止全不选后继续按钮还被选中。
调用图:调用 2 个内部函数(normalize_highlighted_action, schedule_frame);被 1 处调用(handle_key)。
ExternalAgentConfigMigrationScreen::toggle_selected_item389–403 ↗
fn toggle_selected_item(&mut self)
作用:切换当前光标所在项目的勾选状态。用户在自定义页按空格或回车选项目时会用到。
数据流:进去的是当前屏幕状态 → 它先确认正在自定义页且焦点在项目上,再找到当前项目并反转勾选 → 清掉错误、修正按钮高亮、请求重绘。
调用关系:confirm_selection 和 handle_key 都会调用它。它是逐项勾选的核心动作。
调用图:调用 2 个内部函数(normalize_highlighted_action, schedule_frame);被 2 处调用(confirm_selection, handle_key)。
ExternalAgentConfigMigrationScreen::customize405–412 ↗
fn customize(&mut self)
作用:从汇总页进入逐项选择页。用户想自己挑哪些导入时会用它。
数据流:进去的是当前屏幕状态 → 它把页面切到 Customize,光标放到第一个项目,滚动归零,按钮改成 Back → 请求界面重绘。
调用关系:confirm_selection 在用户选择 Customize 时调用它;handle_key 在汇总页按 c 时也调用它。
调用图:调用 1 个内部函数(schedule_frame);被 2 处调用(confirm_selection, handle_key)。
ExternalAgentConfigMigrationScreen::back_to_summary414–421 ↗
fn back_to_summary(&mut self)
作用:从自定义页回到汇总页。用户检查完选择后,用它回到最终确认界面。
数据流:进去的是当前屏幕状态 → 它切回 Summary,重置选择位置和滚动,把焦点放回动作按钮 → 请求重绘。
调用关系:confirm_selection 在 Back 按钮触发时调用它;handle_key 在自定义页按 b 或 Esc 时也调用它。
调用图:调用 2 个内部函数(first_available_action, schedule_frame);被 2 处调用(confirm_selection, handle_key)。
ExternalAgentConfigMigrationScreen::move_up423–459 ↗
fn move_up(&mut self)
作用:处理用户按上键或 k。它让光标在项目列表和按钮之间向上移动。
数据流:进去的是当前屏幕状态 → 它根据当前页面和焦点决定高亮上一个按钮、上一个项目,或在边界处切换区域 → 调整滚动位置并请求重绘。
调用关系:handle_key 调用它响应键盘。它会用 previous_available_action、last_available_action 和 ensure_selected_item_visible 保证导航自然且可见。
调用图:调用 4 个内部函数(ensure_selected_item_visible, last_available_action, previous_available_action, schedule_frame);被 1 处调用(handle_key)。
ExternalAgentConfigMigrationScreen::move_down461–493 ↗
fn move_down(&mut self)
作用:处理用户按下键或 j。它让光标在项目列表和按钮之间向下移动。
数据流:进去的是当前屏幕状态 → 它移动到下一个按钮或项目,到了边界就切换区域或绕回 → 调整滚动位置并请求重绘。
调用关系:handle_key 调用它响应键盘。它会用 next_available_action、first_available_action 和 ensure_selected_item_visible。
调用图:调用 4 个内部函数(ensure_selected_item_visible, first_available_action, next_available_action, schedule_frame);被 1 处调用(handle_key)。
ExternalAgentConfigMigrationScreen::confirm_selection495–505 ↗
fn confirm_selection(&mut self)
作用:处理“确认”动作,也就是用户按回车或数字快捷键选中了某个动作。它根据当前焦点决定是勾选项目还是执行按钮。
数据流:进去的是当前屏幕状态 → 如果焦点在项目上,就切换该项目勾选;如果焦点在按钮上,就执行继续、自定义、取消或返回 → 可能改变页面,也可能结束界面。
调用关系:handle_key 按 Enter 时调用它;select_numbered_action 选中数字按钮后也调用它。它再分派给 proceed、customize、skip、back_to_summary 或 toggle_selected_item。
调用图:调用 5 个内部函数(back_to_summary, customize, proceed, skip, toggle_selected_item);被 2 处调用(handle_key, select_numbered_action)。
ExternalAgentConfigMigrationScreen::handle_key507–538 ↗
fn handle_key(&mut self, key_event: KeyEvent)
作用:把用户按下的键翻译成界面动作。它是键盘操作进入屏幕状态的总入口。
数据流:进去的是一个键盘事件 → 它忽略按键释放事件,识别 Ctrl-C/Ctrl-D、方向键、数字键、空格、a、n、Esc、Enter 等 → 修改屏幕状态、请求重绘,或结束提示框。
调用关系:run_external_agent_config_migration_prompt 收到 TuiEvent::Key 后调用它。它把具体工作交给 move_up、move_down、select_numbered_action、customize、back_to_summary、set_all_enabled、toggle_selected_item、confirm_selection 和 skip。
调用图:调用 10 个内部函数(back_to_summary, confirm_selection, customize, move_down, move_up, select_numbered_action, set_all_enabled, skip, toggle_selected_item, is_ctrl_exit_combo)。
ExternalAgentConfigMigrationScreen::select_numbered_action540–550 ↗
fn select_numbered_action(&mut self, number: char)
作用:支持按数字快速选择按钮,比如 1 代表当前显示的第一个动作。这样用户不用移动光标也能操作。
数据流:进去的是字符数字 → 它把数字转成从 0 开始的位置,到当前可用按钮列表里找对应按钮 → 找到后把焦点切到动作区并执行确认。
调用关系:handle_key 在用户按 1 到 9 时调用它。它依赖 available_actions,所以数字含义会跟着当前可见按钮变化。
调用图:调用 2 个内部函数(available_actions, confirm_selection);被 1 处调用(handle_key)。
ExternalAgentConfigMigrationScreen::ensure_selected_item_visible552–567 ↗
fn ensure_selected_item_visible(&mut self)
作用:保证当前选中的项目没有被滚动窗口遮住。就像列表滚动时自动把光标带进可见区域。
数据流:进去的是当前选择和滚动位置 → 它算出选中项目在渲染行里的位置和可见行数 → 必要时调整 scroll_top,让选中行出现在屏幕里。
调用关系:move_up 和 move_down 移动光标后会调用它。它借助 selected_render_line_index 和 render_line_count 计算位置。
调用图:调用 2 个内部函数(render_line_count, selected_render_line_index);被 2 处调用(move_down, move_up)。
ExternalAgentConfigMigrationScreen::render_line_count569–571 ↗
fn render_line_count(&self) -> usize
作用:计算当前页面总共会生成多少行显示内容。滚动逻辑需要知道这个数。
数据流:进去的是屏幕状态 → 它构建当前页面的渲染行列表并取长度 → 返回行数,不改状态。
调用关系:ensure_selected_item_visible 调用它来估算可滚动内容范围。
调用图:调用 1 个内部函数(build_render_lines);被 1 处调用(ensure_selected_item_visible)。
ExternalAgentConfigMigrationScreen::selected_render_line_index573–578 ↗
fn selected_render_line_index(&self, selected_item_idx: usize) -> usize
作用:找出某个项目在实际显示行里的位置。因为一个项目可能带说明行,所以项目编号不等于显示行号。
数据流:进去的是项目编号 → 它构建当前页面显示行,查找对应项目所在行 → 返回显示行位置;找不到时用项目编号兜底。
调用关系:ensure_selected_item_visible 用它决定滚动到哪里。它依赖 build_render_lines。
调用图:调用 1 个内部函数(build_render_lines);被 1 处调用(ensure_selected_item_visible)。
ExternalAgentConfigMigrationScreen::section_title580–588 ↗
ExternalAgentConfigMigrationScreen::build_render_lines590–595 ↗
fn build_render_lines(&self) -> Vec<RenderLineEntry>
作用:根据当前页面选择要生成哪种显示行。汇总页和自定义页的内容结构不同,所以这里做分流。
数据流:进去的是当前屏幕状态 → 它查看 view 是 Summary 还是 Customize → 返回对应页面的渲染行列表。
调用关系:render_line_count 和 selected_render_line_index 会调用它。它再把工作交给 build_summary_render_lines 或 build_customize_render_lines。
调用图:调用 2 个内部函数(build_customize_render_lines, build_summary_render_lines);被 2 处调用(render_line_count, selected_render_line_index)。
ExternalAgentConfigMigrationScreen::build_summary_render_lines597–620 ↗
fn build_summary_render_lines(&self) -> Vec<RenderLineEntry>
作用:生成汇总页中间那部分列表内容。每个分组显示一行名称和一行说明。
数据流:进去的是分组信息和当前勾选状态 → 它为每个分组生成勾选标记、标题和描述行 → 返回可渲染的行列表。
调用关系:build_render_lines 在 Summary 页面调用它。它使用 group_selection_marker 显示全选、未选或部分选。
调用图:被 1 处调用(build_render_lines)。
ExternalAgentConfigMigrationScreen::build_customize_render_lines622–678 ↗
fn build_customize_render_lines(&self) -> Vec<RenderLineEntry>
作用:生成自定义页的详细列表内容,包括分区标题、每个项目的勾选框、说明和插件详情。
数据流:进去的是全部项目和当前勾选状态 → 它按项目目录分区,逐项生成标签、描述、附加详情和插件摘要 → 返回一组显示行。
调用关系:build_render_lines 在 Customize 页面调用它。它会用 section_title、display_description、external_agent_config_migration_item_label、external_agent_config_migration_item_detail 和 plugin_detail_lines。
调用图:调用 1 个内部函数(external_agent_config_migration_item_detail);被 1 处调用(build_render_lines);外部调用 6 个(from, plugin_detail_lines, section_title, new, format!, vec!)。
is_ctrl_exit_combo681–684 ↗
fn is_ctrl_exit_combo(key_event: KeyEvent) -> bool
作用:判断一个按键是不是 Ctrl-C 或 Ctrl-D,也就是终端里常见的退出组合键。
数据流:进去的是键盘事件 → 它检查键是否为 c 或 d,并且是否按住 Control → 返回 true 或 false。
调用关系:handle_key 在处理其他按键前先调用它。如果命中,就直接取消提示框。
调用图:被 1 处调用(handle_key);外部调用 1 个(matches!)。
tests::sample_plugin_details706–732 ↗
tests::sample_project_root740–742 ↗
fn sample_project_root() -> PathBuf
作用:给测试提供一个假的项目根目录,并且在 Windows 和非 Windows 系统上使用不同路径格式。
数据流:进去没有参数 → 它按编译平台选择路径字符串 → 返回 PathBuf 路径对象。
调用关系:tests::sample_project_path 和 tests::sample_items 会调用它,保证快照测试在不同系统上路径格式可控。
调用图:外部调用 1 个(from)。
tests::sample_project_path744–746 ↗
fn sample_project_path(path: &str) -> String
作用:把一个相对路径拼到测试用项目根目录后面,生成完整项目路径。
数据流:进去的是相对路径字符串 → 它先取 sample_project_root,再拼接传入路径 → 返回显示用字符串。
调用关系:tests::sample_items 用它构造描述文字里的文件路径。
调用图:外部调用 1 个(sample_project_root)。
tests::sample_items748–792 ↗
fn sample_items() -> Vec<ExternalAgentConfigMigrationItem>
作用:生成一组完整的测试导入项,覆盖配置、会话、插件和 AGENTS 文件等情况。
数据流:进去没有参数 → 它构造多个 ExternalAgentConfigMigrationItem,包括全局项和项目项 → 返回测试用项目列表。
调用关系:大多数测试都调用它来获得一致的输入数据,避免每个测试重复手写复杂结构。
调用图:外部调用 2 个(sample_project_root, vec!)。
tests::render_screen794–814 ↗
fn render_screen(
screen: &ExternalAgentConfigMigrationScreen,
width: u16,
height: u16,
) -> String
作用:把一个屏幕状态真正画到测试终端里,并取回画面文字。快照测试用它比较界面有没有变。
数据流:进去的是屏幕对象、宽度和高度 → 它创建虚拟终端,渲染屏幕,刷新后读取终端内容 → 返回多行字符串。
调用关系:prompt_snapshot、customize_snapshot 和 customize_action_snapshot 调用它生成快照内容。
调用图:调用 2 个内部函数(with_options, new);外部调用 1 个(new)。
tests::prompt_snapshot817–831 ↗
fn prompt_snapshot()
作用:检查汇总页默认画面是否符合预期。它能发现文案、布局或勾选标记的意外变化。
数据流:进去没有参数 → 它创建样例项目和初始屏幕,渲染 80x24 的画面 → 用快照断言比较输出。
调用关系:它调用 sample_items、ExternalAgentConfigMigrationScreen::new 和 render_screen,是界面回归测试的一部分。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 3 个(assert_snapshot!, render_screen, sample_items)。
tests::customize_snapshot834–852 ↗
fn customize_snapshot()
作用:检查进入自定义页后的画面是否符合预期。它覆盖详细列表、分区和插件折叠显示。
数据流:进去没有参数 → 它创建屏幕,调用 customize 进入自定义页,渲染较高的终端画面 → 和保存的快照比较。
调用关系:它依赖 sample_items、new、customize 和 render_screen,专门保护自定义页显示效果。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 3 个(assert_snapshot!, render_screen, sample_items)。
tests::customize_action_snapshot855–874 ↗
fn customize_action_snapshot()
作用:检查自定义页里焦点移动到动作按钮时的画面。这样能确认高亮位置和导航显示没有坏。
数据流:进去没有参数 → 它创建屏幕,进入自定义页,再向上移动一次焦点 → 渲染后和快照比较。
调用关系:它调用 customize、move_up 和 render_screen,测试项目区到按钮区的焦点切换。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 3 个(assert_snapshot!, render_screen, sample_items)。
tests::proceed_returns_selected_items877–893 ↗
fn proceed_returns_selected_items()
作用:验证默认选中所有项目时,按回车会返回继续导入并带上所有项目。
数据流:进去没有参数 → 它创建全选屏幕,模拟按 Enter → 检查屏幕已结束,结果是 Proceed 且项目列表完整。
调用关系:它间接测试 handle_key、confirm_selection、proceed、selected_items 和 outcome 这一整条确认链路。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(new, assert!, assert_eq!, sample_items)。
tests::toggle_item_then_proceed_keeps_remaining_selection896–919 ↗
fn toggle_item_then_proceed_keeps_remaining_selection()
作用:验证用户取消勾选一个项目后,最终只导入剩下的项目。
数据流:进去没有参数 → 它进入自定义页,按空格取消第一个项目,返回汇总页,再用数字选择导入 → 检查结果只包含后三项。
调用关系:它覆盖 handle_key、customize、toggle_selected_item、back_to_summary 和 select_numbered_action 的配合。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 5 个(Char, new, assert!, assert_eq!, sample_items)。
tests::escape_skips_prompt922–935 ↗
fn escape_skips_prompt()
作用:验证在汇总页按 Esc 会取消整个提示。
数据流:进去没有参数 → 它创建屏幕并模拟 Esc → 检查屏幕结束,结果是 Skip。
调用关系:它测试 handle_key 对 Esc 的处理,以及 skip 和 outcome 是否正确。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(new, assert!, assert_eq!, sample_items)。
tests::numeric_shortcuts_follow_visible_actions_when_proceed_is_disabled938–953 ↗
fn numeric_shortcuts_follow_visible_actions_when_proceed_is_disabled()
作用:验证数字快捷键会跟随当前可见按钮变化。全不选时,第一个按钮不再是继续导入,而应是自定义。
数据流:进去没有参数 → 它进入自定义页、全不选、回到汇总页,再按数字 1 → 检查页面进入自定义而不是继续导入。
调用关系:它重点测试 available_actions、normalize_highlighted_action 和 select_numbered_action 在无选中项目时的行为。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(Char, new, assert_eq!, sample_items)。
tests::empty_selection_enter_opens_customize_instead_of_proceeding956–969 ↗
fn empty_selection_enter_opens_customize_instead_of_proceeding()
作用:验证一开始没有任何选中项时,按回车不会误导入,而是打开自定义选择页。
数据流:进去没有参数 → 它用空的 selected_items 创建屏幕,模拟 Enter → 检查界面没有结束,并切到 Customize。
调用关系:它覆盖 proceed_enabled 影响 available_actions,再影响 confirm_selection 的流程。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(new, assert!, assert_eq!, sample_items)。
tests::control_exit_shortcuts_cancel_prompt972–987 ↗
fn control_exit_shortcuts_cancel_prompt()
作用:验证 Ctrl-C 和 Ctrl-D 都会取消提示。这符合终端用户对退出快捷键的直觉。
数据流:进去没有参数 → 它分别创建屏幕并模拟 Ctrl-C、Ctrl-D → 每次都检查结果为 Skip。
调用关系:它测试 is_ctrl_exit_combo 和 handle_key 的优先处理逻辑。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 5 个(Char, new, assert!, assert_eq!, sample_items)。
tests::numeric_shortcuts_choose_actions990–1027 ↗
fn numeric_shortcuts_choose_actions()
作用:验证数字快捷键能选择继续、自定义、返回和取消等动作。
数据流:进去没有参数 → 它分别创建多个屏幕,按不同数字 → 检查结果或页面状态是否符合对应按钮动作。
调用关系:它集中测试 select_numbered_action、available_actions 和 confirm_selection 的配合。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(Char, new, assert_eq!, sample_items)。
tests::summary_does_not_toggle_selection1030–1042 ↗
fn summary_does_not_toggle_selection()
作用:验证在汇总页按空格不会改变勾选状态。只有自定义页才能逐项勾选。
数据流:进去没有参数 → 它创建全选屏幕,在汇总页模拟空格 → 检查 selected_items 仍然是原来的全部项目。
调用关系:它保护 handle_key 和 toggle_selected_item 的边界规则,防止汇总页误改用户选择。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(Char, new, assert_eq!, sample_items)。
tui/src/external_agent_config_migration/render.rs源码 ↗
这个文件解决的是“把迁移选择过程清楚地显示在终端里”的问题。终端界面不像网页那样能自动排版,所以这里要手动决定每一块放在哪里、占几行、内容太长时怎么截断。它先清空屏幕区域,再留出内边距,按顺序放标题、说明、错误提示、项目列表、操作区和底部按键提示。项目列表会根据当前选中的项目自动滚动,保证光标所在的那一项能看见;选中的行会变成青色加粗,并用一个箭头标出来。它还会根据当前页面是“总览”还是“自定义选择”,显示不同的说明和快捷键。可以把它想成一个柜台告示牌:上面告诉你规则,中间列出可选东西,下面告诉你按什么键确认或返回。
ExternalAgentConfigMigrationScreen::render_items18–70 ↗
fn render_items(&self, area: Rect, buf: &mut Buffer)
作用:这个函数专门画中间的“可导入项目列表”。它会处理滚动、当前选中项的高亮,以及文字太长时用省略号截断,避免内容挤出屏幕。
数据流:进去的是一块可绘制的屏幕区域和一个终端缓冲区(一块临时画布)。它先从当前界面状态生成要显示的每一行,再根据滚动位置和选中项算出应该从哪一行开始画;然后逐行判断是否选中,给选中行加箭头、青色和粗体,给说明类行变暗;最后把每行按区域宽度截短后写进缓冲区。出来的结果不是返回一个值,而是缓冲区里多了列表内容。
调用关系:它是整个界面绘制流程里的一个零件,只负责列表这一块。ExternalAgentConfigMigrationScreen::render_ref 在画完整屏幕时会调用它;它自己会把每行交给 truncate_line_with_ellipsis_if_overflow,确保终端宽度不够时不会把布局撑乱。
调用图:调用 1 个内部函数(truncate_line_with_ellipsis_if_overflow);被 1 处调用(render_ref)。
ExternalAgentConfigMigrationScreen::render_ref74–205 ↗
fn render_ref(&self, area: Rect, buf: &mut Buffer)
作用:这个函数负责画完整的迁移界面。只要终端需要刷新这个页面,它就把所有部分重新摆好并画出来。
数据流:进去的是整块屏幕区域和一个终端缓冲区。它先清空这块区域,再缩进出内部内容区;然后根据当前视图选择标题和说明文字,根据是否有错误决定错误区高度,根据项目数量和屏幕高度决定列表能显示几行;接着画标题、说明、错误、项目列表、已选择数量、可执行动作,以及底部快捷键提示。出来的结果同样是缓冲区被填上完整页面内容,等待终端真正显示。
调用关系:它是这个文件的主绘制入口,被终端 UI 框架在刷新控件时调用。它把列表部分交给 ExternalAgentConfigMigrationScreen::render_items,把动作按钮行交给 selection_option_row_with_dim,并使用布局工具把屏幕切成一块块区域,保证各部分按固定顺序显示。
调用图:调用 3 个内部函数(render_items, vh, selection_option_row_with_dim);外部调用 10 个(Fill, Length, vertical, from, new, inset, format!, repeat_n, from, vec!)。
tui/src/model_migration.rs源码 ↗
这个文件像一个小型的终端弹窗。它先根据旧模型、新模型、说明文字、链接或 Markdown 文本,拼出用户能看懂的提示内容。然后它进入终端的“备用屏幕”(可以理解成临时白板,不会把大段提示留在正常聊天记录里),画出升级说明和可选菜单。用户按上下键、j/k、数字键、回车、Esc、Ctrl-C 或 Ctrl-D 时,屏幕会更新高亮项,最后产出三种结果:接受新模型、继续旧模型、或退出。里面也包含渲染普通文字、渲染 Markdown、给菜单加快捷键提示这些细节。文件底部的测试用假终端把画面截图下来,确保提示页在不同模型、不同宽度和长链接场景下都不会显示坏掉。
migration_copy_for_models61–135 ↗
fn migration_copy_for_models(
current_model: &str,
target_model: &str,
model_link: Option<String>,
migration_copy: Option<String>,
migration_markdown: Option<String>,
target_di
作用:根据当前模型、新模型、说明链接和配置文案,组装出升级提示页要显示的文字。别人不用关心提示语怎么拼,只要把模型信息传进来就能得到一份完整文案。
数据流:进去的是旧模型名、新模型名、可选链接、可选自定义说明、可选 Markdown 模板、新模型显示名、新模型描述、是否允许拒绝升级 → 它优先使用 Markdown 模板;没有模板时就拼标题、推荐语、说明、链接和是否可跳过的提示 → 出来的是 ModelMigrationCopy,里面装着标题、正文、是否能拒绝、以及可能的 Markdown 内容。
调用关系:这是展示前的文案准备步骤。测试里的多个截图用例都会先调用它生成提示内容;如果传了 Markdown,它会把模板交给 fill_migration_markdown 替换模型名。
调用图:调用 1 个内部函数(fill_migration_markdown);被 6 处调用(escape_key_accepts_prompt, prompt_snapshot, prompt_snapshot_gpt5_codex, prompt_snapshot_gpt5_codex_mini, prompt_snapshot_gpt5_family, selecting_use_existing_model_rejects_upgrade);外部调用 5 个(from, from, new, format!, vec!)。
run_model_migration_prompt137–169 ↗
async fn run_model_migration_prompt(
tui: &mut Tui,
copy: ModelMigrationCopy,
) -> ModelMigrationOutcome
作用:真正运行这页模型升级提示:打开临时屏幕、画界面、等用户按键,然后返回用户最后的选择。
数据流:进去的是正在运行的 TUI 终端对象和已经准备好的提示文案 → 它进入备用屏幕,创建 ModelMigrationScreen,先画一次界面,然后不断读取按键、粘贴、重绘和窗口变化事件 → 出来的是 ModelMigrationOutcome,表示接受、拒绝或退出,同时终端会离开备用屏幕。
调用关系:这是外部代码最可能调用的入口函数。它先用 AltScreenGuard::enter 保护终端显示,再创建 ModelMigrationScreen;之后按键事件交给 screen.handle_key,绘制事件交给 ratatui 渲染 screen。
ModelMigrationScreen::new180–188 ↗
fn new(request_frame: FrameRequester, copy: ModelMigrationCopy) -> Self
作用:创建一个新的升级提示屏幕对象,保存文案和初始状态。默认高亮“试用新模型”,默认结果也偏向接受。
数据流:进去的是一个请求重画的工具 FrameRequester 和一份提示文案 ModelMigrationCopy → 它把 done 设成 false,把 outcome 设成 Accepted,把高亮项设成 TryNewModel → 出来的是可被绘制、可处理按键的屏幕状态对象。
调用关系:run_model_migration_prompt 会用它启动真实提示页;测试也用它创建屏幕,再把屏幕画到假终端里检查显示效果和按键行为。
调用图:被 8 处调用(run_model_migration_prompt, escape_key_accepts_prompt, markdown_prompt_keeps_long_url_tail_visible_when_narrow, prompt_snapshot, prompt_snapshot_gpt5_codex, prompt_snapshot_gpt5_codex_mini, prompt_snapshot_gpt5_family, selecting_use_existing_model_rejects_upgrade)。
ModelMigrationScreen::finish_with190–194 ↗
fn finish_with(&mut self, outcome: ModelMigrationOutcome)
作用:用某个最终结果结束提示页。它是接受、拒绝、退出三个动作背后的共同收尾函数。
数据流:进去的是一个结果,比如 Accepted、Rejected 或 Exit → 它把屏幕的 outcome 改成这个结果,把 done 改成 true,并请求终端再画一帧 → 出来没有单独返回值,但屏幕状态已经变成“结束”。
调用关系:ModelMigrationScreen::accept、reject、exit 都把最后收尾交给它。它还会调用 schedule_frame,让外层循环知道界面状态变了,需要刷新。
调用图:调用 1 个内部函数(schedule_frame);被 3 处调用(accept, exit, reject)。
ModelMigrationScreen::accept196–198 ↗
fn accept(&mut self)
作用:把用户选择记录为“接受新模型”。这是回车确认、数字 1、不可拒绝提示确认等场景都会用到的动作。
数据流:进去不需要额外输入 → 它调用 finish_with,把结果设为 Accepted → 出来时屏幕已标记完成,外层循环会停止等待。
调用关系:它被 confirm_selection、handle_key 和 handle_menu_key 使用。也就是说,无论用户从菜单确认还是直接按键确认,最终都走这条接受路径。
调用图:调用 1 个内部函数(finish_with);被 3 处调用(confirm_selection, handle_key, handle_menu_key)。
ModelMigrationScreen::reject200–202 ↗
fn reject(&mut self)
作用:把用户选择记录为“继续使用旧模型”。只有允许拒绝升级时,这个动作才有意义。
数据流:进去不需要额外输入 → 它调用 finish_with,把结果设为 Rejected → 出来时屏幕结束,外层可以据此不切换模型。
调用关系:它被 confirm_selection 和 handle_menu_key 调用。用户高亮“Use existing model”再确认,或直接按数字 2 时,会走到这里。
调用图:调用 1 个内部函数(finish_with);被 2 处调用(confirm_selection, handle_menu_key)。
ModelMigrationScreen::exit204–206 ↗
fn exit(&mut self)
作用:把用户操作记录为“退出”。这不是选择新旧模型,而是表示用户用 Ctrl-C 或 Ctrl-D 想中断流程。
数据流:进去不需要额外输入 → 它调用 finish_with,把结果设为 Exit → 出来时屏幕结束,调用方可以停止后续启动或迁移流程。
调用关系:它由 handle_key 在检测到退出组合键后调用。这样普通 Esc 不会被误当成退出,而 Ctrl-C/Ctrl-D 才是退出。
调用图:调用 1 个内部函数(finish_with);被 1 处调用(handle_key)。
ModelMigrationScreen::confirm_selection208–217 ↗
fn confirm_selection(&mut self)
作用:根据当前高亮的菜单项执行确认。它把“用户现在选中哪一行”转换成真正的接受或拒绝结果。
数据流:进去读取屏幕里的 can_opt_out 和 highlighted_option → 如果允许拒绝,就看高亮项:新模型就 accept,旧模型就 reject;如果不允许拒绝,就直接 accept → 出来时屏幕通常已经结束。
调用关系:它被 handle_menu_key 在用户按回车或 Esc 时调用。它再把具体决定交给 accept 或 reject。
调用图:调用 2 个内部函数(accept, reject);被 1 处调用(handle_menu_key)。
ModelMigrationScreen::highlight_option219–224 ↗
fn highlight_option(&mut self, option: MigrationMenuOption)
作用:移动菜单高亮光标,让用户能看出当前选中的是哪一项。只有高亮真的变了,才请求重画。
数据流:进去的是想高亮的新选项 → 它和当前高亮项比较;不同就更新 highlighted_option,并请求下一帧重画 → 出来没有返回值,但界面状态可能已改变。
调用关系:它被 handle_menu_key 调用。用户按上、下、j、k 或数字键时,菜单处理函数会先用它改高亮,再决定是否确认。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_menu_key)。
ModelMigrationScreen::handle_key226–241 ↗
fn handle_key(&mut self, key_event: KeyEvent)
作用:处理用户按下的键,是提示页的按键总入口。它负责区分退出键、菜单键、确认键这些不同情况。
数据流:进去的是一个 KeyEvent,里面有按键、修饰键和按键阶段 → 它先忽略“松开按键”的事件,再检查 Ctrl-C/Ctrl-D;如果能选择新旧模型,就交给 handle_menu_key;否则 Esc 或 Enter 都当作接受 → 出来没有返回值,但屏幕可能会改变高亮或结束。
调用关系:run_model_migration_prompt 的事件循环收到 TuiEvent::Key 后会调用它。它内部会用 is_ctrl_exit_combo 判断退出组合键,并按情况调用 exit、accept 或 handle_menu_key。
调用图:调用 4 个内部函数(accept, exit, handle_menu_key, is_ctrl_exit_combo);外部调用 1 个(matches!)。
ModelMigrationScreen::is_done243–245 ↗
fn is_done(&self) -> bool
作用:告诉外层提示页是不是已经结束。事件循环靠它决定还要不要继续等用户输入。
数据流:进去不需要输入 → 它读取 screen.done → 出来是 true 或 false。
调用关系:run_model_migration_prompt 的 while 循环会反复检查它。只要它变成 true,提示流程就停止,并返回 outcome。
ModelMigrationScreen::outcome247–249 ↗
fn outcome(&self) -> ModelMigrationOutcome
作用:取出用户最后的选择结果。外层代码用这个结果决定是否切换模型、保留旧模型或退出。
数据流:进去不需要输入 → 它读取 screen.outcome → 出来是 ModelMigrationOutcome 的一个值。
调用关系:run_model_migration_prompt 在屏幕结束后调用它,把结果交回给调用方。测试也用它确认按键行为是否符合预期。
ModelMigrationScreen::render_ref253–270 ↗
fn render_ref(&self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer)
作用:把整个升级提示页画到终端缓冲区里。可以把它理解成这页界面的“总画师”。
数据流:进去的是可绘制区域 area 和终端画布 buf → 它先清空区域,再创建一列内容;有 Markdown 就画 Markdown,没有就画标题和普通正文;如果允许拒绝升级,还会画选择菜单 → 出来没有普通返回值,但终端画布里已经有了完整界面。
调用关系:run_model_migration_prompt 里的 draw 会触发它。它把具体工作分给 heading_line、render_content、render_markdown_content 和 render_menu。
调用图:调用 5 个内部函数(heading_line, render_content, render_markdown_content, render_menu, new);外部调用 1 个(from)。
ModelMigrationScreen::heading_line295–299 ↗
fn heading_line(&self) -> Line<'static>
作用:生成提示页标题那一行,并在前面加上一个 “> ” 符号,让标题更像提示块。
数据流:进去不需要额外输入,只读取 copy.heading → 它把固定前缀和标题文字拼成一行 → 出来是一条可渲染的 Line。
调用关系:render_ref 在渲染普通文案模式时调用它。Markdown 模式下不走这条标题生成逻辑。
调用图:被 1 处调用(render_ref);外部调用 2 个(from, vec!)。
ModelMigrationScreen::render_content301–303 ↗
fn render_content(&self, column: &mut ColumnRenderable)
作用:把普通正文内容放进页面列里。它本身不逐行画,而是把行列表交给通用的 render_lines。
数据流:进去的是一个可追加内容的 ColumnRenderable → 它读取 copy.content,并调用 render_lines 按行加入段落 → 出来时 column 里多了正文段落。
调用关系:render_ref 在没有 Markdown 时调用它。它再把细节交给 render_lines,保持主渲染流程简单。
调用图:调用 1 个内部函数(render_lines);被 1 处调用(render_ref)。
ModelMigrationScreen::render_lines305–315 ↗
fn render_lines(&self, lines: &[Line<'static>], column: &mut ColumnRenderable)
作用:把多行文字逐行变成带缩进、可自动换行的段落。这样长句不会直接挤出屏幕。
数据流:进去的是若干 Line 和一个页面列 column → 它遍历每一行,为每行创建 Paragraph,设置自动换行和左侧缩进,然后 push 到 column → 出来时 column 里增加了这些段落。
调用关系:它由 render_content 调用,是普通正文渲染的底层小工具。它会用 Insets 设置缩进,并用 ColumnRenderable::push 把内容接到页面里。
调用图:调用 2 个内部函数(tlbr, push);被 1 处调用(render_content);外部调用 1 个(new)。
ModelMigrationScreen::render_markdown_content317–339 ↗
fn render_markdown_content(
&self,
markdown: &str,
area_width: u16,
column: &mut ColumnRenderable,
)
作用:把 Markdown 文本渲染到提示页里。Markdown 可以理解成一种带简单格式的文本,比如链接、列表、强调等。
数据流:进去的是 Markdown 字符串、屏幕宽度和页面列 → 它先扣掉左右缩进算出可用宽度,再调用 render_markdown_text_with_width 把 Markdown 转成适合终端宽度的多行内容,最后逐行放入带缩进的段落 → 出来时 column 里多了渲染后的 Markdown 内容。
调用关系:render_ref 在 copy.markdown 存在时调用它。它依赖 markdown_render 模块做真正的 Markdown 排版,自己负责宽度和缩进。
调用图:调用 3 个内部函数(render_markdown_text_with_width, tlbr, push);被 1 处调用(render_ref);外部调用 1 个(new)。
AltScreenGuard::enter386–389 ↗
fn enter(tui: &'a mut Tui) -> Self
作用:让终端进入“备用屏幕”。备用屏幕像临时白板,提示页关闭后不会在正常滚动记录里留下一大块空白。
数据流:进去的是可操作的 Tui 对象 → 它调用 enter_alt_screen 尝试切到备用屏幕,然后把这个 Tui 引用包进 AltScreenGuard → 出来的是一个守卫对象,活着时代表当前在备用屏幕里。
调用关系:run_model_migration_prompt 会在显示升级提示前调用它;调用图里另一个 session picker 流程也会复用它。它和 AltScreenGuard::drop 配成一对,保证离开时能切回来。
调用图:被 2 处调用(run_model_migration_prompt, run_session_picker_with_loader);外部调用 1 个(enter_alt_screen)。
AltScreenGuard::drop393–395 ↗
fn drop(&mut self)
作用:在备用屏幕守卫被销毁时,自动把终端切回正常屏幕。这样即使中途结束,也不容易把终端留在奇怪状态。
数据流:进去的是即将被释放的 AltScreenGuard → 它调用 leave_alt_screen → 出来没有返回值,但终端会尝试恢复到普通屏幕。
调用关系:Rust 的 Drop 机制会自动调用它,不需要手写调用。run_model_migration_prompt 结束时,AltScreenGuard 离开作用域,就会触发这里。
调用图:外部调用 1 个(leave_alt_screen)。
is_ctrl_exit_combo398–401 ↗
fn is_ctrl_exit_combo(key_event: KeyEvent) -> bool
作用:判断一个按键是不是 Ctrl-C 或 Ctrl-D,也就是常见的“我要中断/退出”组合键。
数据流:进去的是 KeyEvent → 它检查是否带 Control 修饰键,并且主键是不是字符 c 或 d → 出来是布尔值,true 表示这是退出组合键。
调用关系:ModelMigrationScreen::handle_key 会先调用它。只要它返回 true,handle_key 就会走 exit,而不会继续当普通菜单按键处理。
调用图:被 1 处调用(handle_key);外部调用 1 个(matches!)。
fill_migration_markdown403–407 ↗
fn fill_migration_markdown(template: &str, current_model: &str, target_model: &str) -> String
作用:把 Markdown 模板里的占位符替换成真实模型名。比如把 {model_from} 换成旧模型,把 {model_to} 换成新模型。
数据流:进去的是模板字符串、当前模型名、目标模型名 → 它做两次文本替换 → 出来是一段可以直接显示的 Markdown 字符串。
调用关系:migration_copy_for_models 在收到 migration_markdown 时调用它。这样配置里可以写通用模板,运行时再填入具体模型。
调用图:被 1 处调用(migration_copy_for_models)。
tests::prompt_snapshot423–454 ↗
fn prompt_snapshot()
作用:检查可拒绝升级的提示页长什么样。它用快照测试防止以后改代码时不小心把界面排版改坏。
数据流:进去不需要外部输入 → 它创建一个假终端,生成迁移文案,创建屏幕,把屏幕画进去并刷新 → 出来是一次快照断言:当前画面必须和保存的标准画面一致。
调用关系:它调用 migration_copy_for_models 和 ModelMigrationScreen::new 搭出屏幕,再用 assert_snapshot! 比较终端输出。
调用图:调用 5 个内部函数(with_options, new, migration_copy_for_models, new, test_dummy);外部调用 2 个(new, assert_snapshot!)。
tests::prompt_snapshot_gpt5_family457–481 ↗
fn prompt_snapshot_gpt5_family()
作用:检查 gpt-5 到 gpt-5.1 这类不可拒绝升级提示的显示效果,尤其包含模型链接时的排版。
数据流:进去不需要外部输入 → 它创建 65x22 的假终端,生成带链接、不可拒绝的迁移文案,渲染屏幕 → 出来是快照比对结果。
调用关系:它沿用 migration_copy_for_models 和 ModelMigrationScreen::new,与其他快照测试一起覆盖不同模型文案场景。
调用图:调用 5 个内部函数(with_options, new, migration_copy_for_models, new, test_dummy);外部调用 2 个(new, assert_snapshot!)。
tests::prompt_snapshot_gpt5_codex484–508 ↗
fn prompt_snapshot_gpt5_codex()
作用:检查从 gpt-5-codex 升到 gpt-5.1-codex-max 时,提示页是否按预期显示。
数据流:进去不需要外部输入 → 它建立假终端和迁移文案,画出不可拒绝升级页面 → 出来是和保存快照的比较结果。
调用关系:它调用 migration_copy_for_models 生成 codex-max 场景的文案,再用 ModelMigrationScreen::new 创建屏幕进行渲染测试。
调用图:调用 5 个内部函数(with_options, new, migration_copy_for_models, new, test_dummy);外部调用 2 个(new, assert_snapshot!)。
tests::prompt_snapshot_gpt5_codex_mini511–535 ↗
fn prompt_snapshot_gpt5_codex_mini()
作用:检查 mini 模型升级提示页的显示效果。这个测试确保便宜、更快但能力较弱这类描述也能正常排版。
数据流:进去不需要外部输入 → 它创建 60x22 的假终端,生成 gpt-5-codex-mini 到 gpt-5.1-codex-mini 的文案,渲染并刷新 → 出来是快照断言结果。
调用关系:它和其他 prompt_snapshot 测试一样,覆盖 migration_copy_for_models 生成的不同内容如何被 ModelMigrationScreen 画出来。
调用图:调用 5 个内部函数(with_options, new, migration_copy_for_models, new, test_dummy);外部调用 2 个(new, assert_snapshot!)。
tests::escape_key_accepts_prompt538–564 ↗
fn escape_key_accepts_prompt()
作用:确认 Esc 键在这个提示页里不是“退出程序”,而是像回车一样确认当前选择。这个行为避免用户按 Esc 后被误判成中断。
数据流:进去不需要外部输入 → 它创建一个允许拒绝升级的屏幕,模拟按下 Esc → 出来时断言屏幕已结束,并且结果是 Accepted。
调用关系:它通过 ModelMigrationScreen::handle_key 测试真实按键路径,再用 is_done 和 outcome 检查状态。
调用图:调用 3 个内部函数(new, migration_copy_for_models, test_dummy);外部调用 2 个(new, assert!)。
tests::selecting_use_existing_model_rejects_upgrade567–596 ↗
fn selecting_use_existing_model_rejects_upgrade()
作用:确认用户选择“继续使用旧模型”时,结果真的会变成拒绝升级。
数据流:进去不需要外部输入 → 它创建可拒绝升级的屏幕,模拟按下 Down 把高亮移到旧模型,再按 Enter 确认 → 出来时断言屏幕结束且 outcome 是 Rejected。
调用关系:它走的是 handle_key 到 handle_menu_key,再到 highlight_option 和 confirm_selection/reject 的路径,用来验证菜单选择链路。
调用图:调用 3 个内部函数(new, migration_copy_for_models, test_dummy);外部调用 2 个(new, assert!)。
tests::markdown_prompt_keeps_long_url_tail_visible_when_narrow599–626 ↗
fn markdown_prompt_keeps_long_url_tail_visible_when_narrow()
作用:检查很窄的终端里显示超长 URL 时,末尾不会被吞掉。这个很重要,因为链接尾巴常常包含真正有用的信息。
数据流:进去不需要外部输入 → 它把一条很长的 URL 放进 Markdown 文案,创建 40 列宽的假终端并渲染 → 出来时检查渲染文本里仍然包含 URL 末尾的 tail42。
调用关系:它专门覆盖 render_markdown_content 调用 render_markdown_text_with_width 后的换行效果,防止窄屏 Markdown 渲染把长链接截断。
调用图:调用 4 个内部函数(with_options, new, new, test_dummy);外部调用 3 个(new, new, assert!)。
会话恢复与应用组装
这些文件处理先前会话的恢复,并组装驱动交互式 UI 的主应用和聊天组件状态。
tui/src/app.rs源码 ↗
这个文件解决的是“整个终端应用怎么跑起来、怎么一直响应、怎么安全退出”的问题。可以把它想成剧场里的舞台监督:它不负责每个演员的全部台词,但负责让聊天窗口、后台服务、文件搜索、权限弹窗、模型信息、子线程消息、终端绘制都按顺序上场。App::run 会先读配置、连接 app-server、创建聊天窗口,然后进入主循环,同时听四类事情:用户按键、界面刷新、内部事件、服务器事件。收到事件后,它再转交给更具体的模块处理。文件里还有一些小工具函数,用来判断协作代理消息、生成退出提示、识别服务器竞争错误、启动初始线程等。没有它,各个模块会各干各的,界面就无法形成一个完整应用。
collab_receiver_thread_ids251–269 ↗
fn collab_receiver_thread_ids(notification: &ServerNotification) -> Option<&[String]>
作用:从服务器通知里找出“协作代理工具调用”要发给哪些接收线程。它只关心开始和完成两类通知,其他通知一律当作没有接收者。
数据流:进去的是一条服务器通知 → 函数查看通知类型和里面的线程条目是不是协作代理工具调用 → 如果是,就把接收线程 id 列表借出来;如果不是,就返回空。
调用关系:它是事件分流时用的小筛子,帮助上层逻辑判断某条服务器消息是否还要转给其他协作线程,而不是直接显示在当前主聊天里。
sub_agent_activity_item271–283 ↗
fn sub_agent_activity_item(notification: &ServerNotification) -> Option<&ThreadItem>
作用:从服务器通知里挑出“子代理活动”这类条目。子代理可以理解为主助手派出去干活的小助手,这个函数判断通知是不是在汇报小助手的活动。
数据流:进去的是服务器通知 → 它只检查条目开始和条目完成两种通知 → 如果里面的条目是子代理活动,就返回这个条目;否则返回空。
调用关系:它给线程事件处理逻辑提供快速识别能力,让界面能把子代理活动放到合适的位置展示,而不是混成普通消息。
collab_receiver_is_not_found285–303 ↗
fn collab_receiver_is_not_found(
notification: &ServerNotification,
receiver_thread_id: &str,
) -> bool
作用:判断一次协作代理调用结束时,某个指定的接收线程是不是被服务器标成“没找到”。这能帮助界面发现目标线程已经不存在或不可用。
数据流:进去的是一条服务器通知和一个接收线程 id → 它只看协作代理工具调用的完成通知 → 在代理状态表里查这个 id,如果状态是 NotFound 就返回真,否则返回假。
调用关系:它通常服务于协作线程的容错流程:当服务器说某个接收线程不存在时,上层可以避免继续往那个线程投递消息,并采取回退或提示。
default_exec_approval_decisions305–353 ↗
fn default_exec_approval_decisions(
network_approval_context: Option<&codex_app_server_protocol::NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<&codex_app_server_protocol::Exec
作用:给“是否允许执行命令”的弹窗准备默认可选项。比如允许一次、允许本次会话、修改网络规则后允许、取消。
数据流:进去的是网络审批上下文、可能的执行策略修改、可能的网络策略修改、可能的额外权限 → 函数根据场景组合一组审批决定 → 出来的是按钮/选项列表,不直接执行命令。
调用关系:它给权限审批界面提供基础菜单。后续弹窗会把这些决定展示给用户,用户选中后才会继续交给执行和权限系统。
调用图:外部调用 1 个(vec!)。
auto_review_mode366–374 ↗
fn auto_review_mode() -> AutoReviewMode
作用:生成一套“自动审查”模式的默认权限设置。用户开启这个实验功能时,界面会马上切到相配套的权限模式。
数据流:不需要输入 → 函数固定创建一个 AutoReviewMode,里面包含按请求审批、自动审查者、内置工作区权限档案 → 返回这套模式对象。
调用关系:它是自动审查功能的统一入口之一。其他配置同步代码需要知道“自动审查应该长什么样”时,会以这里的结果为准。
调用图:调用 1 个内部函数(new)。
AutoReviewMode::permission_profile378–381 ↗
fn permission_profile(&self) -> PermissionProfile
作用:在测试里把自动审查模式使用的活动权限档案,转换成实际的权限档案内容。权限档案可以理解为一份“允许做什么、不允许做什么”的清单。
数据流:进去的是 AutoReviewMode 自己保存的活动权限档案标识 → 调用内置权限档案查找函数 → 出来的是完整 PermissionProfile;如果找不到,测试会直接失败。
调用关系:它只在测试配置下存在,用来验证 auto_review_mode 生成的档案确实是内置且可用的。
调用图:外部调用 1 个(builtin_permission_profile_for_active_permission_profile)。
managed_filesystem_sandbox_is_restricted385–390 ↗
fn managed_filesystem_sandbox_is_restricted(permission_profile: &PermissionProfile) -> bool
作用:在 Windows 上判断当前文件系统沙箱是不是“受限制”模式。沙箱可以理解为给程序划出的安全活动范围。
数据流:进去的是一份权限档案 → 函数查看它的文件系统沙箱策略种类 → 如果种类是 Restricted,就返回真,否则返回假。
调用关系:App::run 启动时会用它决定是否需要扫描并提醒危险的可写目录。它只在 Windows 版本里编译使用。
AppExitInfo::fatal408–416 ↗
fn fatal(message: impl Into<String>) -> Self
作用:快速创建一个“程序因为严重错误退出”的退出信息。这样调用者不用手动填一堆默认字段。
数据流:进去的是错误消息 → 函数把 token 用量、线程 id、恢复提示、更新动作都设成空或默认 → 出来的是带 Fatal 退出原因的 AppExitInfo。
调用关系:外层交互式 TUI 入口 run_interactive_tui 会在启动失败等严重情况用它,把错误包装成统一的退出结果。
调用图:被 1 处调用(run_interactive_tui);外部调用 3 个(into, default, Fatal)。
session_summary431–448 ↗
fn session_summary(
token_usage: TokenUsage,
thread_id: Option<ThreadId>,
thread_name: Option<String>,
rollout_path: Option<&Path>,
) -> Option<SessionSummary>
作用:为会话结束后的总结准备内容,比如用了多少 token,以及还能不能用命令恢复这个会话。token 可以理解为模型处理文字时计费和计量的单位。
数据流:进去的是 token 用量、线程 id、线程名、会话记录文件路径 → 它先把非零用量转成文字,再尝试生成恢复提示 → 如果两者都没有就返回空,否则返回 SessionSummary。
调用关系:它依赖 resume_hint_for_resumable_thread 来判断能否恢复会话,给退出或历史展示相关代码提供一段简短总结。
调用图:调用 2 个内部函数(is_zero, resume_hint_for_resumable_thread)。
resumable_thread456–467 ↗
fn resumable_thread(
thread_id: Option<ThreadId>,
thread_name: Option<String>,
rollout_path: Option<&Path>,
) -> Option<ResumableThread>
作用:判断一个线程是否真的可以被恢复。光有线程 id 不够,还要有一份存在且非空的记录文件。
数据流:进去的是可选线程 id、可选线程名、可选记录文件路径 → 缺任何关键项就返回空;路径存在且是非空文件时,组装 ResumableThread → 出来的是可恢复线程信息或空。
调用关系:resume_hint_for_resumable_thread 会先调用它确认“确实能恢复”,再生成给用户看的恢复命令提示。
调用图:调用 1 个内部函数(rollout_path_is_resumable);被 1 处调用(resume_hint_for_resumable_thread)。
resume_hint_for_resumable_thread469–476 ↗
fn resume_hint_for_resumable_thread(
thread_id: Option<ThreadId>,
thread_name: Option<String>,
rollout_path: Option<&Path>,
) -> Option<String>
作用:给可恢复的会话生成一条用户看得懂的恢复提示。比如告诉用户以后可以用某个 resume 命令继续这个线程。
数据流:进去的是线程 id、线程名、记录文件路径 → 先调用 resumable_thread 检查是否满足恢复条件 → 满足时调用命令行工具函数生成提示文字;不满足则返回空。
调用关系:App::run 在退出时会用它填 AppExitInfo,session_summary 也会用它组成会话总结。
调用图:调用 1 个内部函数(resumable_thread);被 2 处调用(run, session_summary);外部调用 1 个(resume_hint)。
rollout_path_is_resumable478–480 ↗
fn rollout_path_is_resumable(rollout_path: &Path) -> bool
作用:检查会话记录文件路径是否适合用于恢复。标准很简单:路径必须指向一个存在的普通文件,而且文件不能是空的。
数据流:进去的是一个路径 → 函数读取这个路径的文件元信息 → 如果它是文件且长度大于 0,返回真;读取失败或不符合条件就返回假。
调用关系:它是 resumable_thread 的底层检查。这样恢复提示不会误导用户去恢复一份不存在或空白的会话记录。
调用图:被 1 处调用(resumable_thread);外部调用 1 个(metadata)。
errors_for_cwd482–489 ↗
fn errors_for_cwd(cwd: &Path, response: &SkillsListResponse) -> Vec<SkillErrorInfo>
作用:从技能列表响应里取出当前工作目录对应的技能加载错误。工作目录就是用户现在所在的项目文件夹。
数据流:进去的是一个目录路径和服务器返回的技能列表 → 函数在列表里找 cwd 相同的那项 → 找到就克隆它的错误列表,找不到就返回空列表。
调用关系:它给启动技能刷新和警告展示使用,让界面只显示当前项目真正相关的技能错误。
RuntimePermissionProfileOverride::from_config597–603 ↗
fn from_config(config: &Config) -> Self
作用:把当前配置里的运行时权限状态拍一张快照。后面即使配置刷新,也能知道用户当时覆盖过哪些权限设置。
数据流:进去的是 Config → 函数读取权限档案、活动权限档案标识、网络权限配置 → 出来的是 RuntimePermissionProfileOverride,不修改原配置。
调用关系:权限选择、自动审查同步、功能开关更新、事件处理等流程会调用它,用来保存或比较运行中的权限覆盖状态。
调用图:被 4 处调用(apply_permission_profile_selection, sync_auto_review_runtime_state_from_effective_config, update_feature_flags, handle_event)。
active_turn_not_steerable_turn_error606–616 ↗
fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option<AppServerTurnError>
作用:从服务器错误里识别“当前轮次不能被引导”的情况。轮次可以理解为用户发一次请求后,助手正在处理的那一回合。
数据流:进去的是一个带类型的请求错误 → 如果不是服务器错误就返回空;如果是,就尝试把错误数据解析成 TurnError → 只有错误类型是 ActiveTurnNotSteerable 时才返回这个 TurnError。
调用关系:它给转向或干预当前轮次的逻辑做错误分类。识别出来后,上层可以用更合适的方式提示用户,而不是显示一段生硬的服务器错误。
调用图:外部调用 2 个(matches!, from_value)。
resolve_runtime_model_provider_base_url618–627 ↗
async fn resolve_runtime_model_provider_base_url(provider: &ModelProviderInfo) -> Option<String>
作用:异步查出当前模型服务实际使用的基础网址。模型服务就是负责接收请求并返回模型结果的后端。
数据流:进去的是模型服务配置 → 函数创建一个模型服务对象,再向它询问运行时 base URL → 成功就返回网址;失败会写日志警告并返回空。
调用关系:App::run 启动时调用它,把结果放进聊天窗口状态栏等地方,让用户能看到当前连到哪里。
调用图:被 1 处调用(run);外部调用 4 个(create_model_provider, clone, runtime_base_url, warn!)。
spawn_startup_thread_start629–648 ↗
fn spawn_startup_thread_start(
app_server: &AppServerSession,
config: Config,
app_event_tx: AppEventSender,
)
作用:在后台启动初始会话线程,避免界面启动时被卡住。这里的线程不是操作系统线程的概念,而是一次 Codex 对话会话。
数据流:进去的是 app-server 会话、配置、应用事件发送器 → 函数取出请求句柄、线程参数模式、远程目录覆盖 → 开一个异步任务去启动线程,完成后把成功或失败结果发回应用事件队列。
调用关系:App::run 在新开会话或准备退出流程时调用它。它把耗时的启动工作交给后台,主循环之后通过 AppEvent::StartupThreadStarted 接收结果。
调用图:调用 5 个内部函数(send, remote_cwd_override, request_handle, thread_params_mode, start_thread_with_request_handle);被 1 处调用(run);外部调用 1 个(spawn)。
active_turn_steer_race656–679 ↗
fn active_turn_steer_race(error: &TypedRequestError) -> Option<ActiveTurnSteerRace>
作用:识别“引导当前轮次”时出现的竞态错误。竞态就是两边状态变化太快,本地以为还在处理 A,服务器已经切到 B。
数据流:进去的是请求错误 → 它只关心 turn/steer 方法的服务器错误 → 如果服务器说没有活动轮次,返回 Missing;如果错误消息里带着实际活动轮次 id,就解析出来返回 ExpectedTurnMismatch。
调用关系:它帮助事件处理代码在转向当前回答失败时判断是否能重新同步服务器的活动轮次,并可能重试一次。
session_start_error681–692 ↗
fn session_start_error(
action: &str,
target_session: &SessionTarget,
err: color_eyre::eyre::Report,
) -> color_eyre::eyre::Report
作用:把恢复或派生会话失败的底层错误,整理成用户更容易理解的错误信息。
数据流:进去的是动作名称、目标会话、原始错误 → 先询问 archived_session_guidance 是否能提取“会话已归档”的专门提示 → 如果没有,就拼出“从某个会话执行某动作失败”的完整错误。
调用关系:App::run 在恢复 Resume 或派生 Fork 会话失败时使用它,避免直接把底层错误原样丢给用户。
调用图:调用 2 个内部函数(archived_session_guidance, display_label);外部调用 1 个(eyre!)。
archived_session_guidance694–704 ↗
fn archived_session_guidance(err: &color_eyre::eyre::Report) -> Option<String>
作用:从错误文字里提取“会话已归档,需要先 unarchive”的友好提示。它专门处理这一种常见失败原因。
数据流:进去的是错误报告 → 函数转成字符串,查找包含 session 和 unarchive 命令的片段 → 找到就去掉多余错误码并返回提示文字;不匹配就返回空。
调用关系:session_start_error 会先调用它。如果它能识别归档会话,用户看到的就是明确的恢复办法,而不是笼统失败。
调用图:被 1 处调用(session_start_error);外部调用 2 个(find, to_string)。
active_turn_interrupt_race706–723 ↗
fn active_turn_interrupt_race(error: &TypedRequestError) -> Option<String>
作用:识别“中断当前轮次”时本地记录和服务器记录不一致的情况,并取出服务器认为真正活跃的轮次 id。
数据流:进去的是请求错误 → 它只看 turn/interrupt 方法的服务器错误 → 如果错误消息符合“期望某 id 但发现另一个 id”的格式,就解析并返回实际 id;否则返回空。
调用关系:它给中断流程做状态修正参考。上层可以知道服务器当前认为哪个轮次还在跑,从而决定如何继续处理。
App::chatwidget_init_for_forked_or_resumed_thread726–756 ↗
fn chatwidget_init_for_forked_or_resumed_thread(
&self,
tui: &mut tui::Tui,
cfg: crate::legacy_core::config::Config,
initial_user_message: Option<crate::chatwidget::Use
作用:为恢复或派生出来的新会话创建聊天窗口初始化参数。这样新聊天窗口能继承当前应用的账号、模型、反馈、遥测等上下文。
数据流:进去的是当前 App、TUI、目标配置、可选初始用户消息 → 函数从现有聊天窗口和应用状态中读取账号、模型、计划、状态栏、事件发送器等 → 出来的是 ChatWidgetInit,供创建新 ChatWidget 使用。
调用关系:当应用切到恢复线程或派生线程时,相关流程会用它生成一致的聊天窗口配置,而不用到处重复拼装这些字段。
调用图:外部调用 10 个(frame_requester, clone, clone, clone, current_model, current_plan_type, has_chatgpt_account, has_codex_backend_auth, runtime_model_provider_base_url, status_account_display)。
App::run759–1252 ↗
async fn run(
tui: &mut tui::Tui,
mut app_server: AppServerSession,
mut config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOve
作用:这是终端应用真正跑起来的主函数。它完成启动准备,然后进入事件循环,一直响应用户、服务器和界面刷新,直到需要退出。
数据流:进去的是 TUI、app-server、配置、命令行覆盖项、初始提示、会话选择、反馈、状态库、环境管理器等大量启动材料 → 它初始化服务器、模型、遥测、聊天窗口、文件搜索、键位映射、线程状态,并启动必要的后台请求 → 主循环不断接收应用事件、活动线程事件、终端事件、服务器事件并分派处理 → 退出时关闭服务器、清屏、生成 AppExitInfo。
调用关系:它是本文件的核心调度器。它会调用 resolve_runtime_model_provider_base_url、spawn_startup_thread_start、resume_hint_for_resumable_thread、managed_filesystem_sandbox_is_restricted 等辅助函数,也会把具体事件交给 handle_tui_event、handle_app_server_event、handle_event 等处理。
调用图:调用 23 个内部函数(originator, new, new, managed_filesystem_sandbox_is_restricted, resolve_runtime_model_provider_base_url, resume_hint_for_resumable_thread, spawn_startup_thread_start, new, bootstrap, fork_thread (+13 more));外部调用 38 个(new, new, new, pin, new, now, should_prompt_for_paused_goal_after_startup_resume, should_wait_for_initial_session, spawn_world_writable_scan, new (+15 more))。
App::handle_tui_event1254–1327 ↗
async fn handle_tui_event(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
event: TuiEvent,
) -> Result<AppRunControl>
作用:处理来自终端界面的事件,比如按键、粘贴、绘制、窗口大小变化。它让用户操作真正影响聊天窗口和应用状态。
数据流:进去的是当前 App、TUI、app-server、一个 TuiEvent → 如果需要绘制,先做渲染前准备;如果有覆盖层,就把事件交给覆盖层;否则按事件类型处理按键、粘贴、绘制和 resize → 最后通常返回 Continue,表示主循环继续。
调用关系:App::run 的主循环在收到终端事件时调用它。它内部会调用 render_chat_widget_frame 来画聊天窗口,也可能发送 LaunchExternalEditor 事件去打开外部编辑器。
调用图:调用 3 个内部函数(render_chat_widget_frame, send, pre_draw_tick);外部调用 14 个(new, draw_ambient_pet_image, draw_pet_picker_preview_image, frame_requester, matches!, ambient_pet_draw, ambient_pet_image_enabled, external_editor_state, handle_paste, handle_paste_burst_tick (+4 more))。
App::show_shutdown_feedback1329–1336 ↗
fn show_shutdown_feedback(&mut self, tui: &mut tui::Tui) -> Result<()>
作用:在退出前给用户显示“正在关闭”的反馈。这样关闭后台会话时,用户不会以为程序卡死了。
数据流:进去的是当前 App 和 TUI → 它先关闭环境宠物图像,告诉聊天窗口显示关闭中,再做一次渲染前处理和绘制 → 成功则返回空结果,失败返回错误。
调用关系:退出流程在需要先让 app-server 或线程关停时会用它。它复用 render_chat_widget_frame 做实际画面刷新。
调用图:调用 3 个内部函数(render_chat_widget_frame, pre_draw_tick, show_shutdown_in_progress)。
App::render_chat_widget_frame1338–1351 ↗
fn render_chat_widget_frame(&mut self, tui: &mut tui::Tui) -> Result<Rect>
作用:把聊天窗口画到终端上,并设置光标位置。它是本文件里真正执行“画一帧聊天界面”的小入口。
数据流:进去的是当前 App 和 TUI → 它先按终端宽度询问聊天窗口想要多高,再调用 TUI 的带重排绘制方法 → 在绘制回调里渲染聊天内容、设置光标样式和位置 → 出来的是本次实际绘制区域。
调用关系:handle_tui_event 在正常刷新时调用它,show_shutdown_feedback 在退出提示时也调用它。它把高层事件处理和底层 ratatui 绘制连接起来。
调用图:被 2 处调用(handle_tui_event, show_shutdown_feedback);外部调用 3 个(default, draw_with_resize_reflow, desired_height)。
App::drop1355–1359 ↗
fn drop(&mut self)
作用:在 App 被销毁时做最后的终端标题清理。Drop 是 Rust 的自动清理钩子,可以理解为对象离场前的收尾动作。
数据流:进去的是即将被销毁的 App → 它尝试让聊天窗口清掉自己管理过的终端标题 → 成功就安静结束;失败只写调试日志,不阻止程序退出。
调用关系:它不需要别人手动调用,Rust 在 App 生命周期结束时自动触发。它保证程序退出后尽量不把自定义终端标题留给用户。
调用图:外部调用 2 个(debug!, clear_managed_terminal_title)。
tui/src/resume_picker.rs源码 ↗
可以把这个文件想成一个“旧会话档案柜”的前台界面。它先根据配置决定要看哪些会话,比如只看当前目录下的,还是全部都看;再向 app-server(后台服务,负责保存和读取会话)一页一页要数据。用户在终端里可以打字搜索、上下移动、翻页、切换排序、切换显示密度,还能展开某条会话看预览,或者打开完整 transcript(对话记录)。界面本身不直接卡住等待数据,而是把加载工作丢给后台任务,再把结果送回来刷新画面。这样即使会话很多,界面也还能保持可操作。它还处理远端工作区、本地目录过滤、快捷键配置、保存显示偏好等细节,保证“继续会话”和“fork 会话”两种入口共用同一套可靠流程。 这一段主要在管选择器的“长什么样”。它会根据屏幕宽度,把底部按键提示变成完整文字、短文字,甚至只剩按键名;列表区域会画出会话标题、时间、目录、Git 分支,还会在空间不够时自动截断或换行。用户选中某条会话时,它会加上箭头和高亮;展开时,会显示更详细的信息和最近对话预览。这里还特别照顾了浅色/深色终端背景,避免文字看不清。可以把它想成一个终端版的排版师:同样的数据,屏幕宽时摆得从容,屏幕窄时就压缩重点,但尽量不把重要信息挤没。 从这段代码能看出,这个文件把会话列表当成一个小型“文件柜”来操作:上方有搜索和筛选,列表里有一条条历史会话,底部有当前位置和加载进度。它会处理键盘输入,比如 Ctrl+O 切换紧凑/舒适视图,Ctrl+T 打开完整 transcript(对话记录),Ctrl+E 展开当前会话预览,方向键和翻页键移动选择。它还会在列表不够填满屏幕时提前加载更多,并在分页加载时避免重复行。这里的测试特别关注用户体验细节:加载记录时先显示一个“正在加载”的画面,窄屏时自动隐藏不重要信息,选中行和斑马纹背景要铺满整行,保存视图偏好失败也不能把界面状态回滚。 从这段代码能看出,resume_picker.rs 像一个“旧会话大厅”:它把本地或服务器来的会话整理成一行行卡片,让用户用键盘上下移动、搜索、粘贴关键词、退出或开始新会话。这里的测试重点不是复杂算法,而是防止界面小毛病变成用户困惑:比如远程线程没有文件路径时不能被丢掉;聊天记录要能显示用户消息、助手回复和计划;模型的原始推理内容必须按开关决定显示或隐藏;列表太长时滚动位置要跟着选中项走;搜索没有命中时要继续加载下一页,但遇到扫描上限要停下。可以把它理解成给“会话列表遥控器”做验收:按向下、向上、Esc、粘贴,每个按钮都要产生用户预期的结果。
SessionTarget::display_label88–93 ↗
fn display_label(&self) -> String
作用:给一个目标会话生成适合显示给人的名字。有文件路径就显示路径,没有路径就退而显示 thread id,也就是后台给这段会话的编号。
数据流:输入是一个会话目标,里面可能有路径和一定有会话编号;它先看路径是否存在,存在就把路径转成文字;如果没有路径,就拼出“thread xxx”;最后返回这段可读文字。
调用关系:当恢复会话或报错时,外层流程需要告诉用户当前指的是哪一个会话,就会调用它来拿一个好懂的标签。
调用图:被 2 处调用(resume_target_session, session_start_error)。
SessionPickerAction::title117–122 ↗
fn title(self) -> &'static str
作用:返回选择器顶部标题,说明当前界面是在“继续旧会话”还是“fork 旧会话”。
数据流:输入是当前动作类型;它根据 Resume 或 Fork 选择固定英文标题;输出给界面渲染使用。
调用关系:绘制标题栏时会间接用到这个文字,让同一个选择器界面能清楚区分两种用途。
SessionPickerAction::action_label124–129 ↗
fn action_label(self) -> &'static str
作用:返回动作的小写短标签,比如 resume 或 fork。这个标签适合放在提示语、日志或错误说明里。
数据流:输入是动作类型;它把 Resume 映射成 resume,把 Fork 映射成 fork;输出一段固定文字。
调用关系:它是动作类型的文字化辅助函数,帮助界面或提示保持用词统一。
SessionPickerAction::selection131–137 ↗
fn selection(self, path: Option<PathBuf>, thread_id: ThreadId) -> SessionSelection
作用:把用户选中的路径和会话编号包装成最终结果。调用者靠这个结果知道用户到底要继续会话,还是要 fork 会话。
数据流:输入是可能存在的会话文件路径和 thread id;它先组成 SessionTarget;再根据当前动作生成 Resume 或 Fork 类型的选择结果;输出 SessionSelection。
调用关系:用户按确认键时,PickerState::handle_key 会调用它,把当前高亮行变成整个选择器要返回给外层程序的结果。
调用图:被 1 处调用(handle_key);外部调用 2 个(Fork, Resume)。
SessionFilterMode::from_show_all169–175 ↗
fn from_show_all(show_all: bool, filter_cwd: Option<&Path>) -> Self
作用:根据启动参数决定初始过滤模式:看全部会话,还是只看当前目录相关会话。
数据流:输入是 show_all 开关和可选的目录过滤条件;如果要求显示全部,或者根本没有目录可过滤,就返回 All;否则返回 Cwd。
调用关系:PickerState::new 初始化状态时调用它,决定选择器一打开时列表范围有多大。
调用图:被 1 处调用(new)。
SessionFilterMode::toggle177–183 ↗
fn toggle(self, filter_cwd: Option<&Path>) -> Self
作用:在“当前目录”和“全部”两个过滤模式之间切换。没有当前目录可用时,它不会假装能切回当前目录。
数据流:输入是当前过滤模式和可选目录;当前是 Cwd 就切到 All;当前是 All 且有目录就切到 Cwd;没有目录就保持 All;输出新的过滤模式。
调用关系:用户在工具栏里切换过滤条件时,PickerState::toggle_filter_mode 会调用它。
调用图:被 1 处调用(toggle_filter_mode)。
ToolbarControl::previous193–198 ↗
fn previous(self) -> Self
作用:把工具栏焦点切到上一个控件。这里只有过滤和排序两个控件,所以就是两者来回切。
数据流:输入当前聚焦的工具栏控件;如果是 Filter 就返回 Sort,如果是 Sort 就返回 Filter;输出新的焦点。
调用关系:用户按反向 Tab 时,PickerState::focus_previous_toolbar_control 会调用它。
调用图:被 1 处调用(focus_previous_toolbar_control)。
ToolbarControl::next200–205 ↗
fn next(self) -> Self
作用:把工具栏焦点切到下一个控件。因为工具栏只有两个选择,所以效果也是在过滤和排序之间切换。
数据流:输入当前焦点;Filter 变 Sort,Sort 变 Filter;输出新的焦点。
调用关系:用户按 Tab 时,PickerState::focus_next_toolbar_control 会调用它。
调用图:被 1 处调用(focus_next_toolbar_control)。
SessionListDensity::toggle215–220 ↗
fn toggle(self) -> Self
作用:切换列表显示密度:舒适模式和紧凑模式互相切换。
数据流:输入当前密度;Comfortable 变 Dense,Dense 变 Comfortable;输出新的密度。
调用关系:用户按切换密度快捷键时,PickerState::toggle_density 会调用它,并尝试保存这个偏好。
调用图:被 1 处调用(toggle_density)。
SessionListDensity::from224–229 ↗
fn from(mode: SessionPickerViewMode) -> Self
作用:把配置里的视图模式转换成运行时列表密度。配置是存盘格式,密度是界面实际使用的状态。
数据流:输入 SessionPickerViewMode;Comfortable 对应 Comfortable,Dense 对应 Dense;输出 SessionListDensity。
调用关系:启动恢复选择器或 fork 选择器时会调用它,把用户配置读成界面初始显示方式。
调用图:被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context)。
SessionPickerViewMode::from233–238 ↗
fn from(density: SessionListDensity) -> Self
作用:把当前列表密度转换回配置里的视图模式,方便写入配置文件。
数据流:输入 SessionListDensity;按同名关系转成 SessionPickerViewMode;输出可保存的配置值。
调用关系:PickerState::persist_density 保存用户密度偏好时会调用它。
run_resume_picker_with_app_server304–320 ↗
async fn run_resume_picker_with_app_server(
tui: &mut Tui,
config: &Config,
show_all: bool,
include_non_interactive: bool,
app_server: AppServerSession,
) -> Result<SessionSelectio
作用:从程序启动入口打开“继续旧会话”的选择器。它是给外层主程序用的方便入口。
数据流:输入终端对象、配置、过滤开关、是否包含非交互会话、app-server 会话;它补上启动场景为 Startup,然后交给更通用的 run_resume_picker_with_launch_context;输出用户最终选择。
调用关系:run_ratatui_app 会调用它。它自己不跑完整逻辑,而是把工作交给 run_resume_picker_with_launch_context。
调用图:调用 1 个内部函数(run_resume_picker_with_launch_context);被 1 处调用(run_ratatui_app)。
run_resume_picker_from_existing_session_with_app_server322–338 ↗
async fn run_resume_picker_from_existing_session_with_app_server(
tui: &mut Tui,
config: &Config,
show_all: bool,
include_non_interactive: bool,
app_server: AppServerSession,
) ->
作用:从一个已经打开的会话里再弹出“继续旧会话”的选择器。它和启动时选择器类似,但会标记来源是 ExistingSession。
数据流:输入终端、配置、过滤开关、是否包含非交互会话和 app-server;它设置 launch_context 为 ExistingSession;再调用通用运行函数;输出用户选择。
调用关系:运行中的事件处理流程 handle_event 会调用它,用于在已有会话里切换或恢复别的会话。
调用图:调用 1 个内部函数(run_resume_picker_with_launch_context);被 1 处调用(handle_event)。
run_resume_picker_with_launch_context340–385 ↗
async fn run_resume_picker_with_launch_context(
tui: &mut Tui,
config: &Config,
show_all: bool,
include_non_interactive: bool,
app_server: AppServerSession,
launch_context: Ses
作用:准备并启动“继续旧会话”选择器的核心入口。它负责把配置、远端状态、过滤规则、快捷键和后台加载器都装配好。
数据流:输入终端、配置、显示范围、是否包含非交互会话、app-server 和启动场景;它计算目录过滤、提供商过滤、快捷键、初始密度等选项;再创建后台加载器并运行选择器;输出 SessionSelection。
调用关系:两个 resume 入口都会调用它。它向下调用 run_session_picker_with_loader,真正进入界面事件循环。
调用图:调用 10 个内部函数(remote_cwd_override, uses_remote_workspace, from, local_picker_cwd_filter, picker_cwd_filter, picker_provider_filter, picker_runtime_keymap, raw_reasoning_visibility, run_session_picker_with_loader, spawn_app_server_page_loader);被 2 处调用(run_resume_picker_from_existing_session_with_app_server, run_resume_picker_with_app_server);外部调用 1 个(unbounded_channel)。
run_fork_picker_with_app_server387–430 ↗
async fn run_fork_picker_with_app_server(
tui: &mut Tui,
config: &Config,
show_all: bool,
app_server: AppServerSession,
) -> Result<SessionSelection>
作用:启动“fork 旧会话”的选择器。fork 可以理解为从旧对话复制出一个新分支继续走。
数据流:输入终端、配置、是否显示全部和 app-server;它计算过滤、快捷键、密度等选项,并把动作设为 Fork;创建后台加载器后进入通用选择器;输出用户选择。
调用关系:run_ratatui_app 会在需要 fork 会话时调用它。它和 resume 入口共用 run_session_picker_with_loader。
调用图:调用 10 个内部函数(remote_cwd_override, uses_remote_workspace, from, local_picker_cwd_filter, picker_cwd_filter, picker_provider_filter, picker_runtime_keymap, raw_reasoning_visibility, run_session_picker_with_loader, spawn_app_server_page_loader);被 1 处调用(run_ratatui_app);外部调用 1 个(unbounded_channel)。
run_session_picker_with_loader432–501 ↗
async fn run_session_picker_with_loader(
tui: &mut Tui,
options: SessionPickerRunOptions,
picker_loader: PickerLoader,
bg_rx: mpsc::UnboundedReceiver<BackgroundEvent>,
) -> Result<Sess
作用:这是选择器真正的运行循环。它像前台接待员一样,一边听键盘和窗口事件,一边收后台加载结果,然后刷新列表或返回用户选择。
数据流:输入终端、运行选项、加载器和后台事件通道;它进入备用屏幕、创建 PickerState、开始首轮加载;循环处理按键、粘贴、绘制、后台数据;如果用户选中或退出就返回结果,事件流结束则返回 StartFresh。
调用关系:resume 和 fork 的入口都调用它。它把用户输入交给 PickerState,把画面交给 draw_picker,把后台消息交给 handle_background_event。
调用图:调用 2 个内部函数(enter, new);被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context);外部调用 2 个(new, select!)。
raw_reasoning_visibility503–509 ↗
fn raw_reasoning_visibility(config: &Config) -> RawReasoningVisibility
作用:决定完整对话记录里是否显示模型的原始推理内容。这里尊重配置开关。
数据流:输入配置;如果 show_raw_agent_reasoning 为真就返回 Visible,否则返回 Hidden;输出给 transcript 加载逻辑。
调用关系:启动 resume 或 fork 选择器时会调用它,随后传给 spawn_app_server_page_loader。
调用图:被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context)。
local_picker_cwd_filter511–520 ↗
fn local_picker_cwd_filter(
cwd_filter: &Option<PathBuf>,
uses_remote_workspace: bool,
) -> Option<PathBuf>
作用:决定本地界面是否还要自己按当前目录过滤。远端工作区已经在远端按目录筛选时,本地就不要重复筛了。
数据流:输入目录过滤条件和是否使用远端工作区;远端模式返回 None;本地模式复制原过滤路径返回。
调用关系:选择器启动时调用它。PickerState::row_matches_filter 后面会用这个本地过滤条件。
调用图:被 3 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context, remote_picker_sends_cwd_filter_without_local_post_filtering)。
picker_provider_filter522–528 ↗
fn picker_provider_filter(config: &Config, uses_remote_workspace: bool) -> ProviderFilter
作用:决定是否按默认模型提供商过滤会话。远端工作区下不强行按本机默认提供商筛选。
数据流:输入配置和远端标记;远端返回 Any;本地返回 MatchDefault,并带上配置里的 model_provider_id;输出 ProviderFilter。
调用关系:resume 和 fork 入口都调用它,之后 thread_list_params 会把这个决定放进请求参数。
调用图:被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context);外部调用 1 个(MatchDefault)。
picker_runtime_keymap530–533 ↗
fn picker_runtime_keymap(config: &Config) -> Result<RuntimeKeymap>
作用:把配置里的快捷键设置变成运行时能直接判断的快捷键表。配置错了会给出清楚错误。
数据流:输入配置;它读取 tui_keymap 并调用 RuntimeKeymap::from_config;成功输出 RuntimeKeymap,失败输出带说明的错误。
调用关系:选择器启动前必须调用它,否则按键处理不知道哪些键代表确认、取消、翻页等动作。
调用图:调用 1 个内部函数(from_config);被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context)。
picker_cwd_filter535–548 ↗
fn picker_cwd_filter(
config_cwd: &Path,
show_all: bool,
uses_remote_workspace: bool,
remote_cwd_override: Option<&Path>,
) -> Option<PathBuf>
作用:决定向后台请求会话时要不要带“当前目录”过滤。这样列表可以只显示和当前项目有关的会话。
数据流:输入配置目录、show_all、远端标记和远端目录覆盖值;show_all 返回 None;远端使用远端目录覆盖;本地使用配置目录;输出可选路径。
调用关系:resume 和 fork 入口都调用它,结果会进入 PageLoadRequest 和 thread_list_params。
调用图:被 3 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context, local_picker_thread_list_params_include_cwd_filter);外部调用 1 个(to_path_buf)。
spawn_app_server_page_loader550–605 ↗
fn spawn_app_server_page_loader(
app_server: AppServerSession,
include_non_interactive: bool,
raw_reasoning_visibility: RawReasoningVisibility,
bg_tx: mpsc::UnboundedSender<BackgroundE
作用:启动一个后台加载工人,专门向 app-server 要列表页、预览和完整对话记录。这样前台界面不会因为读数据而卡住。
数据流:输入 app-server 会话、是否包含非交互会话、推理显示策略和回传通道;它创建请求通道并 spawn 异步任务;任务收到 Page、Preview、Transcript 请求后分别加载,再把 BackgroundEvent 发回前台;最后关闭 app-server。
调用关系:resume 和 fork 入口创建它;PickerState 后续通过返回的 PickerLoader 发加载请求,run_session_picker_with_loader 从 bg_rx 收结果。
调用图:调用 4 个内部函数(shutdown, load_app_server_page, load_transcript_preview, load_session_transcript);被 2 处调用(run_fork_picker_with_app_server, run_resume_picker_with_launch_context);外部调用 4 个(new, send, spawn, warn!)。
sort_key_label608–613 ↗
fn sort_key_label(sort_key: ThreadSortKey) -> &'static str
作用:把排序字段变成界面上能看懂的标签。
数据流:输入排序字段;CreatedAt 返回 Created,UpdatedAt 返回 Updated;输出固定文字。
调用关系:排序工具栏渲染时会调用它,让用户知道当前按创建时间还是更新时间排序。
AltScreenGuard::enter621–624 ↗
fn enter(tui: &'a mut Tui) -> Self
作用:进入终端备用屏幕。备用屏幕像临时白板,选择器关闭后不会把原来的终端内容弄乱。
数据流:输入 Tui;它尝试调用 enter_alt_screen;然后保存这个 Tui 引用到守卫对象里;输出 AltScreenGuard。
调用关系:run_session_picker_with_loader 开始时调用它。后面 AltScreenGuard::drop 会自动退出备用屏幕。
调用图:外部调用 1 个(enter_alt_screen)。
AltScreenGuard::drop628–630 ↗
fn drop(&mut self)
作用:在守卫对象销毁时自动离开备用屏幕。这样即使中途返回或出错,也尽量恢复终端。
数据流:输入自身持有的 Tui;它调用 leave_alt_screen;不返回业务结果,只做清理动作。
调用关系:这是 Rust 的 Drop 清理逻辑,由作用域结束自动触发,和 AltScreenGuard::enter 成对出现。
调用图:外部调用 1 个(leave_alt_screen)。
LoadingState::is_pending728–730 ↗
fn is_pending(&self) -> bool
作用:快速判断当前是否正在加载数据。
数据流:输入加载状态;如果是 Pending 就返回 true,否则 false。
调用关系:翻页、页脚进度、补充加载等地方都会用它避免重复发请求或显示错误状态。
调用图:外部调用 1 个(matches!)。
load_app_server_page733–763 ↗
async fn load_app_server_page(
app_server: &mut AppServerSession,
cursor: Option<String>,
cwd_filter: Option<&Path>,
provider_filter: ProviderFilter,
sort_key: ThreadSortKey,
i
作用:从 app-server 加载一页会话列表,并整理成选择器能显示的行。
数据流:输入 app-server、分页游标、目录过滤、提供商过滤、排序字段和是否包含非交互会话;它组装 thread_list_params 发请求;把返回的线程数据转换成 Row;输出 PickerPage。
调用关系:后台加载任务 spawn_app_server_page_loader 收到 Page 请求时调用它。
调用图:调用 2 个内部函数(thread_list, thread_list_params);被 1 处调用(spawn_app_server_page_loader)。
load_transcript_preview765–815 ↗
async fn load_transcript_preview(
app_server: &mut AppServerSession,
thread_id: ThreadId,
) -> std::io::Result<Vec<TranscriptPreviewLine>>
作用:加载某个会话的简短预览,最多保留最后几行用户和助手消息。这样展开列表项时不用打开完整记录。
数据流:输入 app-server 和 thread id;它读取带 turns 的线程内容;抽取用户文本和助手文本,助手文本会解析 Markdown 后取可见内容;拆成非空行,只留下最后 6 行;输出预览行列表或错误。
调用关系:后台加载任务收到 Preview 请求时调用它,结果回到 PickerState 存进 transcript_previews。
调用图:调用 1 个内部函数(thread_read);被 1 处调用(spawn_app_server_page_loader)。
SearchState::active_token818–823 ↗
fn active_token(&self) -> Option<usize>
作用:取出当前搜索任务的编号。编号用来区分旧搜索和新搜索,避免过期结果乱入。
数据流:输入搜索状态;Idle 返回 None;Active 返回里面的 token;输出可选编号。
调用关系:SearchState::is_active 和 PickerState 的继续搜索逻辑都会用它判断当前有没有正在进行的搜索。
调用图:被 1 处调用(is_active)。
SearchState::is_active825–827 ↗
fn is_active(&self) -> bool
作用:判断搜索是否正在继续向后翻页找结果。
数据流:输入搜索状态;它调用 active_token,看是否能拿到编号;有编号返回 true,否则 false。
调用关系:这是搜索状态的小工具,帮助其他逻辑决定是否还要继续加载更多页。
调用图:调用 1 个内部函数(active_token)。
Row::seen_key849–854 ↗
fn seen_key(&self) -> Option<SeenRowKey>
作用:给一行会话生成去重用的钥匙。优先用文件路径,没有路径才用 thread id。
数据流:输入列表行;如果有 path,就返回 SeenRowKey::Path;否则如果有 thread_id,就返回 SeenRowKey::Thread;都没有则返回 None。
调用关系:PickerState::ingest_page 用它防止同一个会话在多页数据里重复出现。
调用图:外部调用 1 个(Path)。
Row::display_preview856–858 ↗
fn display_preview(&self) -> &str
作用:决定列表里这一行主要显示哪段文字。会话有名字就优先显示名字,没有名字再显示预览。
数据流:输入一行 Row;检查 thread_name;有就返回名字引用,没有就返回 preview 引用。
调用关系:渲染列表行时会用它,让命名过的会话显示得更清楚。
Row::matches_query860–890 ↗
fn matches_query(&self, query: &str) -> bool
作用:判断这一行是否命中用户输入的搜索词。它会查预览、会话名、编号、Git 分支和工作目录。
数据流:输入 Row 和已经小写化的查询词;它逐项把可搜索字段转小写后检查是否包含查询词;任一命中返回 true,否则 false。
调用关系:PickerState::apply_filter 在用户搜索或新数据到来时调用它,决定哪些行留在 filtered_rows。
PickerState::new894–945 ↗
fn new(
requester: FrameRequester,
picker_loader: PickerLoader,
provider_filter: ProviderFilter,
show_all: bool,
filter_cwd: Option<PathBuf>,
action: Se
作用:创建选择器的全部运行状态。它像给界面开张前摆好柜台、清空列表、设置默认焦点和快捷键。
数据流:输入绘制请求器、加载器、提供商过滤、显示全部开关、目录过滤和动作;它填充分页、搜索、列表、工具栏、密度、缓存等字段;输出一个初始 PickerState。
调用关系:run_session_picker_with_loader 启动界面时调用它,很多测试也用它构造状态来验证行为。
调用图:调用 2 个内部函数(defaults, from_show_all);被 68 处调用(run_session_picker_with_loader, all_filter_can_switch_back_to_cwd_when_cwd_candidate_exists, cached_transcript_still_shows_loading_frame_before_opening_overlay, comfortable_zebra_lines_use_full_width_background, ctrl_c_exits_even_when_cancel_is_remapped_to_ctrl_c, ctrl_e_toggles_selected_session_expansion, ctrl_o_keeps_toggled_density_when_persistence_fails, ctrl_o_persists_density_preference, ctrl_o_toggles_density_without_typing_into_search, ctrl_t_on_row_without_thread_id_shows_inline_error (+15 more));外部调用 4 个(new, new, new, new)。
PickerState::request_frame947–949 ↗
fn request_frame(&self)
作用:请求终端重新画一帧。状态变了但不请求刷新,用户就看不到变化。
数据流:输入当前状态;它调用 requester.schedule_frame;没有直接输出,只安排后续 Draw 事件。
调用关系:列表变化、错误提示、加载完成、焦点移动等很多函数都会调用它。
调用图:调用 1 个内部函数(schedule_frame);被 13 处调用(apply_filter, begin_transcript_loading, clear_query_preserving_selection, complete_pending_page_down, handle_background_event, handle_key, handle_overlay_event, load_more_if_needed, open_pending_transcript_if_ready, open_selected_transcript (+3 more))。
PickerState::is_transcript_loading951–953 ↗
fn is_transcript_loading(&self) -> bool
作用:判断当前是否正在准备打开完整对话记录。
数据流:输入状态;检查 pending_transcript_open 是否有 thread id;有就返回 true,否则 false。
调用关系:按键、粘贴和页脚提示会用它,在加载完整记录期间限制普通列表操作。
调用图:被 3 处调用(handle_key, handle_paste, footer_hint_lines)。
PickerState::note_transcript_loading_frame_drawn955–962 ↗
fn note_transcript_loading_frame_drawn(&mut self) -> bool
作用:记录“加载中”画面已经画过一帧。这样即使记录很快加载完,也能让用户看到界面有响应。
数据流:输入可变状态;如果有待打开 transcript,就把 transcript_loading_frame_shown 设为 true 并返回 true;否则返回 false。
调用关系:绘制循环 draw_picker 后调用它,随后可能触发 open_pending_transcript_if_ready。
PickerState::open_pending_transcript_if_ready964–982 ↗
fn open_pending_transcript_if_ready(&mut self)
作用:在完整对话记录已经加载好,并且加载中画面也显示过以后,真正打开覆盖层。
数据流:输入可变状态;它检查是否已经画过加载帧、是否有待打开 thread id、缓存里是否有 Loaded cells;满足条件就创建 transcript overlay,清掉等待标记并请求刷新。
调用关系:后台 transcript 加载完成后,或绘制加载帧后,会调用它把用户带入完整记录查看界面。
调用图:调用 2 个内部函数(new_transcript, request_frame);被 1 处调用(handle_background_event);外部调用 1 个(clone)。
PickerState::begin_transcript_loading984–988 ↗
fn begin_transcript_loading(&mut self, thread_id: ThreadId)
作用:开始进入“正在打开完整对话记录”的状态。
数据流:输入 thread id;它记录待打开的会话编号,清掉“加载帧已显示”标记,并请求重画;输出为空。
调用关系:PickerState::open_selected_transcript 在用户要求打开记录时调用它。
调用图:调用 1 个内部函数(request_frame);被 1 处调用(open_selected_transcript)。
PickerState::handle_overlay_event990–1000 ↗
fn handle_overlay_event(&mut self, tui: &mut Tui, event: TuiEvent) -> Result<()>
作用:如果当前有覆盖层,比如完整对话记录窗口,就把事件交给覆盖层处理。
数据流:输入 Tui 和事件;若没有 overlay 就什么也不做;有的话调用 overlay.handle_event;如果 overlay 表示结束,就关闭它并请求刷新;输出成功或错误。
调用关系:主事件循环发现 overlay 存在时优先调用它,避免列表快捷键干扰覆盖层。
调用图:调用 1 个内部函数(request_frame)。
PickerState::open_selected_transcript1002–1026 ↗
fn open_selected_transcript(&mut self)
作用:打开当前选中会话的完整对话记录。如果还没加载,就先发后台加载请求。
数据流:输入当前状态;它取当前选中行,确认有 thread id;如果没有就显示错误;如果记录已加载或加载中就进入等待打开状态;如果未加载或失败过,就标记 Loading 并通过 picker_loader 请求 Transcript。
调用关系:用户按打开 transcript 的快捷键时,PickerState::handle_key 调用它。
调用图:调用 2 个内部函数(begin_transcript_loading, request_frame);被 1 处调用(handle_key)。
PickerState::handle_transcript_loading_key1028–1037 ↗
fn handle_transcript_loading_key(&mut self, key: KeyEvent) -> Option<SessionSelection>
作用:在完整记录加载期间只处理少量按键,目前主要允许 Ctrl-C 退出。
数据流:输入按键事件;如果是 Ctrl-C 返回 Exit;其他按键返回 None。
调用关系:PickerState::handle_key 一开始发现 transcript 正在加载时,会把按键交给它。
调用图:被 1 处调用(handle_key)。
PickerState::handle_key1039–1229 ↗
async fn handle_key(&mut self, key: KeyEvent) -> Result<Option<SessionSelection>>
作用:处理选择器里的所有键盘操作。它决定用户是在搜索、移动选择、确认、取消、翻页、切换过滤、切换排序、展开预览,还是打开完整记录。
数据流:输入一个按键;它先清错误、处理加载中特殊情况,再按快捷键配置和固定控制键修改状态;可能发起加载、刷新画面,或返回 SessionSelection;没有最终选择时返回 None。
调用关系:主事件循环收到 TuiEvent::Key 时调用它。它会调用许多 PickerState 小函数,也会在确认时调用 SessionPickerAction::selection。
调用图:调用 17 个内部函数(is_plain_text_key_event, change_focused_toolbar_value, clear_query_preserving_selection, ensure_selected_visible, focus_next_toolbar_control, focus_previous_toolbar_control, handle_transcript_loading_key, is_transcript_loading, load_more_if_needed, maybe_load_more_for_scroll (+7 more));外部调用 2 个(from, format!)。
PickerState::handle_paste1231–1244 ↗
fn handle_paste(&mut self, pasted: String)
作用:处理用户粘贴到搜索框的文字。它会把粘贴内容整理成适合搜索的一段文本。
数据流:输入粘贴字符串;如果正在加载完整记录就忽略;否则规范化文本,必要时在现有查询后加空格,再追加粘贴内容并调用 set_query;输出为空但会改变搜索状态。
调用关系:主事件循环收到 TuiEvent::Paste 时调用它。
调用图:调用 3 个内部函数(normalize_pasted_search_query, is_transcript_loading, set_query)。
PickerState::start_initial_load1246–1280 ↗
fn start_initial_load(&mut self)
作用:从头开始加载列表。切换过滤或排序时,也会用它清空旧列表重新请求第一页。
数据流:输入当前状态;它记录时间基准、重置分页和列表、清去重集合、重置选择;如果已有搜索词就分配搜索 token;再分配请求 token、标记 Pending、请求刷新,并通过 picker_loader 请求第一页。
调用关系:run_session_picker_with_loader 启动后调用它;toggle_filter_mode 和 toggle_sort_key 也会调用它重载数据。
调用图:调用 5 个内部函数(active_cwd_filter, allocate_request_token, allocate_search_token, request_frame, reset_pagination);被 2 处调用(toggle_filter_mode, toggle_sort_key);外部调用 4 个(now, Pending, Page, clone)。
PickerState::handle_background_event1282–1339 ↗
async fn handle_background_event(&mut self, event: BackgroundEvent) -> Result<()>
作用:处理后台加载结果。列表页、预览、完整记录都是从这里回到前台状态里的。
数据流:输入 BackgroundEvent;如果是 Page,就核对请求 token、防止旧结果覆盖新结果,然后合并页面并继续搜索;如果是 Preview,就缓存预览或失败状态;如果是 Transcript,就缓存完整记录、打开覆盖层或显示错误;最后按需请求刷新。
调用关系:主事件循环从后台通道收到事件时调用它。后台事件来自 spawn_app_server_page_loader。
调用图:调用 5 个内部函数(complete_pending_page_down, continue_search_if_token_matches, ingest_page, open_pending_transcript_if_ready, request_frame);外部调用 2 个(Loaded, Loaded)。
PickerState::reset_pagination1341–1347 ↗
fn reset_pagination(&mut self)
作用:把分页状态清回起点。
数据流:输入可变状态;它清掉下一页游标、已扫描数量、扫描上限标记、加载状态和冻结的页脚百分比;无返回。
调用关系:PickerState::start_initial_load 开始新一轮列表加载前调用它。
调用图:被 1 处调用(start_initial_load)。
PickerState::ingest_page1349–1374 ↗
fn ingest_page(&mut self, page: PickerPage)
作用:把新加载的一页会话合并进列表,并马上重新应用过滤和搜索。
数据流:输入 PickerPage;它更新下一页游标、扫描数量和扫描上限;逐行用 seen_key 去重后加入 all_rows;最后调用 apply_filter 生成当前可见列表。
调用关系:PickerState::handle_background_event 收到 Page 结果后调用它。
调用图:调用 1 个内部函数(apply_filter);被 1 处调用(handle_background_event)。
PickerState::complete_pending_page_down1376–1395 ↗
fn complete_pending_page_down(&mut self)
作用:完成一次跨页 PageDown 操作。用户想跳到还没加载出来的位置时,它会等新页回来后继续。
数据流:输入当前状态;如果没有待完成目标就返回;如果目标仍超过当前列表且还有下一页,就继续加载;否则把 selected 移到目标或末尾,保证可见,必要时继续预加载并刷新。
调用关系:后台新页面到达后,PickerState::handle_background_event 调用它。
调用图:调用 4 个内部函数(ensure_selected_visible, load_more_if_needed, maybe_load_more_for_scroll, request_frame);被 1 处调用(handle_background_event)。
PickerState::apply_filter1397–1416 ↗
fn apply_filter(&mut self)
作用:根据当前过滤模式和搜索词重新生成可见列表。
数据流:输入当前状态;它先用 row_matches_filter 筛目录,再用 matches_query 筛搜索词;更新 filtered_rows;修正 selected 和 scroll_top;保证选中项可见并请求刷新。
调用关系:新数据进入、搜索词变化、清空搜索时都会调用它。
调用图:调用 2 个内部函数(ensure_selected_visible, request_frame);被 3 处调用(clear_query_preserving_selection, ingest_page, set_query)。
PickerState::row_matches_filter1418–1429 ↗
fn row_matches_filter(&self, row: &Row) -> bool
作用:判断一行是否符合当前目录过滤规则。
数据流:输入一行 Row;如果当前是 All 直接通过;如果没有本地目录过滤也通过;否则要求 row.cwd 存在并和过滤目录匹配;输出 true 或 false。
调用关系:PickerState::apply_filter 调用它。实际路径比较交给 paths_match。
调用图:调用 1 个内部函数(paths_match)。
PickerState::set_query1431–1453 ↗
fn set_query(&mut self, new_query: String)
作用:设置新的搜索词,并在当前已加载数据里搜索;如果没搜到但后面还有页面,就继续向后台加载更多来找。
数据流:输入新查询字符串;如果没变化就返回;否则更新 query、重置 selected、应用过滤;空查询或已有结果就结束搜索;如果还有下一页且没到扫描上限,就分配搜索 token 并请求加载更多。
调用关系:键盘输入和粘贴都会调用它。它会触发 apply_filter 和 load_more_if_needed。
调用图:调用 3 个内部函数(allocate_search_token, apply_filter, load_more_if_needed);被 2 处调用(handle_key, handle_paste)。
PickerState::clear_query_preserving_selection1455–1473 ↗
fn clear_query_preserving_selection(&mut self)
作用:清空搜索词,但尽量保持之前选中的那条会话仍然被选中。
数据流:输入当前状态;它先记下选中行的 seen_key;清空 query 和搜索状态;重新过滤;如果能在新列表里找到同一行,就把 selected 移过去并刷新。
调用关系:用户按取消键且搜索框不为空时,PickerState::handle_key 调用它。
调用图:调用 3 个内部函数(apply_filter, ensure_selected_visible, request_frame);被 1 处调用(handle_key)。
PickerState::continue_search_if_needed1475–1488 ↗
fn continue_search_if_needed(&mut self)
作用:搜索还没找到结果时,决定是否继续翻下一页找。
数据流:输入当前状态;如果没有活跃搜索就返回;如果已有结果、到达扫描上限或没有下一页,就结束搜索;否则按当前搜索 token 请求加载更多。
调用关系:continue_search_if_token_matches 会调用它,是搜索跨页继续的核心步骤。
调用图:调用 1 个内部函数(load_more_if_needed);被 1 处调用(continue_search_if_token_matches);外部调用 1 个(active_token)。
PickerState::continue_search_if_token_matches1490–1500 ↗
fn continue_search_if_token_matches(&mut self, completed_token: Option<usize>)
作用:只在刚完成的后台请求属于当前搜索时,才继续搜索。这样旧搜索结果不会影响新搜索。
数据流:输入完成的搜索 token;它读取当前活跃 token;如果完成 token 存在且不一致就忽略;一致或没有完成 token 时调用 continue_search_if_needed。
调用关系:PickerState::handle_background_event 处理完 Page 后调用它。
调用图:调用 1 个内部函数(continue_search_if_needed);被 1 处调用(handle_background_event);外部调用 1 个(active_token)。
PickerState::ensure_selected_visible1502–1517 ↗
fn ensure_selected_visible(&mut self)
作用:调整滚动位置,保证当前选中行出现在可视区域里。
数据流:输入当前状态;如果列表空就把 scroll_top 设为 0;否则根据视口高度、行渲染高度和上下提示占用,逐步移动 scroll_top,直到选中项可见。
调用关系:移动选择、过滤、窗口大小变化、密度变化等场景都会调用它。
调用图:调用 2 个内部函数(available_content_rows, rendered_height_between);被 6 处调用(apply_filter, clear_query_preserving_selection, complete_pending_page_down, handle_key, toggle_density, update_viewport)。
PickerState::ensure_minimum_rows_for_view1519–1539 ↗
fn ensure_minimum_rows_for_view(&mut self, minimum_rows: usize)
作用:保证当前窗口至少有足够内容可显示。如果已加载行太少且还有下一页,就自动补加载。
数据流:输入期望的最小行数;如果高度为 0、正在加载或没有下一页就返回;计算当前渲染高度是否够用;不够时根据是否处于搜索状态发起加载更多。
调用关系:绘制前窗口尺寸已知时会调用它,避免大窗口里列表空荡荡但其实后面还有数据。
调用图:调用 3 个内部函数(available_content_rows, load_more_if_needed, rendered_height_between);外部调用 1 个(active_token)。
PickerState::update_viewport1541–1545 ↗
fn update_viewport(&mut self, rows: usize, width: u16)
作用:更新选择器知道的可视区域大小,并重新确保选中行可见。
数据流:输入行数和宽度;行数为 0 就清掉 view_rows,否则保存;保存 view_width;调用 ensure_selected_visible。
调用关系:主绘制流程在 Draw 或 Resize 事件里根据终端尺寸调用它。
调用图:调用 1 个内部函数(ensure_selected_visible)。
PickerState::maybe_load_more_for_scroll1547–1561 ↗
fn maybe_load_more_for_scroll(&mut self)
作用:用户快滚到列表底部时,提前加载下一页。
数据流:输入当前状态;如果正在加载、没有下一页或列表为空就返回;计算选中项下面还剩多少行;少于阈值时调用 load_more_if_needed。
调用关系:向下移动、跳到底部、完成跨页 PageDown 后会调用它,让滚动更顺滑。
调用图:调用 1 个内部函数(load_more_if_needed);被 2 处调用(complete_pending_page_down, handle_key)。
PickerState::load_more_if_needed1563–1590 ↗
fn load_more_if_needed(&mut self, trigger: LoadTrigger)
作用:在确实需要且可以加载时,请求后台加载下一页。
数据流:输入加载触发原因;如果已经加载中或没有下一页游标就返回;冻结页脚百分比,分配请求 token,根据触发原因带不带搜索 token;标记 Pending、请求刷新,并通过 picker_loader 发 Page 请求。
调用关系:搜索、滚动、窗口填充和跨页翻页都会调用它;真正的数据读取由后台加载器完成。
调用图:调用 4 个内部函数(active_cwd_filter, allocate_request_token, freeze_footer_percent, request_frame);被 6 处调用(complete_pending_page_down, continue_search_if_needed, ensure_minimum_rows_for_view, handle_key, maybe_load_more_for_scroll, set_query);外部调用 3 个(Pending, Page, clone)。
PickerState::allocate_request_token1597–1601 ↗
fn allocate_request_token(&mut self) -> usize
作用:生成一个新的请求编号,用来识别后台返回的是哪次请求。
数据流:输入可变状态;读取 next_request_token 作为结果,然后把计数器加一,溢出时环绕;输出 token。
调用关系:start_initial_load 和 load_more_if_needed 发请求前调用它。
调用图:被 2 处调用(load_more_if_needed, start_initial_load)。
PickerState::allocate_search_token1603–1607 ↗
fn allocate_search_token(&mut self) -> usize
作用:生成一个新的搜索编号,用来区分不同搜索词对应的加载过程。
数据流:输入可变状态;读取 next_search_token,然后把计数器加一,溢出时环绕;输出 token。
调用关系:start_initial_load 和 set_query 在需要跨页搜索时调用它。
调用图:被 2 处调用(set_query, start_initial_load)。
PickerState::toggle_sort_key1614–1620 ↗
fn toggle_sort_key(&mut self)
作用:在按更新时间排序和按创建时间排序之间切换,并重新加载列表。
数据流:输入当前状态;UpdatedAt 变 CreatedAt,CreatedAt 变 UpdatedAt;然后调用 start_initial_load 清空并重载第一页。
调用关系:工具栏焦点在 Sort 上且用户改变值时,change_focused_toolbar_value 调用它。
调用图:调用 1 个内部函数(start_initial_load);被 1 处调用(change_focused_toolbar_value)。
PickerState::toggle_filter_mode1622–1629 ↗
fn toggle_filter_mode(&mut self)
作用:切换列表过滤范围,比如当前目录和全部会话之间切换,并重新加载。
数据流:输入当前状态;调用 SessionFilterMode::toggle 计算下一个模式;如果没变化就返回;否则保存新模式并 start_initial_load。
调用关系:工具栏焦点在 Filter 上且用户改变值时,change_focused_toolbar_value 调用它。
调用图:调用 2 个内部函数(start_initial_load, toggle);被 1 处调用(change_focused_toolbar_value)。
PickerState::active_cwd_filter1631–1636 ↗
fn active_cwd_filter(&self) -> Option<PathBuf>
作用:返回当前真正应该发给后台的目录过滤条件。
数据流:输入当前状态;如果过滤模式是 Cwd,就复制 filter_cwd;如果是 All,就返回 None。
调用关系:start_initial_load 和 load_more_if_needed 发 Page 请求时调用它。
调用图:被 2 处调用(load_more_if_needed, start_initial_load)。
PickerState::focus_previous_toolbar_control1638–1640 ↗
fn focus_previous_toolbar_control(&mut self)
作用:把工具栏焦点移到上一个控件。
数据流:输入可变状态;调用 ToolbarControl::previous;把结果写回 toolbar_focus。
调用关系:PickerState::handle_key 处理 BackTab 时调用它。
调用图:调用 1 个内部函数(previous);被 1 处调用(handle_key)。
PickerState::focus_next_toolbar_control1642–1644 ↗
fn focus_next_toolbar_control(&mut self)
作用:把工具栏焦点移到下一个控件。
数据流:输入可变状态;调用 ToolbarControl::next;把结果写回 toolbar_focus。
调用关系:PickerState::handle_key 处理 Tab 时调用它。
调用图:调用 1 个内部函数(next);被 1 处调用(handle_key)。
PickerState::change_focused_toolbar_value1646–1651 ↗
fn change_focused_toolbar_value(&mut self)
作用:改变当前聚焦工具栏控件的值。焦点在排序上就切排序,焦点在过滤上就切过滤。
数据流:输入当前状态;检查 toolbar_focus;Sort 调用 toggle_sort_key,Filter 调用 toggle_filter_mode;无直接返回。
调用关系:PickerState::handle_key 处理左右键一类工具栏操作时调用它。
调用图:调用 2 个内部函数(toggle_filter_mode, toggle_sort_key);被 1 处调用(handle_key)。
PickerState::toggle_density1653–1661 ↗
async fn toggle_density(&mut self)
作用:切换列表疏密显示,并尝试把这个偏好保存到配置文件。
数据流:输入可变状态;调用 density.toggle 更新密度;修正滚动位置;调用 persist_density 保存;如果失败就记录警告并在界面显示错误;最后请求刷新。
调用关系:PickerState::handle_key 处理 Ctrl-O 相关快捷键时调用它。
调用图:调用 4 个内部函数(ensure_selected_visible, persist_density, request_frame, toggle);被 1 处调用(handle_key);外部调用 2 个(format!, warn!)。
PickerState::persist_density1663–1675 ↗
async fn persist_density(&self) -> Result<()>
作用:把当前列表密度写入配置文件,方便下次打开选择器时保持同样显示方式。
数据流:输入当前状态;如果没有 view_persistence 就直接成功;否则用 ConfigEditsBuilder 指向 codex_home,把 density 转成 SessionPickerViewMode 后写入 config.toml;输出成功或错误。
调用关系:PickerState::toggle_density 调用它。写入失败不会阻止界面继续用新密度,只会显示错误。
调用图:调用 1 个内部函数(new);被 1 处调用(toggle_density);外部调用 1 个(from)。
PickerState::toggle_selected_expansion1677–1697 ↗
fn toggle_selected_expansion(&mut self)
作用:展开或收起当前选中会话的简短预览。第一次展开时会请求后台加载预览。
数据流:输入当前状态;取当前选中行和 thread id;如果已经展开同一条就收起;否则记录 expanded_thread_id;如果预览缓存没有这条,就标记 Loading 并发 Preview 请求;最后刷新。
调用关系:PickerState::handle_key 处理展开快捷键时调用它,后台结果由 handle_background_event 接收。
调用图:调用 1 个内部函数(request_frame);被 1 处调用(handle_key)。
PickerState::rendered_height_between1699–1723 ↗
fn rendered_height_between(&self, start: usize, end_inclusive: usize) -> usize
作用:计算列表中一段行真正画出来会占多少终端行。因为展开项、宽度换行和显示密度都会影响高度。
数据流:输入起始和结束索引;它遍历这段 filtered_rows,调用 render_session_lines 得到每行渲染行数,再加上行间分隔高度;输出总高度。
调用关系:滚动可见性、页脚百分比、是否需要加载更多等逻辑都依赖它。
调用图:调用 1 个内部函数(row_separator_height);被 3 处调用(ensure_minimum_rows_for_view, ensure_selected_visible, picker_footer_scroll_percent)。
PickerState::has_more_above1725–1727 ↗
fn has_more_above(&self) -> bool
作用:判断当前视口上方是否还有没显示的内容。
数据流:输入当前状态;检查 scroll_top 是否大于 0;输出 true 或 false。
调用关系:render_list 和 available_content_rows 会用它决定是否显示上方还有内容的提示,以及计算可用高度。
调用图:被 2 处调用(available_content_rows, render_list)。
PickerState::has_more_below1729–1759 ↗
fn has_more_below(&self, viewport_height: usize) -> bool
作用:判断当前视口下方是否还有没显示的内容,或者后台还有下一页。
数据流:输入视口高度;如果列表空返回 false;如果有下一页直接 true;否则从 scroll_top 开始累加每行渲染高度和分隔线,超过可用高度就说明下方还有内容;输出布尔值。
调用关系:render_list 调用它决定是否显示“下面还有”的提示。
调用图:调用 3 个内部函数(available_content_rows, row_separator_height, render_session_lines);被 1 处调用(render_list);外部调用 1 个(from)。
PickerState::available_content_rows1761–1769 ↗
fn available_content_rows(&self, viewport_height: usize) -> usize
作用:计算列表区域里真正能用来显示内容的行数。上下“还有内容”的提示会占掉空间。
数据流:输入视口高度;根据 has_more_above 和是否还有下方内容扣掉 0 或 1 行;最少返回 1。
调用关系:滚动、页脚百分比和是否需要补加载都会用它。
调用图:调用 1 个内部函数(has_more_above);被 4 处调用(ensure_minimum_rows_for_view, ensure_selected_visible, has_more_below, picker_footer_scroll_percent);外部调用 1 个(from)。
PickerState::row_separator_height1771–1776 ↗
fn row_separator_height(&self) -> usize
作用:返回列表行之间的空白高度。舒适模式有一行间隔,紧凑模式没有。
数据流:输入当前状态;根据 density 返回 1 或 0。
调用关系:rendered_height_between 和 has_more_below 用它计算列表实际占用高度。
调用图:被 2 处调用(has_more_below, rendered_height_between)。
row_from_app_server_thread1779–1804 ↗
fn row_from_app_server_thread(thread: Thread) -> Option<Row>
作用:把 app-server 返回的 Thread 数据转换成选择器的一行 Row。无效的 thread id 会被跳过。
数据流:输入 Thread;它解析 thread.id,失败就记录警告并返回 None;成功后整理预览、名字、时间、路径、工作目录和 Git 分支;输出 Row。
调用关系:load_app_server_page 把每页 app-server 数据转成界面行时会用它。
调用图:调用 1 个内部函数(from_string);被 1 处调用(app_server_row_keeps_pathless_threads);外部调用 3 个(from, from_timestamp, warn!)。
thread_list_params1806–1829 ↗
fn thread_list_params(
cursor: Option<String>,
cwd_filter: Option<&Path>,
provider_filter: ProviderFilter,
sort_key: ThreadSortKey,
include_non_interactive: bool,
) -> ThreadListPa
作用:组装请求 app-server 会话列表时需要的参数。它把分页、排序、过滤和来源范围都放进请求里。
数据流:输入游标、目录过滤、提供商过滤、排序字段和是否包含非交互会话;它设置页大小、排序、模型提供商、来源类型、未归档、目录条件等;输出 ThreadListParams。
调用关系:load_app_server_page 调用它,然后把参数交给 app_server.thread_list。
调用图:被 4 处调用(load_app_server_page, local_picker_thread_list_params_include_cwd_filter, remote_thread_list_params_can_include_non_interactive_sources, remote_thread_list_params_omit_provider_filter);外部调用 2 个(resume_source_kinds, vec!)。
paths_match1831–1833 ↗
fn paths_match(a: &Path, b: &Path) -> bool
作用:比较两个路径是否指向同一个位置,比较前会做规范化。这样可以避免路径写法不同但实际相同导致过滤失败。
数据流:输入两个路径;它交给 path_utils::paths_match_after_normalization;输出是否匹配。
调用关系:PickerState::row_matches_filter 用它判断会话工作目录是否等于当前过滤目录。
调用图:被 1 处调用(row_matches_filter);外部调用 1 个(paths_match_after_normalization)。
parse_timestamp_str1836–1840 ↗
fn parse_timestamp_str(ts: &str) -> Option<DateTime<Utc>>
作用:把 RFC3339 格式的时间字符串解析成 UTC 时间。RFC3339 是一种常见的标准时间写法,比如带时区的 ISO 时间。
数据流:输入时间字符串;解析成功就转换到 UTC 并返回 Some;失败返回 None。
调用关系:主要给测试和构造 Row 的辅助代码使用,用来准备带时间的会话行。
调用图:被 12 处调用(comfortable_zebra_lines_use_full_width_background, dense_session_line_prefers_thread_name_over_preview, dense_session_snapshot_uses_no_blank_lines_between_rows, dense_snapshot_row, density_toggle_clears_stale_more_indicator, expanded_session_details_include_metadata, expanded_session_snapshot, make_row, narrow_session_snapshot, render_dense_row_snapshot (+2 more));外部调用 1 个(parse_from_rfc3339)。
draw_picker1842–1892 ↗
fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()>
作用:把整个会话选择器画到终端上,包括标题、搜索栏、列表、加载遮罩和页脚。
数据流:输入 Tui 和 PickerState;它读取终端大小,切分出 header、search、list、footer 区域;分别渲染标题、搜索行、列表、transcript 加载遮罩和页脚;输出绘制成功或 I/O 错误。
调用关系:主事件循环收到 Draw 或 Resize 时调用它。它串起多个渲染函数。
调用图:外部调用 1 个(draw)。
list_viewport_width1894–1896 ↗
fn list_viewport_width(width: u16) -> u16
作用:计算列表实际可用宽度,给左右留出固定缩进。
数据流:输入终端区域宽度;减去 PICKER_LIST_HORIZONTAL_INSET,若不够则安全地归零;输出宽度。
调用关系:draw_picker 和运行循环更新视口时都会用它。
search_line1898–1933 ↗
fn search_line(state: &PickerState, width: u16) -> Line<'_>
作用:生成搜索栏这一行的显示内容,左边是搜索词或提示,右边是过滤和排序工具栏。
数据流:输入状态和可用宽度;如果有错误就显示红色错误;否则生成搜索文本和工具栏,宽度不够时切成紧凑工具栏,并必要时截断搜索文本;输出 Line。
调用关系:draw_picker 渲染搜索区域时调用它。它会调用 toolbar_line 和 truncate_text 等辅助函数。
调用图:调用 2 个内部函数(toolbar_line, truncate_text);被 3 处调用(resume_search_error_snapshot, search_line_compacts_toolbar_on_narrow_width, search_line_renders_sort_and_filter_tabs);外部调用 4 个(from, width, format!, vec!)。
toolbar_line1935–1941 ↗
fn toolbar_line(state: &PickerState, compact: bool) -> Line<'static>
作用:生成工具栏文字,把过滤控件和排序控件拼在一起。
数据流:输入状态和是否紧凑;它收集 filter_control_spans,再加空隔,再收集 sort_control_spans;输出一行 Line。
调用关系:search_line 根据宽度决定普通或紧凑工具栏时调用它。
调用图:调用 2 个内部函数(filter_control_spans, sort_control_spans);被 1 处调用(search_line);外部调用 1 个(new)。
sort_control_spans1943–1968 ↗
fn sort_control_spans(state: &PickerState, compact: bool) -> Vec<Span<'static>>
作用:生成排序控件的显示片段,突出当前排序值和当前焦点。
数据流:输入状态和是否紧凑;紧凑模式只显示当前排序;普通模式显示 Updated 和 Created 两个选项,并标出哪一个激活;输出 Span 列表。
调用关系:toolbar_line 调用它组成右侧工具栏。
调用图:被 1 处调用(toolbar_line);外部调用 1 个(vec!)。
filter_control_spans1970–1995 ↗
fn filter_control_spans(state: &PickerState, compact: bool) -> Vec<Span<'static>>
作用:生成过滤控件的显示片段,显示当前是 Cwd 还是 All,并标出焦点。
数据流:输入状态和是否紧凑;如果紧凑或没有目录过滤条件,只显示当前模式;否则显示 Cwd 和 All 两个选项;输出 Span 列表。
调用关系:toolbar_line 调用它组成右侧工具栏。
调用图:被 1 处调用(toolbar_line);外部调用 1 个(vec!)。
toolbar_value1997–2008 ↗
fn toolbar_value(label: &'static str, active: bool, focused: bool) -> Span<'static>
作用:把工具栏里的某个值包装成带样式的文字。激活值用方括号包起来,聚焦时用强调颜色。
数据流:输入标签、是否激活、是否聚焦;激活时生成 [label],聚焦则变洋红色;未激活时生成灰色淡化文字;输出 Span。
调用关系:filter_control_spans 和 sort_control_spans 都用它来统一工具栏样式。
调用图:外部调用 1 个(format!)。
filter_mode_label2010–2015 ↗
fn filter_mode_label(filter_mode: SessionFilterMode) -> &'static str
作用:把过滤模式变成界面标签。
数据流:输入 SessionFilterMode;Cwd 返回 Cwd,All 返回 All;输出固定文字。
调用关系:filter_control_spans 调用它显示过滤选项。
hint_line_for_row2247–2274 ↗
fn hint_line_for_row(hints: &[PickerFooterHint], width: u16) -> Line<'static>
作用:把一排按键提示压进指定宽度里。宽的时候用完整说明,窄的时候用短说明,再窄就只保留按键名和高优先级项目。
数据流:输入一组提示和可用宽度 → 依次尝试完整标签、短标签、只显示按键 → 如果还放不下,就按优先级删掉不重要的提示 → 输出一行文字,实在放不下则输出空行。
调用关系:footer_hint_lines 用它处理每一行提示;它再调用 fit_footer_hints 和 fit_footer_hint_refs 来实际计算宽度和生成 Line。
调用图:调用 2 个内部函数(fit_footer_hint_refs, fit_footer_hints);外部调用 2 个(default, len)。
render_transcript_loading_overlay2276–2311 ↗
fn render_transcript_loading_overlay(frame: &mut crate::custom_terminal::Frame, area: Rect)
作用:在界面中间画一个“正在加载对话记录”的浮层,告诉用户当前不是卡住了,而是在等内容。
数据流:输入终端画布和要覆盖的区域 → 算出居中的小矩形,填上背景色,再把 Loading transcript… 居中写进去 → 直接改动画布内容。
调用关系:快照测试会调用它检查显示效果;它使用 transcript_loading_overlay_style 决定背景色,并用 truncate_text 防止文字超出。
调用图:调用 3 个内部函数(render_widget_ref, transcript_loading_overlay_style, truncate_text);被 1 处调用(transcript_loading_overlay_snapshot);外部调用 3 个(from, new, width)。
transcript_loading_overlay_style2313–2323 ↗
fn transcript_loading_overlay_style() -> Style
作用:决定加载浮层的背景颜色,让它在浅色和深色终端里都能看出来但不刺眼。
数据流:读取默认背景色 → 如果不知道背景,就用深灰;如果知道,就把黑色或白色按一点透明度混进去 → 输出一个 Style(样式对象,包含颜色等显示设置)。
调用关系:render_transcript_loading_overlay 在画浮层前会调用它;它依赖颜色混合和亮暗判断工具。
调用图:调用 4 个内部函数(blend, is_light, best_color, default_bg);被 1 处调用(render_transcript_loading_overlay);外部调用 1 个(default)。
render_list2418–2498 ↗
fn render_list(frame: &mut crate::custom_terminal::Frame, area: Rect, state: &PickerState)
作用:把会话列表画到终端上,是列表显示的核心函数。它会处理空列表、滚动提示、选中项、展开项和加载更多提示。
数据流:输入画布、显示区域和选择器状态 → 清空区域;如果没有行就显示空状态;否则计算上下是否还有内容,逐行画会话 → 输出不是返回值,而是把画布改成新的列表画面。
调用关系:多个快照测试直接调用它;它把单条会话交给 render_session_lines,把上下还有更多内容的提示交给 more_line。
调用图:调用 6 个内部函数(render_widget_ref, has_more_above, has_more_below, more_line, render_empty_state_line, render_session_lines);被 8 处调用(dense_session_snapshot_uses_no_blank_lines_between_rows, density_toggle_clears_stale_more_indicator, expanded_session_snapshot, narrow_session_snapshot, render_dense_row_snapshot, resume_table_snapshot, session_list_more_indicators_snapshot, transcript_loading_overlay_snapshot);外部调用 3 个(new, from, vec!)。
more_line2500–2502 ↗
fn more_line(label: &'static str) -> Line<'static>
作用:生成一行淡色的“还有更多”提示,比如向上还有更多或向下还有更多。
数据流:输入固定文字 → 给文字加 dim(变暗)样式 → 输出一行可绘制文字。
调用关系:render_list 在列表上方或下方需要提示滚动时调用它。
调用图:被 1 处调用(render_list);外部调用 1 个(vec!)。
render_session_lines2504–2520 ↗
fn render_session_lines(
row: &Row,
state: &PickerState,
is_selected: bool,
is_expanded: bool,
is_zebra: bool,
width: u16,
) -> Vec<Line<'static>>
作用:根据当前列表密度,选择用宽松模式还是紧凑模式来画一条会话。
数据流:输入一条会话、状态、是否选中、是否展开、是否斑马纹和宽度 → 看 state.density → 输出这一条会话需要占用的多行文字。
调用关系:render_list 用它画列表;has_more_below 也会用它估算行数;它把工作分给 render_comfortable_session_lines 或 render_dense_session_lines。
调用图:调用 2 个内部函数(render_comfortable_session_lines, render_dense_session_lines);被 2 处调用(has_more_below, render_list)。
render_comfortable_session_lines2522–2577 ↗
fn render_comfortable_session_lines(
row: &Row,
state: &PickerState,
is_selected: bool,
is_expanded: bool,
is_zebra: bool,
width: u16,
) -> Vec<Line<'static>>
作用:用“宽松模式”画一条会话:标题一行,下面再放时间、目录、分支等信息。
数据流:输入会话和显示状态 → 生成选中箭头和标题;给选中行或隔行背景上色;如果展开则加入详情和对话预览,否则加入底部元信息 → 输出多行文字。
调用关系:render_session_lines 在 Comfortable 模式下调用它;它会使用 render_footer_lines、render_transcript_preview_lines、format_relative_time 等函数。
调用图:调用 9 个内部函数(apply_session_row_background, dense_selected_style, dense_zebra_style, format_relative_time, render_footer_lines, render_transcript_preview_lines, selected_session_title_span, selection_marker, truncate_text);被 2 处调用(render_session_lines, comfortable_zebra_lines_use_full_width_background);外部调用 3 个(from, display_preview, vec!)。
apply_session_row_background2579–2588 ↗
fn apply_session_row_background(
lines: Vec<Line<'static>>,
style: Style,
width: u16,
) -> Vec<Line<'static>>
作用:给一条会话的所有显示行统一铺背景色。
数据流:输入多行文字、样式和宽度 → 对每一行调用 apply_line_background → 输出带完整背景的新行列表。
调用关系:render_comfortable_session_lines 在选中或斑马纹行上调用它。
调用图:被 1 处调用(render_comfortable_session_lines)。
apply_line_background2590–2600 ↗
fn apply_line_background(mut line: Line<'static>, style: Style, width: u16) -> Line<'static>
作用:把单行文字的背景填满整行,而不是只给有字的地方上色。
数据流:输入一行、样式和目标宽度 → 算出还差多少空格;补上带样式的空格;再把整行和每个文字片段都合并样式 → 输出更新后的行。
调用关系:apply_session_row_background 用它处理每一行。
调用图:外部调用 1 个(width)。
render_dense_session_lines2602–2630 ↗
fn render_dense_session_lines(
row: &Row,
state: &PickerState,
is_selected: bool,
is_expanded: bool,
is_zebra: bool,
width: u16,
) -> Vec<Line<'static>>
作用:用“紧凑模式”画一条会话,把日期和标题压在同一行,适合一次看更多会话。
数据流:输入会话和状态 → 根据排序方式选 created 或 updated 时间 → 生成一行摘要;如果展开,再追加详情和对话预览 → 输出一到多行文字。
调用关系:render_session_lines 在 Dense 模式下调用它;摘要行由 dense_summary_line 生成,展开内容交给 render_transcript_preview_lines。
调用图:调用 3 个内部函数(format_relative_time, render_transcript_preview_lines, selection_marker);被 2 处调用(render_session_lines, dense_session_line_prefers_thread_name_over_preview);外部调用 1 个(vec!)。
dense_summary_line2641–2673 ↗
fn dense_summary_line(input: DenseSummaryInput<'_>) -> Line<'static>
作用:生成紧凑模式下的一行摘要,包含选中标记、日期和标题。
数据流:输入摘要所需字段 → 按宽度分配日期列和标题列;标题太长就截断;选中或斑马纹时补齐整行背景 → 输出一行文字。
调用关系:render_dense_session_lines 用它画每个紧凑行;它调用 dense_columns、dense_column_text 和样式函数。
调用图:调用 5 个内部函数(dense_column_text, dense_columns, dense_selected_style, dense_zebra_style, selected_session_title_span);被 2 处调用(dense_selected_summary_line_uses_full_width_selection_style, dense_zebra_summary_line_uses_full_width_background);外部调用 2 个(from, vec!)。
dense_columns2680–2686 ↗
fn dense_columns(width: usize) -> DenseColumns
作用:决定紧凑行里日期列和标题列各占多宽。
数据流:输入可用宽度 → 日期列使用固定宽度,剩下的给标题 → 输出列宽配置。
调用关系:dense_summary_line 在排版前调用它。
调用图:被 1 处调用(dense_summary_line)。
dense_zebra_style2688–2690 ↗
fn dense_zebra_style() -> Style
作用:取得隔行背景色,让列表像表格一样更容易横向看齐。
数据流:没有外部输入 → 调用 dense_row_background_style 并说明这不是选中行 → 输出样式。
调用关系:dense_summary_line 和 render_comfortable_session_lines 在需要斑马纹时调用它。
调用图:调用 1 个内部函数(dense_row_background_style);被 2 处调用(dense_summary_line, render_comfortable_session_lines)。
dense_selected_style2692–2694 ↗
fn dense_selected_style() -> Style
作用:取得选中行的样式,既有醒目的文字色,也有选中背景。
数据流:没有外部输入 → 把 selected_session_style 的前景色和 dense_row_background_style 的背景色合并 → 输出样式。
调用关系:dense_summary_line 和 render_comfortable_session_lines 在画当前选中会话时使用它。
调用图:调用 2 个内部函数(dense_row_background_style, selected_session_style);被 2 处调用(dense_summary_line, render_comfortable_session_lines)。
dense_row_background_style2696–2706 ↗
fn dense_row_background_style(selected: bool) -> Style
作用:根据终端背景和是否选中,算出合适的行背景色。
数据流:输入 selected 标志 → 读取默认背景;没有背景就用默认样式;有背景就按浅色/深色混入少量黑或白 → 输出背景样式。
调用关系:dense_zebra_style 和 dense_selected_style 都通过它得到背景色。
调用图:调用 4 个内部函数(blend, is_light, best_color, default_bg);被 2 处调用(dense_selected_style, dense_zebra_style);外部调用 1 个(default)。
dense_column_text2708–2712 ↗
fn dense_column_text(text: &str, width: usize) -> String
作用:把一段文字整理成固定宽度的一列,太长截断,太短补空格。
数据流:输入文字和列宽 → 先截断到合适长度,再按实际显示宽度补空格 → 输出正好适合列宽的字符串。
调用关系:dense_summary_line 用它排日期列和标题列。
调用图:调用 1 个内部函数(truncate_text);被 1 处调用(dense_summary_line);外部调用 2 个(width, format!)。
selection_marker2714–2720 ↗
fn selection_marker(is_selected: bool, is_expanded: bool) -> Span<'static>
作用:生成列表左侧的选中符号:选中未展开是箭头,选中并展开是向下箭头,没选中是空白。
数据流:输入是否选中、是否展开 → 选择对应符号并给选中符号加粗和颜色 → 输出一个文字片段。
调用关系:宽松和紧凑两种会话行渲染都会先调用它。
调用图:调用 1 个内部函数(selected_session_style);被 4 处调用(render_comfortable_session_lines, render_dense_session_lines, dense_selected_summary_line_uses_full_width_selection_style, dense_zebra_summary_line_uses_full_width_background)。
selected_session_style2722–2728 ↗
fn selected_session_style() -> Style
作用:决定选中会话文字用什么颜色,兼顾浅色和深色终端。
数据流:读取默认背景 → 浅色背景用洋红,深色背景用黄色 → 输出样式。
调用关系:selection_marker、selected_session_title_span 和 dense_selected_style 都依赖它。
调用图:调用 1 个内部函数(default_bg);被 3 处调用(dense_selected_style, selected_session_title_span, selection_marker);外部调用 1 个(default)。
selected_session_title_span2730–2732 ↗
fn selected_session_title_span(title: String) -> Span<'static>
作用:把选中会话的标题变成带高亮颜色的文字片段。
数据流:输入标题字符串 → 套上 selected_session_style → 输出可绘制的 Span。
调用关系:render_comfortable_session_lines 和 dense_summary_line 在标题被选中时调用它。
调用图:调用 1 个内部函数(selected_session_style);被 2 处调用(dense_summary_line, render_comfortable_session_lines)。
cwd_column_width2812–2817 ↗
fn cwd_column_width(width: usize) -> usize
作用:给当前目录这一列估算一个合理宽度,既不要太窄也不要无限占空间。
数据流:输入整行宽度 → 扣掉缩进、日期和间隔后取一半,再限制在最小和最大范围内 → 输出目录列宽。
调用关系:pack_footer_parts 在排元信息前调用它。
调用图:被 1 处调用(pack_footer_parts)。
render_transcript_preview_lines2912–2939 ↗
fn render_transcript_preview_lines(
row: &Row,
state: &PickerState,
width: u16,
) -> Vec<Line<'static>>
作用:在展开会话时,生成详情和最近对话预览。
数据流:输入会话、状态和宽度 → 先生成会话详情;如果有 thread_id,就查预览状态:加载中、失败、已加载或没有 → 追加对应预览行 → 输出多行文字。
调用关系:宽松和紧凑模式展开行时都会调用它;已加载内容交给 render_conversation_preview_lines。
调用图:调用 2 个内部函数(render_conversation_preview_lines, render_expanded_session_details);被 2 处调用(render_comfortable_session_lines, render_dense_session_lines);外部调用 2 个(new, vec!)。
render_expanded_session_details2941–2978 ↗
fn render_expanded_session_details(
row: &Row,
state: &PickerState,
width: u16,
) -> Vec<Line<'static>>
作用:生成展开会话时的基本信息区:会话名、创建时间、更新时间、目录、分支和 Conversation 标题。
数据流:输入会话、状态和宽度 → 整理会话名和 ID、目录显示、分支显示、相对时间 → 输出一组详情行。
调用关系:render_transcript_preview_lines 先调用它,再追加对话预览;测试会检查这些元信息是否完整。
调用图:被 2 处调用(render_transcript_preview_lines, expanded_session_details_include_metadata);外部调用 2 个(format!, vec!)。
render_conversation_preview_lines2980–3011 ↗
fn render_conversation_preview_lines(
lines: &[TranscriptPreviewLine],
width: u16,
) -> Vec<Line<'static>>
作用:把最近对话预览转成带树形连接线的多行显示。
数据流:输入对话预览行和宽度 → 如果为空就显示无预览;否则逐条渲染内容,再给每行加 │ 或 └ 前缀 → 输出多行文字。
调用关系:render_transcript_preview_lines 在预览加载成功时调用它;每条内容由 render_transcript_content_lines 处理。
调用图:调用 1 个内部函数(render_transcript_content_lines);被 1 处调用(render_transcript_preview_lines);外部调用 3 个(new, is_empty, vec!)。
render_transcript_content_lines3013–3032 ↗
fn render_transcript_content_lines(line: &TranscriptPreviewLine, width: u16) -> Vec<Line<'static>>
作用:渲染一条对话内容,并按用户/助手使用不同样式;助手内容还会按 Markdown(常见富文本标记)来解析显示。
数据流:输入一条预览内容和宽度 → 用户消息直接变成斜体弱色;助手消息先走 Markdown 渲染再套样式;最后按可用宽度自动换行 → 输出多行文字。
调用关系:render_conversation_preview_lines 对每条预览调用它;它使用 conversation_content_line 和不同的说话人样式函数。
调用图:调用 5 个内部函数(append_markdown, conversation_assistant_style, conversation_content_line, new, adaptive_wrap_lines);被 1 处调用(render_conversation_preview_lines);外部调用 3 个(new, clone, vec!)。
conversation_content_line3034–3040 ↗
fn conversation_content_line(mut line: Line<'static>, style: Style) -> Line<'static>
作用:给一行对话内容整体套上指定样式。
数据流:输入一行文字和样式 → 把行本身和里面每个文字片段都合并这个样式 → 输出更新后的行。
调用关系:render_transcript_content_lines 用它统一用户消息和助手消息的颜色。
调用图:被 1 处调用(render_transcript_content_lines)。
prefix_transcript_line3042–3046 ↗
transcript_prefix_style3048–3056 ↗
fn transcript_prefix_style(line: &Line<'_>) -> Style
作用:让对话连接线的颜色跟随内容,不显得突兀。
数据流:输入一行内容 → 找到第一个非空文字片段,合并它和整行的样式 → 提取适合连接线的颜色 → 输出样式。
调用关系:prefix_transcript_line 用它给前缀上色;它把最后一步交给 connector_style_from_content。
调用图:调用 1 个内部函数(connector_style_from_content)。
connector_style_from_content3058–3064 ↗
fn connector_style_from_content(style: Style) -> Style
作用:从内容样式里提取前景色和背景色,去掉粗体、斜体等文字效果,只保留连接线需要的颜色。
数据流:输入内容样式 → 复制前景色和背景色,其余恢复默认 → 输出连接线样式。
调用关系:transcript_prefix_style 用它把内容样式转换成前缀样式。
调用图:被 1 处调用(transcript_prefix_style);外部调用 1 个(default)。
conversation_assistant_style3066–3072 ↗
fn conversation_assistant_style() -> Style
作用:决定助手消息在预览里用什么颜色。
数据流:读取默认背景 → 浅色背景用灰色,深色背景用深灰 → 输出样式。
调用关系:render_transcript_content_lines 在处理助手发言时调用它。
调用图:调用 1 个内部函数(default_bg);被 1 处调用(render_transcript_content_lines);外部调用 1 个(default)。
conversation_user_style3074–3080 ↗
fn conversation_user_style() -> Style
作用:决定用户消息在预览里用什么颜色,并用斜体区分出来。
数据流:读取默认背景 → 根据浅色或深色选择灰度颜色,并加上 italic(斜体) → 输出样式。
调用关系:render_transcript_content_lines 在处理用户发言时调用它。
调用图:调用 1 个内部函数(default_bg);外部调用 1 个(default)。
expanded_detail_line3082–3096 ↗
fn expanded_detail_line(label: &'static str, value: &str, width: u16) -> Line<'static>
作用:生成展开详情里的一行普通字段,比如 Directory 或 Branch。
数据流:输入标签、值和宽度 → 算出值能用多少空间;标签固定宽度对齐,值太长就截断 → 输出一行详情。
调用关系:render_expanded_session_details 多次调用它;expanded_time_detail_line 也用它来显示时间字段。
调用图:被 1 处调用(expanded_time_detail_line);外部调用 1 个(vec!)。
expanded_time_detail_line3098–3113 ↗
fn expanded_time_detail_line(
label: &'static str,
reference: DateTime<Utc>,
ts: Option<DateTime<Utc>>,
width: u16,
) -> Line<'static>
作用:生成展开详情里的时间字段,同时显示“多久以前”和完整时间。
数据流:输入标签、参考时间、可能为空的时间戳和宽度 → 没有时间就显示横杠;有时间就格式化成相对时间加完整时间 → 输出详情行。
调用关系:render_expanded_session_details 用它显示 Created 和 Updated。
调用图:调用 1 个内部函数(expanded_detail_line);外部调用 1 个(format!)。
format_relative_time3115–3136 ↗
fn format_relative_time(reference: DateTime<Utc>, ts: Option<DateTime<Utc>>) -> String
作用:把时间戳变成短格式相对时间,比如 now、42s ago、5m ago。
数据流:输入参考时间和可能为空的时间戳 → 为空返回 -;否则算相差秒数,并按秒、分钟、小时、天选择单位 → 输出短字符串。
调用关系:会话列表的宽松和紧凑显示都会调用它。
调用图:被 2 处调用(render_comfortable_session_lines, render_dense_session_lines);外部调用 1 个(format!)。
format_relative_time_long3138–3155 ↗
fn format_relative_time_long(reference: DateTime<Utc>, ts: DateTime<Utc>) -> String
作用:把时间戳变成较完整的英文相对时间,比如 20 minutes ago。
数据流:输入参考时间和时间戳 → 计算差值 → 按秒、分钟、小时、天选择单位,并处理单复数 → 输出长字符串。
调用关系:expanded_time_detail_line 用它生成展开详情里的时间说明;单复数交给 plural_time。
调用图:调用 1 个内部函数(plural_time)。
plural_time3157–3163 ↗
fn plural_time(value: i64, unit: &str) -> String
作用:给英文时间单位处理单数和复数。
数据流:输入数字和单位名 → 如果是 1 就不加 s,否则加 s → 输出类似 1 hour ago 或 2 hours ago 的字符串。
调用关系:format_relative_time_long 调用它避免复数写错。
调用图:被 1 处调用(format_relative_time_long);外部调用 1 个(format!)。
format_timestamp3165–3167 ↗
fn format_timestamp(ts: DateTime<Utc>) -> String
作用:把时间戳格式化成固定的人类可读格式。
数据流:输入 UTC 时间 → 按 年-月-日 时:分:秒 排版 → 输出字符串。
调用关系:expanded_time_detail_line 用它和相对时间一起显示。
调用图:外部调用 1 个(format)。
render_empty_state_line3169–3194 ↗
fn render_empty_state_line(state: &PickerState) -> Line<'static>
作用:当列表没有内容时,决定该显示哪句提示。
数据流:输入选择器状态 → 如果正在搜索就显示搜索中、无结果或扫描上限提示;如果正在加载就显示加载中;否则显示还没有会话 → 输出一行淡色提示。
调用关系:render_list 在 filtered_rows 为空时调用它。
调用图:被 1 处调用(render_list);外部调用 2 个(format!, vec!)。
tests::page3217–3229 ↗
fn page(
rows: Vec<Row>,
next_cursor: Option<&str>,
num_scanned_files: usize,
reached_scan_cap: bool,
) -> PickerPage
作用:测试用的小工具,用几项参数快速造一个分页结果。
数据流:输入行列表、下一页游标、扫描数量和是否到达扫描上限 → 包装成 PickerPage → 输出测试用分页对象。
调用关系:供测试构造加载结果,避免每个测试重复写结构体。
tests::page_only_loader3231–3237 ↗
fn page_only_loader(loader: impl Fn(PageLoadRequest) + Send + Sync + 'static) -> PickerLoader
作用:测试用加载器,只关心分页请求,忽略其他类型请求。
数据流:输入一个处理 PageLoadRequest 的闭包 → 包成 PickerLoader;收到请求时只在它是 Page 请求时转交 → 输出可给 PickerState 使用的加载器。
调用关系:很多测试创建 PickerState 时用它假装后台加载。
调用图:外部调用 1 个(new)。
tests::make_row3239–3251 ↗
fn make_row(path: &str, ts: &str, preview: &str) -> Row
作用:测试用的小工具,用路径、时间和预览文字快速造一条会话行。
数据流:输入路径、时间字符串和预览 → 解析时间,填入 Row 的常用字段,其他字段留空 → 输出 Row。
调用关系:分页、进度等测试用它批量造假会话。
调用图:调用 1 个内部函数(parse_timestamp_str);外部调用 1 个(from)。
tests::row_display_preview_prefers_thread_name3286–3299 ↗
fn row_display_preview_prefers_thread_name()
作用:确认会话有名字时,列表优先显示会话名,而不是第一条消息预览。
数据流:构造一个带 thread_name 和 preview 的 Row → 调用 display_preview → 断言结果是会话名。
调用关系:保护 Row 的显示规则,避免列表标题退回到不友好的预览文本。
调用图:外部调用 3 个(from, from, assert_eq!)。
tests::local_picker_thread_list_params_include_cwd_filter3302–3321 ↗
fn local_picker_thread_list_params_include_cwd_filter()
作用:确认本地选择器请求会把当前目录过滤条件带上。
数据流:构造 cwd_filter → 调用 thread_list_params → 检查参数里的 cwd 是指定目录 → 测试通过或失败。
调用关系:覆盖请求参数生成逻辑,保证本地只看相关目录的会话。
调用图:调用 2 个内部函数(picker_cwd_filter, thread_list_params);外部调用 4 个(new, from, assert_eq!, MatchDefault)。
tests::row_search_matches_metadata_fields3324–3341 ↗
fn row_search_matches_metadata_fields()
作用:确认搜索不只搜预览文字,也能搜目录、分支和会话 ID。
数据流:构造带目录、分支、thread_id 的 Row → 分别用关键词搜索 → 断言都能匹配。
调用关系:保护 Row.matches_query 的搜索范围。
调用图:调用 1 个内部函数(from_string);外部调用 3 个(from, from, assert!)。
tests::relative_time_formats_zero_seconds_as_now3344–3354 ↗
fn relative_time_formats_zero_seconds_as_now()
作用:确认短相对时间在刚刚发生时显示 now,而不是 0s ago。
数据流:准备固定参考时间 → 测同一时间和早一秒的时间 → 断言输出分别是 now 和 1s ago。
调用关系:直接测试 format_relative_time 的边界行为。
调用图:外部调用 2 个(parse_from_rfc3339, assert_eq!)。
tests::long_relative_time_uses_words3357–3371 ↗
fn long_relative_time_uses_words()
作用:确认长相对时间使用完整英文单位,并正确处理单复数。
数据流:准备固定参考时间 → 测 now、20 分钟前、1 小时前 → 断言文字符合预期。
调用关系:测试 format_relative_time_long 和 plural_time 的组合效果。
调用图:外部调用 2 个(parse_from_rfc3339, assert_eq!)。
tests::expanded_session_details_include_metadata3374–3414 ↗
fn expanded_session_details_include_metadata()
作用:确认展开会话时会显示完整元信息,包括会话名、ID、时间、目录、分支和 Conversation 标题。
数据流:构造状态和一条完整 Row → 调用 render_expanded_session_details → 转成字符串 → 检查关键字段都存在。
调用关系:覆盖 render_expanded_session_details,防止展开详情少字段或格式退化。
调用图:调用 5 个内部函数(from_string, new, parse_timestamp_str, render_expanded_session_details, test_dummy);外部调用 6 个(from, from, assert!, format_directory_display, MatchDefault, page_only_loader)。
tests::assert_metadata_order3525–3530 ↗
tests::remote_thread_list_params_omit_provider_filter3533–3552 ↗
fn remote_thread_list_params_omit_provider_filter()
作用:确认远程请求在 ProviderFilter::Any 时不会额外限制模型提供方。
数据流:调用 thread_list_params 构造请求参数 → 检查 cursor、model_providers、source_kinds 和 cwd → 测试通过或失败。
调用关系:覆盖远程分页请求参数生成逻辑。
调用图:调用 1 个内部函数(thread_list_params);外部调用 3 个(new, from, assert_eq!)。
tests::remote_thread_list_params_can_include_non_interactive_sources3555–3568 ↗
fn remote_thread_list_params_can_include_non_interactive_sources()
作用:确认远程请求可以按开关包含非交互来源的会话。
数据流:include_non_interactive 传 true → 构造参数 → 和 resume_source_kinds 的结果比较 → 测试通过或失败。
调用关系:保护 thread_list_params 对来源类型的选择。
调用图:调用 1 个内部函数(thread_list_params);外部调用 3 个(from, assert_eq!, resume_source_kinds)。
tests::remote_picker_sends_cwd_filter_without_local_post_filtering3571–3609 ↗
fn remote_picker_sends_cwd_filter_without_local_post_filtering()
作用:确认远程工作区会把目录过滤发给服务器,但本地不再用路径做二次过滤。
数据流:用记录请求的假 loader 创建状态 → 启动初始加载 → 检查请求带了远程 cwd;再造一条 cwd 不同的远程行 → 断言仍然匹配。
调用关系:测试 PickerState 的远程过滤策略,避免本地路径和服务器真实路径不一致导致误删结果。
调用图:调用 4 个内部函数(new, new, local_picker_cwd_filter, test_dummy);外部调用 8 个(new, new, from, from, new, assert!, assert_eq!, page_only_loader)。
tests::remote_picker_does_not_filter_rows_by_local_cwd3612–3634 ↗
fn remote_picker_does_not_filter_rows_by_local_cwd()
作用:确认远程会话行不会被本地当前目录规则误过滤。
数据流:创建没有本地过滤的远程状态 → 构造一条远程路径 Row → 调用 row_matches_filter → 断言为真。
调用关系:补充保护远程场景下的过滤行为。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 4 个(from, from, assert!, page_only_loader)。
tests::resume_table_snapshot3637–3706 ↗
fn resume_table_snapshot()
作用:用虚拟终端画出一张典型会话列表,并和保存的快照比较。
数据流:构造三条会话和固定时间 → 设置选中项和视口 → 调用 render_list 画到虚拟终端 → 读取画面并做快照断言。
调用关系:覆盖 render_list、render_session_lines、时间格式和选中样式的整体效果。
调用图:调用 6 个内部函数(with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 6 个(new, from, assert_snapshot!, MatchDefault, page_only_loader, vec!)。
tests::resume_search_error_snapshot3709–3741 ↗
fn resume_search_error_snapshot()
作用:检查搜索栏显示错误信息时的最终画面。
数据流:构造带 inline_error 的状态 → 调用 search_line 并画到一行虚拟终端 → 读取画面做快照比较。
调用关系:虽然 search_line 不在本段源码里,但这个测试保证选择器错误提示显示不变。
调用图:调用 5 个内部函数(with_options, new, search_line, new, test_dummy);外部调用 5 个(new, from, assert_snapshot!, MatchDefault, page_only_loader)。
tests::hint_line_switches_esc_label_for_search_mode3744–3760 ↗
fn hint_line_switches_esc_label_for_search_mode()
作用:确认 Esc 的说明会随搜索状态变化:没搜索时是开始新会话,搜索中是清空搜索。
数据流:创建状态 → 读取 footer 文本并检查 esc start new;设置 query 后再读 → 检查 esc clear search。
调用关系:测试 footer_hint_lines 对 query 的判断。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(from, assert!, MatchDefault, page_only_loader)。
tests::hint_line_labels_cancel_keys_as_exit_for_existing_session_resume_picker3763–3786 ↗
fn hint_line_labels_cancel_keys_as_exit_for_existing_session_resume_picker()
作用:确认从已有会话里打开选择器时,Esc 和 Ctrl+C 的说明是 exit,而不是 quit 或 start new。
数据流:设置 launch_context 为 ExistingSession → 检查宽版和窄版 footer;再设置搜索词 → 检查 Esc 变成 clear search。
调用关系:覆盖 footer_hint_lines 对启动场景和搜索状态的组合处理。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 5 个(from, assert!, MatchDefault, footer_lines_text, page_only_loader)。
tests::hint_line_switches_density_label3789–3807 ↗
fn hint_line_switches_density_label()
作用:确认切换列表密度的提示文字会根据当前密度反过来显示下一步动作。
数据流:默认宽松模式下检查 ctrl+o dense view、ctrl+t transcript、ctrl+e expand → 改成 Dense → 检查 ctrl+o comfortable view。
调用关系:测试 footer_hint_lines 里的密度相关文案。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(from, assert!, MatchDefault, page_only_loader)。
tests::hint_line_compacts_on_narrow_width3810–3830 ↗
fn hint_line_compacts_on_narrow_width()
作用:确认屏幕较窄时,底部提示会自动用短标签。
数据流:用宽度 119 生成 footer 文本 → 检查 new、focus、option、dense、preview、exp 等短词出现,长词 focus sort/filter 不出现。
调用关系:覆盖 hint_line_for_row 和 fit_footer_hints 的紧凑模式选择。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 5 个(from, assert!, MatchDefault, footer_lines_text, page_only_loader)。
tests::hint_line_prioritizes_keybinds_when_very_narrow3871–3899 ↗
fn hint_line_prioritizes_keybinds_when_very_narrow()
作用:确认屏幕非常窄时,footer 会优先保留关键按键,而不是硬挤长说明。
数据流:用 38 宽度生成提示行 → 检查每行不超宽,并且 enter、esc、ctrl+c、ctrl+o 等关键按键还在。
调用关系:直接测试 hint_line_for_row 的优先级删减策略。
调用图:调用 3 个内部函数(new, footer_hint_lines, test_dummy);外部调用 4 个(from, assert!, MatchDefault, page_only_loader)。
tests::hint_line_shows_loading_transcript_mode3902–3919 ↗
fn hint_line_shows_loading_transcript_mode()
作用:确认正在打开 transcript 时,footer 只显示加载和退出,不再显示普通操作。
数据流:设置 pending_transcript_open → 生成 footer 文本 → 检查 loading transcript 和 ctrl+c quit 出现,enter 不出现。
调用关系:覆盖 footer_hint_lines 的加载中分支。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 5 个(from, assert!, MatchDefault, footer_lines_text, page_only_loader)。
tests::ctrl_o_toggles_density_without_typing_into_search4062–4081 ↗
async fn ctrl_o_toggles_density_without_typing_into_search()
作用:检查按 Ctrl+O 会切换列表密度,而不会把字母 o 输入到搜索框。这样快捷键和正常打字不会混在一起。
数据流:测试先把搜索词设成“pick” → 模拟用户按 Ctrl+O → 状态里的显示密度变成紧凑模式,搜索词仍然是“pick”。
调用关系:它通过 PickerState::handle_key 走真实按键处理流程,验证 Ctrl+O 被当作快捷键,而不是普通字符输入。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 6 个(Char, new, from, assert_eq!, MatchDefault, page_only_loader)。
tests::ctrl_t_requests_selected_session_transcript4084–4124 ↗
async fn ctrl_t_requests_selected_session_transcript()
作用:检查按 Ctrl+T 会请求当前选中会话的完整对话记录。用户想查看详情时,这一步会触发后台去拿 transcript。
数据流:测试准备一条带 thread_id 的会话,并用记录器收集加载请求 → 模拟 Ctrl+T → 加载器收到 transcript 请求,状态进入等待打开记录的流程,并把该记录标成 Loading。
调用关系:它验证 handle_key 会把 Ctrl+T 交给 transcript 打开逻辑,再通过 PickerLoader 发出 PickerLoadRequest::Transcript。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 10 个(new, Char, new, new, from, new, assert!, assert_eq!, MatchDefault, vec!)。
tests::transcript_loading_consumes_picker_input4127–4178 ↗
async fn transcript_loading_consumes_picker_input()
作用:检查对话记录正在加载时,普通列表按键会被吞掉,不会继续移动选择或改搜索词。这样用户不会在等待期间不小心改变当前列表状态。
数据流:测试设置 pending_transcript_open,表示正在等记录打开 → 先按下方向键,再输入字母 x → 两次都没有选择结果,选中位置和搜索框都不变。
调用关系:它验证 handle_key 在 transcript 加载期间会进入一种临时“锁住列表输入”的状态,只允许少数安全操作通过。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 8 个(Char, new, from, assert!, assert_eq!, MatchDefault, page_only_loader, vec!)。
tests::transcript_loading_still_allows_ctrl_c_exit4181–4199 ↗
async fn transcript_loading_still_allows_ctrl_c_exit()
作用:检查对话记录加载中时,Ctrl+C 仍然可以退出。等待中的界面不能把用户困住。
数据流:测试把状态设成正在等待打开 transcript → 模拟 Ctrl+C → 返回 Exit 选择结果。
调用关系:它补充验证上一条测试里的“吞输入”不是全吞,handle_key 仍然保留紧急退出通道。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 6 个(Char, new, from, assert!, MatchDefault, page_only_loader)。
tests::transcript_loading_overlay_snapshot4202–4263 ↗
fn transcript_loading_overlay_snapshot()
作用:用快照测试检查 transcript 加载遮罩层长什么样。快照测试就是把界面输出存成样板,以后改坏了能立刻发现。
数据流:测试搭出一个 80×7 的假终端,渲染会话列表,再叠加加载遮罩 → 把终端画面转成字符串 → 和保存的快照比较。
调用关系:它把 render_list 和 render_transcript_loading_overlay 连起来测,确保加载完整记录时用户看到的是预期的覆盖提示。
调用图:调用 7 个内部函数(new, with_options, new, render_list, render_transcript_loading_overlay, new, test_dummy);外部调用 6 个(new, from, assert_snapshot!, MatchDefault, page_only_loader, vec!)。
tests::raw_ctrl_t_requests_selected_session_transcript4266–4300 ↗
async fn raw_ctrl_t_requests_selected_session_transcript()
作用:检查原始控制字符形式的 Ctrl+T 也能触发 transcript 请求。有些终端会把快捷键传成控制字符,而不是带修饰键的按键。
数据流:测试准备一条带 thread_id 的会话 → 输入字符 \u0014 → 记录器收到这个 thread_id 的 transcript 请求。
调用关系:它验证 handle_key 同时兼容两种键盘事件表达方式,避免不同终端下 Ctrl+T 失效。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 9 个(new, Char, new, new, from, new, assert_eq!, MatchDefault, vec!)。
tests::ctrl_t_on_row_without_thread_id_shows_inline_error4303–4333 ↗
async fn ctrl_t_on_row_without_thread_id_shows_inline_error()
作用:检查当前行没有可用会话 ID 时,按 Ctrl+T 会在界面内显示错误,而不是崩溃或无反应。
数据流:测试准备一条只有文件路径、没有 thread_id 的行 → 模拟 Ctrl+T → inline_error 变成“No transcript available for this session”。
调用关系:它验证 transcript 打开逻辑会先检查当前行是否真的能拿到远端记录,失败时把原因留在 PickerState 里给界面显示。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 7 个(Char, new, from, assert_eq!, MatchDefault, page_only_loader, vec!)。
tests::loaded_transcript_waits_for_loading_frame_before_opening_overlay4336–4373 ↗
async fn loaded_transcript_waits_for_loading_frame_before_opening_overlay()
作用:检查 transcript 即使已经加载好了,也要先让“正在加载”的画面至少显示一帧,再打开详情层。这样用户不会看到界面突然跳变。
数据流:测试先标记正在等待某个 thread_id 的 transcript → 模拟后台返回已加载内容 → 此时 overlay 还不开;再通知“加载帧已经画过”并尝试打开 → overlay 才变成 Transcript。
调用关系:它串起 handle_background_event、note_transcript_loading_frame_drawn 和 open_pending_transcript_if_ready,验证打开详情层的时机控制。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 6 个(from, assert!, assert_eq!, MatchDefault, page_only_loader, vec!)。
tests::cached_transcript_still_shows_loading_frame_before_opening_overlay4376–4419 ↗
async fn cached_transcript_still_shows_loading_frame_before_opening_overlay()
作用:检查 transcript 已经在缓存里时,按 Ctrl+T 也仍然先显示加载帧。这样缓存命中和真实加载的视觉体验保持一致。
数据流:测试先把某个 thread_id 的记录放进缓存 → 模拟 Ctrl+T → overlay 不立刻打开,只设置 pending;等加载帧画过后 → 再打开 Transcript overlay。
调用关系:它验证 handle_key 对缓存数据也走同一套“先加载提示、后打开覆盖层”的流程。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 9 个(Char, new, from, assert!, assert_eq!, MatchDefault, Loaded, page_only_loader, vec!)。
tests::ctrl_o_persists_density_preference4422–4451 ↗
async fn ctrl_o_persists_density_preference()
作用:检查 Ctrl+O 切换视图密度后,会把用户偏好保存到配置文件。下次打开时就能记住用户喜欢紧凑还是舒适。
数据流:测试用临时目录当配置目录 → 按 Ctrl+O → 状态变成 Dense,并在 config.toml 写入 session_picker_view = "dense"。
调用关系:它通过真实文件读写验证 handle_key 触发的视图偏好保存逻辑,确保内存状态和磁盘配置一致。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 8 个(Char, new, from, assert_eq!, read_to_string, tempdir, MatchDefault, page_only_loader)。
tests::ctrl_o_keeps_toggled_density_when_persistence_fails4454–4485 ↗
async fn ctrl_o_keeps_toggled_density_when_persistence_fails()
作用:检查保存视图偏好失败时,界面已经切换的密度不会被撤回。用户操作应该立即生效,保存失败只提示错误。
数据流:测试故意把配置目录设成一个普通文件,让保存必然失败 → 按 Ctrl+O → 密度仍变成 Dense,同时 inline_error 里出现保存失败提示。
调用关系:它验证 Ctrl+O 的流程把“切换界面”和“保存偏好”分开处理,保存失败不会破坏当前体验。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(Char, new, from, assert!, assert_eq!, write, tempdir, MatchDefault, page_only_loader)。
tests::raw_ctrl_o_toggles_density_without_typing_into_search4488–4507 ↗
async fn raw_ctrl_o_toggles_density_without_typing_into_search()
作用:检查原始控制字符形式的 Ctrl+O 也能切换密度,并且不会写进搜索框。
数据流:测试把搜索词设成“pick” → 输入字符 \u000f → 密度变成 Dense,搜索词保持不变。
调用关系:它补充覆盖不同终端传来的 Ctrl+O 表达方式,仍然走快捷键逻辑。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 6 个(Char, new, from, assert_eq!, MatchDefault, page_only_loader)。
tests::space_appends_to_search_query4510–4533 ↗
async fn space_appends_to_search_query()
作用:检查空格键会正常加入搜索词。这样用户可以搜索带空格的短语,而不是空格被误当成别的快捷键。
数据流:测试先把搜索词设成“resize” → 输入空格和 r → 搜索词变成“resize r”,展开状态也没有被误触发。
调用关系:它验证 handle_key 对普通字符输入的处理,尤其是空格不会和展开、选择等列表操作冲突。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 6 个(Char, new, from, assert_eq!, MatchDefault, page_only_loader)。
tests::ctrl_e_toggles_selected_session_expansion4536–4578 ↗
async fn ctrl_e_toggles_selected_session_expansion()
作用:检查 Ctrl+E 会展开或收起当前会话预览。展开时还会请求后台加载预览内容。
数据流:测试准备一条带 thread_id 的会话和一个记录请求的加载器 → 第一次 Ctrl+E 后 expanded_thread_id 设为该会话,并发出 Preview 请求 → 第二次 Ctrl+E 后收起。
调用关系:它验证 handle_key 把 Ctrl+E 接到会话预览展开逻辑,并通过 PickerLoader 请求 PickerLoadRequest::Preview。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 9 个(new, Char, new, new, from, new, assert_eq!, MatchDefault, vec!)。
tests::raw_ctrl_e_toggles_selected_session_expansion4581–4609 ↗
async fn raw_ctrl_e_toggles_selected_session_expansion()
作用:检查原始控制字符形式的 Ctrl+E 也能展开当前会话。
数据流:测试准备一条带 thread_id 的会话 → 输入字符 \u0005 → expanded_thread_id 变成这条会话的 ID。
调用关系:它保证不同终端传来的 Ctrl+E 事件都能被 handle_key 识别。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 7 个(Char, new, from, assert_eq!, MatchDefault, page_only_loader, vec!)。
tests::search_line_renders_sort_and_filter_tabs4612–4642 ↗
fn search_line_renders_sort_and_filter_tabs()
作用:用快照检查搜索栏里是否正确显示筛选和排序标签。用户需要知道当前是在看当前目录,还是全部会话,以及按什么排序。
数据流:测试创建一个带当前目录筛选的状态 → 在假终端里渲染 search_line → 把终端输出和快照比较。
调用关系:它直接测试 search_line 的界面输出,确保顶部工具栏的信息不会被改坏。
调用图:调用 5 个内部函数(with_options, new, search_line, new, test_dummy);外部调用 6 个(from, new, from, assert_snapshot!, MatchDefault, page_only_loader)。
tests::search_line_compacts_toolbar_on_narrow_width4645–4661 ↗
fn search_line_compacts_toolbar_on_narrow_width()
作用:检查窗口很窄时,搜索栏会压缩工具栏,但仍保留筛选和排序信息。
数据流:测试用宽度 40 生成 search_line 字符串 → 确认里面有 Filter:[Cwd] 和 Sort:[Updated],并且筛选显示在排序前面。
调用关系:它验证 search_line 的窄屏布局规则,避免小终端下工具栏顺序混乱或内容消失。
调用图:调用 3 个内部函数(new, search_line, test_dummy);外部调用 5 个(from, from, assert!, MatchDefault, page_only_loader)。
tests::dense_snapshot_row4663–4680 ↗
fn dense_snapshot_row() -> Row
作用:提供一条固定内容的会话行,给紧凑视图的截图测试反复使用。固定数据能让快照测试稳定,不会因为时间或随机 ID 改变。
数据流:函数不接收外部输入 → 构造一条带路径、标题预览、线程 ID、时间、工作目录和 Git 分支的 Row → 返回这条 Row。
调用关系:它是多个 dense 相关测试的测试夹具,也就是专门准备测试数据的小工具。
调用图:调用 2 个内部函数(from_string, parse_timestamp_str);外部调用 2 个(from, from)。
tests::render_dense_row_snapshot4682–4718 ↗
fn render_dense_row_snapshot(
show_all: bool,
filter_cwd: Option<PathBuf>,
width: u16,
) -> String
作用:把一条固定会话按紧凑模式渲染成字符串,方便多个快照测试复用。
数据流:函数接收是否显示全部、当前目录筛选和终端宽度 → 创建 PickerState,放入 dense_snapshot_row,并设成 Dense → 在假终端里 render_list → 返回终端画面字符串。
调用关系:它被多个 dense_session_snapshot_* 测试调用,把重复的搭状态、建终端、渲染列表步骤集中起来。
调用图:调用 6 个内部函数(with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 6 个(new, from, MatchDefault, dense_snapshot_row, page_only_loader, vec!)。
tests::dense_session_snapshot_omits_cwd_in_cwd_filter4721–4732 ↗
fn dense_session_snapshot_omits_cwd_in_cwd_filter()
作用:检查紧凑视图在已经按当前目录筛选时,会省略工作目录信息。因为这时目录信息对用户是重复的。
数据流:测试调用 render_dense_row_snapshot,传入当前目录筛选 → 生成画面 → 和“dense_cwd”快照比较。
调用关系:它复用渲染 helper,专门验证 render_list 在 Cwd 筛选下的元信息取舍。
调用图:外部调用 1 个(assert_snapshot!)。
tests::dense_session_snapshot_includes_cwd_in_all_filter4735–4742 ↗
fn dense_session_snapshot_includes_cwd_in_all_filter()
作用:检查紧凑视图在显示全部会话时,会包含工作目录信息。因为跨项目浏览时,目录能帮用户分辨会话来源。
数据流:测试用 show_all=true 且无当前目录筛选渲染紧凑行 → 和对应快照比较。
调用关系:它验证紧凑行渲染会根据筛选模式决定是否显示 cwd。
调用图:外部调用 1 个(assert_snapshot!)。
tests::dense_session_snapshot_auto_hides_cwd_when_narrow4745–4752 ↗
fn dense_session_snapshot_auto_hides_cwd_when_narrow()
作用:检查宽度不够时,紧凑视图会自动隐藏工作目录,以免主要标题被挤没。
数据流:测试在较窄宽度下渲染全部会话的紧凑行 → 把结果和自动隐藏 cwd 的快照比较。
调用关系:它验证 render_list 的自适应布局:空间不足时先牺牲次要信息。
调用图:外部调用 1 个(assert_snapshot!)。
tests::dense_session_snapshot_forces_cwd_when_narrow4755–4762 ↗
fn dense_session_snapshot_forces_cwd_when_narrow()
作用:检查非常窄的情况下,紧凑视图仍有一套固定表现,避免布局崩掉。
数据流:测试用宽度 48 渲染紧凑行 → 和 forced_cwd 快照比较。
调用关系:它覆盖紧凑视图极窄窗口下的边界布局,防止改动造成内容错位。
调用图:外部调用 1 个(assert_snapshot!)。
tests::dense_session_snapshot_drops_metadata_when_narrow4765–4772 ↗
fn dense_session_snapshot_drops_metadata_when_narrow()
作用:检查窄屏时会丢弃部分元信息,只保留更重要的内容。元信息指时间、目录、分支这类辅助说明。
数据流:测试用宽度 48 渲染紧凑行 → 和 narrow 快照比较。
调用关系:它和其他紧凑快照一起,保护 render_list 在不同宽度下的取舍规则。
调用图:外部调用 1 个(assert_snapshot!)。
tests::dense_session_line_prefers_thread_name_over_preview4775–4803 ↗
fn dense_session_line_prefers_thread_name_over_preview()
作用:检查会话有正式名称时,紧凑行优先显示名称,而不是原始预览文字。正式名称通常更像用户想看的标题。
数据流:测试把固定行改成同时有 thread_name 和 preview → 渲染紧凑行 → 确认结果包含“Named session”,不包含“Raw conversation preview”。
调用关系:它直接验证 render_dense_session_lines 的标题选择规则。
调用图:调用 4 个内部函数(new, parse_timestamp_str, render_dense_session_lines, test_dummy);外部调用 5 个(from, assert!, MatchDefault, dense_snapshot_row, page_only_loader)。
tests::dense_selected_summary_line_uses_full_width_selection_style4806–4819 ↗
fn dense_selected_summary_line_uses_full_width_selection_style()
作用:检查紧凑视图里被选中的摘要行,选中样式会铺满整行。这样光标所在行一眼就能看出来。
数据流:测试构造 DenseSummaryInput,标记为 selected,宽度 80 → 生成 dense_summary_line → 确认行宽是 80、颜色是选中样式、开头是选择箭头。
调用关系:它验证 dense_summary_line 和 selection_marker 配合出的选中行外观。
调用图:调用 2 个内部函数(dense_summary_line, selection_marker);外部调用 1 个(assert_eq!)。
tests::dense_zebra_summary_line_uses_full_width_background4822–4834 ↗
fn dense_zebra_summary_line_uses_full_width_background()
作用:检查紧凑视图的斑马纹背景会铺满整行。斑马纹就是隔行换底色,方便眼睛跟行。
数据流:测试构造未选中但 is_zebra=true 的摘要行 → 生成 dense_summary_line → 确认宽度为 80,背景色是斑马纹样式。
调用关系:它保护 dense_summary_line 的背景填充规则,避免只给文字部分上色。
调用图:调用 2 个内部函数(dense_summary_line, selection_marker);外部调用 1 个(assert_eq!)。
tests::comfortable_zebra_lines_use_full_width_background4837–4867 ↗
fn comfortable_zebra_lines_use_full_width_background()
作用:检查舒适视图的斑马纹也会给每一行铺满整宽。舒适视图一条会话可能占多行,所以每行都要一致。
数据流:测试创建一条会话,按舒适模式渲染成多行 → 确认有 2 行,每行宽度都是 100,背景色都是斑马纹色。
调用关系:它验证 render_comfortable_session_lines 的多行背景处理,与紧凑模式保持一致体验。
调用图:调用 4 个内部函数(new, parse_timestamp_str, render_comfortable_session_lines, test_dummy);外部调用 6 个(from, assert!, assert_eq!, MatchDefault, make_row, page_only_loader)。
tests::dense_session_snapshot_uses_no_blank_lines_between_rows4870–4912 ↗
fn dense_session_snapshot_uses_no_blank_lines_between_rows()
作用:检查紧凑视图的多条会话之间没有空白行。紧凑模式的目的就是尽量多显示内容。
数据流:测试准备两条会话并选中第二条 → 在 80×2 的假终端里渲染 → 和无空行快照比较。
调用关系:它通过 render_list 的完整输出验证 Dense 模式不会沿用舒适模式的留白。
调用图:调用 6 个内部函数(with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 8 个(from, new, from, assert_snapshot!, MatchDefault, dense_snapshot_row, page_only_loader, vec!)。
tests::expanded_session_snapshot4915–4981 ↗
fn expanded_session_snapshot()
作用:用快照检查展开会话后的样子,包括会话摘要和最近几条预览内容。
数据流:测试准备一条会话,设置 expanded_thread_id,并放入已加载的预览行 → 在假终端渲染列表 → 把输出整理后和快照比较。
调用关系:它把展开状态、TranscriptPreviewState::Loaded 和 render_list 放在一起测,确保 Ctrl+E 展开后的界面稳定。
调用图:调用 7 个内部函数(from_string, with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 8 个(from, new, from, assert_snapshot!, MatchDefault, Loaded, page_only_loader, vec!)。
tests::narrow_session_snapshot4984–5031 ↗
fn narrow_session_snapshot()
作用:用快照检查普通会话行在窄窗口里的显示效果。窄窗口很容易出现截断和错位,所以需要专门保护。
数据流:测试构造一条带目录和分支的会话 → 在 58 宽的假终端里 render_list → 和窄屏快照比较。
调用关系:它验证 render_list 的窄屏布局,和紧凑视图的窄屏测试互相补充。
调用图:调用 7 个内部函数(from_string, with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 7 个(from, new, from, assert_snapshot!, MatchDefault, page_only_loader, vec!)。
tests::session_list_more_indicators_snapshot5034–5083 ↗
fn session_list_more_indicators_snapshot()
作用:检查列表上下还有内容时,会显示“more”提示。这个提示像电梯里的上下箭头,告诉用户还能继续滚。
数据流:测试准备 5 条数据,把滚动位置放在中间 → 渲染 80×6 的列表 → 和 more indicators 快照比较。
调用关系:它验证 update_viewport 和 render_list 配合后,会在正确位置显示还有更多内容的提示。
调用图:调用 6 个内部函数(with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 5 个(new, from, assert_snapshot!, MatchDefault, page_only_loader)。
tests::density_toggle_clears_stale_more_indicator5086–5140 ↗
fn density_toggle_clears_stale_more_indicator()
作用:检查从舒适视图切到紧凑视图后,旧的“more”提示不会残留在屏幕上。
数据流:测试先按舒适视图渲染,确认画面有“↓ more” → 改成 Dense 并重新计算视口再渲染 → 确认画面里不再有这个旧提示。
调用关系:它验证密度改变后 update_viewport 和 render_list 会重新计算可见内容,而不是沿用旧布局标记。
调用图:调用 6 个内部函数(with_options, new, parse_timestamp_str, render_list, new, test_dummy);外部调用 5 个(new, from, assert!, MatchDefault, page_only_loader)。
tests::pageless_scrolling_deduplicates_and_keeps_order5143–5195 ↗
fn pageless_scrolling_deduplicates_and_keeps_order()
作用:检查分页加载多批数据时,会去掉重复会话,并保持正确顺序。没有这个逻辑,用户会在列表里看到同一个会话出现两次。
数据流:测试依次塞入三页数据,其中第二页包含一条重复路径 → ingest_page 合并数据 → 最终预览顺序是 third、second、first、very old,且路径总数为 4 个唯一值。
调用关系:它验证 reset_pagination 和 ingest_page 的分页合并规则,是滚动加载历史会话时的核心保障。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 6 个(from, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tests::ensure_minimum_rows_prefetches_when_underfilled5198–5229 ↗
fn ensure_minimum_rows_prefetches_when_underfilled()
作用:检查当前加载的行数太少、不够填满视图时,会自动预取下一页。这样用户一打开不会看到大片空白。
数据流:测试先加载 2 条并保留下页游标 → 调用 ensure_minimum_rows_for_view,要求至少 10 行 → 加载器收到 1 个新的分页请求。
调用关系:它验证 PickerState 在列表不足时会主动通过 PickerLoader 请求更多数据。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 10 个(new, new, from, new, assert!, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tests::ensure_minimum_rows_does_not_prefetch_when_comfortable_cards_fill_view5232–5264 ↗
fn ensure_minimum_rows_does_not_prefetch_when_comfortable_cards_fill_view()
作用:检查舒适视图下,如果现有卡片已经填满屏幕,就不要多加载。舒适视图每条会话占更多高度,不能只按条数粗暴判断。
数据流:测试加载 4 条,设置 6 行高的视口 → 调用 ensure_minimum_rows_for_view → 没有产生新的分页请求。
调用关系:它验证预取逻辑会考虑当前密度和实际占用高度,避免无谓加载。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, new, from, new, assert!, MatchDefault, page, page_only_loader, vec!)。
tests::ensure_minimum_rows_still_prefetches_when_dense_rows_underfill_view5267–5300 ↗
fn ensure_minimum_rows_still_prefetches_when_dense_rows_underfill_view()
作用:检查紧凑视图下,如果行数确实不够填满屏幕,仍然会预取更多。
数据流:测试切到 Dense,只加载 2 条,并设置 10 行视口 → 调用 ensure_minimum_rows_for_view → 收到 1 个分页请求。
调用关系:它和舒适视图测试形成对照,确认预取判断会随显示密度变化。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 10 个(new, new, from, new, assert!, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tests::list_viewport_width_matches_rendered_list_inset5303–5306 ↗
fn list_viewport_width_matches_rendered_list_inset()
作用:检查列表内容宽度会扣掉左右缩进,并且太窄时不会变成负数。
数据流:测试调用 list_viewport_width:宽度 80 得到 76,宽度 3 得到 0。
调用关系:它保护列表布局的基础计算,render_list 和视口更新都依赖这种宽度判断。
调用图:外部调用 1 个(assert_eq!)。
tests::toggle_sort_key_reloads_with_new_sort5309–5344 ↗
async fn toggle_sort_key_reloads_with_new_sort()
作用:检查切换排序方式后,会用新的排序键重新加载数据。否则界面上显示排序变了,实际列表却还是旧顺序。
数据流:测试先启动初次加载,记录排序键是 UpdatedAt → 按 Tab 聚焦工具栏,再按 Ctrl+L 切换排序 → 第二次加载请求的排序键变成 CreatedAt。
调用关系:它验证 handle_key 的工具栏操作会重置并触发分页加载,请求里带上新的 ThreadSortKey。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, Char, new, new, from, new, assert_eq!, MatchDefault, page_only_loader)。
tests::default_filter_focus_arrows_reload_with_new_filter5347–5378 ↗
async fn default_filter_focus_arrows_reload_with_new_filter()
作用:检查默认处在当前目录筛选时,按右箭头切到全部会话后会重新加载。
数据流:测试创建 show_all=false 且 filter_cwd=/tmp/project 的状态 → 初次加载请求带 cwd_filter → 按右箭头 → 第二次请求的 cwd_filter 变成 None。
调用关系:它验证筛选标签的键盘切换会影响后续 PageLoadRequest,而不只是改界面文字。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, new, new, from, from, new, assert_eq!, MatchDefault, page_only_loader)。
tests::all_filter_can_switch_back_to_cwd_when_cwd_candidate_exists5381–5412 ↗
async fn all_filter_can_switch_back_to_cwd_when_cwd_candidate_exists()
作用:检查当前显示全部会话时,如果知道当前目录候选,按右箭头可以切回当前目录筛选。
数据流:测试创建 show_all=true 且有 filter_cwd 候选的状态 → 初次请求不过滤目录 → 按右箭头 → 第二次请求带上 /tmp/project 作为 cwd_filter。
调用关系:它验证筛选切换是双向的:不仅能从当前目录到全部,也能从全部回到当前目录。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, new, new, from, from, new, assert_eq!, MatchDefault, page_only_loader)。
tests::filter_stays_all_when_no_cwd_candidate_exists5415–5448 ↗
async fn filter_stays_all_when_no_cwd_candidate_exists()
作用:检查没有当前目录候选时,筛选栏不会显示 Cwd,也不会切到不存在的当前目录筛选。
数据流:测试创建没有 filter_cwd 的状态 → 确认 search_line 里没有 Cwd → 初次加载后按右箭头 → 请求数量仍只有 1 个,且不带 cwd_filter。
调用关系:它保护筛选工具栏的边界行为,避免界面提供一个实际不可用的选项。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 6 个(new, new, new, new, assert_eq!, page_only_loader)。
tests::ctrl_c_exits_even_when_cancel_is_remapped_to_ctrl_c5572–5590 ↗
async fn ctrl_c_exits_even_when_cancel_is_remapped_to_ctrl_c()
作用:检查即使用户把取消键映射到 Ctrl+C,Ctrl+C 仍然明确表示退出。这个键是终端里最常见的“停下来”。
数据流:测试把 list_keymap.cancel 设置成 Ctrl+C → 模拟 Ctrl+C → 返回 SessionSelection::Exit。
调用关系:它保护 Ctrl+C 的特殊地位,避免按键重映射让退出行为变得含糊。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 7 个(Char, new, from, assert!, MatchDefault, page_only_loader, vec!)。
tests::end_jumps_to_last_known_row_and_starts_loading_more5593–5638 ↗
async fn end_jumps_to_last_known_row_and_starts_loading_more()
作用:检查按 End 会跳到当前已知最后一条,并在还有下一页时开始加载更多。用户想去底部时,系统顺便准备后面的内容。
数据流:测试加载 10 条并保留下页游标 → 按 End → selected 变成 9,分页状态变成 pending,发出 1 个加载请求,底部显示“10 / 10… · 100%”。
调用关系:它验证导航动作和分页加载会联动,handle_key 在跳到底部时会触发加载更多。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 10 个(new, new, new, from, new, assert!, assert_eq!, MatchDefault, page, page_only_loader)。
tests::enter_on_row_without_resolvable_thread_id_shows_inline_error5641–5677 ↗
async fn enter_on_row_without_resolvable_thread_id_shows_inline_error()
作用:检查用户按 Enter 选择一条无法解析出会话 ID 的记录时,会显示清楚的错误,而不是退出或崩溃。
数据流:测试准备一条有文件路径但没有 thread_id 的行 → 按 Enter → 没有返回选择结果,inline_error 写入“Failed to read session metadata from /tmp/missing.jsonl”。
调用关系:它验证会话选择流程在缺少必要元数据时会留在选择器里,并给用户错误信息。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 8 个(new, from, from, assert!, assert_eq!, MatchDefault, page_only_loader, vec!)。
tests::enter_on_pathless_thread_uses_thread_id5680–5716 ↗
async fn enter_on_pathless_thread_uses_thread_id()
作用:检查没有本地文件路径、但有 thread_id 的会话也能被选中恢复。这样远端或无路径会话不会被误认为不可用。
数据流:测试准备一条 path=None、thread_id=某个值的行 → 按 Enter → 返回 Resume 选择,目标里路径为空但 thread_id 正确。
调用关系:它验证选择逻辑不强依赖文件路径,只要有可用 thread_id,就能构造 SessionTarget。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 7 个(new, from, assert_eq!, panic!, MatchDefault, page_only_loader, vec!)。
tests::app_server_row_keeps_pathless_threads5719–5749 ↗
fn app_server_row_keeps_pathless_threads()
作用:这个测试确认:从应用服务器来的远程线程即使没有本地文件路径,也不能被过滤掉。这样用户才能看到只存在服务器上的会话,而不是因为缺少路径就凭空消失。
数据流:它先造了一个假的服务器线程,里面有线程编号、名字、预览文字,但 path 是空的 → 然后交给 row_from_app_server_thread 转成选择器列表里的一行 → 最后检查这一行仍然保留了空路径、正确的线程编号和线程名字。
调用关系:这是在保护服务器线程进入选择器列表的入口。测试直接使用 row_from_app_server_thread,确保后面的列表显示和选择流程拿到的是完整的远程线程信息。
调用图:调用 2 个内部函数(new, row_from_app_server_thread);外部调用 4 个(from, new, assert_eq!, test_path_buf)。
tests::thread_to_transcript_cells_renders_core_message_types5752–5818 ↗
fn thread_to_transcript_cells_renders_core_message_types()
作用:这个测试确认聊天记录预览能显示最基本的几类内容:用户说的话、助手说的话,以及助手提出的计划。没有这个保证,恢复会话时用户可能看不到关键上下文。
数据流:它先构造一个带一轮对话的假线程,里面放入用户消息、助手消息和计划 → 调用 thread_to_transcript_cells 把结构化聊天内容变成终端里可画出来的文本块 → 再把文本行拼起来,检查用户文字、助手文字、计划标题和计划内容都出现了。
调用关系:它测试的是线程详情展示链路。resume picker 选中某个会话后,需要把线程内容交给 crate::thread_transcript::thread_to_transcript_cells 渲染成可读预览,这个测试确保常见消息类型不会漏。
调用图:调用 1 个内部函数(new);外部调用 4 个(from, assert!, test_path_buf, vec!)。
tests::thread_to_transcript_cells_hides_raw_reasoning_when_not_enabled5821–5876 ↗
fn thread_to_transcript_cells_hides_raw_reasoning_when_not_enabled()
作用:这个测试确认模型的原始推理内容在没有开启显示时会被藏起来。原始推理可以理解为模型内部草稿,默认不该随便展示给用户。
数据流:它创建一个只包含 Reasoning 内容的假线程,内容里写着一段私密原始推理 → 分别用 Hidden 和 Visible 两种开关调用 thread_to_transcript_cells → 最后检查隐藏模式看不到这段文字,显示模式能看到这段文字。
调用关系:它守住聊天记录渲染里的隐私边界。选择器显示会话详情时会走 thread_to_transcript_cells,这个测试确保 RawReasoningVisibility 这个开关真的控制了原始推理是否出现。
调用图:调用 1 个内部函数(new);外部调用 4 个(from, assert!, test_path_buf, vec!)。
tests::thread_to_transcript_cells_shows_raw_reasoning_over_summary_when_enabled5879–5928 ↗
fn thread_to_transcript_cells_shows_raw_reasoning_over_summary_when_enabled()
作用:这个测试确认:如果用户明确允许显示原始推理,就优先显示原始内容,而不是只显示摘要。这样开发者或调试场景能看到更完整的信息。
数据流:它造了一个 Reasoning 项,同时带有公开摘要和原始推理内容 → 用 Visible 开关渲染成终端文本 → 检查结果里有原始推理,但没有摘要。
调用关系:它补充验证 thread_to_transcript_cells 的选择规则。当前面那个测试保证“能藏起来”时,这个测试保证“打开后显示的是最完整版本”。
调用图:调用 1 个内部函数(new);外部调用 4 个(from, assert!, test_path_buf, vec!)。
tests::moving_to_last_card_scrolls_when_cards_exceed_viewport5931–5970 ↗
async fn moving_to_last_card_scrolls_when_cards_exceed_viewport()
作用:这个测试确认列表卡片比屏幕可见区域还多时,用户按向下移动到最后一项,滚动条会跟着移动。否则选中的东西可能跑到屏幕外,看起来像界面卡住了。
数据流:它创建一个 PickerState,塞入 3 条假会话,并把窗口高度设成只能舒服显示有限内容 → 连续模拟两次向下键 → 检查选中位置到了最后一条,同时 scroll_top 也更新到能看到选中卡片的位置。
调用关系:它测试键盘事件处理 handle_key 和可见区域计算的配合。用户在主循环里按方向键时,PickerState 必须同时更新 selected 和 scroll_top。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, from, new, assert_eq!, format!, MatchDefault, make_row, page, page_only_loader)。
tests::up_from_bottom_keeps_viewport_stable_when_card_remains_visible5973–6012 ↗
async fn up_from_bottom_keeps_viewport_stable_when_card_remains_visible()
作用:这个测试确认用户从列表底部往上移时,画面滚动不要乱跳,只在需要时跟着轻微调整。这样列表操作才像正常菜单,而不是每按一次都闪一下。
数据流:它造出 10 条假会话,把选中项放到最后,并先让选择器滚动到能看到最后一项 → 模拟按一次向上键 → 检查选中项上移一格,滚动顶部也只按预期上移一格。
调用关系:它覆盖 PickerState 的 ensure_selected_visible 和 handle_key 这条链路。测试关心的是键盘移动后,选中项和滚动窗口之间保持稳定关系。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, from, new, assert_eq!, format!, MatchDefault, make_row, page, page_only_loader)。
tests::up_scrolls_only_after_crossing_top_edge6015–6050 ↗
async fn up_scrolls_only_after_crossing_top_edge()
作用:这个测试确认向上移动时,只有当选中项碰到可见区域上边缘时,列表才向上滚动。这样用户不会因为过早滚动而迷失当前位置。
数据流:它造出 10 条假会话,手动把选中项和滚动顶部都放在第 8 项附近 → 模拟按一次向上键 → 检查选中项变成第 7 项,滚动顶部也变成第 7 项,说明画面刚好跟着边缘移动。
调用关系:它是方向键滚动规则的边界测试。handle_key 收到 Up 后,会调整 selected,再依靠可见性规则修正 scroll_top。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 9 个(new, from, new, assert_eq!, format!, MatchDefault, make_row, page, page_only_loader)。
tests::list_reports_more_rows_above_and_below6053–6086 ↗
fn list_reports_more_rows_above_and_below()
作用:这个测试确认列表能正确告诉界面:上面还有没有没显示的内容,下面还有没有没显示的内容。这个信息通常用来画提示符,比如“上方还有更多”。
数据流:它创建 5 条假会话并设置窗口高度 → 一开始检查顶部没有更多内容、底部还有更多内容 → 再把 scroll_top 改到中间 → 检查上方和下方都报告还有内容。
调用关系:它测试 PickerState 的 has_more_above 和 has_more_below。渲染列表时会用这两个判断来决定是否提示用户还能继续滚动。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 8 个(from, new, assert!, format!, MatchDefault, make_row, page, page_only_loader)。
tests::set_query_loads_until_match_and_respects_scan_cap6089–6207 ↗
async fn set_query_loads_until_match_and_respects_scan_cap()
作用:这个测试确认搜索时如果当前页找不到匹配项,选择器会继续请求下一页;但如果扫描达到上限,就会停下来。这样搜索既尽量帮用户找,又不会无限扫下去。
数据流:它先准备一个会记录加载请求的假加载器,并塞入一页不匹配的初始数据 → 设置搜索词 target 后,检查发出了第一页搜索请求 → 模拟后台返回仍不匹配的一页,选择器继续请求下一页 → 再模拟返回包含 target 的一页,搜索停止并显示结果 → 接着搜索 missing,并模拟旧请求返回,确认旧结果被忽略 → 最后模拟当前请求达到扫描上限,检查搜索停止、结果为空,并记录 reached_scan_cap 为真。
调用关系:它测试搜索输入、分页加载、后台事件处理三者的配合。set_query 启动搜索,handle_background_event 接收后台页面,pagination 和 search_state 决定是否继续请求。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 10 个(new, new, from, new, assert!, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tests::paste_appends_to_existing_query6210–6225 ↗
async fn paste_appends_to_existing_query()
作用:这个测试确认用户粘贴文字时,不会粗暴覆盖已有搜索词,而是追加进去。比如已经搜 resize,再粘贴 results,就变成 resize results。
数据流:它创建一个 PickerState,并把当前查询设为 resize → 调用 handle_paste 传入 results → 检查查询字符串变成 resize results。
调用关系:它测试粘贴输入的基本行为。终端收到粘贴事件后会交给 handle_paste,这个函数再复用搜索更新路径。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(from, assert_eq!, MatchDefault, page_only_loader)。
tests::whitespace_only_paste_is_ignored6228–6243 ↗
async fn whitespace_only_paste_is_ignored()
作用:这个测试确认只包含空格、换行、制表符的粘贴内容会被忽略。这样用户误粘贴空白时,不会把搜索词弄脏。
数据流:它先把查询设为 resize → 调用 handle_paste,传入一串只有空白字符的内容 → 检查查询仍然是 resize,没有变化。
调用关系:它保护 handle_paste 的输入清理规则。粘贴事件进入选择器后,空白内容会在这里被挡住,不会触发新的搜索。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 4 个(from, assert_eq!, MatchDefault, page_only_loader)。
tests::paste_uses_existing_search_loading_path6246–6280 ↗
async fn paste_uses_existing_search_loading_path()
作用:这个测试确认粘贴文字后,走的是和正常输入搜索一样的加载流程。也就是说,粘贴不是特殊捷径,而是会发起带搜索标记的分页请求。
数据流:它准备一个会记录请求的假加载器,先塞入一页已有数据,再清空请求记录 → 调用 handle_paste 粘贴 target → 检查查询变成 target,并且发出了一个带 search_token 的加载请求。
调用关系:它把粘贴功能和搜索分页功能连起来验证。handle_paste 更新查询后,应当像 set_query 一样触发后台加载,后续结果再由 handle_background_event 接收。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 10 个(new, new, from, new, assert!, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tests::esc_with_empty_query_starts_fresh6283–6300 ↗
async fn esc_with_empty_query_starts_fresh()
作用:这个测试确认在没有搜索词时按 Esc,会选择“开始一个新会话”。这让 Esc 在空搜索框里成为退出恢复流程、直接新开的快捷操作。
数据流:它创建一个没有查询内容的 PickerState → 模拟按下 Esc 键 → 检查返回的选择结果是 SessionSelection::StartFresh。
调用关系:它测试键盘事件 handle_key 对 Esc 的顶层决定。主界面收到这个选择结果后,就知道不要恢复旧会话,而是启动新会话。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 5 个(new, from, assert!, MatchDefault, page_only_loader)。
tests::esc_with_query_clears_search_and_preserves_selected_result6303–6337 ↗
async fn esc_with_query_clears_search_and_preserves_selected_result()
作用:这个测试确认有搜索词时按 Esc,不是立刻退出,而是先清空搜索;同时还要尽量保留用户刚才选中的那条结果。这样用户取消筛选后不会丢掉当前位置。
数据流:它先放入 alpha 和 beta 两条会话,再设置搜索词 beta,让列表只关注 beta → 模拟按 Esc → 检查没有返回最终选择,查询被清空,完整列表恢复成两条,并且当前选中项仍然指向 beta 那条会话。
调用关系:它测试 handle_key 里 Esc 的另一条分支。和空查询时直接 StartFresh 不同,有查询时它会先清理搜索状态,并重新计算 filtered_rows 和 selected。
调用图:调用 2 个内部函数(new, test_dummy);外部调用 8 个(new, from, assert!, assert_eq!, MatchDefault, page, page_only_loader, vec!)。
tui/src/chatwidget/constructor.rs源码 ↗
这个文件像是在给聊天窗口“装机”。外部先交进来一包启动资料,比如配置、事件发送器、初始消息、模型列表、账号状态和快捷键设置。它会先整理这些资料:决定当前用哪个模型、标题上显示什么、是否防止电脑空闲休眠、输入框里放哪句随机提示语。然后它创建 ChatWidget 这个大对象,把底部输入区、对话记录、会话标题、限额状态、插件状态、协作模式、终端信息等很多小零件都放到正确位置。最后它还会做一轮收尾:预取限额信息,应用快捷键,打开或关闭一些命令入口,刷新状态栏。简单说,这里不是处理聊天内容的地方,而是确保聊天界面一出生就是能工作的状态。
ChatWidget::new_with_app_event6–8 ↗
fn new_with_app_event(common: ChatWidgetInit) -> Self
作用:这是创建 ChatWidget 的常用入口。它把操作目标固定为应用事件,也就是让这个聊天界面默认把后续动作发回主应用处理。
数据流:进去的是一整包 ChatWidgetInit 启动资料 → 它不自己组装界面,只是补上默认的 CodexOpTarget::AppEvent → 出来的是已经创建好的 ChatWidget。
调用关系:它像一个简化按钮,外部代码只要想按默认方式创建聊天窗口,就调用它。真正繁重的搭建工作会交给 ChatWidget::new_with_op_target。
调用图:外部调用 1 个(new_with_op_target)。
ChatWidget::new_with_op_target10–278 ↗
fn new_with_op_target(
common: ChatWidgetInit,
codex_op_target: CodexOpTarget,
) -> Self
作用:这是 ChatWidget 真正的构造函数。它把配置、账号、模型、输入框、状态栏、快捷键、插件、限额提示等启动时需要的状态一次性拼成一个完整聊天界面。
数据流:进去的是 ChatWidgetInit 启动资料和一个 CodexOpTarget 操作目标 → 它拆开启动资料,清理模型名,更新配置,随机挑输入提示语,计算当前显示模型和服务档位,读取终端与快捷键信息,启动需要的宠物加载,再创建并填满 ChatWidget 的各个字段 → 出来的是一个已经初始化、快捷键和状态显示也同步好的 ChatWidget;过程中还会触发限额预取、状态刷新和一些功能开关更新。
调用关系:它是这个文件的核心装配线。ChatWidget::new_with_app_event 会把默认目标交给它;它内部会调用 BottomPane、TranscriptState、SessionHeader、TurnLifecycleState 等组件的创建方法,也会调用 start_configured_pet_load_if_needed 这类启动辅助,最后再调用自身的同步和刷新方法,让界面刚创建完就能直接参与主程序运行。
调用图:调用 10 个内部函数(new, new, start_configured_pet_load_if_needed, new, default, new, new, defaults, from_config, effective_service_tier);外部调用 24 个(new, new, new, now, initial_collaboration_mask, placeholder_session_header_cell, new, new, matches!, default (+14 more))。
tui/src/chatwidget/pets.rs源码 ↗
这个文件像是聊天窗口旁边那只“小宠物”的管家。它先看配置里用户选了哪只宠物,如果用户关掉了宠物,就什么也不做;如果选了宠物,就尝试从本地目录加载。宠物资源可能需要准备,所以它会把加载工作丢到后台线程里做,避免聊天界面卡死。界面绘制时,它会判断当前有没有弹窗;有弹窗就不画宠物,免得挡住重要内容。它还会给聊天记录留出宠物图片占用的宽度,避免文字和宠物挤在一起。另一个重要部分是“宠物选择器”:用户打开选择器后,它会显示列表,并为当前选中的宠物生成预览;如果加载失败,就把错误显示出来。文件里还特别处理了终端是否支持图片,因为不是所有终端都能显示宠物图像;不支持时会直接提示用户。
load_ambient_pet6–22 ↗
fn load_ambient_pet(
config: &Config,
frame_requester: FrameRequester,
) -> Option<crate::pets::AmbientPet>
作用:根据配置尝试加载当前选中的终端宠物。有人会在用户改了宠物设置后用它,把配置里的宠物变成界面里真正能显示的宠物对象。
数据流:进去的是配置、一个请求重画界面的工具。它先读配置里的宠物编号:没有编号或编号表示“禁用”就直接返回空;否则用用户的主目录、动画开关和宠物编号去加载宠物。出来的是一个可用的宠物,加载失败时也返回空,不把错误继续往外抛。
调用关系:它会调用底层的宠物加载能力 load。ChatWidget::set_tui_pet 在用户改变宠物配置后会调用它,用新配置刷新当前界面里的宠物。
调用图:调用 1 个内部函数(load);被 1 处调用(set_tui_pet)。
start_configured_pet_load_if_needed24–53 ↗
fn start_configured_pet_load_if_needed(
config: &Config,
ambient_pet_missing: bool,
frame_requester: FrameRequester,
app_event_tx: AppEventSender,
)
作用:启动时如果配置里写了宠物,但当前还没有成功加载,就在后台补一次加载。这样资源没准备好时,也不会让界面启动过程停在那里等。
数据流:进去的是配置、是否缺少宠物、重画请求器和应用事件发送器。它先检查用户是否真的选了宠物、宠物有没有被禁用、当前是否确实缺宠物;满足条件后,把宠物编号和目录复制出来,放到后台任务里准备资源并加载宠物。最后通过事件发送器发出“配置宠物加载完成”的消息,里面带成功的宠物或错误文字。
调用关系:它由 new_with_op_target 在创建聊天界面时触发。它把耗时工作交给 spawn_pet_load,后台完成后不直接改界面,而是发 AppEvent::ConfiguredPetLoaded,让主界面流程安全地接手。
调用图:调用 1 个内部函数(spawn_pet_load);被 1 处调用(new_with_op_target)。
ChatWidget::set_ambient_pet_notification56–64 ↗
fn set_ambient_pet_notification(
&mut self,
kind: crate::pets::PetNotificationKind,
body: Option<String>,
)
作用:给当前正在显示的宠物设置一个小通知,比如让宠物显示某种状态或提示文字。没有宠物时它什么也不做。
数据流:进去的是通知类型和可选的文字内容。它查看当前聊天窗口里有没有宠物;有的话,把通知交给宠物对象保存或显示;没有的话,不产生任何变化。出来没有返回值,只可能改变宠物的通知状态。
调用关系:它是聊天窗口给宠物传话的入口。外部流程想让宠物表现某种状态时会调用它,具体显示细节由宠物对象自己处理。
ChatWidget::ambient_pet_image_enabled66–70 ↗
fn ambient_pet_image_enabled(&self) -> bool
作用:判断当前宠物是否真的启用了图片显示。这个结果会影响界面是否需要给宠物图像留空间。
数据流:进去不需要额外参数,只读取聊天窗口里的当前宠物。它检查有没有宠物;有的话再问宠物自己图片是否可用。出来是一个真假值:true 表示宠物图片会显示,false 表示没有宠物或图片不可用。
调用关系:它是界面其他部分做布局判断时可用的小查询函数,不负责加载或绘制,只回答“图片宠物现在算不算开启”。
ChatWidget::disable_ambient_pet_for_session72–75 ↗
fn disable_ambient_pet_for_session(&mut self)
作用:临时关掉本次会话里的宠物显示。它不会说一定改永久配置,只是让当前聊天窗口不再显示宠物。
数据流:进去不需要参数。它把当前宠物清空,然后请求界面重画。出来没有返回值,但聊天窗口状态从“可能有宠物”变成“没有宠物”。
调用关系:它用于用户或程序想马上隐藏宠物的场景。清空后通过 request_redraw 让界面下一帧反映这个变化。
ChatWidget::ambient_pet_draw77–93 ↗
fn ambient_pet_draw(
&self,
area: Rect,
composer_bottom_y: u16,
) -> Option<crate::pets::AmbientPetDraw>
作用:为当前宠物生成一次绘制请求,也就是告诉渲染层“宠物应该画在哪里、怎么画”。如果界面上有弹窗或菜单,它会选择不画,避免宠物挡住操作。
数据流:进去的是可绘制区域和输入框底部位置。它先检查底部面板有没有弹窗或选择器;如果有,就返回空。然后根据配置决定宠物贴着输入框,还是贴着屏幕底部,再把区域和锚点交给宠物生成绘制请求。出来是可选的绘制请求。
调用关系:它会用 bottom 之类的界面区域信息来算位置。主渲染流程在需要画聊天窗口时会问它要不要画宠物,它再把具体画法交给 AmbientPet。
调用图:外部调用 1 个(bottom)。
ChatWidget::ambient_pet_wrap_reserved_cols95–104 ↗
fn ambient_pet_wrap_reserved_cols(&self) -> u16
作用:计算聊天文字右侧要给宠物图片让出多少列宽。这样宠物不会盖住聊天记录,聊天记录也不会挤到宠物身上。
数据流:进去不需要参数,只读取当前宠物。它先确认宠物存在且图片开启;如果是,就取宠物图片宽度,再加上一点间隔;否则返回 0。出来是需要预留的终端列数。
调用关系:它被 ChatWidget::history_wrap_width 调用。它像排版前的尺子,先量出宠物占的地方,再让聊天记录按剩下的宽度换行。
调用图:被 1 处调用(history_wrap_width)。
ChatWidget::history_wrap_width106–110 ↗
fn history_wrap_width(&self, width: u16) -> u16
作用:算聊天历史文本实际能用的换行宽度。宠物占了右边空间时,它会把那部分宽度扣掉。
数据流:进去的是整个聊天区域宽度。它调用 ChatWidget::ambient_pet_wrap_reserved_cols 算出宠物要占多少列,然后从总宽度里减掉;如果减到太小,至少保留 1 列。出来是文本换行应该使用的宽度。
调用关系:它在聊天记录排版时使用,依赖 ChatWidget::ambient_pet_wrap_reserved_cols 提供宠物占位信息。这样文字排版和宠物显示能互相避让。
调用图:调用 1 个内部函数(ambient_pet_wrap_reserved_cols)。
ChatWidget::pet_picker_preview_draw112–122 ↗
fn pet_picker_preview_draw(&self) -> Option<crate::pets::AmbientPetDraw>
作用:在宠物选择器打开时,为右侧或指定区域的宠物预览生成绘制请求。没有打开选择器、没有预览区域或宠物还没加载好时,它会返回空。
数据流:进去不需要参数。它先确认当前活动视图确实是宠物选择器,再读取预览区域,然后让已加载的预览宠物生成绘制请求。成功后,它会记下“预览图片现在可见”。出来是可选的预览绘制请求。
调用关系:它属于宠物选择器的渲染环节。选择器打开并且 ChatWidget::finish_pet_picker_preview_load 已经放入预览宠物后,渲染流程会通过它拿到要画的内容。
ChatWidget::should_clear_pet_picker_preview_image124–126 ↗
fn should_clear_pet_picker_preview_image(&self) -> bool
作用:判断上一次宠物预览图片是否需要清掉。它还会顺手把“图片可见”的标记重置掉。
数据流:进去不需要参数。它读取并替换内部的“预览图片可见”标记:如果之前是可见,就返回 true,并把标记改成 false;如果之前不可见,就返回 false。出来是是否需要清理旧图片的真假值。
调用关系:它用于渲染循环或清屏逻辑。ChatWidget::pet_picker_preview_draw 会把标记设为可见,而这个函数在下一轮判断旧图是否需要擦掉。
ChatWidget::fail_pet_picker_preview_render128–132 ↗
fn fail_pet_picker_preview_render(&mut self, message: String)
作用:当宠物预览图真正绘制失败时,把失败原因记到预览状态里,并清掉预览宠物。用户看到的会是错误提示,而不是坏掉的图片。
数据流:进去的是错误消息。它把预览状态设成错误,删除当前预览宠物,然后请求界面重画。出来没有返回值,但选择器预览区会从“显示宠物”变成“显示错误”。
调用关系:它通常在渲染层发现预览画不出来时被调用。它不负责重新加载,只负责把失败状态反映到 ChatWidget 的界面状态里。
ChatWidget::open_pets_picker134–154 ↗
fn open_pets_picker(&mut self)
作用:打开宠物选择器,让用户挑选终端宠物,并自动开始加载当前选中宠物的预览。它会先确认当前终端支持宠物图片。
数据流:进去不需要参数。它先调用 ChatWidget::warn_if_pets_unsupported 检查支持情况;如果不支持,就提示并停止。支持时,它清空旧预览,构造选择器参数,显示选择列表,再取当前配置里的宠物编号或默认宠物编号,调用 ChatWidget::start_pet_picker_preview 开始预览加载。出来没有返回值,但界面会出现宠物选择器。
调用关系:它是用户打开宠物选择界面的入口。它会调用外部的 build_pet_picker_params 准备列表数据,并把预览加载交给 ChatWidget::start_pet_picker_preview。
调用图:调用 2 个内部函数(start_pet_picker_preview, warn_if_pets_unsupported);外部调用 1 个(build_pet_picker_params)。
ChatWidget::select_pet_by_id156–162 ↗
fn select_pet_by_id(&mut self, pet_id: String)
作用:用户选中某个宠物编号后,把选择结果发给应用主流程。它不会自己立刻完成所有安装和保存,而是发事件让统一流程处理。
数据流:进去的是宠物编号字符串。它先检查终端是否支持宠物图片;不支持就提示并结束。支持时,它通过事件发送器发出 AppEvent::PetSelected,里面带着宠物编号。出来没有直接返回值,但应用事件队列多了一条选择宠物的消息。
调用关系:它会调用 ChatWidget::warn_if_pets_unsupported 做前置检查。选择器或命令触发选择时会用它,后续加载、保存或应用选择由收到事件的主流程继续做。
调用图:调用 1 个内部函数(warn_if_pets_unsupported)。
ChatWidget::warn_if_pets_unsupported164–172 ↗
fn warn_if_pets_unsupported(&mut self) -> bool
作用:检查当前终端能不能显示宠物图片;不能的话,就给用户加一条警告消息。它返回 true 表示“不支持,已经提醒了”。
数据流:进去不需要参数。它调用 ChatWidget::pet_image_support 获取支持情况;如果支持,就返回 false。若不支持,它拿到可读的原因文字,加入聊天窗口警告消息,然后返回 true。出来是一个真假值,用来决定后续是否停止。
调用关系:它被 ChatWidget::open_pets_picker 和 ChatWidget::select_pet_by_id 调用,作为打开选择器或选择宠物前的守门员。它把具体检测交给 ChatWidget::pet_image_support。
调用图:调用 1 个内部函数(pet_image_support);被 2 处调用(open_pets_picker, select_pet_by_id)。
ChatWidget::pet_image_support174–187 ↗
fn pet_image_support(&self) -> crate::pets::PetImageSupport
作用:判断当前运行环境是否支持宠物图片显示。测试时它可以用假结果,正式运行时会真的检测终端能力。
数据流:进去不需要参数。测试环境里,如果设置了覆盖值,就返回这个假支持结果;部分测试路径会默认返回“不支持终端”。正式环境里,它调用 detect_pet_image_support 检测当前终端。出来是一个支持状态,里面可能包含不支持的原因。
调用关系:它被 ChatWidget::warn_if_pets_unsupported 调用。它把“怎么判断终端支持图片”这件事封装起来,让上层只关心能不能继续。
调用图:被 1 处调用(warn_if_pets_unsupported);外部调用 2 个(detect_pet_image_support, Unsupported)。
ChatWidget::set_tui_pet190–195 ↗
fn set_tui_pet(&mut self, pet: Option<String>)
作用:把聊天窗口内部配置里的宠物改成指定值,并立刻尝试加载这个宠物。它用于用户已经改变宠物设置后的界面刷新。
数据流:进去的是可选宠物编号:有值表示选某只宠物,空值表示没有选择。它更新配置副本,调用 load_ambient_pet 尝试创建当前宠物对象,再应用测试用的图片支持覆盖,最后请求重画。出来没有返回值,但配置、当前宠物和界面状态都会更新。
调用关系:它调用 load_ambient_pet 做实际加载,并调用 ChatWidget::apply_ambient_pet_image_support_override_for_tests 处理测试环境。它是配置变化进入 ChatWidget 的主要入口之一。
调用图:调用 2 个内部函数(apply_ambient_pet_image_support_override_for_tests, load_ambient_pet)。
ChatWidget::set_tui_pet_loaded197–206 ↗
fn set_tui_pet_loaded(
&mut self,
pet: Option<String>,
ambient_pet: Option<crate::pets::AmbientPet>,
)
作用:直接把“选中的宠物编号”和“已经加载好的宠物对象”放进聊天窗口。它适合后台加载完成后,或测试里已经准备好宠物时使用。
数据流:进去的是可选宠物编号和可选的已加载宠物。它把两者写入聊天窗口配置和状态,再应用测试用图片支持覆盖,最后请求界面重画。出来没有返回值,但界面会使用这份新宠物状态。
调用关系:它被 ChatWidget::install_test_ambient_pet_for_tests 调用来安装测试宠物。正式流程里类似的加载完成事件也可以用这种模式把后台结果接回界面状态。
调用图:调用 1 个内部函数(apply_ambient_pet_image_support_override_for_tests);被 1 处调用(install_test_ambient_pet_for_tests)。
ChatWidget::apply_ambient_pet_image_support_override_for_tests218–218 ↗
fn apply_ambient_pet_image_support_override_for_tests(&mut self)
作用:在测试里,把人为指定的图片支持结果套到当前宠物上。正式运行时它是空操作,不改变任何东西。
数据流:进去不需要参数。测试版本会读取测试覆盖值和当前宠物;两者都有时,就把支持状态写进宠物。正式版本进去后什么也不做。出来没有返回值,只可能在测试中改变宠物的支持状态。
调用关系:它被 ChatWidget::set_tui_pet、ChatWidget::set_tui_pet_loaded 和 ChatWidget::set_pet_image_support_for_tests 调用。它让测试不用依赖真实终端,就能模拟支持或不支持图片的情况。
调用图:被 3 处调用(set_pet_image_support_for_tests, set_tui_pet, set_tui_pet_loaded)。
ChatWidget::start_pet_picker_preview220–250 ↗
fn start_pet_picker_preview(&mut self, pet_id: String)
作用:开始为宠物选择器加载某只宠物的预览图。它用请求编号防止旧请求回来后覆盖新选择。
数据流:进去的是宠物编号。它先增加预览请求编号,清掉旧预览。如果编号表示“禁用宠物”,就把预览状态设成禁用并重画。否则把状态设为加载中,复制目录、重画请求器和事件发送器,然后在后台准备资源并加载宠物;完成后发送 AppEvent::PetPreviewLoaded,带请求编号和结果。出来没有直接返回值,但后台会稍后送回加载结果。
调用关系:它由 ChatWidget::open_pets_picker 启动初次预览,也可能在用户切换选择时使用。它把耗时工作交给 spawn_pet_load,完成后由 ChatWidget::finish_pet_picker_preview_load 接收结果。
调用图:调用 1 个内部函数(spawn_pet_load);被 1 处调用(open_pets_picker)。
ChatWidget::finish_pet_picker_preview_load252–278 ↗
fn finish_pet_picker_preview_load(
&mut self,
request_id: u64,
result: Result<crate::pets::AmbientPet, String>,
)
作用:接收宠物预览的后台加载结果,并把选择器预览区更新成成功或失败状态。它会忽略过期请求,避免用户快速切换时显示错宠物。
数据流:进去的是请求编号和加载结果。它先比较请求编号,不是当前最新请求就直接丢掉。是最新请求时,成功就把预览状态设为就绪并保存宠物;失败就保存错误消息并清空宠物。测试环境下还会把图片支持覆盖套到预览宠物上。最后请求界面重画。
调用关系:它和 ChatWidget::start_pet_picker_preview 配成一对:前者启动后台加载,后者接收 AppEvent::PetPreviewLoaded 后落地结果。它不负责加载,只负责确认结果是否还有效并更新界面。
ChatWidget::show_pet_selection_loading_popup280–297 ↗
fn show_pet_selection_loading_popup(&mut self) -> u64
作用:显示一个“正在加载所选宠物”的临时弹窗,并生成这次加载的编号。这样用户知道系统正在准备宠物,而不是界面没反应。
数据流:进去不需要参数。它增加宠物选择加载请求编号,清空预览状态和预览宠物,然后在底部面板显示一个选择视图样式的加载提示,里面只有一条不可选的“正在加载”项目。出来是这次请求编号,供之后关闭弹窗时核对。
调用关系:它会构造 SelectionViewParams 和 SelectionItem 来展示加载界面。后续 ChatWidget::finish_pet_selection_loading_popup 会用返回的编号确认是否关闭这个弹窗。
ChatWidget::finish_pet_selection_loading_popup299–306 ↗
fn finish_pet_selection_loading_popup(&mut self, request_id: u64) -> bool
作用:在宠物选择加载结束后关闭对应的加载弹窗。它会检查编号,防止关掉后来的新弹窗。
数据流:进去的是请求编号。它和当前保存的宠物选择加载编号比较;不一致就返回 false,表示没有关闭。编号一致时,它让底部面板关闭指定的加载视图,然后返回 true。出来是真假值,说明这次是否真的关闭了弹窗。
调用关系:它和 ChatWidget::show_pet_selection_loading_popup 配套使用。显示弹窗时拿到编号,加载完成后带着编号回来,确保只结束属于同一次操作的提示。
ChatWidget::set_pet_image_support_for_tests309–315 ↗
fn set_pet_image_support_for_tests(
&mut self,
support: crate::pets::PetImageSupport,
)
作用:测试专用:手动指定终端是否支持宠物图片。这样测试不用依赖真实终端软件的能力。
数据流:进去的是一个图片支持状态。它把这个状态保存为测试覆盖值,然后调用 ChatWidget::apply_ambient_pet_image_support_override_for_tests,把覆盖值同步到当前宠物。出来没有返回值,但后续支持检测和宠物显示会按测试指定结果走。
调用关系:它只在测试编译时存在。它调用 ChatWidget::apply_ambient_pet_image_support_override_for_tests,服务于那些需要模拟不同终端能力的测试。
调用图:调用 1 个内部函数(apply_ambient_pet_image_support_override_for_tests)。
ChatWidget::install_test_ambient_pet_for_tests318–326 ↗
fn install_test_ambient_pet_for_tests(&mut self, animations_enabled: bool)
作用:测试专用:给聊天窗口安装一只假的测试宠物。这样测试可以直接验证宠物显示和布局,不用准备真实宠物资源。
数据流:进去的是动画是否启用。它创建一个测试宠物,并调用 ChatWidget::set_tui_pet_loaded,把宠物编号设为 test,把宠物对象放进窗口。出来没有返回值,但聊天窗口会像已经加载了一只宠物一样。
调用关系:它只在测试中使用。它调用外部的 test_ambient_pet 创建假宠物,再通过 ChatWidget::set_tui_pet_loaded 走和正常已加载宠物类似的安装路径。
调用图:调用 1 个内部函数(set_tui_pet_loaded);外部调用 1 个(test_ambient_pet)。
spawn_pet_load329–335 ↗
fn spawn_pet_load(f: impl FnOnce() + Send + 'static)
作用:把宠物加载这类可能比较慢的工作放到后台执行,避免卡住终端界面。它会优先使用现有的 tokio 运行时;没有运行时时,就退回普通系统线程。
数据流:进去的是一个只执行一次的任务。它先尝试取得当前 tokio 运行时句柄;成功就用 spawn_blocking 开一个适合阻塞工作的后台任务,并丢弃任务句柄;失败就用 std::thread::spawn 开普通线程。出来没有返回值,但传入的任务会在后台运行。
调用关系:它被 start_configured_pet_load_if_needed 和 ChatWidget::start_pet_picker_preview 调用。它是宠物加载流程的后台执行器,把耗时的资源准备和加载从界面主流程里搬出去。
调用图:被 2 处调用(start_pet_picker_preview, start_configured_pet_load_if_needed);外部调用 3 个(drop, spawn, try_current)。
tui/src/chatwidget/mcp_startup.rs源码 ↗
MCP 可以理解成给应用外挂能力的服务。启动这些服务时,后端会一条条发来“某个服务器开始了、好了、失败了、取消了”的消息。这个文件的作用,就是把这些零散消息整理成聊天界面能看懂的状态:比如顶部显示“正在启动哪些服务器”,失败时弹出警告,全部结束后恢复正常界面。这里还有一个重要细节:后端消息可能会延迟或乱序。为了避免“上一轮启动已经结束,但迟到的旧消息又把界面拖回启动中”,它会在结束后短暂进入忽略模式,把疑似下一轮启动的消息先缓存起来,等确认是一整轮新的启动后再启用。它就像前台接待员,一边看启动名单,一边更新公告牌,还要分辨迟到的旧通知和真正的新通知。
ChatWidget::update_mcp_startup_status35–175 ↗
fn update_mcp_startup_status(
&mut self,
server: String,
status: McpStartupStatus,
complete_when_settled: bool,
)
作用:记录某个 MCP 服务器的一次启动状态变化,并决定界面该显示什么。它还会判断这一轮启动是不是已经全部结束,结束的话就触发收尾。
数据流:进去的是服务器名字、它的新状态,以及“状态稳定后是否自动结束”的开关。函数先判断当前是不是在防旧消息干扰的忽略期:如果是,就把消息放进下一轮候选缓存;如果不是,就把消息合并进当前启动状态。遇到失败会立刻发警告。之后它更新“任务正在运行”的状态、刷新顶部启动提示;如果所有预期服务器都不再是 Starting,就整理失败和取消的名单,调用收尾函数。出来的结果不是返回值,而是改动聊天界面的启动状态、警告、状态栏和重绘请求。
调用关系:它是 MCP 启动状态处理的核心入口之一,由 ChatWidget::on_mcp_server_status_updated 在收到后端通知后调用。它在确认一轮启动完成时,会把后续工作交给 ChatWidget::finish_mcp_startup;内部还会用系统格式化文字、判断状态、取走缓存数据等基础操作。
调用图:调用 1 个内部函数(finish_mcp_startup);被 1 处调用(on_mcp_server_status_updated);外部调用 4 个(new, format!, matches!, take)。
ChatWidget::set_mcp_startup_expected_servers177–182 ↗
fn set_mcp_startup_expected_servers(&mut self, server_names: I)
作用:告诉聊天界面:这一轮 MCP 启动预计会有哪些服务器参与。没有这份名单,界面就很难判断“是不是所有服务器都已经报到”。
数据流:进去的是一批服务器名字。函数把它们收集起来,存进聊天组件的“预期服务器名单”里。之后其他函数会用这份名单判断启动是否完整、是否可以结束。
调用关系:它通常在启动流程早期被调用,用来先登记名单。后面的 ChatWidget::update_mcp_startup_status 和 ChatWidget::finish_mcp_startup_after_lag 会参考这份名单,判断哪些服务器还没完成或需要算作取消。
调用图:外部调用 1 个(into_iter)。
ChatWidget::finish_mcp_startup184–211 ↗
fn finish_mcp_startup(&mut self, failed: Vec<String>, cancelled: Vec<String>)
作用:正式结束一轮 MCP 启动流程。它负责提示失败或取消的服务器,清掉启动状态,并让界面回到正常工作状态。
数据流:进去的是失败服务器名单和取消服务器名单。函数先根据名单生成用户能看懂的警告;然后检查当前状态栏是不是由 MCP 启动流程占用;接着清空当前启动状态,打开“暂时忽略旧更新”的保护,清掉下一轮缓存。最后它更新任务运行状态,必要时恢复原来的思考状态栏,尝试发送之前排队的用户输入,并要求界面重画。
调用关系:它是启动流程的收尾点。ChatWidget::update_mcp_startup_status 在发现所有服务器都稳定后会调用它;ChatWidget::finish_mcp_startup_after_lag 在等待太久后也会调用它。它会先用 ChatWidget::status_header_is_mcp_startup_owned 判断状态栏是否属于 MCP 启动提示,避免误删别的提示。
调用图:调用 1 个内部函数(status_header_is_mcp_startup_owned);被 2 处调用(finish_mcp_startup_after_lag, update_mcp_startup_status);外部调用 2 个(new, format!)。
ChatWidget::finish_mcp_startup_after_lag213–248 ↗
fn finish_mcp_startup_after_lag(&mut self)
作用:在启动消息等得太久、可能有延迟或缺失时,强行把这一轮 MCP 启动收尾。它把还没成功确认的服务器按取消处理,避免界面一直卡在“启动中”。
数据流:它不需要外部输入,读取当前已知状态和预期服务器名单。它把当前出现过的服务器、预期名单里的服务器合并成一份完整名单;成功的忽略,失败的放进失败名单,还在 Starting、Cancelled 或完全没消息的放进取消名单。最后去重、排序,并调用正常收尾函数。它会改变启动状态、警告和界面显示。
调用关系:它是处理“消息迟到或丢失”的备用收尾路线。它会把整理好的失败和取消名单交给 ChatWidget::finish_mcp_startup。它也会调整下一轮缓存的容忍规则,让后续只有终态消息的下一轮也有机会被识别出来。
调用图:调用 1 个内部函数(finish_mcp_startup);外部调用 1 个(new)。
ChatWidget::status_header_is_mcp_startup_owned250–260 ↗
fn status_header_is_mcp_startup_owned(&self) -> bool
作用:判断当前顶部状态栏是不是 MCP 启动流程写上去的。这样收尾时只会恢复自己改过的提示,不会误动别的状态信息。
数据流:进去的是聊天组件当前保存的状态栏文字。函数检查这段文字是不是以“Booting MCP server:”或“Starting MCP servers”开头。出来的是一个真假值:真表示这是 MCP 启动提示,假表示不是。
调用关系:它被 ChatWidget::finish_mcp_startup 调用,用在清理启动状态之前。这个小检查保护了界面上的其他状态提示,避免 MCP 收尾时把无关内容覆盖掉。
调用图:被 1 处调用(finish_mcp_startup)。
ChatWidget::on_mcp_server_status_updated262–281 ↗
fn on_mcp_server_status_updated(
&mut self,
notification: McpServerStatusUpdatedNotification,
)
作用:接收后端发来的 MCP 服务器状态通知,并把协议里的状态翻译成聊天界面内部使用的状态。它是外部通知进入这个文件逻辑的门口。
数据流:进去的是一条通知,里面有服务器名字、启动状态,以及可能存在的错误信息。函数把后端协议里的 Starting、Ready、Failed、Cancelled 转成内部的 McpStartupStatus;如果失败但后端没给错误文字,就补一条默认错误说明。最后把服务器名字和状态交给 ChatWidget::update_mcp_startup_status 继续处理。
调用关系:它位于后端通知和界面状态机之间。后端每次报告 MCP 服务器状态变化时,会走到这里;它不自己更新复杂界面,而是把翻译好的结果交给 ChatWidget::update_mcp_startup_status。
调用图:调用 1 个内部函数(update_mcp_startup_status)。
tui/src/chatwidget/replay.rs源码 ↗
这个文件专门处理“回放”。可以把它想成打开一本聊天记录本:程序不是重新聊天,而是把已经发生过的用户消息、助手回复、命令结果、文件改动、图片、搜索等内容重新画到界面里。它会尽量模拟正常聊天时的显示效果,但又很保守:只做适合重放的界面更新,不把历史事件当成新的实时事件。核心做法是先按一个个回合处理,再按回合里的一个个项目处理。遇到进行中的回合,会让界面进入“任务开始”的状态;遇到已完成、失败或中断的回合,会补一个“回合结束”的通知。每个具体项目会被分发给 ChatWidget 里已有的显示函数,比如显示用户消息、助手消息、命令执行结果、文件变化等。特别要注意的是,回放事件没有真实事件 id,用来和现场新来的事件区分开,避免界面误判。
ChatWidget::replay_thread_turns14–55 ↗
fn replay_thread_turns(&mut self, turns: Vec<Turn>, replay_kind: ReplayKind)
作用:这个函数按“回合”回放一段旧聊天记录,用来在恢复会话时先把聊天窗口填起来。它像翻聊天记录一样,一页一页把历史内容送回界面,但会避免把旧内容当成新事件来处理。
数据流:进去的是一组 Turn(一次对话回合,里面有多条消息或工具结果)和 replay_kind(说明这次回放是哪种来源)。函数先看每个回合是不是还在进行中,如果是,就清掉旧错误并通知界面“任务开始了”;然后把回合里的每个 ThreadItem 交给 ChatWidget::replay_thread_item;最后如果这个回合已经完成、失败或中断,就构造一个回合结束通知,交给界面已有的结束处理流程。出来没有直接返回值,但聊天界面的状态和显示内容会被补齐。
调用关系:它是这个文件的入口式函数:恢复旧会话时会从这里开始。它自己不细看每条消息怎么画,而是把每个项目交给 ChatWidget::replay_thread_item;回合结束时则调用已有的通知处理能力,让回放出来的历史和实时聊天看起来尽量一致。
调用图:调用 1 个内部函数(replay_thread_item);外部调用 2 个(new, matches!)。
ChatWidget::replay_thread_item57–64 ↗
fn replay_thread_item(
&mut self,
item: ThreadItem,
turn_id: String,
replay_kind: ReplayKind,
)
作用:这个函数回放单个聊天项目,比如一条用户消息、一段助手回复、一次命令结果。它的主要作用是给项目打上“这是回放来的”这个标记,再交给统一处理器。
数据流:进去的是一个 ThreadItem(聊天中的某个具体项目)、它所属的 turn_id(回合编号)和 replay_kind(回放类型)。函数把 replay_kind 包装成 ThreadItemRenderSource::Replay,表示来源是回放,然后调用 ChatWidget::handle_thread_item。出来没有返回值,但这个项目会被进一步显示到界面上,或者更新界面状态。
调用关系:它由 ChatWidget::replay_thread_turns 在遍历每个回合项目时调用。它本身很薄,像一个贴标签的小关卡:贴上“回放来源”后,就把真正的分类处理交给 ChatWidget::handle_thread_item。
调用图:调用 1 个内部函数(handle_thread_item);被 1 处调用(replay_thread_turns);外部调用 1 个(Replay)。
ChatWidget::handle_thread_item66–202 ↗
fn handle_thread_item(
&mut self,
item: ThreadItem,
turn_id: String,
render_source: ThreadItemRenderSource,
)
作用:这个函数是单个聊天项目的分拣台:看项目是什么类型,就调用对应的界面更新函数。它同时会判断这个项目是不是回放来的,从而决定哪些动作可以做、哪些要跳过。
数据流:进去的是一个 ThreadItem、所属回合编号 turn_id,以及 render_source(说明它来自实时流还是历史回放)。函数先判断是否为回放,并取出回放类型;接着按项目类型分支处理:用户消息会显示成已发送内容,助手消息会显示成文本回复,计划、推理、命令执行、文件变化、工具调用、网页搜索、图片生成、评审模式等都会走各自的界面更新函数。有些项目会被有意忽略,比如进行中的文件改动或某些提示项,因为回放它们可能造成误导或副作用。函数没有返回值,但会改变聊天窗口里的内容、状态提示、评审模式或重绘请求。
调用关系:它被 ChatWidget::replay_thread_item 调用,是回放单条项目时真正干活的地方。它不会重新实现所有显示细节,而是把不同项目转交给 ChatWidget 里已有的专门函数,例如处理助手消息、命令执行、文件变更、工具调用等;这样历史回放和实时显示能共用同一套界面表现。最后,如果这是线程快照回放且缺少回合编号,它会主动请求界面重画,确保用户能看到最新补上的内容。
调用图:调用 2 个内部函数(is_replay, replay_kind);被 1 处调用(replay_thread_item);外部调用 2 个(matches!, vec!)。
tui/src/collaboration_modes.rs源码 ↗
这个文件像一个“模式菜单筛选器”。系统里可能内置了很多协作模式,但不是每一种都适合出现在终端界面里,所以这里先把不可见的模式过滤掉,只留下 TUI 能展示的。之后,别的界面代码就不用自己到处判断了:想要默认模式,就调用这里;想进入计划模式,也调用这里;用户按快捷键想切到下一个模式,还是调用这里。这里的核心数据是 CollaborationModeMask,可以理解成“一套模式设置包”,里面说明当前是哪种模式,以及相关配置。一个值得注意的点是,这些函数都接收 ModelCatalog(模型目录,记录可用模型的信息),但当前过滤逻辑还没有真正用它;这给以后按模型能力筛模式留了位置。
filtered_presets7–12 ↗
fn filtered_presets(_model_catalog: &ModelCatalog) -> Vec<CollaborationModeMask>
作用:取出系统内置的协作模式预设,然后只保留适合在终端界面里给用户看的那些。这样后面的代码拿到的就是一份“干净菜单”,不会把隐藏模式暴露出来。
数据流:进去的是 ModelCatalog,但当前没有实际使用;函数会读取 builtin_collaboration_mode_presets 提供的内置模式列表;然后逐个检查每个模式是不是 TUI 可见;最后出来的是一个 CollaborationModeMask 列表,只包含终端界面允许显示的模式。
调用关系:它是这个文件里的基础筛选步骤。default_mask、mask_for_kind 和 next_mask 都先找它要一份可见模式列表,再分别做“选默认”“按名字找”“切下一个”的工作。它把真正读取内置预设的活儿交给 builtin_collaboration_mode_presets。
调用图:调用 1 个内部函数(builtin_collaboration_mode_presets);被 3 处调用(default_mask, mask_for_kind, next_mask)。
default_mask14–21 ↗
fn default_mask(model_catalog: &ModelCatalog) -> Option<CollaborationModeMask>
作用:给终端界面找一个启动时或需要兜底时使用的协作模式。它优先找明确标记为 Default 的模式;如果没有,就拿可见列表里的第一个,避免界面没有模式可用。
数据流:进去的是 ModelCatalog;它先通过 filtered_presets 得到 TUI 可见的模式列表;接着查找 mode 等于 Default 的那一项;如果找到了就复制返回,如果没找到但列表不空,就返回第一项;如果列表为空,就返回 None,表示没有可用模式。
调用关系:它通常在初始化协作模式时被使用,也会被一些模式切换和状态显示相关流程依赖。它自己不直接读内置预设,而是先交给 filtered_presets 统一过滤,保证默认值不会选到终端界面不该显示的模式。
调用图:调用 1 个内部函数(filtered_presets);被 6 处调用(initial_collaboration_mask, thread_settings_updated_preserves_default_settings_for_plan_mode, mode_switch_surfaces_model_change_notification_when_effective_model_changes, submit_user_message_with_mode_errors_when_mode_changes_during_running_turn, submit_user_message_with_mode_submits_when_plan_stream_is_not_active, status_line_model_with_reasoning_updates_on_mode_switch_without_manual_refresh)。
mask_for_kind23–33 ↗
fn mask_for_kind(
model_catalog: &ModelCatalog,
kind: ModeKind,
) -> Option<CollaborationModeMask>
作用:按指定的模式种类找对应的模式设置包。比如外部代码说“我要计划模式”,它就负责确认这个模式能不能在 TUI 里出现,并找到对应预设。
数据流:进去的是 ModelCatalog 和一个 ModeKind;它先问这个 ModeKind 是否 TUI 可见,如果不可见就立刻返回 None;如果可见,就通过 filtered_presets 拿到可见预设列表,再找 mode 正好等于传入 kind 的那一项;找到就返回对应 CollaborationModeMask,找不到就返回 None。
调用关系:它是很多具体模式入口的通用查找器。default_mode_mask 和 plan_mask 都只是把固定的模式种类交给它;界面里进入计划模式、提交消息时带模式、模式切换提示等流程也会用它来确保拿到的是合法且可显示的模式。它会调用 is_tui_visible 做第一道安全检查。
调用图:调用 2 个内部函数(is_tui_visible, filtered_presets);被 20 处调用(enter_submits_when_plan_stream_is_not_active, mode_switch_surfaces_model_change_notification_when_effective_model_changes, plan_completion_restores_status_indicator_after_streaming_plan_output, plan_implementation_popup_shows_after_new_plan_follows_steer, plan_implementation_popup_shows_after_proposed_plan_output, plan_implementation_popup_shows_once_when_replay_precedes_live_turn_complete, plan_implementation_popup_skips_replayed_turn_complete, plan_implementation_popup_skips_when_messages_queued, plan_implementation_popup_skips_when_rate_limit_prompt_pending, plan_implementation_popup_skips_when_steer_follows_proposed_plan (+10 more))。
next_mask36–50 ↗
fn next_mask(
model_catalog: &ModelCatalog,
current: Option<&CollaborationModeMask>,
) -> Option<CollaborationModeMask>
作用:在可见协作模式列表里切到下一个模式,用来支持用户循环切换。就像遥控器按“下一台”,到最后一个后会绕回第一个。
数据流:进去的是 ModelCatalog 和当前模式 current;它先通过 filtered_presets 得到可切换的模式列表;如果列表为空,返回 None;否则取出当前模式的 kind,在列表里找它的位置;找到了就返回后一项,最后一项会回到第一项;如果当前模式为空或不在列表里,就返回第一项。
调用关系:它主要服务于 cycle_collaboration_mode,也就是用户触发“切换协作模式”时的流程。它依赖 filtered_presets 保证循环范围只包含终端界面可见的模式,避免切到隐藏或不适合展示的状态。
调用图:调用 1 个内部函数(filtered_presets);被 1 处调用(cycle_collaboration_mode)。
default_mode_mask52–54 ↗
fn default_mode_mask(model_catalog: &ModelCatalog) -> Option<CollaborationModeMask>
作用:这是一个更直白的快捷入口:直接拿“默认模式”的设置包。调用方不用自己传 ModeKind::Default。
数据流:进去的是 ModelCatalog;它把 Default 这个固定模式种类交给 mask_for_kind;出来的是默认模式对应的 CollaborationModeMask,或者在默认模式不可见、找不到时返回 None。
调用关系:它是 mask_for_kind 的薄封装,主要让调用处更好读。比如打开计划实现提示、提交用户消息时设置编码协作模式、检查清空上下文条件等流程,会通过它明确表示“这里需要默认模式”。
调用图:调用 1 个内部函数(mask_for_kind);被 3 处调用(plan_implementation_clear_context_requires_default_mode_and_plan, submit_user_message_with_mode_sets_coding_collaboration_mode, open_plan_implementation_prompt)。
plan_mask56–58 ↗
fn plan_mask(model_catalog: &ModelCatalog) -> Option<CollaborationModeMask>
作用:这是获取“计划模式”设置包的快捷入口。计划模式通常用于先让系统思考和列计划,而不是马上改代码。
数据流:进去的是 ModelCatalog;它把 Plan 这个固定模式种类交给 mask_for_kind;出来的是计划模式对应的 CollaborationModeMask,或者在计划模式不可见、找不到时返回 None。
调用关系:它也是 mask_for_kind 的薄封装,服务于所有需要进入或判断计划模式的地方。比如打开计划推理范围提示、设置计划模式的推理力度、应用计划模式斜杠命令、判断是否显示计划模式提示等流程,都会用它来拿到统一的计划模式配置。
调用图:调用 1 个内部函数(mask_for_kind);被 16 处调用(open_plan_reasoning_scope_prompt, set_plan_mode_reasoning_effort, should_show_plan_mode_nudge, apply_plan_slash_command, interrupted_turn_restore_keeps_active_mode_for_resubmission, mode_switch_surfaces_reasoning_change_notification_when_model_stays_same, plan_mode_nudge_shows_only_for_eligible_default_mode_drafts, plan_mode_reasoning_override_is_marked_current_in_reasoning_popup, reasoning_selection_in_plan_mode_matching_plan_effort_but_different_global_opens_scope_prompt, reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_prompt_event (+6 more))。
tui/src/history_cell/session.rs源码 ↗
这个文件像终端界面的“开场白设计师”。如果没有它,用户进入会话时只会看到冷冰冰的对话区域,不容易知道自己正在用哪个模型、在哪个文件夹里工作、权限是不是很开放,也不知道有哪些快捷命令。它主要做三件事:先算出卡片在当前终端宽度下能有多宽;再把标题、模型、目录、权限等文字拼成一行行带样式的内容;最后给这些内容套上边框,变成一张清楚的卡片。它还会在第一次进入时显示入门命令,在后续事件里按配置显示小提示。如果用户开了“YOLO mode”(这里指无需确认、权限很开放的模式),它会醒目地标出来,避免用户不知道当前操作风险更高。
card_inner_width7–13 ↗
fn card_inner_width(width: u16, max_inner_width: usize) -> Option<usize>
作用:根据终端总宽度,算出卡片里面真正能放文字的宽度。它会留出左右边框和空白,避免文字贴着边或者超出屏幕。
数据流:输入是终端宽度和允许的最大内容宽度。它先判断宽度是否太窄,窄到连边框都放不下就返回空;否则从总宽度里扣掉边框空间,再和最大宽度取较小值,输出一个可用的内部宽度。
调用关系:SessionHeaderHistoryCell::display_lines 在真正画会话头卡片前会先问它“里面能写多宽”。它只负责算尺寸,不负责画内容。
调用图:被 1 处调用(display_lines);外部调用 1 个(min)。
with_border16–18 ↗
fn with_border(lines: Vec<Line<'static>>) -> Vec<Line<'static>>
作用:给一组文字行外面套上漂亮的圆角边框。调用者只要准备内容,不用自己算边框。
数据流:输入是一组要显示的行。它把这些行交给 with_border_internal,并告诉它不要强制指定宽度;内部会按最宽的一行来决定边框大小,最后输出带上边框的新行列表。
调用关系:SessionHeaderHistoryCell::display_lines 用它把标题、模型、目录等信息包成一张卡片。具体的边框计算工作交给 with_border_internal。
调用图:调用 1 个内部函数(with_border_internal);被 1 处调用(display_lines)。
with_border_with_inner_width25–30 ↗
fn with_border_with_inner_width(
lines: Vec<Line<'static>>,
inner_width: usize,
) -> Vec<Line<'static>>
作用:也是给文字套边框,但允许调用者指定卡片内部至少要有多宽。适合内容已经按某个宽度裁好时使用。
数据流:输入是一组文字行和指定的内部宽度。它把两者交给 with_border_internal;内部会保证边框内宽不小于指定值,也不小于实际内容宽度,输出带边框的行。
调用关系:它和 with_border 共用同一个底层实现 with_border_internal,只是多传了一个“至少这么宽”的要求。
调用图:调用 1 个内部函数(with_border_internal)。
with_border_internal32–72 ↗
fn with_border_internal(
lines: Vec<Line<'static>>,
forced_inner_width: Option<usize>,
) -> Vec<Line<'static>>
作用:真正负责画边框、补空格、对齐内容的核心函数。它保证每一行都一样宽,看起来像一张整齐的卡片。
数据流:输入是一组内容行,以及一个可选的强制内部宽度。它先逐行计算文字显示宽度,注意这里按 Unicode 字符宽度算,中文、emoji 等宽度不会简单按字节算;然后决定内容区宽度;接着生成顶部边框、每一行左右竖线和补齐空格、底部边框,最后输出完整卡片行。
调用关系:with_border 和 with_border_with_inner_width 都把活交给它。外层函数负责提供简单入口,它负责实际的排版细节。
调用图:被 2 处调用(with_border, with_border_with_inner_width);外部调用 4 个(from, from, with_capacity, vec!)。
padded_emoji77–79 ↗
fn padded_emoji(emoji: &str) -> String
作用:给 emoji 后面加一个很窄的空白,让 emoji 和后面的文字不要挤在一起。这个空白比普通空格更细,看起来更自然。
数据流:输入是一个 emoji 字符串。它在后面拼上 U+200A hair space,也就是一种很窄的空格,然后输出新的字符串;不修改别的状态。
调用关系:这是一个小工具函数,供需要在终端里显示 emoji 的地方使用,让视觉间距统一。
调用图:外部调用 1 个(format!)。
TooltipHistoryCell::new88–93 ↗
fn new(tip: String, cwd: &Path) -> Self
作用:创建一条“小提示”历史单元。它把提示文字和当前工作目录一起保存下来,方便后面按目录解析和显示提示内容。
数据流:输入是提示文字和当前目录路径。它把目录复制成自己的 PathBuf,连同提示文字放进 TooltipHistoryCell,输出这个新对象。
调用关系:new_session_info 在需要显示提示时会调用它。之后这个对象会通过 TooltipHistoryCell::display_lines 或 raw_lines 被渲染出来。
调用图:外部调用 1 个(to_path_buf)。
TooltipHistoryCell::display_lines97–112 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>
作用:把一条提示渲染成终端里可显示的多行文字,并加上缩进。提示内容按 Markdown(轻量标记,比如粗体)方式处理,所以能显示得更清楚。
数据流:输入是可用宽度,同时读取对象里的 tip 和 cwd。它先扣掉缩进宽度,得到实际换行宽度;再把“Tip: ...”这段文字交给 markdown 渲染函数,必要时按宽度换行;最后给每行加上两个空格缩进,输出显示用的行。
调用关系:当 new_session_info 把 TooltipHistoryCell 放进组合历史单元后,界面刷新时会调用这个函数来显示提示。它依赖 append_markdown 做文字格式化,依赖 prefix_lines 加缩进。
TooltipHistoryCell::raw_lines114–116 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:给提示生成不带终端样式的纯文本版本。适合复制记录、生成 transcript(会话文本记录)或其他不需要花哨显示的地方。
数据流:输入是对象自身保存的 tip。它直接拼成一行“Tip: ...”,输出一个只含这一行的列表;不做换行、缩进或样式处理。
调用关系:这是 HistoryCell 接口的一部分。显示界面用 display_lines,纯文本记录用 raw_lines。
调用图:外部调用 1 个(vec!)。
SessionInfoCell::display_lines123–125 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>
作用:把整个会话信息块渲染成终端显示行。它自己不重新排版,而是把工作交给内部的 CompositeHistoryCell。
数据流:输入是终端宽度。它把宽度传给内部组合单元 self.0,拿回已经排好的显示行并返回;自身不改数据。
调用关系:SessionInfoCell 是外面对会话信息块的统一入口。真正的内容可能包括头部卡片、入门帮助、提示、模型变更说明,都由内部 CompositeHistoryCell 组合处理。
SessionInfoCell::desired_height127–129 ↗
fn desired_height(&self, width: u16) -> u16
作用:询问整个会话信息块在某个宽度下大概要占多少行。界面布局可以用它提前安排空间。
数据流:输入是终端宽度。它把宽度传给内部 CompositeHistoryCell,返回内部算出的高度;不修改内容。
调用关系:这是 HistoryCell 接口的一部分。布局阶段会用它估算显示高度,实际计算由内部组合单元完成。
SessionInfoCell::transcript_lines131–133 ↗
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>
作用:生成适合写进会话记录的版本。它保留重要信息,但通常不像屏幕显示那样依赖样式和边框。
数据流:输入是宽度。它把请求转给内部 CompositeHistoryCell,返回组合后的 transcript 行;自身只是转发。
调用关系:当系统需要保存或展示会话文本记录时会走这个接口。SessionInfoCell 作为包装层,把任务交给内部各个历史单元。
SessionInfoCell::raw_lines135–137 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:生成整个会话信息块的纯文本行。用于不需要颜色、边框、终端样式的场景。
数据流:它读取内部 CompositeHistoryCell,取出所有组成部分的纯文本行并返回;不接收额外输入,也不改变状态。
调用关系:这是 HistoryCell 接口的纯文本出口。SessionInfoCell 继续扮演统一外壳,实际内容来自内部组合单元。
new_session_info140–217 ↗
fn new_session_info(
config: &Config,
requested_model: &str,
session: &ThreadSessionState,
is_first_event: bool,
tooltip_override: Option<String>,
auth_plan: Option<PlanType>,
作用:组装一整块“会话开头信息”。它决定要显示会话头卡片、首次使用帮助、小提示,还是模型被替换的说明。
数据流:输入包括配置、用户请求的模型名、真实会话状态、是否是第一条事件、可选提示覆盖、账户套餐、是否显示 fast 状态。它先创建 SessionHeaderHistoryCell,并根据权限判断是否标出 YOLO mode;如果是第一次事件,就加入入门命令列表;否则按配置加入提示,若请求模型和实际模型不同,还加入“模型 changed”的说明。最后把这些部分装进 CompositeHistoryCell,输出 SessionInfoCell。
调用关系:这是本文件的主要装配函数。上层会话流程在需要往历史记录顶部插入会话信息时调用它;它会调用 SessionHeaderHistoryCell::new 和 has_yolo_permissions,并可能创建 TooltipHistoryCell。
调用图:调用 2 个内部函数(new, has_yolo_permissions);外部调用 2 个(new, vec!)。
is_yolo_mode219–224 ↗
fn is_yolo_mode(config: &Config) -> bool
作用:判断当前配置是不是处在“YOLO mode”。这里的意思是审批策略不再询问用户,而且权限范围很大。
数据流:输入是 Config。它从配置里取出审批策略和实际权限配置,把审批策略转换成 AskForApproval,再交给 has_yolo_permissions 判断,最后输出 true 或 false。
调用关系:这是给其他地方快速判断风险模式的小入口。真正的规则集中在 has_yolo_permissions,避免多处重复写判断。
调用图:调用 2 个内部函数(from, has_yolo_permissions)。
has_yolo_permissions226–239 ↗
fn has_yolo_permissions(
approval_policy: AskForApproval,
permission_profile: &PermissionProfile,
) -> bool
作用:判断一组权限条件是否等同于“YOLO mode”。这个模式风险更高,所以界面要能明确提醒用户。
数据流:输入是审批策略和权限档案。它检查两件事:审批策略必须是 Never,也就是不再询问;权限档案要么完全禁用沙箱限制,要么是文件系统不受限且网络沙箱开启的特定组合。满足就输出 true,否则输出 false。
调用关系:new_session_info 用它决定会话头是否显示 YOLO mode;is_yolo_mode 也用它对完整配置做同样判断。
调用图:被 2 处调用(is_yolo_mode, new_session_info);外部调用 1 个(matches!)。
SessionHeaderHistoryCell::new252–267 ↗
fn new(
model: String,
reasoning_effort: Option<ReasoningEffortConfig>,
show_fast_status: bool,
directory: PathBuf,
version: &'static str,
) -> Self
作用:用默认文字样式创建会话头卡片对象。调用者只要提供模型、推理强度、目录和版本号即可。
数据流:输入是模型名、可选推理强度、是否显示 fast、目录路径和版本号。它补上默认样式,然后调用 SessionHeaderHistoryCell::new_with_style 创建对象并返回。
调用关系:new_session_info 用它创建正常会话头。测试和其他 UI 辅助代码也会用它构造头部卡片;如果需要自定义模型文字样式,则改用 new_with_style。
调用图:被 5 处调用(clear_ui_header_lines_with_version, new_session_info, session_header_hides_fast_status_when_disabled, session_header_includes_reasoning_level_when_present, session_header_indicates_yolo_mode);外部调用 2 个(new_with_style, default)。
SessionHeaderHistoryCell::new_with_style269–286 ↗
fn new_with_style(
model: String,
model_style: Style,
reasoning_effort: Option<ReasoningEffortConfig>,
show_fast_status: bool,
directory: PathBuf,
versi
作用:创建会话头卡片对象,并允许指定模型文字的显示样式。适合测试、占位显示或需要强调模型名的场景。
数据流:输入是模型名、模型样式、推理强度、fast 状态、目录和版本号。它把这些值存进结构体,并把 yolo_mode 初始设为 false,输出新的 SessionHeaderHistoryCell。
调用关系:SessionHeaderHistoryCell::new 会通过它完成真正构造。调用图里 placeholder_session_header_cell 也会直接调用它,以便设置特殊样式。
调用图:被 1 处调用(placeholder_session_header_cell)。
SessionHeaderHistoryCell::with_yolo_mode288–291 ↗
fn with_yolo_mode(mut self, yolo_mode: bool) -> Self
作用:给已经创建好的会话头卡片设置是否显示 YOLO mode。它用链式写法,方便创建后立刻补上权限状态。
数据流:输入是会话头对象本身和一个布尔值。它把对象里的 yolo_mode 改成这个值,然后把修改后的对象返回。
调用关系:new_session_info 创建 header 后会接着调用它,把 has_yolo_permissions 的判断结果写进去。之后 display_lines 和 raw_lines 会根据这个标记决定是否显示权限提醒。
SessionHeaderHistoryCell::format_directory293–295 ↗
fn format_directory(&self, max_width: Option<usize>) -> String
作用:把会话目录格式化成适合显示的字符串。比如能用“~”表示家目录,并在太长时裁短。
数据流:输入是可选最大宽度,同时读取对象里的 directory。它把目录和宽度交给 format_directory_inner,拿回处理好的路径字符串并返回。
调用关系:SessionHeaderHistoryCell::display_lines 在画目录那一行时调用它。真正的路径缩短规则放在 format_directory_inner。
调用图:被 1 处调用(display_lines);外部调用 1 个(format_directory_inner)。
SessionHeaderHistoryCell::format_directory_inner297–318 ↗
fn format_directory_inner(directory: &Path, max_width: Option<usize>) -> String
作用:执行目录显示的具体规则:能相对家目录显示就用“~”,太长就从中间裁掉,保证卡片不被撑破。
数据流:输入是目录路径和可选最大宽度。它先尝试把路径改成相对用户家目录的形式,比如“~/project”;如果不行就用完整路径。若给了最大宽度且字符串显示宽度超出限制,它会调用 center_truncate_path 从中间缩短;宽度为 0 时直接返回空字符串。输出最终用于界面显示的路径。
调用关系:format_directory 会调用它;测试用例也会直接检查它在长路径、长目录名时是否裁剪正确。
调用图:调用 1 个内部函数(center_truncate_path);被 2 处调用(session_header_directory_center_truncates, session_header_directory_front_truncates_long_segment);外部调用 4 个(display, new, width, format!)。
SessionHeaderHistoryCell::reasoning_label320–324 ↗
fn reasoning_label(&self) -> Option<&str>
作用:取出当前模型的推理强度标签,比如某种 reasoning effort 的文字形式。没有配置时就返回空。
数据流:它读取对象里的 reasoning_effort。如果有值,就调用其 as_str 得到可显示文本;如果没有,就返回 None。对象本身不变。
调用关系:SessionHeaderHistoryCell::display_lines 用它决定模型名后面要不要追加推理强度;raw_lines 也用同样的概念生成纯文本。
调用图:被 1 处调用(display_lines)。
SessionHeaderHistoryCell::display_lines328–401 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>
作用:把会话头真正画成终端里的卡片。它会显示 Codex 版本、模型、推理强度、fast 状态、工作目录,以及必要时的 YOLO mode 提醒。
数据流:输入是终端宽度,同时读取对象里保存的版本、模型、样式、目录和权限标记。它先用 card_inner_width 判断能不能画卡片;太窄就返回空。然后组装标题行、模型行和目录行,目录会按剩余宽度裁剪;如果是 YOLO mode,再加一行权限提醒。最后调用 with_border 给这些行套上边框,输出屏幕显示行。
调用关系:这是会话头在界面里出现时的关键渲染函数。SessionInfoCell 最终会通过内部组合单元调用到它;它会使用 card_inner_width、format_directory、reasoning_label 和 with_border 这些小零件完成排版。
调用图:调用 4 个内部函数(format_directory, reasoning_label, card_inner_width, with_border);外部调用 7 个(from, styled, magenta, width, new, format!, vec!)。
SessionHeaderHistoryCell::raw_lines403–422 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:生成会话头的纯文本版本,不带边框和颜色。这样日志、复制内容或 transcript 里也能保留关键信息。
数据流:它读取版本、模型、推理强度、目录和 yolo_mode。先生成 Codex 版本、模型、目录三行;如果 yolo_mode 为真,再追加“permissions: YOLO mode”。最后输出这些纯文本行。
调用关系:这是 HistoryCell 接口的纯文本出口。和 display_lines 相比,它不做终端排版,只保留用户需要知道的会话配置。
tui/src/bottom_pane/status_surface_preview.rs源码 ↗
状态栏里会显示很多小块信息,比如应用名、当前目录、Git 分支、模型名、剩余额度等。问题是:用户在设置状态栏时,有些信息可能暂时拿不到;如果直接空着,预览就很难懂。这个文件就像给展厅里的样机贴上示例标签:先为每个可显示项目准备一个占位值,也就是“假的但像真的”的文字;等运行时有真实值,再用真实值覆盖。它还特别处理了用量限制这类信息,因为同一个位置可能代表 5 小时、每日、每周等不同限制,需要根据真实文字判断该叫它什么、怎么解释。最后,它能把选中的项目和值交给状态栏绘制函数,生成真正能显示在终端界面里的预览行。
StatusSurfacePreviewItem::placeholder40–70 ↗
fn placeholder(self) -> &'static str
作用:给某一种状态栏项目提供默认示例文字。比如没有真实 Git 分支时,就用“feat/awesome-feature”这种看起来合理的文字撑起预览。
数据流:输入是一个状态栏项目类型,比如项目名、当前目录、模型名 → 它按项目种类查一张固定对照表 → 输出一段静态示例文本,不改动任何外部数据。
调用关系:它是预览数据的底层素材来源。StatusSurfacePreviewData::default 会遍历所有项目,并用这里给出的文字填满初始预览。
StatusSurfacePreviewItem::iter72–103 ↗
fn iter() -> impl Iterator<Item = Self>
作用:按固定顺序列出所有可以出现在状态栏预览里的项目。这样别的代码不用自己记有哪些项目,也不容易漏掉。
数据流:不需要输入 → 它返回一个迭代器,也就是可以一个个取出项目的列表 → 调用者拿到完整项目清单,用来初始化或生成配置界面。
调用关系:StatusSurfacePreviewData::default 用它给每个项目放入占位文字;status_surface_preview_data 这类测试或辅助代码也会用它确认项目集合。
调用图:被 2 处调用(default, status_surface_preview_data)。
StatusSurfacePreviewData::default118–126 ↗
fn default() -> Self
作用:创建一份完整的状态栏预览数据,里面每个项目都先放好占位文字。这样预览界面一打开就不会空荡荡。
数据流:不需要输入 → 它先建一个空的 BTreeMap(按键排序的字典),再通过 StatusSurfacePreviewItem::iter 拿到全部项目,并逐个写入占位值 → 输出一份填满默认示例文字的 StatusSurfacePreviewData。
调用关系:它会调用 StatusSurfacePreviewItem::iter 和内部的 set_placeholder。renders_title_setup_popup 打开标题设置弹窗时会用它,让用户马上看到可读的预览。
调用图:调用 1 个内部函数(iter);被 1 处调用(renders_title_setup_popup);外部调用 1 个(new)。
StatusSurfacePreviewData::from_iter130–140 ↗
fn from_iter(values: I) -> Self
作用:用一批真实运行值创建预览数据,同时保留缺失项的占位文字。适合测试或实际界面把“当前知道的信息”塞进预览里。
数据流:输入是一组“项目 → 文字”的配对 → 它先调用默认构造,得到全套占位值,再把输入里的项目改成真实值 → 输出一份真假混合的预览数据:有真实值就显示真实值,没有就显示占位值。
调用关系:它建立在 StatusSurfacePreviewData::default 之上。preview_includes_thread_title、preview_uses_runtime_values、setup_view_snapshot_uses_runtime_preview_values 等测试会用它检查预览是否正确使用真实值和占位值。
调用图:被 5 处调用(preview_includes_thread_title, preview_uses_placeholders_when_runtime_values_are_missing, preview_uses_runtime_values, setup_view_snapshot_uses_runtime_preview_values, status_surface_preview_data);外部调用 1 个(default)。
StatusSurfacePreviewData::set_live142–153 ↗
fn set_live(&mut self, item: StatusSurfacePreviewItem, value: V)
作用:把某个状态栏项目设置成真实运行值。真实值比占位值更重要,之后显示和判断时会优先把它当成真的信息。
数据流:输入是一个项目和一段可转成字符串的值 → 它把值转成 String,并标记为不是占位值 → 写入内部字典,覆盖原来的同项目内容。
调用关系:StatusSurfacePreviewData::from_iter 会用它把传进来的运行时数据盖到默认预览上。其他需要更新预览真实内容的地方也可以直接调用它。
调用图:外部调用 1 个(into)。
StatusSurfacePreviewData::set_placeholder155–173 ↗
fn set_placeholder(&mut self, item: StatusSurfacePreviewItem, value: V)
作用:给某个项目设置占位文字,但不会覆盖已经存在的真实值。这样示例文字不会不小心把真正的数据挤掉。
数据流:输入是一个项目和一段占位文字 → 它先查看内部字典,如果该项目已有真实值就直接停止 → 如果没有真实值,就写入这段文字并标记为占位值。
调用关系:StatusSurfacePreviewData::default 用它批量填充初始示例数据。它和 set_live 配合,形成“先有假数据,后有真数据,真数据优先”的规则。
调用图:外部调用 1 个(into)。
StatusSurfacePreviewData::suppress_placeholder175–183 ↗
fn suppress_placeholder(&mut self, item: StatusSurfacePreviewItem)
作用:在某些项目只有占位值时,把这个占位值隐藏掉。也就是说,有些信息如果没有真实值,宁可不显示,也不要显示假文字。
数据流:输入是一个项目 → 它查看这个项目当前是不是占位值 → 如果是,就从内部字典删除;如果已经是真实值或不存在,就什么也不改。
调用关系:它给调用方一个细调预览的开关。比如某些状态栏块在没有真实数据时应该省略,而不是拿示例文字冒充。
StatusSurfacePreviewData::rate_limit_item_name185–194 ↗
fn rate_limit_item_name(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String
作用:给“用量限制”类状态栏项目算出一个更准确的名字。因为真实文字可能表示主用量、5 小时限制、每日限制、每周限制等不同东西。
数据流:输入是一个项目和备用名字 → 它只读取该项目的真实值,不看占位值;再把真实值交给 rate_limit_preview_copy 判断属于哪种限制 → 如果识别出来就输出对应名字,否则输出备用名字。
调用关系:它会调用 live_value_for 取得真实值。status_line_select_item 和 title_select_item 在展示可选项目名称时会用它,让用量限制项的名字跟当前真实含义一致。
调用图:调用 1 个内部函数(live_value_for);被 2 处调用(status_line_select_item, title_select_item)。
StatusSurfacePreviewData::rate_limit_item_description196–205 ↗
fn rate_limit_item_description(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String
作用:给“用量限制”类项目生成更准确的说明文字。它帮助设置界面解释这个项目到底是在显示哪一种剩余额度。
数据流:输入是一个项目和备用说明 → 它读取该项目的真实值,并尝试用 rate_limit_preview_copy 识别限制类型 → 识别成功就输出对应说明,失败就输出备用说明。
调用关系:它会调用 live_value_for。status_line_select_item 和 title_select_item 在展示项目说明时会用它,避免用户看到含糊或错误的解释。
调用图:调用 1 个内部函数(live_value_for);被 2 处调用(status_line_select_item, title_select_item)。
StatusSurfacePreviewData::value_for207–209 ↗
fn value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str>
作用:取出某个项目当前应该显示的文字,不管它是真实值还是占位值。适合真正拼预览状态栏时使用。
数据流:输入是一个项目 → 它到内部字典里查这个项目 → 找到就输出文字引用,找不到就输出空结果;不会修改数据。
调用关系:StatusSurfacePreviewData::status_line_for_items 会用它拿每个选中项目的显示文字。它是预览渲染读取数据的主要入口。
StatusSurfacePreviewData::live_value_for211–216 ↗
fn live_value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str>
作用:只取真实运行值,故意忽略占位值。这样需要根据真实信息做判断的地方,不会被示例文字误导。
数据流:输入是一个项目 → 它查内部字典,并检查该值是否不是占位值 → 如果是真实值就输出文字引用,否则输出空结果;不改动数据。
调用关系:rate_limit_item_name 和 rate_limit_item_description 会调用它。这样用量限制的名称和说明只根据真实运行数据变化,不会因为默认示例文字而误判。
调用图:被 2 处调用(rate_limit_item_description, rate_limit_item_name)。
StatusSurfacePreviewData::status_line_for_items218–231 ↗
fn status_line_for_items(
&self,
items: I,
use_theme_colors: bool,
) -> Option<Line<'static>>
作用:把一组选中的状态栏项目变成一行真正可显示的预览文本。它是“数据”走向“界面显示”的最后一步之一。
数据流:输入是一批 StatusLineItem(状态栏配置项)和是否使用主题颜色的开关 → 它逐个找到对应的预览文字,丢掉没有值的项目,再把“项目 + 文字”交给 status_line_from_segments → 输出一条 ratatui 的 Line,也就是终端界面可以画出来的一行文本;如果没有可显示内容则可能为空。
调用关系:它会使用 value_for 读取预览文字,并把整理好的片段交给外部的 status_line_from_segments 负责真正拼装和上色。它位于预览数据和 TUI 终端显示之间。
调用图:外部调用 2 个(into_iter, status_line_from_segments)。
rate_limit_preview_copy239–279 ↗
fn rate_limit_preview_copy(value: &str) -> Option<RateLimitPreviewCopy>
作用:根据一段真实用量限制文字,判断它代表哪一种限制,并给出对应的展示名称和说明。比如开头是“weekly ”,就认作每周限制。
数据流:输入是一段字符串 → 它先去掉开头空白,再检查文字是否以 secondary usage、usage、5h、daily、weekly、monthly、annual 等前缀开头 → 匹配成功就输出一组固定的名称和说明,匹配失败就输出空结果。
调用关系:rate_limit_item_name 和 rate_limit_item_description 会通过它把原始状态文字翻译成用户能看懂的项目名和说明。它不碰界面,只做这一个小判断。
tui/src/public_widgets/mod.rs源码 ↗
这个文件很小,只有一行,但它的作用类似书的章节目录。pub(crate) mod composer_input; 的意思是:在当前代码包内部公开一个子模块,名字叫 composer_input。模块可以理解成代码文件夹里的一个小房间,用来把相关代码放在一起。这里的 pub(crate) 表示“只给本项目内部使用”,不会暴露给外部依赖。没有它,其他地方就不能通过这个模块入口正常引用 composer_input 里的输入框组件代码,项目结构也会变得散乱。
终端运行时与所有权
这些文件为 TUI 建立终端能力、运行时 shell 行为、挂起/恢复处理、通知,以及底层终端所有权机制。
tui/src/resize_reflow_cap.rs源码 ↗
终端就像一个会滚动的聊天窗口,它能保存的旧行数是有限的。程序在窗口尺寸变化时,需要把已有内容按新宽度重新排版,这叫 resize reflow(窗口缩放后的重新换行)。如果不限制行数,可能会重算成千上万行,用户却看不到这些历史内容,白白拖慢交互。这个文件就是给不同终端定一个“最多回头整理多少行”的上限。它先看用户配置:用户可以选择自动、完全不限制,或者指定一个数字。自动模式下,它会识别当前终端,比如 VS Code、Windows Terminal、WezTerm、Alacritty,并使用这些终端常见的滚动历史大小;认不出来时就用保守的默认值。特别的是,VS Code 里运行的 shell 有时会伪装成别的终端,所以这里还单独探测“是不是在 VS Code 终端里”。
resize_reflow_max_rows29–35 ↗
fn resize_reflow_max_rows(config: TerminalResizeReflowConfig) -> Option<usize>
作用:这是外部最常用的入口,用来根据当前配置和真实运行环境算出“最多重排多少行”。如果用户关掉了限制,它会返回没有上限。
数据流:进去的是用户的窗口重排配置;它会读取当前终端信息,还会检查程序是不是跑在 VS Code 终端里;然后把这些信息交给内部函数判断;出来的是一个数字上限,或者 None,表示不限制行数。
调用关系:它站在最外层,把环境探测这一步做好,再调用 resize_reflow_max_rows_for 做真正判断。调用图里显示它作为同名流程的一部分被使用;它还依赖外部的 terminal_info 和 running_in_vscode_terminal 来拿到现场情况。
调用图:调用 2 个内部函数(resize_reflow_max_rows_for, running_in_vscode_terminal);被 1 处调用(resize_reflow_max_rows);外部调用 1 个(terminal_info)。
resize_reflow_max_rows_for37–50 ↗
fn resize_reflow_max_rows_for(
config: TerminalResizeReflowConfig,
terminal: &TerminalInfo,
running_in_vscode_terminal: bool,
) -> Option<usize>
作用:这个函数把“用户怎么配置”和“终端是什么”合在一起,得出最终行数上限。它是可测试的核心判断函数,因为调用者可以直接传入假终端信息。
数据流:进去的是配置、终端资料,以及是否在 VS Code 终端里的布尔值;它先看配置是自动、禁用还是指定数字;自动时继续算默认值,禁用时返回 None,指定数字时原样返回这个数字。
调用关系:resize_reflow_max_rows 会把真实环境交给它。它自己在自动模式下把活儿交给 auto_resize_reflow_max_rows;测试也围绕它验证“手动配置优先”和“禁用限制”的行为。
调用图:调用 1 个内部函数(auto_resize_reflow_max_rows);被 1 处调用(resize_reflow_max_rows)。
auto_resize_reflow_max_rows52–76 ↗
fn auto_resize_reflow_max_rows(
terminal_name: TerminalName,
running_in_vscode_terminal: bool,
) -> usize
作用:这个函数专门处理自动模式:根据终端名字选一个合适的行数上限。它像一张“不同终端对应不同容量”的对照表。
数据流:进去的是终端名称,以及是否明确检测到 VS Code 终端;如果是 VS Code 环境,就直接用 VS Code 的较小上限;否则按终端名称匹配不同数字;认不出的终端返回保守的默认上限。
调用关系:它只被 resize_reflow_max_rows_for 在自动配置时调用。几个测试直接检查它的对照表是否正确,尤其是 VS Code 探测会优先于普通终端名字。
调用图:被 1 处调用(resize_reflow_max_rows_for)。
tests::test_terminal83–91 ↗
fn test_terminal(name: TerminalName) -> TerminalInfo
作用:这是测试用的小帮手,用一个终端名字快速造出一份假的终端信息。这样测试不用每次手写一大堆无关字段。
数据流:进去的是一个终端名称;它把其他字段都填成空值;出来的是一份 TerminalInfo 测试对象,用来模拟某种终端。
调用关系:它服务于测试函数 configured_resize_reflow_max_rows_overrides_auto_detection 和 disabled_resize_reflow_max_rows_keeps_all_rows,让这些测试只关注配置判断,而不用关心真实机器上的终端环境。
tests::auto_resize_reflow_max_rows_uses_terminal_defaults94–122 ↗
fn auto_resize_reflow_max_rows_uses_terminal_defaults()
作用:这个测试确认自动模式下,不同终端会拿到各自预期的默认行数。它防止有人以后改坏这张终端对照表。
数据流:进去的是一组写死的终端名称和期望数字;测试逐个调用自动计算函数;出来的是断言结果:实际数字必须等于期望数字,否则测试失败。
调用关系:它直接检查 auto_resize_reflow_max_rows 的主要分支,用 assert_eq! 对比结果。它覆盖了已知终端和未知终端,保证 fallback(兜底默认值)也正常。
调用图:外部调用 1 个(assert_eq!)。
tests::auto_resize_reflow_max_rows_prefers_vscode_probe125–133 ↗
fn auto_resize_reflow_max_rows_prefers_vscode_probe()
作用:这个测试确认只要探测到 VS Code 终端,就优先按 VS Code 算,而不是按终端名字算。这样可以处理 VS Code 里终端名字不准的情况。
数据流:进去的是一个看起来像 Windows Terminal 的名字,以及“确实在 VS Code 终端里”的标记;函数计算后必须输出 VS Code 的上限;如果不是这个数字,测试失败。
调用关系:它直接测试 auto_resize_reflow_max_rows 里最靠前的 VS Code 特判,用 assert_eq! 确认这个特判不会被后面的终端名称匹配覆盖。
调用图:外部调用 1 个(assert_eq!)。
tests::configured_resize_reflow_max_rows_overrides_auto_detection136–148 ↗
fn configured_resize_reflow_max_rows_overrides_auto_detection()
作用:这个测试确认用户手动写的行数会压过自动识别结果。也就是说,用户说 42 行,就必须是 42 行。
数据流:进去的是一个假的 VS Code 终端和一个 max_rows = 42 的配置;函数不会采用 VS Code 默认值,而是直接返回 Some(42);断言检查这个结果。
调用关系:它调用 test_terminal 造测试终端,再调用 resize_reflow_max_rows_for 检查配置优先级,最后用 assert_eq! 验证结果。它保护的是“用户配置最大”的规则。
调用图:外部调用 3 个(assert_eq!, Limit, test_terminal)。
tests::disabled_resize_reflow_max_rows_keeps_all_rows151–163 ↗
fn disabled_resize_reflow_max_rows_keeps_all_rows()
作用:这个测试确认用户如果明确关闭行数限制,程序就不会再裁剪重排行数。返回 None 在这里表示“没有上限”。
数据流:进去的是一个假的 VS Code 终端和 Disabled 配置;函数看到禁用设置后返回 None;断言确认结果不是某个数字上限。
调用关系:它调用 test_terminal 生成终端信息,再调用 resize_reflow_max_rows_for。它验证禁用配置不会被自动终端识别误改成某个默认数字。
调用图:外部调用 2 个(assert_eq!, test_terminal)。
tests::unknown_terminal_uses_fallback_even_under_multiplexer166–182 ↗
fn unknown_terminal_uses_fallback_even_under_multiplexer()
作用:这个测试确认即使程序跑在 tmux 这类终端复用器里,只要真正终端识别不出来,也还是用保守的默认上限。终端复用器可以理解成一个把多个终端会话装在一起的外壳。
数据流:进去的是一份手写的未知终端信息,里面带有 xterm 字样和 tmux 信息,再加默认配置;函数计算后应该返回 fallback 默认行数;断言检查这个值。
调用关系:它直接调用 resize_reflow_max_rows_for,使用默认配置路径。它补上一个容易出错的场景:不要因为看到 tmux 或 TERM 字符串就误判成某个具体终端。
调用图:外部调用 2 个(assert_eq!, default)。
tui/src/tui.rs源码 ↗
终端界面不像普通窗口,有很多容易踩坑的状态:比如原始模式(按键立刻交给程序,不等回车)、备用屏幕(像全屏临时画布)、粘贴模式、焦点事件、窗口大小变化等。这个文件就是统一管这些状态的地方。启动时它检查输入输出真的是终端,打开需要的终端能力,探测光标位置和键盘能力;运行时它提供事件流,让上层知道用户按键、粘贴、 resize 和需要重画;绘制时它会调整“内嵌视口”,把聊天历史安全写到滚动区,把当前界面画在正确位置;退出、崩溃或临时运行外部程序时,它会尽量恢复终端,避免把用户的 shell 搞成看不见输入、光标异常或屏幕残留。可以把它理解成剧场舞台经理:开场布置灯光和舞台,演出中调度演员和幕布,散场后负责把场地恢复原样。
running_in_vscode_terminal76–78 ↗
fn running_in_vscode_terminal() -> bool
作用:判断当前程序是不是跑在 VS Code 自带的终端里。上层会用这个信息来决定某些终端尺寸或重排策略,因为 VS Code 终端有自己的行为差异。
数据流:它不接收参数,只是把问题转交给键盘模式相关模块去检测当前终端环境;检测结果是一个布尔值,表示“是”或“不是”。
调用关系:它是一个小包装函数,调用同名的底层检测逻辑;调用方如 resize_reflow_max_rows 需要知道终端环境时,会来问它。
调用图:调用 1 个内部函数(running_in_vscode_terminal);被 1 处调用(resize_reflow_max_rows)。
should_emit_notification80–85 ↗
fn should_emit_notification(condition: NotificationCondition, terminal_focused: bool) -> bool
作用:决定现在该不该弹桌面通知。比如设置为“只在终端不聚焦时通知”,那用户正看着终端时就不打扰。
数据流:输入是通知条件和终端当前是否聚焦;它按规则判断:Always 永远通知,Unfocused 只有不聚焦才通知;输出一个布尔值。
调用关系:Tui::notify 在真正发通知前先调用它,相当于门卫先判断这条通知有没有资格出去。
调用图:被 1 处调用(notify)。
Tui::drop88–92 ↗
fn drop(&mut self)
作用:当 Tui 对象被销毁时,顺手清掉终端里的宠物背景图。这样退出或释放界面时不会留下图像残影。
数据流:它读取当前 Tui 里的宠物图像状态,调用清理函数;如果失败,只记一条调试日志,不阻止销毁流程。
调用关系:这是 Rust 的 Drop 清理钩子,会在 Tui 生命周期结束时自动触发;它把具体清理交给 Tui::clear_ambient_pet_image。
调用图:调用 1 个内部函数(clear_ambient_pet_image);外部调用 1 个(debug!)。
tests::unfocused_notification_condition_is_suppressed_when_focused108–113 ↗
fn unfocused_notification_condition_is_suppressed_when_focused()
作用:测试“只在不聚焦时通知”的规则:如果终端正被用户看着,就不应该发通知。
数据流:它把条件设为 Unfocused,并告诉函数终端已聚焦;期望输出是 false。
调用关系:这是 should_emit_notification 的单元测试之一,用来防止以后改代码时把通知规则改坏。
调用图:外部调用 1 个(assert!)。
tests::always_notification_condition_emits_when_focused116–121 ↗
fn always_notification_condition_emits_when_focused()
作用:测试“总是通知”的规则:即使终端正聚焦,也应该允许通知。
数据流:它把条件设为 Always,并告诉函数终端已聚焦;期望输出是 true。
调用关系:这是 should_emit_notification 的单元测试之一,覆盖 Always 这条分支。
调用图:外部调用 1 个(assert!)。
tests::unfocused_notification_condition_emits_when_unfocused124–129 ↗
fn unfocused_notification_condition_emits_when_unfocused()
作用:测试终端不聚焦时,“只在不聚焦时通知”确实会发通知。
数据流:它把条件设为 Unfocused,并告诉函数终端未聚焦;期望输出是 true。
调用关系:这是 should_emit_notification 的单元测试之一,和前两个测试一起锁住通知判断的完整行为。
调用图:外部调用 1 个(assert!)。
tests::first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty132–170 ↗
fn first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty()
作用:测试第一次设置视口时,会清掉新界面区域里的旧字符,但保留视口上方原本的 shell 内容。
数据流:它创建一个假的 VT100 终端,先写入几行模拟旧内容,再调用 clear_for_viewport_change;最后读取屏幕,确认上方 shell 行还在、视口里的 stale 字样被清掉。
调用关系:这个测试专门保护 clear_for_viewport_change 的边界行为,避免程序启动时旧终端文字透过空白区域露出来。
调用图:调用 2 个内部函数(new, clear_for_viewport_change);外部调用 4 个(with_options_and_cursor_position, new, assert!, write!)。
set_modes173–189 ↗
fn set_modes() -> Result<()>
作用:把终端切到适合 TUI 使用的模式。没有它,程序可能收不到细粒度按键、粘贴内容可能混乱,界面也难以稳定绘制。
数据流:它先确保终端支持 ANSI 控制码,然后打开括号粘贴、原始模式、键盘增强和焦点变化事件;成功时终端进入可交互界面状态,失败时返回错误。
调用关系:init 启动界面时会调用它;Tui::with_restored 临时运行外部程序回来后也会重新调用它,把终端切回 Codex 需要的状态。
调用图:调用 2 个内部函数(ensure_virtual_terminal_processing, enable_keyboard_enhancement);被 2 处调用(with_restored, init);外部调用 2 个(execute!, enable_raw_mode)。
EnableAlternateScroll::write_ansi195–197 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:写出开启“备用屏幕滚轮转方向键”的 ANSI 控制码。ANSI 控制码可以理解成发给终端的隐藏指令。
数据流:输入是一个可写文本缓冲;它往里面写入转义序列 \x1b[?1007h;结果是终端之后可能把鼠标滚轮当成上下键。
调用关系:Tui::enter_alt_screen 进入备用屏幕时会通过 crossterm 执行这个命令,让全屏界面里的滚动体验更像应用。
调用图:外部调用 1 个(write!)。
EnableAlternateScroll::execute_winapi200–204 ↗
fn execute_winapi(&self) -> Result<()>
作用:在 Windows 原生 API 路径下拒绝执行这个命令,明确要求用 ANSI 方式。这样可以避免走错实现。
数据流:它不真正改终端,而是直接返回一个错误,说明这个命令不该通过 WinAPI 执行。
调用关系:这是 crossterm Command 接口在 Windows 上需要的实现;正常路径会使用 ANSI,因为 is_ansi_code_supported 声明支持。
调用图:外部调用 1 个(other)。
EnableAlternateScroll::is_ansi_code_supported207–209 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉 crossterm:这个命令可以用 ANSI 控制码执行。
数据流:没有输入状态要处理,固定返回 true。
调用关系:它配合 EnableAlternateScroll::write_ansi 使用,让 Windows 上也走 ANSI 路径。
DisableAlternateScroll::write_ansi216–218 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:写出关闭“备用屏幕滚轮转方向键”的 ANSI 控制码。离开备用屏幕后要关掉,避免影响普通 shell。
数据流:输入是一个可写文本缓冲;它写入转义序列 \x1b[?1007l;结果是终端恢复普通滚轮行为。
调用关系:Tui::leave_alt_screen 离开备用屏幕时会执行它,和 EnableAlternateScroll::write_ansi 成对出现。
调用图:外部调用 1 个(write!)。
DisableAlternateScroll::execute_winapi221–225 ↗
fn execute_winapi(&self) -> Result<()>
作用:在 Windows 原生 API 路径下拒绝执行关闭命令,要求仍然用 ANSI 方式。
数据流:它不修改终端,直接返回一个说明用法错误的错误对象。
调用关系:这是 crossterm Command 接口的一部分,用来保证 DisableAlternateScroll 也按 ANSI 路径执行。
调用图:外部调用 1 个(other)。
DisableAlternateScroll::is_ansi_code_supported228–230 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉 crossterm:关闭备用滚动这个命令支持 ANSI 控制码。
数据流:不读取外部信息,固定返回 true。
调用关系:它让 DisableAlternateScroll::write_ansi 能在需要时被正常采用。
restore_common245–276 ↗
fn restore_common(
raw_mode_restore: RawModeRestore,
keyboard_restore: KeyboardRestore,
) -> Result<()>
作用:执行恢复终端状态的公共步骤。它负责关掉粘贴模式、焦点事件、键盘增强、必要时关闭原始模式,并恢复光标。
数据流:输入是两个恢复策略:原始模式要不要关、键盘增强要怎么恢复;它逐项执行恢复动作,记录第一个错误;最后返回成功或第一个错误。
调用关系:restore、restore_keep_raw 和 restore_after_exit 都复用它,只是传入的策略不同;它内部会调用 ensure_virtual_terminal_processing、键盘恢复函数和 disable_raw_mode。
调用图:调用 3 个内部函数(ensure_virtual_terminal_processing, reset_keyboard_reporting_after_exit, restore_keyboard_enhancement_stack);被 3 处调用(restore, restore_after_exit, restore_keep_raw);外部调用 3 个(execute!, matches!, disable_raw_mode)。
restore280–282 ↗
fn restore() -> Result<()>
作用:把终端完整恢复到正常 shell 可用的状态。通常在临时让出终端或普通恢复时使用。
数据流:它没有额外输入,调用 restore_common,要求关闭原始模式并按栈恢复键盘增强;输出是恢复是否成功。
调用关系:RestoreMode::restore 选择 Full 模式时会调用它;它是 set_modes 的常规反向操作。
调用图:调用 1 个内部函数(restore_common);被 1 处调用(restore)。
reapply_raw_mode_after_resume291–294 ↗
fn reapply_raw_mode_after_resume() -> Result<()>
作用:Unix 上从 Ctrl-Z 挂起后恢复程序时,重新同步原始模式。这样可以修复 shell 和程序同时改终端状态造成的错位。
数据流:它先让 crossterm 认为原始模式已关闭,再重新打开;成功后终端回到程序需要的按键输入状态。
调用关系:它服务于 Unix 作业控制恢复流程,和挂起/继续运行相关的模块配合使用。
调用图:外部调用 2 个(disable_raw_mode, enable_raw_mode)。
restore_after_exit300–311 ↗
fn restore_after_exit() -> Result<()>
作用:程序退出时使用的更强恢复流程。它比普通 restore 更用力地重置键盘报告,并收尾 stderr 保护,尽量保证父 shell 正常。
数据流:它调用 restore_common 做终端恢复,再调用 terminal_stderr::finish 收尾标准错误输出保护;如果某步失败,返回第一个错误。
调用关系:set_panic_hook 在程序崩溃前会调用它;正常退出路径也可用它做最终清理。
调用图:调用 2 个内部函数(restore_common, finish);被 2 处调用(restore, restore)。
restore_keep_raw314–316 ↗
fn restore_keep_raw() -> Result<()>
作用:恢复大部分终端设置,但保留原始模式。适合某些外部流程仍然希望按键立刻送达的场景。
数据流:它调用 restore_common,要求保留 raw mode,同时按栈恢复键盘增强;输出恢复结果。
调用关系:RestoreMode::restore 选择 KeepRaw 时会调用它,Tui::with_restored 通过 RestoreMode 间接使用。
调用图:调用 1 个内部函数(restore_common);被 1 处调用(restore)。
RestoreMode::restore326–331 ↗
fn restore(self) -> Result<()>
作用:根据枚举值选择具体恢复方式。枚举就是一组固定选项,这里选“完全恢复”或“保留原始模式”。
数据流:输入是 RestoreMode 自身;如果是 Full 就调用 restore,如果是 KeepRaw 就调用 restore_keep_raw;输出底层恢复结果。
调用关系:Tui::with_restored 在运行外部交互程序前调用它,决定该把终端让出去到什么程度。
调用图:调用 2 个内部函数(restore, restore_keep_raw);被 1 处调用(with_restored)。
flush_terminal_input_buffer371–371 ↗
fn flush_terminal_input_buffer()
作用:清空终端底层已经排队但还没被程序处理的输入。比如暂停事件监听期间用户按了键,回来后不希望这些旧按键突然生效。
数据流:它不接收业务参数;在 Unix 上用 tcflush 清 stdin 队列,在 Windows 上用控制台 API 清输入缓冲,其他平台可能什么也不做;失败只记录警告。
调用关系:init 启动后调用它,Tui::with_restored 从外部程序回来后也调用它,避免陈旧输入污染当前界面。
调用图:被 2 处调用(with_restored, init);外部调用 3 个(last_os_error, tcflush, warn!)。
init374–463 ↗
fn init() -> Result<InitializedTerminal>
作用:初始化整个终端界面运行环境。它是从普通命令行进入 TUI 世界的入口准备步骤。
数据流:它先确认 stdin/stdout 真的是终端,然后 set_modes、清输入、安装崩溃恢复钩子;接着探测光标位置、默认颜色和键盘增强支持,创建终端后端并安装 stderr 保护;最后返回 InitializedTerminal。
调用关系:run_ratatui_app 会在启动时调用它;它把 set_modes、flush_terminal_input_buffer、平台探测和 CustomTerminal 创建串成一条启动流水线。
调用图:调用 9 个内部函数(startup, cursor_position_with_crossterm, detect_keyboard_enhancement_supported, flush_terminal_input_buffer, keyboard_enhancement_disabled, probe_windows_default_colors, set_modes, set_panic_hook, install);被 1 处调用(run_ratatui_app);外部调用 9 个(new, with_options_and_cursor_position, other, set_default_colors_from_startup_probe, stdin, stdout, now, info!, warn!)。
cursor_position_with_crossterm466–471 ↗
fn cursor_position_with_crossterm(backend: &mut CrosstermBackend<Stdout>) -> Position
作用:在非 Unix 平台上读取启动时的光标位置。读不到时就退回左上角,保证程序还能继续。
数据流:输入是 crossterm 后端;它调用后端读取光标位置,成功返回真实位置,失败记录警告并返回 Position { x: 0, y: 0 }。
调用关系:init 在非 Unix 平台用它补齐创建 CustomTerminal 所需的初始光标位置。
调用图:被 1 处调用(init);外部调用 1 个(get_cursor_position)。
detect_keyboard_enhancement_supported474–478 ↗
fn detect_keyboard_enhancement_supported() -> bool
作用:在非 Unix 平台上检测终端是否支持键盘增强。键盘增强能区分带修饰键的 Enter 等按键。
数据流:它调用 crossterm 的检测函数;如果检测失败,就保守地返回 false。
调用关系:init 用它设置 enhanced_keys_supported,之后 Tui::enhanced_keys_supported 会把这个能力告诉上层。
调用图:被 1 处调用(init);外部调用 1 个(supports_keyboard_enhancement)。
probe_windows_default_colors481–500 ↗
fn probe_windows_default_colors()
作用:Windows 上探测终端默认前景色和背景色。这样界面配色能更贴近用户当前终端主题。
数据流:它记录开始时间,调用默认颜色探测;成功就把颜色写入终端调色板缓存,失败则记录警告并写入 None。
调用关系:init 在 Windows 平台调用它;它和 terminal_palette 模块配合,为后续绘制提供颜色基准。
调用图:调用 1 个内部函数(default_colors);被 1 处调用(init);外部调用 4 个(set_default_colors_from_startup_probe, now, info!, warn!)。
set_panic_hook502–508 ↗
clear_for_viewport_change556–566 ↗
fn clear_for_viewport_change(terminal: &mut CustomTerminal<B>, new_area: Rect) -> Result<()>
作用:当界面视口位置变化时,清掉可能露出来的旧字符。它特别处理首次视口为空的情况。
数据流:输入是终端和新的视口区域;如果旧视口为空,就从新视口起点清理,否则从旧视口起点清理;最后调用终端的 clear_after_position。
调用关系:Tui::draw 调整视口时会用它;测试 first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty 专门验证它的首次清理行为。
调用图:被 1 处调用(first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty);外部调用 2 个(clear_after_position, as_position)。
Tui::new569–602 ↗
fn new(
terminal: Terminal,
enhanced_keys_supported: bool,
stderr_guard: terminal_stderr::TerminalStderrGuard,
) -> Self
作用:创建一个 Tui 主对象,把绘制请求、事件中介、终端、宠物图像状态、通知和平台状态都装配好。
数据流:输入是已经初始化好的终端、键盘增强支持标记和 stderr 保护;它创建广播通道、FrameRequester、EventBroker,缓存颜色能力,检测是否在 Zellij 中,最后返回可运行的 Tui。
调用关系:init 返回 InitializedTerminal 后,上层通常会用这些材料调用 Tui::new;之后所有绘制、事件和通知都围绕这个对象展开。
调用图:调用 4 个内部函数(detect_backend, new, new, new);外部调用 10 个(new, new, channel, terminal_info, default, default, default_colors, on_cached, default, vec!)。
Tui::set_alt_screen_enabled605–607 ↗
fn set_alt_screen_enabled(&mut self, enabled: bool)
作用:开关备用屏幕功能。关闭后,进入备用屏幕的调用会变成什么也不做。
数据流:输入是 enabled 布尔值;它把这个值存进 Tui,影响之后 enter_alt_screen 和 leave_alt_screen 的行为。
调用关系:上层如果不希望使用全屏临时画布,可以调用它;Tui::enter_alt_screen 和 Tui::leave_alt_screen 会读取这个设置。
Tui::set_notification_settings609–616 ↗
fn set_notification_settings(
&mut self,
method: NotificationMethod,
condition: NotificationCondition,
)
作用:设置桌面通知的方式和触发条件。比如用系统通知,且只在终端不聚焦时提醒。
数据流:输入是通知方法和通知条件;它根据方法重新检测可用通知后端,并保存条件;之后 notify 会按这些设置工作。
调用关系:配置变化或启动配置应用时会调用它;它把 detect_backend 的结果保存给 Tui::notify 使用。
调用图:调用 1 个内部函数(detect_backend)。
Tui::frame_requester618–620 ↗
fn frame_requester(&self) -> FrameRequester
作用:拿到一个可以请求重画的工具。其他代码不需要直接碰绘制通道,只要用它安排下一帧。
数据流:它读取 Tui 内部的 FrameRequester 并克隆一份返回;不改变终端状态。
调用关系:insert_history_hyperlink_lines_with_wrap_policy 在新增历史内容后调用它来安排重画。
调用图:被 1 处调用(insert_history_hyperlink_lines_with_wrap_policy);外部调用 1 个(clone)。
Tui::enhanced_keys_supported622–624 ↗
fn enhanced_keys_supported(&self) -> bool
作用:告诉外部当前终端是否支持键盘增强。上层可以据此决定是否启用某些快捷键体验。
数据流:它读取 Tui 创建时保存的 enhanced_keys_supported 布尔值并返回。
调用关系:这个值来自 init 的平台探测;需要了解键盘能力的 UI 逻辑会调用它。
Tui::is_alt_screen_active626–628 ↗
fn is_alt_screen_active(&self) -> bool
作用:查询当前是否正在使用备用屏幕。备用屏幕就像临时全屏画布,离开后原来的 shell 内容会回来。
数据流:它从原子布尔值读取状态并返回;原子布尔值可以安全地被不同任务共享读取。
调用关系:Tui::with_restored 会先问它,如果外部程序运行前正在备用屏幕里,就先退出备用屏幕。
调用图:被 1 处调用(with_restored)。
Tui::pause_events631–633 ↗
fn pause_events(&mut self)
作用:暂停终端事件读取,避免和外部程序抢 stdin。stdin 是标准输入,可以理解成键盘输入管道。
数据流:它调用 event_broker 暂停事件流;之后 TUI 暂时不再读按键事件。
调用关系:Tui::with_restored 在运行外部交互程序前调用它,为外部程序让出键盘输入。
调用图:被 1 处调用(with_restored)。
Tui::resume_events637–639 ↗
fn resume_events(&mut self)
作用:恢复终端事件读取,让 TUI 重新接收按键、粘贴和窗口事件。
数据流:它调用 event_broker 恢复事件流;之后事件流继续产生 TuiEvent。
调用关系:Tui::with_restored 在外部程序结束、终端模式恢复后调用它,和 pause_events 成对使用。
调用图:被 1 处调用(with_restored)。
Tui::with_restored646–684 ↗
async fn with_restored(&mut self, mode: RestoreMode, f: F) -> R
作用:临时把终端还给一个外部交互程序使用,等它结束后再把 TUI 状态恢复回来。比如打开编辑器、运行 shell 命令时需要这样做。
数据流:输入是恢复模式和一个异步任务 f;它先暂停事件、必要时离开备用屏幕、恢复终端和 stderr,然后运行 f;结束后重新压制 stderr、set_modes、清输入、恢复备用屏幕和事件,最后返回 f 的结果。
调用关系:这是 TUI 和外部程序之间的“借用终端”流程;它会调用 pause_events、RestoreMode::restore、leave_alt_screen、set_modes、flush_terminal_input_buffer、enter_alt_screen 和 resume_events。
调用图:调用 10 个内部函数(restore, enter_alt_screen, is_alt_screen_active, leave_alt_screen, pause_events, resume_events, flush_terminal_input_buffer, set_modes, pause, resume);外部调用 1 个(warn!)。
Tui::notify688–712 ↗
fn notify(&mut self, message: impl AsRef<str>) -> bool
作用:尝试发送一条桌面通知。它会尊重用户设置,比如终端聚焦时不通知。
数据流:输入是一段消息;它读取终端聚焦状态和通知条件,先用 should_emit_notification 判断;如果允许,就交给通知后端发送;失败会记录警告并禁用后续通知。
调用关系:上层有需要提醒用户的事件时调用它;它把规则判断交给 should_emit_notification,把实际发送交给 notification_backend。
调用图:调用 1 个内部函数(should_emit_notification);外部调用 2 个(as_ref, warn!)。
Tui::event_stream714–730 ↗
fn event_stream(&self) -> Pin<Box<dyn Stream<Item = TuiEvent> + Send + 'static>>
作用:创建 TUI 的事件流。事件流就是一条持续产出按键、粘贴、窗口变化和重画请求的管道。
数据流:它读取事件中介、绘制广播订阅、聚焦状态,以及 Unix 上的挂起上下文和备用屏幕状态;然后创建 TuiEventStream 并装进 Pin<Box<dyn Stream>> 返回。
调用关系:主循环会消费这个流;它把 EventBroker 和 FrameRequester 产生的信号统一包装成 TuiEvent。
Tui::enter_alt_screen734–753 ↗
fn enter_alt_screen(&mut self) -> Result<()>
作用:进入备用屏幕,并把绘制视口扩展到整个终端。适合显示临时全屏界面。
数据流:它先检查备用屏幕是否启用;启用时向终端发送进入备用屏幕和开启备用滚动的命令,保存原视口,设置全屏视口并清屏,最后标记 alt_screen_active 为 true。
调用关系:Tui::with_restored 在外部程序结束后可能重新调用它;其他需要全屏覆盖界面的地方也会用它。
调用图:调用 3 个内部函数(clear, set_viewport_area, size);被 1 处调用(with_restored);外部调用 2 个(execute!, new)。
Tui::leave_alt_screen756–768 ↗
fn leave_alt_screen(&mut self) -> Result<()>
作用:离开备用屏幕,恢复之前保存的内嵌视口。这样用户回到普通滚动历史所在的界面。
数据流:它先检查备用屏幕是否启用;启用时关闭备用滚动、发送离开备用屏幕命令,如果保存过视口就恢复,最后把 alt_screen_active 标为 false。
调用关系:Tui::with_restored 在运行外部程序前会调用它,避免外部程序和备用屏幕互相干扰。
调用图:调用 1 个内部函数(set_viewport_area);被 1 处调用(with_restored);外部调用 1 个(execute!)。
Tui::insert_history_lines770–772 ↗
fn insert_history_lines(&mut self, lines: Vec<Line<'static>>)
作用:把普通文本行加入待写入的历史区。历史区是当前界面上方的滚动内容。
数据流:输入是一组 ratatui 的 Line;它默认使用预先换行策略,然后转交给 insert_history_lines_with_wrap_policy。
调用关系:这是给常规调用者用的简化入口;真正合并和排队工作在后续函数里完成。
调用图:调用 1 个内部函数(insert_history_lines_with_wrap_policy)。
Tui::insert_history_lines_with_wrap_policy774–783 ↗
fn insert_history_lines_with_wrap_policy(
&mut self,
lines: Vec<Line<'static>>,
wrap_policy: HistoryLineWrapPolicy,
)
作用:把普通文本行按指定换行策略加入历史区。换行策略决定长行是程序先切好,还是交给终端自己换。
数据流:输入是文本行和换行策略;它先把普通行转成支持超链接的行,再交给 insert_history_hyperlink_lines_with_wrap_policy。
调用关系:Tui::insert_history_lines 会调用它;它是普通文本到超链接历史行之间的转换层。
调用图:调用 2 个内部函数(plain_hyperlink_lines, insert_history_hyperlink_lines_with_wrap_policy);被 1 处调用(insert_history_lines)。
Tui::insert_history_hyperlink_lines_with_wrap_policy785–802 ↗
fn insert_history_hyperlink_lines_with_wrap_policy(
&mut self,
lines: Vec<HyperlinkLine>,
wrap_policy: HistoryLineWrapPolicy,
)
作用:把带超链接能力的历史行放进待写队列,并安排下一次重画。这样不会马上打断当前绘制流程。
数据流:输入是超链接行和换行策略;如果队尾已有同策略批次,就合并进去,否则新建批次;然后通过 frame_requester 安排重画。
调用关系:insert_history_lines_with_wrap_policy 会调用它;稍后 Tui::draw 或 Tui::draw_with_resize_reflow 会通过 flush_pending_history_lines 真正写入终端。
调用图:调用 1 个内部函数(frame_requester);被 1 处调用(insert_history_lines_with_wrap_policy)。
Tui::clear_pending_history_lines804–806 ↗
fn clear_pending_history_lines(&mut self)
作用:清空还没写入终端的历史行队列。适合在状态重置或不再需要这些内容时使用。
数据流:它不接收参数,直接清空 pending_history_lines;已经写入终端的内容不受影响。
调用关系:它是历史写入缓冲区的手动清理口,和插入历史行的几个函数相对应。
Tui::update_inline_viewport_for_resize_reflow813–849 ↗
fn update_inline_viewport_for_resize_reflow(
terminal: &mut Terminal,
height: u16,
) -> Result<bool>
作用:在启用 resize reflow(窗口变大变小时重新排版历史)的路径里,更新内嵌视口的位置和大小。
数据流:输入是终端和期望高度;它读取当前终端大小、旧屏幕大小和旧视口,计算新视口,必要时滚动上方区域、清理变化区域,并返回是否需要整块重画。
调用关系:Tui::draw_with_resize_reflow 会调用它;它替代旧 draw 路径里的视口猜测逻辑,让重排功能自己重建历史。
调用图:调用 4 个内部函数(backend_mut, clear_after_position, set_viewport_area, size);外部调用 1 个(new)。
Tui::flush_pending_history_lines852–876 ↗
fn flush_pending_history_lines(
terminal: &mut Terminal,
pending_history_lines: &mut Vec<PendingHistoryLines>,
is_zellij: bool,
) -> Result<()>
作用:把排队的历史行真正写到当前视口上方,并清空队列。
数据流:输入是终端、待写历史批次和是否在 Zellij 中;它逐批选择写入模式,Zellij 且终端换行时用特殊 raw 模式,否则用标准模式;写完后清空队列。
调用关系:Tui::draw 和 Tui::draw_with_resize_reflow 在绘制当前界面前调用它,确保历史先落到滚动区。
调用图:调用 1 个内部函数(insert_history_hyperlink_lines_with_mode_and_wrap_policy)。
Tui::draw878–951 ↗
fn draw(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut custom_terminal::Frame),
) -> Result<()>
作用:按传统路径绘制一帧界面。它不仅调用上层画内容,还负责调整视口、写入历史、处理挂起恢复和同步刷新。
数据流:输入是界面高度和一个绘制回调;它先准备 Unix 恢复动作、计算可能的新视口、确保终端控制码可用;在同步更新里应用恢复、调整视口、清理旧区域、写入历史、更新挂起光标位置,最后调用 terminal.draw 执行回调。
调用关系:主循环收到 Draw 或 Resize 事件后会调用它;它会使用 pending_viewport_area、clear_for_viewport_change 和 flush_pending_history_lines 等内部零件。
调用图:调用 3 个内部函数(pending_viewport_area, ensure_virtual_terminal_processing, prepare_resume_action);外部调用 1 个(stdout)。
Tui::draw_ambient_pet_image953–970 ↗
fn draw_ambient_pet_image(
&mut self,
request: Option<crate::pets::AmbientPetDraw>,
) -> std::result::Result<(), crate::pets::PetImageRenderError>
作用:绘制或更新常驻宠物图片。这里的宠物图像是在终端里额外渲染的装饰内容。
数据流:输入是可选的宠物绘制请求;它先确保终端支持控制码,然后在同步更新中调用 render_ambient_pet_image;终端错误直接返回,资源错误也按宠物错误返回。
调用关系:需要显示 ambient pet 时由上层调用;它和 clear_ambient_pet_image 使用同一份 ambient_pet_image_state。
调用图:调用 1 个内部函数(ensure_virtual_terminal_processing);外部调用 2 个(stdout, Terminal)。
Tui::draw_pet_picker_preview_image972–993 ↗
fn draw_pet_picker_preview_image(
&mut self,
request: Option<crate::pets::AmbientPetDraw>,
) -> std::result::Result<(), crate::pets::PetImageRenderError>
作用:绘制宠物选择器里的预览图。它和常驻宠物图分开保存状态,避免两处图像互相覆盖。
数据流:输入是可选的预览绘制请求;它确认终端控制能力后,在同步更新里调用 render_pet_picker_preview_image,并把终端错误或资源错误返回给调用者。
调用关系:宠物选择界面需要预览时调用它;它使用 pet_picker_preview_image_state,而不是 ambient_pet_image_state。
调用图:调用 1 个内部函数(ensure_virtual_terminal_processing);外部调用 2 个(stdout, Terminal)。
Tui::clear_ambient_pet_image995–1007 ↗
fn clear_ambient_pet_image(
&mut self,
) -> std::result::Result<(), crate::pets::PetImageRenderError>
作用:清除终端里的常驻宠物图片。退出或不再显示宠物时用它避免残留。
数据流:它先确认终端控制码可用,然后用 None 请求调用 render_ambient_pet_image;结果是图像状态被更新,屏幕上的宠物被清掉或返回错误。
调用关系:Tui::drop 会自动调用它;也可以在运行中由需要隐藏宠物的逻辑调用。
调用图:调用 3 个内部函数(backend_mut, render_ambient_pet_image, ensure_virtual_terminal_processing);被 1 处调用(drop);外部调用 1 个(Terminal)。
Tui::draw_with_resize_reflow1014–1065 ↗
fn draw_with_resize_reflow(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut custom_terminal::Frame),
) -> Result<()>
作用:按“窗口变化后重新排版历史”的新路径绘制一帧。它避免旧路径里根据光标位置猜视口的做法。
数据流:输入是界面高度和绘制回调;它准备挂起恢复,确保控制码可用,在同步更新里更新 resize reflow 视口、写入历史,必要时让视口整块失效重画,更新挂起光标位置,最后调用绘制回调。
调用关系:启用 resize reflow 功能时主循环会用它代替 draw;它核心依赖 update_inline_viewport_for_resize_reflow 和 flush_pending_history_lines。
调用图:调用 2 个内部函数(ensure_virtual_terminal_processing, prepare_resume_action);外部调用 1 个(stdout)。
Tui::pending_viewport_area1067–1087 ↗
fn pending_viewport_area(&mut self) -> Result<Option<Rect>>
作用:在传统绘制路径里,尝试根据终端 resize 后的光标移动推测新视口位置。
数据流:它读取当前屏幕大小、上次屏幕大小、当前光标和上次光标;如果屏幕变了且光标 y 坐标也变了,就按差值偏移旧视口并返回;否则返回 None。
调用关系:Tui::draw 在进入同步绘制前调用它,避免绘制时和事件读取抢光标查询。resize reflow 路径故意不使用它。
调用图:被 1 处调用(draw)。
ensure_virtual_terminal_processing1134–1136 ↗
fn ensure_virtual_terminal_processing() -> Result<()>
作用:确保终端能理解 ANSI 控制码。Windows 上需要显式打开这个能力;非 Windows 上通常默认可用。
数据流:在 Windows 上,它读取 stdout 和 stderr 的控制台模式,打开处理输出和虚拟终端处理标志;非 Windows 版本直接返回成功。
调用关系:set_modes、restore_common、draw、宠物绘制和清理等很多地方都会先调用它,因为这些操作都要向终端发送隐藏控制指令。
调用图:被 7 处调用(clear_ambient_pet_image, draw, draw_ambient_pet_image, draw_pet_picker_preview_image, draw_with_resize_reflow, restore_common, set_modes)。
tui/src/custom_terminal.rs源码 ↗
这个文件像一个“舞台管理员”。界面代码先把想显示的内容画到一块内存里的画布上,也就是 Buffer(缓冲区,先暂存画面,不直接写屏幕)。Terminal 会保存“上一帧”和“这一帧”,用 diff_buffers 找出哪些格子变了,再由 draw 把最少的终端控制命令写出去。这样屏幕不会整页乱刷,速度也更稳。Frame 是给界面代码用的临时画板,它还记录这一帧结束后光标要不要显示、放在哪里、用什么形状。文件里还特别处理了两类麻烦:一是中文等宽字符会占两个格子,二是 OSC 转义序列(终端控制文本,比如超链接)看起来有字符但不占屏幕宽度。清屏、清滚动历史、窗口大小变化、恢复光标形状这些细节也都在这里兜底,保证程序退出或切换页面时终端尽量恢复正常。
display_width57–80 ↗
fn display_width(s: &str) -> usize
作用:计算一段终端文字真正占几个屏幕格子。它会忽略 OSC 转义序列,也就是给终端看的控制信息,避免把超链接里的网址当成可见文字来算宽度。
数据流:输入一段字符串 → 如果里面没有转义符,就直接按 Unicode 宽度计算;如果有,就把 ESC ] 到 BEL 之间的 OSC 控制片段跳过,只留下真正会显示的字符 → 输出可见宽度数字,不改动外部状态。
调用关系:diff_buffers 在判断每个格子、中文宽字符和超链接宽度时会调用它。它是后续“只重画该重画部分”的基础尺子。
调用图:被 1 处调用(diff_buffers);外部调用 1 个(with_capacity)。
Frame::area107–109 ↗
fn area(&self) -> Rect
作用:告诉绘图代码这一帧能画的区域有多大。界面布局要靠它知道边界,避免画到窗口外面。
数据流:读取 Frame 里保存的 viewport_area → 原样返回一个矩形区域 → 不修改任何东西。
调用关系:外层绘图流程和 draw_new_task_page 会在排版前问它当前画布范围,然后再决定各个组件放在哪里。
调用图:被 2 处调用(draw, draw_new_task_page)。
Frame::render_widget_ref116–118 ↗
fn render_widget_ref(&mut self, widget: W, area: Rect)
作用:把一个 ratatui 组件画到当前帧的缓冲区里。WidgetRef 可以理解成“会自己画自己的界面小零件”。
数据流:输入一个组件和一个目标矩形区域 → 调用组件自己的 render_ref,把内容写进 Frame 的 Buffer → 返回空,但缓冲区里的画面被更新了。
调用关系:render_list、render_picker_footer、render_picker_footer_separator、render_transcript_loading_overlay 等界面部件会用它把自己放到当前帧上。
调用图:被 4 处调用(render_list, render_picker_footer, render_picker_footer_separator, render_transcript_loading_overlay);外部调用 1 个(render_ref)。
Frame::set_cursor_position130–132 ↗
fn set_cursor_position(&mut self, position: P)
作用:声明这一帧画完后,终端光标应该显示并移动到指定位置。比如输入框需要让用户看到光标在哪里。
数据流:输入一个可转成 Position 的坐标 → 存进 Frame 的 cursor_position → 暂时不动真实终端,等 Terminal::try_draw 刷完画面后再统一处理。
调用关系:draw_new_task_page 会用它标记光标位置;真正移动光标的动作后来由 Terminal::try_draw 交给 Terminal::set_cursor_position 完成。
调用图:被 1 处调用(draw_new_task_page);外部调用 1 个(into)。
Frame::set_cursor_style135–137 ↗
fn set_cursor_style(&mut self, style: SetCursorStyle)
作用:声明这一帧结束后光标应该是什么形状,比如竖线、方块或默认形状。它让输入区域可以按需要改变光标外观。
数据流:输入一个 SetCursorStyle → 保存到 Frame 的 cursor_style → 不立刻写终端,等本帧刷新完再应用。
调用关系:如果这一帧同时设置了光标位置,Terminal::try_draw 会读取这个样式并调用 Terminal::set_cursor_style 写到终端。
Frame::buffer_mut140–142 ↗
fn buffer_mut(&mut self) -> &mut Buffer
作用:把当前帧的底层画布直接交给调用者修改。适合少数需要精细写格子的界面代码。
数据流:读取 Frame 持有的可变 Buffer 引用 → 返回这个可变引用 → 调用者之后可以直接改画面内容。
调用关系:draw_new_task_page 会用它做更底层的绘制;最后这些改动仍然通过 Terminal::flush 和 diff_buffers 输出到屏幕。
调用图:被 1 处调用(draw_new_task_page)。
Terminal::drop176–187 ↗
fn drop(&mut self)
作用:当 Terminal 对象被销毁时,尽量把用户的终端光标恢复正常。它是一个保险丝,防止程序退出后留下隐藏光标或奇怪光标形状。
数据流:读取当前是否隐藏光标 → 先尝试 reset_cursor_style,再在需要时 show_cursor → 如果恢复失败,就把错误打印到标准错误。
调用关系:这是 Rust 的 Drop 清理流程自动调用的。它依赖 Terminal::reset_cursor_style 和 Terminal::show_cursor 做收尾。
调用图:调用 2 个内部函数(reset_cursor_style, show_cursor);外部调用 1 个(eprintln!)。
Terminal::with_options196–209 ↗
fn with_options(mut backend: B) -> io::Result<Self>
作用:用一个真实终端后端创建 Terminal。它会先读取窗口大小和初始光标位置,为后面的内联绘制找准起点。
数据流:输入 backend → 向 backend 询问 size 和 get_cursor_position;如果光标位置读取失败,就用左上角作为安全默认值并记录警告 → 输出初始化好的 Terminal,或者返回读取大小时的错误。
调用关系:很多快照测试和启动代码会用它建终端;它把真正组装对象的活交给 Terminal::with_screen_size_and_cursor_position。
调用图:被 52 处调用(thread_goal_ephemeral_error_message_renders_snapshot, chained_config_error_wraps_in_history_snapshot, approval_modal_exec_snapshot, app_server_guardian_review_denied_renders_denied_request_snapshot, app_server_guardian_review_timed_out_renders_timed_out_request_snapshot, guardian_approved_exec_renders_approved_request, guardian_approved_request_permissions_renders_request_summary, guardian_denied_exec_renders_warning_and_denied_request, guardian_timed_out_exec_renders_warning_and_timed_out_request, app_server_mcp_startup_failure_renders_warning_history (+15 more));外部调用 3 个(get_cursor_position, size, with_screen_size_and_cursor_position)。
Terminal::with_options_and_cursor_position217–224 ↗
fn with_options_and_cursor_position(backend: B, cursor_pos: Position) -> io::Result<Self>
作用:用调用者已经决定好的初始光标位置创建 Terminal。适合外面已经探测过光标,或者想使用同一个 fallback 策略的场景。
数据流:输入 backend 和 cursor_pos → 向 backend 读取屏幕大小 → 把大小和光标位置交给内部构造函数 → 输出 Terminal 或错误。
调用关系:它绕过了内部光标探测,最后仍然通过 Terminal::with_screen_size_and_cursor_position 统一初始化字段。
调用图:外部调用 2 个(size, with_screen_size_and_cursor_position)。
Terminal::with_screen_size_and_cursor_position226–246 ↗
fn with_screen_size_and_cursor_position(
backend: B,
screen_size: Size,
cursor_pos: Position,
) -> Self
作用:真正把 Terminal 的初始状态拼起来。它设置双缓冲、当前屏幕大小、初始光标位置和视口锚点。
数据流:输入 backend、屏幕大小、光标位置 → 创建两块空 Buffer,把 viewport_area 的 y 放在当前光标行,其他状态设成默认值 → 返回一个 Terminal。
调用关系:Terminal::with_options 和 Terminal::with_options_and_cursor_position 都把准备好的信息交给它,避免初始化逻辑重复。
Terminal::get_frame249–256 ↗
fn get_frame(&mut self) -> Frame<'_>
作用:生成一帧绘图用的 Frame。界面代码拿到它后,只是在内存画布上画,不直接碰真实终端。
数据流:读取当前 viewport_area,并取得当前 Buffer 的可变引用 → 创建带默认光标设置的 Frame → 返回给渲染回调使用。
调用关系:Terminal::try_draw 每次绘制都会先调用它,然后把 Frame 交给外部界面代码填内容。
调用图:调用 1 个内部函数(current_buffer_mut);被 1 处调用(try_draw)。
Terminal::current_buffer259–261 ↗
fn current_buffer(&self) -> &Buffer
作用:读取当前帧的缓冲区。它用于比较“这一帧”和“上一帧”有什么不同。
数据流:读取 current 索引 → 从 buffers 数组取出当前 Buffer 的只读引用 → 返回引用,不修改状态。
调用关系:Terminal::flush 会用它拿到新画面,再和 previous_buffer 做差异比较。
调用图:被 1 处调用(flush)。
Terminal::current_buffer_mut264–266 ↗
fn current_buffer_mut(&mut self) -> &mut Buffer
作用:取得当前帧缓冲区的可写引用。绘图和调整视口大小时需要往这块画布里写。
数据流:读取 current 索引 → 从 buffers 数组取出当前 Buffer 的可变引用 → 调用者可以修改这块缓冲区。
调用关系:Terminal::get_frame 用它把画布交给 Frame;Terminal::set_viewport_area 用它调整缓冲区大小。
调用图:被 2 处调用(get_frame, set_viewport_area)。
Terminal::previous_buffer269–271 ↗
fn previous_buffer(&self) -> &Buffer
作用:读取上一帧的缓冲区。它是判断本次屏幕哪里变化了的参照物。
数据流:根据 current 反向取另一个 Buffer → 返回只读引用 → 不改动任何状态。
调用关系:Terminal::flush 会把它和 current_buffer 交给 diff_buffers,算出需要发给终端的命令。
调用图:被 1 处调用(flush)。
Terminal::previous_buffer_mut274–276 ↗
fn previous_buffer_mut(&mut self) -> &mut Buffer
作用:取得上一帧缓冲区的可写引用。常用于把它清空,让下一次绘制强制全量刷新。
数据流:根据 current 反向取另一个 Buffer → 返回可变引用 → 调用者可以 resize 或 reset 它。
调用关系:清屏、滚动历史清理、视口失效、交换缓冲区等流程都会用它,目的通常是告诉系统“别相信旧画面了”。
调用图:被 7 处调用(clear_after_position, clear_scrollback, clear_scrollback_and_visible_screen_ansi, clear_visible_screen, invalidate_viewport, set_viewport_area, swap_buffers)。
Terminal::backend279–281 ↗
fn backend(&self) -> &B
作用:给外部只读访问底层终端后端。后端就是实际和命令行窗口说话的对象。
数据流:读取 Terminal 内的 backend → 返回只读引用 → 不修改终端状态。
调用关系:insert_history_hyperlink_lines_with_mode_and_wrap_policy 等流程会用它查看后端信息,而不直接改变后端。
调用图:被 1 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy)。
Terminal::backend_mut284–286 ↗
fn backend_mut(&mut self) -> &mut B
作用:给外部可写访问底层终端后端。只有需要直接做底层终端操作时才会用它。
数据流:读取 Terminal 内的 backend → 返回可变引用 → 调用者可以写终端命令或调用后端方法。
调用关系:插入历史超链接行、清理 ambient pet 图片、窗口重排等比较底层的流程会用它直接操作后端。
调用图:被 3 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy, clear_ambient_pet_image, update_inline_viewport_for_resize_reflow)。
Terminal::flush290–297 ↗
fn flush(&mut self) -> io::Result<()>
作用:把当前内存画布真正刷新到终端屏幕。它只发送变化的部分,减少闪烁和无用输出。
数据流:读取 previous_buffer 和 current_buffer → 用 diff_buffers 算出 DrawCommand 列表;如果最后有写字符的位置,就更新 last_known_cursor_pos → 调用 draw 把命令写入 backend。
调用关系:Terminal::try_draw 在界面代码画完 Frame 后调用它。它是内存画布到真实终端之间的关键出口。
调用图:调用 4 个内部函数(current_buffer, previous_buffer, diff_buffers, draw);被 1 处调用(try_draw)。
Terminal::resize303–306 ↗
fn resize(&mut self, screen_size: Size) -> io::Result<()>
作用:记录终端窗口的新大小。这里不马上重建所有内容,而是保存大小供后续绘制判断。
数据流:输入新的 Size → 写入 last_known_screen_size → 返回成功。
调用关系:Terminal::autoresize 发现真实窗口大小变了时会调用它。
调用图:被 1 处调用(autoresize)。
Terminal::set_viewport_area309–314 ↗
fn set_viewport_area(&mut self, area: Rect)
作用:设置这次 TUI 要占用的屏幕区域,并让两块缓冲区匹配这个区域。视口可以理解成程序在终端里的“显示窗口”。
数据流:输入一个 Rect → 调整当前和上一帧 Buffer 的大小,保存 viewport_area,并把 visible_history_rows 限制在新区域上方范围内 → Terminal 的绘图范围随之更新。
调用关系:进入/离开备用屏、线程切换清屏、插入历史行、窗口重排和应用布局更新都会调用它来重新确定绘图边界。
调用图:调用 2 个内部函数(current_buffer_mut, previous_buffer_mut);被 6 处调用(clear_terminal_for_thread_switch, insert_history_hyperlink_lines_with_mode_and_wrap_policy, enter_alt_screen, leave_alt_screen, update_inline_viewport_for_resize_reflow, apply);外部调用 1 个(top)。
Terminal::autoresize317–323 ↗
Terminal::draw348–356 ↗
fn draw(&mut self, render_callback: F) -> io::Result<()>
作用:绘制一帧的简单入口。调用者只需要提供一个不会返回错误的绘图闭包。
数据流:输入一个 render_callback → 包装成返回 io::Result 的形式 → 交给 Terminal::try_draw 执行 → 返回绘制结果。
调用关系:draw_footer_frame 会调用它;它本身只是 Terminal::try_draw 的便捷版本。
调用图:调用 1 个内部函数(try_draw);被 1 处调用(draw_footer_frame)。
Terminal::try_draw393–429 ↗
fn try_draw(&mut self, render_callback: F) -> io::Result<()>
作用:绘制一帧的完整入口。它允许绘图回调失败,并负责一整套顺序:检查大小、画到缓冲区、刷新屏幕、处理光标、交换缓冲区。
数据流:输入一个会接收 Frame 的回调 → autoresize;创建 Frame;让回调把界面写进 Buffer;取出光标设置;flush 到终端;按 Frame 要求隐藏或显示并移动光标;swap_buffers;最后 flush 后端 → 返回成功或错误。
调用关系:Terminal::draw 会调用它。它串起 get_frame、flush、hide_cursor、set_cursor_style、show_cursor、set_cursor_position、swap_buffers,是每帧渲染的总调度。
调用图:调用 8 个内部函数(autoresize, flush, get_frame, hide_cursor, set_cursor_position, set_cursor_style, show_cursor, swap_buffers);被 1 处调用(draw);外部调用 1 个(flush)。
Terminal::hide_cursor432–436 ↗
fn hide_cursor(&mut self) -> io::Result<()>
作用:隐藏终端光标。非输入状态下隐藏光标可以避免屏幕上多一个乱跳的小块。
数据流:调用 backend.hide_cursor → 成功后把 hidden_cursor 设为 true → 返回结果。
调用关系:Terminal::try_draw 发现这一帧没有请求光标位置时会调用它。
调用图:被 1 处调用(try_draw);外部调用 1 个(hide_cursor)。
Terminal::show_cursor439–443 ↗
fn show_cursor(&mut self) -> io::Result<()>
作用:显示终端光标。需要用户输入或程序退出恢复时会用到。
数据流:调用 backend.show_cursor → 成功后把 hidden_cursor 设为 false → 返回结果。
调用关系:Terminal::try_draw 在 Frame 请求显示光标时调用它;Terminal::drop 在收尾时也可能调用它恢复用户终端。
调用图:被 2 处调用(drop, try_draw);外部调用 1 个(show_cursor)。
Terminal::set_cursor_style446–448 ↗
fn set_cursor_style(&mut self, style: SetCursorStyle) -> io::Result<()>
作用:向终端发送改变光标形状的命令。比如把光标变成竖线,适合文本输入场景。
数据流:输入 SetCursorStyle → 用 queue! 把控制命令写入 backend 的输出队列 → 返回写入是否成功。
调用关系:Terminal::try_draw 会在显示光标前应用 Frame 指定的样式;Terminal::reset_cursor_style 也通过它恢复默认样式。
调用图:被 2 处调用(reset_cursor_style, try_draw);外部调用 1 个(queue!)。
Terminal::reset_cursor_style451–453 ↗
fn reset_cursor_style(&mut self) -> io::Result<()>
作用:把光标形状恢复成用户终端默认设置。它避免程序结束后留下自定义光标样式。
数据流:不需要输入 → 调用 Terminal::set_cursor_style,传入 DefaultUserShape → 返回写入结果。
调用关系:Terminal::drop 会调用它做退出清理。
调用图:调用 1 个内部函数(set_cursor_style);被 1 处调用(drop)。
Terminal::get_cursor_position459–461 ↗
fn get_cursor_position(&mut self) -> io::Result<Position>
作用:询问当前终端光标位置。这个接口主要用于需要知道真实光标坐标的底层场景。
数据流:调用 backend.get_cursor_position → 返回 Position 或错误 → 不更新 Terminal 自己记录的位置。
调用关系:它是对后端能力的薄封装,当前文件里标为可能未使用。
调用图:外部调用 1 个(get_cursor_position)。
Terminal::set_cursor_position464–469 ↗
fn set_cursor_position(&mut self, position: P) -> io::Result<()>
作用:把终端光标移动到指定坐标,并同步 Terminal 记住的最后位置。
数据流:输入一个可转成 Position 的位置 → 调用 backend.set_cursor_position 移动真实光标 → 成功后更新 last_known_cursor_pos → 返回结果。
调用关系:Terminal::try_draw 用它落实 Frame 请求的光标位置;清滚动历史和清可见屏幕时也用它把光标先移到左上角。
调用图:被 3 处调用(clear_scrollback, clear_visible_screen, try_draw);外部调用 2 个(set_cursor_position, into)。
Terminal::clear472–477 ↗
fn clear(&mut self) -> io::Result<()>
作用:清掉当前视口里的终端内容,并让下一帧强制重画。它用于进入新界面或需要彻底刷新时。
数据流:检查 viewport_area 是否为空 → 如果为空直接成功;否则取视口起点并调用 clear_after_position → 清理屏幕和旧缓冲参照。
调用关系:enter_alt_screen 和 apply 等流程会调用它;具体清除动作交给 Terminal::clear_after_position。
调用图:调用 1 个内部函数(clear_after_position);被 2 处调用(enter_alt_screen, apply);外部调用 2 个(as_position, is_empty)。
Terminal::clear_after_position480–486 ↗
fn clear_after_position(&mut self, position: Position) -> io::Result<()>
作用:从某个位置开始一直清到可见屏幕末尾,并让下一次绘制别再依赖旧画面。适合局部起点后的整片清理。
数据流:输入 Position → 移动后端光标到该点;调用 clear_region(AfterCursor) 清除后续内容;reset 上一帧 Buffer → 返回结果。
调用关系:Terminal::clear、插入历史超链接行和窗口重排流程会调用它,确保屏幕内容和内部缓冲重新对齐。
调用图:调用 1 个内部函数(previous_buffer_mut);被 3 处调用(clear, insert_history_hyperlink_lines_with_mode_and_wrap_policy, update_inline_viewport_for_resize_reflow);外部调用 2 个(clear_region, set_cursor_position)。
Terminal::invalidate_viewport491–493 ↗
fn invalidate_viewport(&mut self)
作用:标记当前视口已经不可信,下一帧必须重画。它本身不清屏,只是清掉差异比较用的旧参照。
数据流:取得 previous_buffer_mut → reset 它 → 下一次 diff_buffers 会认为很多内容需要重新输出。
调用关系:当外部做了 ratatui 不知道的原始终端操作后,可以调用它修复内部状态。
调用图:调用 1 个内部函数(previous_buffer_mut)。
Terminal::clear_scrollback496–509 ↗
fn clear_scrollback(&mut self) -> io::Result<()>
作用:清除终端滚动历史,也就是用户向上翻能看到的旧内容,并强制下一帧重画。某些终端支持这个能力。
数据流:如果视口为空就直接返回 → 把光标移到左上角;发送 Purge 清历史命令;再次回到左上角;flush 写出;reset 上一帧 Buffer → 返回结果。
调用关系:它使用 Terminal::set_cursor_position 和 previous_buffer_mut,专门处理比普通清屏更深一层的历史内容。
调用图:调用 2 个内部函数(previous_buffer_mut, set_cursor_position);外部调用 3 个(is_empty, queue!, flush)。
Terminal::clear_visible_screen512–524 ↗
fn clear_visible_screen(&mut self) -> io::Result<()>
作用:清除整个当前可见屏幕,而不只是应用视口。它还会重置已显示的历史行计数。
数据流:把光标移到左上角 → 调用 backend.clear_region(All) 清可见屏幕 → 再把光标移回左上角并 flush → visible_history_rows 归零,previous_buffer reset → 返回结果。
调用关系:适合需要把终端当前页彻底擦干净的场景,内部依赖 Terminal::set_cursor_position 和后端清屏能力。
调用图:调用 2 个内部函数(previous_buffer_mut, set_cursor_position);外部调用 2 个(clear_region, flush)。
Terminal::clear_scrollback_and_visible_screen_ansi530–543 ↗
fn clear_scrollback_and_visible_screen_ansi(&mut self) -> io::Result<()>
作用:用一串明确的 ANSI 控制码同时清滚动历史和可见屏幕。ANSI 控制码就是终端能识别的特殊文本命令。
数据流:如果视口为空就返回 → 写入重置滚动区域、重置样式、回到左上角、清屏、清历史、再回左上角这一串转义码 → flush;更新 last_known_cursor_pos,visible_history_rows 归零,previous_buffer reset → 返回结果。
调用关系:clear_terminal_for_thread_switch 会调用它。它绕过普通后端命令,是为一些终端兼容性问题准备的硬清理手段。
调用图:调用 1 个内部函数(previous_buffer_mut);被 1 处调用(clear_terminal_for_thread_switch);外部调用 3 个(is_empty, flush, write!)。
Terminal::visible_history_rows545–547 ↗
fn visible_history_rows(&self) -> u16
作用:返回当前记录的、视口上方仍可见的历史行数。这个数帮助内联模式判断有多少旧内容占着屏幕上方。
数据流:读取 visible_history_rows 字段 → 返回这个 u16 数字 → 不修改状态。
调用关系:其他布局或历史插入逻辑可以用它了解当前终端历史占位情况。
Terminal::note_history_rows_inserted549–554 ↗
fn note_history_rows_inserted(&mut self, inserted_rows: u16)
作用:记录刚刚往视口上方插入了多少历史行。它会防止计数超过视口顶部位置。
数据流:输入 inserted_rows → 用饱和加法累加到 visible_history_rows,避免数字溢出;再用 viewport_area.top() 限制最大值 → 更新字段。
调用关系:insert_history_hyperlink_lines_with_mode_and_wrap_policy 插入历史超链接行后会调用它,让 Terminal 知道屏幕上方多了多少可见历史。
调用图:被 1 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy);外部调用 1 个(top)。
Terminal::swap_buffers557–560 ↗
fn swap_buffers(&mut self)
作用:完成一帧后交换当前缓冲区和上一帧缓冲区。下一帧就能拿刚画完的画面当参照。
数据流:先 reset 非当前的那块 Buffer → 把 current 改成另一个索引 → 当前/上一帧角色互换。
调用关系:Terminal::try_draw 在成功刷新并处理光标后调用它,是双缓冲绘制循环的收尾动作。
调用图:调用 1 个内部函数(previous_buffer_mut);被 1 处调用(try_draw)。
Terminal::size563–565 ↗
fn size(&self) -> io::Result<Size>
作用:读取真实终端窗口大小。它是对后端 size 方法的简单封装。
数据流:调用 backend.size → 返回 Size 或错误 → 不修改 Terminal 状态。
调用关系:Terminal::autoresize、enter_alt_screen、窗口重排和 apply 等流程用它决定当前能画多大。
调用图:被 4 处调用(autoresize, enter_alt_screen, update_inline_viewport_for_resize_reflow, apply);外部调用 1 个(size)。
diff_buffers576–639 ↗
fn diff_buffers(a: &Buffer, b: &Buffer) -> Vec<DrawCommand>
作用:比较上一帧和这一帧,生成最少的绘制命令。它是减少闪烁和提高终端绘制效率的核心。
数据流:输入两个 Buffer:旧画面和新画面 → 先逐行找出右边可以用“清到行尾”代替逐个空格输出的位置;再逐格比较变化,处理中文宽字符、零宽字符、被宽字符覆盖的格子和样式变化 → 输出 DrawCommand 列表,不直接写终端。
调用关系:Terminal::flush 会调用它得到命令,再交给 draw 写出;相关测试会直接调用它验证宽字符和行尾清理行为。
调用图:调用 1 个内部函数(display_width);被 3 处调用(flush, diff_buffers_clear_to_end_starts_after_wide_char, diff_buffers_does_not_emit_clear_to_end_for_full_width_row);外部调用 4 个(pos_of, empty, max, vec!)。
draw641–698 ↗
fn draw(writer: &mut impl Write, commands: I) -> io::Result<()>
作用:把 DrawCommand 翻译成真正写给终端的控制命令和文字。它负责移动光标、设置颜色样式、打印字符、清到行尾。
数据流:输入一个 writer 和命令迭代器 → 逐条命令处理:必要时 MoveTo 移动光标,Put 时切换样式颜色并 Print 字符,ClearToEnd 时重置样式、设置背景色并清到行尾;最后恢复前景色、背景色和属性为默认 → 返回写入结果。
调用关系:Terminal::flush 把 diff_buffers 的结果交给它。ModifierDiff::queue 是它在样式变化时使用的小帮手。
ModifierDiff::queue709–764 ↗
fn queue(self, w: &mut W) -> io::Result<()>
作用:只发送“样式变化”的终端命令,而不是每次都重置全部样式。Modifier 指粗体、斜体、下划线、反色等文字效果。
数据流:输入旧 Modifier 和新 Modifier → 先算哪些效果被移除并发送关闭命令,再算哪些效果被新增并发送开启命令 → 把这些命令排进 writer,返回结果。
调用关系:draw 在遇到某个格子的文字样式和当前样式不同时调用它,用更少命令完成样式切换。
调用图:外部调用 2 个(contains, queue!)。
tests::CaptureBackend::new782–788 ↗
fn new(width: u16, height: u16) -> Self
作用:创建一个测试用的假终端后端。它不真的操作屏幕,只把写出的字节收集起来,方便断言。
数据流:输入宽度和高度 → 初始化空 output、指定 size、光标在左上角 → 返回 CaptureBackend。
调用关系:多个测试用它传给 Terminal::with_options,模拟一个可控的终端环境。
调用图:外部调用 1 个(new)。
tests::CaptureBackend::output790–792 ↗
fn output(&self) -> String
作用:把测试后端捕获到的字节转成字符串,方便检查里面有没有某个终端控制码。
数据流:读取 output 字节数组 → 用 UTF-8 宽松转换生成 String → 返回字符串,不清空 output。
调用关系:光标样式相关测试会调用它,看 Terminal 是否真的写出了预期控制序列。
调用图:外部调用 1 个(from_utf8_lossy)。
tests::CaptureBackend::write796–799 ↗
fn write(&mut self, buf: &[u8]) -> io::Result<usize>
作用:实现测试后端的写入行为:把别人写来的内容存起来。它模拟真实终端接收输出。
数据流:输入一段字节 buf → 追加到 output → 返回写入的字节数。
调用关系:crossterm 的 queue! 或 write! 在测试中最终会走到这里,因此测试可以事后检查 output。
tests::CaptureBackend::draw807–812 ↗
fn draw(&mut self, _content: I) -> io::Result<()>
作用:实现 Backend 接口要求的 draw 方法,但测试里不需要它真的画东西。
数据流:接收一批待绘制内容 → 直接忽略 → 返回成功。
调用关系:它只是让 CaptureBackend 满足 ratatui::Backend 接口;本文件主要测试的是自定义 draw 管线。
tests::CaptureBackend::hide_cursor814–816 ↗
fn hide_cursor(&mut self) -> io::Result<()>
作用:测试版隐藏光标方法。它不写任何东西,只表示调用成功。
数据流:无输入 → 不改状态 → 返回成功。
调用关系:Terminal::hide_cursor 在测试中可能调用它,用来避免依赖真实终端。
tests::CaptureBackend::show_cursor818–820 ↗
fn show_cursor(&mut self) -> io::Result<()>
作用:测试版显示光标方法。它保持空实现,让测试聚焦在本文件关心的输出。
数据流:无输入 → 不改状态 → 返回成功。
调用关系:Terminal::show_cursor 和 Terminal::drop 在测试环境里会走到它。
tests::CaptureBackend::get_cursor_position822–824 ↗
fn get_cursor_position(&mut self) -> io::Result<Position>
作用:返回测试后端当前记录的光标位置。这样 Terminal 初始化时能拿到一个稳定坐标。
数据流:读取 cursor 字段 → 返回 Position → 不修改状态。
调用关系:Terminal::with_options 在测试中调用它确定初始视口位置。
tests::CaptureBackend::set_cursor_position826–829 ↗
fn set_cursor_position(&mut self, position: P) -> io::Result<()>
作用:在测试后端里记录光标被移动到了哪里。它不移动真实屏幕,只更新内存字段。
数据流:输入一个可转成 Position 的位置 → 转换后写入 cursor → 返回成功。
调用关系:Terminal::set_cursor_position、清屏流程和测试绘制流程会通过后端调用它。
调用图:外部调用 1 个(into)。
tests::CaptureBackend::clear831–833 ↗
fn clear(&mut self) -> io::Result<()>
作用:测试版全清方法。它什么也不做,只满足 Backend 接口。
数据流:无输入 → 不清 output,也不改状态 → 返回成功。
调用关系:这是 CaptureBackend 作为假后端必须提供的方法之一。
tests::CaptureBackend::clear_region835–837 ↗
fn clear_region(&mut self, _clear_type: ClearType) -> io::Result<()>
作用:测试版区域清理方法。它接受清理类型但不实际处理。
数据流:输入 ClearType → 忽略它 → 返回成功。
调用关系:Terminal 的清屏相关方法在测试中可以调用它,而不会影响真实终端。
tests::CaptureBackend::append_lines839–841 ↗
fn append_lines(&mut self, _line_count: u16) -> io::Result<()>
作用:测试版追加行方法。它不实际滚动或写屏幕。
数据流:输入追加行数 → 忽略 → 返回成功。
调用关系:它用于满足 Backend 接口,让 CaptureBackend 能被 Terminal 使用。
tests::CaptureBackend::scroll_region_up843–849 ↗
fn scroll_region_up(
&mut self,
_region: std::ops::Range<u16>,
_scroll_by: u16,
) -> io::Result<()>
作用:测试版区域上滚方法。它不改变任何内容,只返回成功。
数据流:输入滚动区域和滚动行数 → 忽略 → 返回成功。
调用关系:它是 Backend 接口的一部分,保证测试后端可以替代真实后端。
tests::CaptureBackend::scroll_region_down851–857 ↗
fn scroll_region_down(
&mut self,
_region: std::ops::Range<u16>,
_scroll_by: u16,
) -> io::Result<()>
作用:测试版区域下滚方法。它不改变任何内容,只返回成功。
数据流:输入滚动区域和滚动行数 → 忽略 → 返回成功。
调用关系:它补齐 Backend 接口,让测试不需要真实终端滚动能力。
tests::CaptureBackend::size859–861 ↗
fn size(&self) -> io::Result<Size>
作用:返回测试终端的固定大小。这样测试可以在稳定尺寸下判断绘图结果。
数据流:读取 size 字段 → 返回 Size → 不修改状态。
调用关系:Terminal::with_options 和 Terminal::autoresize 在测试中会调用它。
tests::CaptureBackend::window_size863–868 ↗
fn window_size(&mut self) -> io::Result<WindowSize>
作用:返回测试终端窗口大小信息,包括字符尺寸和像素尺寸。这里两者都用同一个 size 代替。
数据流:读取 size 字段 → 组装 WindowSize,其中 columns_rows 和 pixels 都等于 size → 返回结果。
调用关系:它满足 Backend 接口中更完整的窗口大小查询要求。
tests::CaptureBackend::flush870–872 ↗
fn flush(&mut self) -> io::Result<()>
作用:测试版刷新方法。真实终端会把排队输出送出去,这里直接成功。
数据流:无输入 → 不改 output → 返回成功。
调用关系:Terminal::try_draw 或测试手动 flush 后端时会调用它,避免产生真实 I/O。
tests::diff_buffers_does_not_emit_clear_to_end_for_full_width_row876–901 ↗
fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row()
作用:验证当一行最后一个格子有内容时,diff_buffers 不会错误地发“清到行尾”。否则会把刚画的最后一个字符清掉。
数据流:创建 3x2 的旧空缓冲和新缓冲 → 在第一行最后一格放 X → 调用 diff_buffers → 断言没有针对该行的 ClearToEnd,并且有 Put 到 x=2,y=0。
调用关系:这个测试直接保护 diff_buffers 的行尾优化逻辑,防止优化过头清掉有效字符。
调用图:调用 1 个内部函数(diff_buffers);外部调用 4 个(empty, new, assert!, assert_eq!)。
tests::diff_buffers_clear_to_end_starts_after_wide_char904–919 ↗
fn diff_buffers_clear_to_end_starts_after_wide_char()
作用:验证遇到中文这类双宽字符时,清到行尾的位置要从宽字符占完的后面开始。否则会破坏宽字符显示。
数据流:创建一行宽度 10 的旧新缓冲 → 旧画面写“中文”,新画面只写“中” → 调用 diff_buffers → 断言 ClearToEnd 从 x=2,y=0 开始。
调用关系:这个测试直接覆盖 diff_buffers 对 display_width 和宽字符占位的处理。
调用图:调用 1 个内部函数(diff_buffers);外部调用 4 个(empty, new, default, assert!)。
tests::terminal_draw_applies_requested_cursor_style922–944 ↗
fn terminal_draw_applies_requested_cursor_style()
作用:验证一帧里请求的光标样式真的会写到终端输出中。它防止设置光标形状的功能被绘制流程漏掉。
数据流:创建 CaptureBackend 和 Terminal,设置视口 → try_draw 中设置光标为 SteadyBar 并放到左上角 → 构造预期控制码 → 从测试后端 output 取实际输出并断言包含预期内容。
调用关系:它通过 Terminal::with_options 和 Frame::set_cursor_style 间接测试 Terminal::try_draw 到 Terminal::set_cursor_style 的整条链路。
调用图:调用 1 个内部函数(with_options);外部调用 6 个(new, from_utf8, new, assert!, queue!, new)。
tests::reset_cursor_style_emits_default_user_shape947–963 ↗
fn reset_cursor_style_emits_default_user_shape()
作用:验证恢复光标样式时会发送“回到用户默认形状”的命令。这样退出清理才可靠。
数据流:创建 CaptureBackend 和 Terminal → 调用 reset_cursor_style 并 flush 后端 → 构造 DefaultUserShape 的预期控制码 → 检查捕获输出包含它。
调用关系:它直接保护 Terminal::reset_cursor_style,也间接保证 Terminal::drop 的清理动作有正确命令可用。
调用图:调用 1 个内部函数(with_options);外部调用 6 个(from_utf8, new, assert!, queue!, flush, new)。
tui/src/terminal_probe.rs源码 ↗
文字界面程序刚启动时,需要知道光标在哪、默认前景色和背景色是什么、键盘是否支持更细的按键报告。普通库函数可能会等很久,这会让启动显得很慢。这个文件做的是“短时间试探”:给终端发几段特殊控制码,就像敲门问一句;在很短的截止时间内收集回复;能解析出来就用,解析不出来就返回空值,让上层走默认方案。Unix 下它会尽量复制标准输入输出句柄,必要时退到 /dev/tty,并临时把读取设成非阻塞,避免读终端时卡死。Windows 下它优先用终端的 OSC 颜色回复,不行就读取控制台颜色表。这里也提醒了一个重要副作用:探测期间读到的无关输入会被吃掉,所以只能在正常事件流还没开始或暂停时使用。
imp::Tty::open81–120 ↗
fn open() -> io::Result<Self>
作用:打开一组临时的终端读写通道,专门给探测用。它优先复制当前程序的标准输入输出;如果标准输入输出不是终端,就尝试打开控制终端 /dev/tty。
数据流:进去的是当前进程已有的标准输入和标准输出信息。它先复制这两个文件描述符,成功后交给 imp::Tty::new;失败时整理错误信息,再分别打开 /dev/tty 的读端和写端。出来的是一个 Tty,后续探测可以通过它发问题、收回答。
调用关系:Unix 探测入口会先调用它,例如 imp::cursor_position、imp::startup 和颜色探测。它把底层打开文件的麻烦包起来,再把已打开的 reader 和 writer 交给 imp::Tty::new 设置读取模式。
imp::Tty::new122–136 ↗
fn new(reader: File, writer: File) -> io::Result<Self>
作用:把已经打开的读写文件包装成临时终端对象,并把读取端调成非阻塞。非阻塞的意思是:没数据时马上返回,不会一直等。
数据流:进去的是一个读文件和一个写文件。它读取 reader 原来的系统标志,保存起来,再给 reader 加上 O_NONBLOCK。出来的是 Tty;如果读取或设置系统标志失败,就返回操作系统错误。
调用关系:imp::Tty::open 负责找通道,找到后调用它完成安全设置。它保存的原始标志会在 imp::Tty::drop 里恢复,避免探测结束后影响正常输入。
调用图:外部调用 3 个(as_raw_fd, last_os_error, fcntl)。
imp::Tty::write_all138–141 ↗
imp::Tty::read_available143–169 ↗
fn read_available(&mut self, buffer: &mut Vec<u8>) -> io::Result<()>
作用:把终端当前已经准备好的输入尽量读出来,但不等待新数据。它适合在短超时探测里反复“捞一把”。
数据流:进去的是一个可增长的字节缓冲区。它从 reader 里一次最多读 256 字节,读到就追加进缓冲区;遇到暂时没数据或被信号打断,就正常结束;遇到真正错误就返回错误。缓冲区会因此变长。
调用关系:imp::read_startup_probe 和读取循环会调用它积累终端回复。解析函数随后会在这份缓冲区里找光标位置、颜色或键盘能力。
调用图:外部调用 4 个(as_raw_fd, last_os_error, read, matches!)。
imp::Tty::poll_readable171–201 ↗
fn poll_readable(&self, timeout: Duration) -> io::Result<bool>
作用:在指定时间内等一等,看终端输入是否变得可读。它不会自己读取数据,只回答“有没有东西可读”。
数据流:进去的是最长等待时间。它用系统 poll 等待 reader 出现输入事件,期间会处理被信号打断的情况。出来是 true 表示有数据可读,false 表示超时没有数据,或返回错误。
调用关系:读取循环在每次解析失败后调用它,决定是继续等一点,还是到期放弃。这样启动探测不会无限卡住。
调用图:外部调用 4 个(as_raw_fd, now, last_os_error, poll)。
imp::Tty::drop205–208 ↗
fn drop(&mut self)
作用:在临时终端对象销毁时,把 reader 原来的文件状态恢复回去。它像离开房间前把灯和门锁恢复原样。
数据流:进去的是 Tty 自己保存的 reader 和 original_flags。它调用系统 fcntl 把读取端标志设回原值;错误被忽略,因为这是清理阶段。出来没有显式结果,但会尽量撤销非阻塞设置。
调用关系:imp::Tty::new 设置了非阻塞,imp::Tty::drop 负责善后。所有通过 Tty 做的 Unix 探测结束时都会自动触发它。
调用图:外部调用 2 个(as_raw_fd, fcntl)。
imp::dup_file212–218 ↗
fn dup_file(fd: libc::c_int) -> io::Result<File>
作用:复制一个现有的标准输入或输出描述符,让探测代码使用副本而不是直接动原件。
数据流:进去的是一个底层文件描述符数字。它调用系统 dup 复制;成功后把新描述符包装成 File,失败则返回系统错误。出来的是一个可独立关闭和设置的文件对象。
调用关系:imp::Tty::open 用它复制 stdin 和 stdout。这样后续清理只会影响副本,不会误关或改坏程序真正的标准输入输出。
调用图:外部调用 3 个(from_raw_fd, last_os_error, dup)。
imp::cursor_position240–244 ↗
fn cursor_position(timeout: Duration) -> io::Result<Option<Position>>
作用:向终端询问当前光标位置。恢复文字界面时,上层可以用它知道应该从哪里继续画。
数据流:进去的是一个超时时间。它打开临时 Tty,写入“报告光标位置”的控制码,然后用读取循环等待 parse_cursor_position 认出回复。出来是一个可选位置;没等到或不支持时就是 None。
调用关系:它会在 suspend 恢复相关流程中被调用。它把实际收发交给 imp::Tty::open、imp::Tty::write_all 和 imp::read_until,自己只负责组织这一次光标探测。
调用图:被 1 处调用(suspend);外部调用 2 个(open, read_until)。
imp::startup250–264 ↗
fn startup(
timeout: Duration,
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> io::Result<StartupProbe>
作用:启动时一次性探测多个终端信息:光标位置、默认颜色,以及可选的键盘增强能力。这样比一个问题等一次更快。
数据流:进去的是总超时时间,以及是否要探测键盘增强。它打开 Tty,按需要发送一批控制码,然后把读取和解析交给 imp::read_startup_probe。出来是 StartupProbe,里面每项都可能有值或没有值。
调用关系:它由 init 阶段调用,是 TUI 启动探测的总入口。它把多个小探测打包,避免不支持的终端让启动连续等待多次。
调用图:被 1 处调用(init);外部调用 2 个(open, read_startup_probe)。
imp::read_startup_probe293–327 ↗
fn read_startup_probe(
tty: &mut Tty,
timeout: Duration,
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> io::Result<StartupProbe>
作用:在同一个截止时间内读取启动探测的所有回复,并不断更新结果。它是启动探测的主读取循环。
数据流:进去的是 Tty、超时时间和键盘探测选项。它反复读取当前可用字节,调用 imp::update_startup_probe 解析缓冲区;如果 imp::startup_probe_complete 说信息齐了就返回;如果到期或没有更多输入,就调用 imp::finish_startup_probe 做最后判断。出来是 StartupProbe。
调用关系:imp::startup 发完问题后调用它。它把底层等待交给 Tty,把具体识别交给解析函数,把收尾判断交给 imp::finish_startup_probe。
调用图:外部调用 7 个(now, new, poll_readable, read_available, finish_startup_probe, startup_probe_complete, update_startup_probe)。
imp::update_startup_probe329–358 ↗
fn update_startup_probe(
probe: &mut StartupProbe,
saw_supported_keyboard: &mut bool,
buffer: &[u8],
keyboard_probe: StartupKeyboardEnhancementProbe,
)
作用:看一眼目前攒到的终端回复,把能识别出的光标、颜色、键盘能力填进结果里。
数据流:进去的是可修改的 StartupProbe、一个记录是否见过键盘支持信号的布尔值、当前缓冲区,以及键盘探测选项。它分别尝试解析光标位置、默认颜色和键盘能力;识别到就写进 probe 或更新 saw_supported_keyboard。出来没有单独返回值,但 probe 会被补全。
调用关系:imp::read_startup_probe 每读到一批数据就调用它。它调用 parse_default_colors、imp::parse_cursor_position 和 imp::parse_keyboard_enhancement_support,把杂乱字节变成上层能懂的字段。
调用图:调用 1 个内部函数(parse_default_colors);外部调用 2 个(parse_cursor_position, parse_keyboard_enhancement_support)。
imp::startup_probe_complete360–368 ↗
fn startup_probe_complete(
probe: &StartupProbe,
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> bool
作用:判断启动探测是否已经拿到了足够的信息,可以提前结束等待。
数据流:进去的是当前 StartupProbe 和键盘探测选项。它检查光标和默认颜色是否已有结果;如果要求探测键盘,也检查键盘结果是否已有。出来是 true 或 false。
调用关系:imp::read_startup_probe 在每轮解析后调用它。它让流程在信息齐全时马上返回,而不是死等到超时。
imp::finish_startup_probe370–380 ↗
fn finish_startup_probe(
probe: &mut StartupProbe,
keyboard_probe: StartupKeyboardEnhancementProbe,
saw_supported_keyboard: bool,
)
作用:探测快结束但键盘结果还没完全定下来时,做最后的保守判断。
数据流:进去的是可修改的 StartupProbe、键盘探测选项,以及是否曾经看见支持键盘增强的信号。若确实请求过键盘探测且结果还空,它会在看到过支持信号时填 Some(true),否则保持 None。出来没有返回值,但 probe 可能被补上一项。
调用关系:imp::read_startup_probe 在超时或没有更多可读数据时调用它。它处理一种细节:已经看见键盘支持回复,但还想顺手等 fallback 回复以免漏进正常事件流。
imp::parse_cursor_position382–405 ↗
fn parse_cursor_position(buffer: &[u8]) -> Option<Position>
作用:从终端返回的字节里找出光标位置回复,并把它转成程序内部的位置坐标。
数据流:进去的是一段可能混着杂音的字节。它寻找 ESC[ 开头、R 结尾的形如“行;列”的回复,解析数字,并把终端常用的从 1 开始坐标转成程序里的从 0 开始坐标。出来是 Position 或 None。
调用关系:imp::cursor_position 和 imp::update_startup_probe 都依赖它。它还会用 imp::find_all_subslices 在缓冲区里找所有可能的开头,以便跳过无关输入。
调用图:外部调用 2 个(from_utf8, find_all_subslices)。
imp::parse_keyboard_enhancement_support423–433 ↗
fn parse_keyboard_enhancement_support(buffer: &[u8]) -> KeyboardProbeState
作用:判断终端是否支持更高级的键盘输入报告。这个功能能让程序更准确地区分某些按键。
数据流:进去的是累计收到的字节。它分别寻找键盘增强标志和普通设备属性回复;两者组合后返回 Pending、UnsupportedFallback、Supported 或 SupportedAndFallback。出来的是一个状态枚举,不直接改外部数据。
调用关系:imp::update_startup_probe 调用它来更新 StartupProbe。它把具体搜索交给 imp::find_keyboard_flags 和 imp::find_primary_device_attributes。
调用图:外部调用 2 个(find_keyboard_flags, find_primary_device_attributes)。
imp::find_keyboard_flags435–466 ↗
fn find_keyboard_flags(buffer: &[u8]) -> Option<KeyboardEnhancementFlags>
作用:在终端回复里寻找键盘增强能力的位标志,并把数字位转成库里的标志集合。
数据流:进去的是字节缓冲区。它查找 ESC[? 开头、u 结尾的片段,解析中间数字;数字的每一位代表一种键盘报告能力。出来是 KeyboardEnhancementFlags,找不到或格式不对就是 None。
调用关系:imp::parse_keyboard_enhancement_support 用它判断“是否看见支持信号”。它底层用 imp::find_all_subslices 找所有候选片段。
调用图:外部调用 3 个(empty, from_utf8, find_all_subslices)。
imp::find_primary_device_attributes468–479 ↗
fn find_primary_device_attributes(buffer: &[u8]) -> Option<()>
作用:寻找终端的普通设备属性回复。这个回复常被当作“不支持键盘增强协议”的回退信号。
数据流:进去的是字节缓冲区。它找 ESC[? 开头、c 结尾,并且中间只包含数字和分号的片段。找到就返回 Some(()),否则返回 None。
调用关系:imp::parse_keyboard_enhancement_support 用它和键盘标志一起判断支持状态。它同样依赖 imp::find_all_subslices 扫描候选位置。
调用图:外部调用 1 个(find_all_subslices)。
imp::find_all_subslices481–489 ↗
fn find_all_subslices(
haystack: &'a [u8],
needle: &'a [u8],
) -> impl Iterator<Item = usize> + 'a
作用:在一段字节里找出某个小字节串出现的所有位置。它是几个解析器共用的小工具。
数据流:进去的是大字节串 haystack 和要找的小字节串 needle。它逐个滑动窗口比较,产出所有匹配的起始下标。出来的是一个可迭代的位置列表。
调用关系:光标解析、键盘标志解析和设备属性解析都会用它。它不懂终端协议,只提供“在哪里出现过这个开头”的基础能力。
imp::tests::parses_cursor_position_as_zero_based497–506 ↗
fn parses_cursor_position_as_zero_based()
作用:测试光标位置解析是否会把终端的 1 起始坐标正确转成程序的 0 起始坐标。
数据流:进去的是写死的示例终端回复。测试调用解析函数,对比期望 Position;如果结果不一致,测试失败。出来是测试通过或失败。
调用关系:它保护 imp::parse_cursor_position 的关键约定,尤其是恢复界面时坐标不能差一行或一列。
调用图:外部调用 1 个(assert_eq!)。
imp::tests::parses_keyboard_enhancement_flags_and_pda_fallback509–530 ↗
fn parses_keyboard_enhancement_flags_and_pda_fallback()
作用:测试键盘增强探测能区分支持、不支持、还在等待等几种情况。
数据流:进去的是几种模拟终端回复字节。测试调用 imp::parse_keyboard_enhancement_support,并和预期状态比较。出来是测试结果。
调用关系:它覆盖 imp::find_keyboard_flags、imp::find_primary_device_attributes 和组合判断,防止启动时误判键盘能力。
调用图:外部调用 1 个(assert_eq!)。
imp::tests::startup_probe_parses_batched_terminal_responses533–562 ↗
fn startup_probe_parses_batched_terminal_responses()
作用:测试启动时一大包混在一起的终端回复能被正确拆出光标、颜色和键盘能力。
数据流:进去的是一个空 StartupProbe、一个布尔标记和一段模拟混合回复。它调用 imp::update_startup_probe,然后检查 probe 内容和完成状态。出来是测试通过或失败。
调用关系:它模拟 imp::read_startup_probe 在真实启动中会遇到的情况,确保批量探测不会因为回复顺序混杂而失效。
调用图:外部调用 3 个(assert!, assert_eq!, update_startup_probe)。
imp::default_colors595–607 ↗
fn default_colors(timeout: Duration) -> io::Result<Option<DefaultColors>>
作用:查询终端默认前景色和背景色。默认颜色对调色、混色和主题显示很重要;查不到时,上层会用保守默认值。
数据流:进去的是超时时间。在支持 OSC 回复的路径上,它会发送颜色查询并等待 parse_default_colors 解析;Windows 上如果 OSC 查询失败,还会尝试直接读取控制台颜色表。出来是 DefaultColors、None 或 I/O 错误。
调用关系:它会被 query_default_colors、probe_windows_default_colors 等上层颜色查询流程调用。它把跨平台差异藏在 imp 模块里,让外面只看到“尽力拿默认颜色”。
调用图:被 2 处调用(query_default_colors, probe_windows_default_colors);外部调用 5 个(open, query_console_default_colors, query_osc_default_colors, read_until, std_handle)。
imp::query_osc_default_colors609–617 ↗
fn query_osc_default_colors(
input: HANDLE,
output: HANDLE,
timeout: Duration,
) -> io::Result<Option<DefaultColors>>
作用:Windows 下通过 OSC 控制码询问终端默认颜色。OSC 可以理解成终端专用的“查询命令”。
数据流:进去的是输入句柄、输出句柄和超时时间。它先临时开启虚拟终端输入模式,写出 OSC 10 和 OSC 11 查询,再读取直到 parse_default_colors 解析出颜色或超时。出来是可选 DefaultColors。
调用关系:imp::default_colors 优先尝试它。它依赖 imp::VirtualTerminalInputMode::enable、imp::write_all 和 imp::read_until 完成 Windows 控制台收发。
调用图:外部调用 3 个(enable, read_until, write_all)。
imp::query_console_default_colors619–629 ↗
fn query_console_default_colors(output: HANDLE) -> io::Result<Option<DefaultColors>>
作用:Windows 下在 OSC 查询不可用时,从控制台自己的颜色表里读默认颜色。
数据流:进去的是输出句柄。它调用 GetConsoleScreenBufferInfoEx 取得控制台属性和 16 色颜色表,再交给 imp::decode_console_default_colors 解码。出来是 DefaultColors 或系统错误。
调用关系:imp::default_colors 在 OSC 路线失败时调用它作为兜底。它不需要终端回复,直接问 Windows 控制台当前配置。
调用图:外部调用 3 个(last_os_error, decode_console_default_colors, GetConsoleScreenBufferInfoEx)。
imp::decode_console_default_colors631–640 ↗
fn decode_console_default_colors(attributes: u16, color_table: &[u32; 16]) -> DefaultColors
作用:把 Windows 控制台属性里的前景色和背景色索引,翻译成真正的 RGB 颜色。
数据流:进去的是属性位和 16 项颜色表。它从低 4 位取前景色索引,从高 4 位取背景色索引,再用 imp::decode_color_ref 转成红绿蓝三元组。出来是 DefaultColors。
调用关系:imp::query_console_default_colors 读取到原始控制台信息后调用它。相关测试会确认它正确处理普通颜色、亮色和反色视频标志。
调用图:外部调用 1 个(decode_color_ref)。
imp::decode_color_ref642–648 ↗
fn decode_color_ref(color_ref: u32) -> (u8, u8, u8)
作用:把 Windows 的 COLORREF 数字拆成红、绿、蓝三个 8 位颜色值。
数据流:进去的是一个 u32 颜色数字。它按 Windows 的字节顺序取出低位红色、中间绿色、高位蓝色。出来是 (r, g, b)。
调用关系:imp::decode_console_default_colors 用它解码颜色表中的每一项。它是 Windows 兜底颜色读取里的最小转换步骤。
imp::std_handle650–656 ↗
fn std_handle(kind: u32) -> io::Result<HANDLE>
作用:取得 Windows 的标准输入或标准输出句柄,并检查它是否有效。
数据流:进去的是句柄种类,比如标准输入或标准输出。它调用 GetStdHandle,如果返回空或 INVALID_HANDLE_VALUE 就转成系统错误;否则返回 HANDLE。出来是可用句柄或错误。
调用关系:imp::default_colors 用它拿到输入输出通道。拿不到时,颜色探测会直接放弃或走可用的兜底。
调用图:外部调用 2 个(last_os_error, GetStdHandle)。
imp::VirtualTerminalInputMode::enable664–679 ↗
fn enable(handle: HANDLE) -> io::Result<Self>
作用:临时打开 Windows 控制台的虚拟终端输入模式,让它能把 ESC 控制码回复交给程序读。
数据流:进去的是输入句柄。它先读取原来的控制台模式,再加上 ENABLE_VIRTUAL_TERMINAL_INPUT 并设置回去。出来是一个 VirtualTerminalInputMode 对象,里面保存原模式以便恢复。
调用关系:imp::query_osc_default_colors 在发送 OSC 查询前调用它。对象离开作用域时,imp::VirtualTerminalInputMode::drop 会自动恢复原模式。
调用图:外部调用 3 个(last_os_error, GetConsoleMode, SetConsoleMode)。
imp::VirtualTerminalInputMode::drop683–687 ↗
fn drop(&mut self)
作用:清理临时开启的 Windows 虚拟终端输入模式,把控制台模式恢复成原来的样子。
数据流:进去的是对象里保存的句柄和 original_mode。它调用 SetConsoleMode 恢复原模式,不返回结果。出来的效果是控制台状态尽量回到探测前。
调用关系:它和 imp::VirtualTerminalInputMode::enable 成对出现。颜色 OSC 探测结束、对象销毁时自动执行,避免影响后续正常输入。
调用图:外部调用 1 个(SetConsoleMode)。
imp::write_all690–711 ↗
fn write_all(handle: HANDLE, mut bytes: &[u8]) -> io::Result<()>
作用:Windows 下把一整段字节完整写到控制台输出句柄。它保证不会只写一半就算成功。
数据流:进去的是输出 HANDLE 和要写的字节。它循环调用 WriteFile,每次写掉一部分;如果系统报错或一次写入 0 字节,就返回错误。出来是成功或写入错误。
调用关系:imp::query_osc_default_colors 用它发送 OSC 颜色查询。它是 Windows 版本里对应 Unix Tty 写入的小工具。
调用图:外部调用 4 个(from, last_os_error, null_mut, WriteFile)。
imp::read_until713–739 ↗
fn read_until(
handle: HANDLE,
timeout: Duration,
mut parse: impl FnMut(&[u8]) -> Option<T>,
) -> io::Result<Option<T>>
作用:Windows 下在截止时间内读取控制台输入,直到解析器认出目标回复或时间耗尽。
数据流:进去的是输入 HANDLE、超时时间和一个解析函数。它先用解析函数检查缓冲区;没结果就用 WaitForSingleObject 等输入,再通过 imp::read_once 追加数据。出来是解析出的值、None 或错误。
调用关系:imp::query_osc_default_colors 调用它等待颜色回复。它和 parse_default_colors 配合,把字节流变成 DefaultColors。
调用图:外部调用 7 个(now, new, last_os_error, poll_readable, read_available, read_once, WaitForSingleObject)。
imp::read_once741–758 ↗
fn read_once(handle: HANDLE, buffer: &mut Vec<u8>) -> io::Result<()>
作用:Windows 下从控制台输入句柄读一次数据,并追加到缓冲区。
数据流:进去的是输入 HANDLE 和可修改的字节缓冲区。它调用 ReadFile 最多读 256 字节,把实际读到的部分追加进去。出来是成功或系统错误,缓冲区可能变长。
调用关系:imp::read_until 在发现句柄可读后调用它。它只负责一次读取,是否继续等由外层循环决定。
调用图:外部调用 3 个(last_os_error, null_mut, ReadFile)。
imp::tests::color_table766–772 ↗
fn color_table() -> [u32; 16]
作用:给 Windows 颜色解码测试提供一张固定的 16 色颜色表。
数据流:进去没有参数。它返回一个包含 16 个 COLORREF 数字的数组,代表黑、红、绿、亮色等常见控制台颜色。出来是测试用颜色表。
调用关系:多个 Windows 解码测试调用它,避免每个测试重复写同一张表。
imp::tests::decodes_console_color_attribute_indices775–783 ↗
fn decodes_console_color_attribute_indices()
作用:测试 Windows 控制台属性中的前景色和背景色索引能被正确取出。
数据流:进去的是测试里写死的属性值和颜色表。测试调用 imp::decode_console_default_colors,对比期望的前景和背景 RGB。出来是测试通过或失败。
调用关系:它保护 imp::decode_console_default_colors 的基础索引逻辑,防止前景和背景读错。
调用图:外部调用 1 个(assert_eq!)。
imp::tests::decodes_console_color_intensity_indices786–794 ↗
fn decodes_console_color_intensity_indices()
作用:测试 Windows 控制台的高亮颜色索引也能正确解码。
数据流:进去的是带高位颜色索引的属性值和颜色表。测试解码后检查亮红、亮青等颜色是否符合预期。出来是测试结果。
调用关系:它补充覆盖 imp::decode_console_default_colors,确保亮色不被当成普通颜色。
调用图:外部调用 1 个(assert_eq!)。
imp::tests::decodes_console_color_ref_byte_order797–809 ↗
fn decodes_console_color_ref_byte_order()
作用:测试 Windows COLORREF 的字节顺序没有被读反。
数据流:进去的是一张修改过的测试颜色表。测试调用解码函数,确认 0x00112233 会变成红 0x33、绿 0x22、蓝 0x11。出来是测试通过或失败。
调用关系:它直接保护 imp::decode_color_ref 的约定。若字节顺序错了,默认颜色会显示成错误色调。
调用图:外部调用 2 个(assert_eq!, color_table)。
imp::tests::ignores_reverse_video_when_decoding_default_colors812–823 ↗
fn ignores_reverse_video_when_decoding_default_colors()
作用:测试解码默认颜色时不会因为反色视频标志而交换前景和背景。
数据流:进去的是带 COMMON_LVB_REVERSE_VIDEO 标志的属性值。测试解码后仍然期望使用属性里的原始前景和背景索引。出来是测试结果。
调用关系:它验证 imp::decode_console_default_colors 的一个有意选择:这里要的是配置默认颜色,不是当前单元格实际反转后的显示效果。
调用图:外部调用 1 个(assert_eq!)。
parse_osc_color827–835 ↗
fn parse_osc_color(buffer: &[u8], slot: u8) -> Option<(u8, u8, u8)>
作用:从一段终端回复里提取某一个 OSC 颜色槽位的 RGB 值。槽位 10 通常是默认前景色,11 通常是默认背景色。
数据流:进去的是字节缓冲区和槽位号。它先找 ESC]slot; 这个前缀,再找 OSC 结束符,然后把中间文本按 UTF-8 解释并交给 parse_osc_rgb。出来是 RGB 三元组或 None。
调用关系:parse_default_colors 会分别用它查前景色和背景色。它把查找、截取和颜色格式解析串起来。
调用图:调用 3 个内部函数(find_subslice, osc_payload_end, parse_osc_rgb);被 1 处调用(parse_default_colors);外部调用 2 个(format!, from_utf8)。
parse_default_colors837–841 ↗
fn parse_default_colors(buffer: &[u8]) -> Option<DefaultColors>
作用:从同一段终端回复中同时解析默认前景色和默认背景色。只有两者都拿到时才算成功。
数据流:进去的是累计字节缓冲区。它调用 parse_osc_color 查槽位 10,再查槽位 11;两项都存在时组装 DefaultColors,否则返回 None。出来是可选 DefaultColors。
调用关系:启动探测和颜色查询读取循环都会把它当解析器使用。它保证调色所需的一对颜色不会只拿半套。
调用图:调用 1 个内部函数(parse_osc_color);被 1 处调用(update_startup_probe)。
osc_payload_end843–853 ↗
fn osc_payload_end(buffer: &[u8]) -> Option<(usize, usize)>
作用:找出 OSC 回复正文在哪里结束。OSC 结束可能是响铃字符 BEL,也可能是 ESC 反斜杠 ST。
数据流:进去的是 OSC 前缀之后的字节。它从头扫描,遇到 0x07 就返回正文结束位置和 1 字节结束符长度,遇到 ESC\ 就返回结束位置和 2 字节长度。找不到结束符返回 None。
调用关系:parse_osc_color 用它截出完整颜色正文。没有完整结束符时,解析会等待更多数据而不是误读半截回复。
调用图:被 1 处调用(parse_osc_color)。
parse_osc_rgb855–869 ↗
fn parse_osc_rgb(payload: &str) -> Option<(u8, u8, u8)>
作用:把 OSC 颜色正文里的 rgb 或 rgba 文本转成 8 位 RGB 数值。
数据流:进去的是类似 rgb:ffff/8000/0000 或 rgba:00/80/ff/ff 的字符串。它检查前缀是不是 rgb 或 rgba,逐段解析红绿蓝;rgba 会额外解析但丢弃透明度。出来是 (r, g, b) 或 None。
调用关系:parse_osc_color 截出正文后调用它。它再把每个颜色分量交给 parse_osc_component。
调用图:调用 1 个内部函数(parse_osc_component);被 1 处调用(parse_osc_color)。
parse_osc_component871–879 ↗
fn parse_osc_component(component: &str) -> Option<u8>
作用:解析一个颜色分量,比如 00、ff、8000 或 ffff,并统一变成 0 到 255 的数。
数据流:进去的是一个十六进制字符串。长度为 2 时直接按 8 位解析;长度为 4 时按 16 位解析后除以 257 缩放到 8 位;其他长度返回 None。出来是 u8 或 None。
调用关系:parse_osc_rgb 会对红、绿、蓝以及可选 alpha 分量调用它。它保证不同终端返回的 2 位或 4 位颜色格式都能被理解。
调用图:被 1 处调用(parse_osc_rgb);外部调用 2 个(from_str_radix, from_str_radix)。
find_subslice881–885 ↗
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize>
作用:在一段字节里找某个小字节串第一次出现的位置。
数据流:进去的是大字节串和要找的小字节串。它用滑动窗口逐个比较,找到就返回起始下标,找不到就返回 None。出来是可选位置。
调用关系:parse_osc_color 用它定位 OSC 颜色回复的前缀。它是通用查找工具,不关心颜色含义。
调用图:被 1 处调用(parse_osc_color)。
tests::parses_osc_colors_with_bel_and_st896–905 ↗
fn parses_osc_colors_with_bel_and_st()
作用:测试 OSC 颜色解析能识别两种常见结束符:BEL 和 ST。
数据流:进去的是两段模拟颜色回复。测试调用 parse_osc_color,并检查解析出的 RGB 是否正确。出来是测试通过或失败。
调用关系:它保护 parse_osc_color 和 osc_payload_end 的配合,确保不同终端的结束方式都能用。
调用图:外部调用 1 个(assert_eq!)。
tests::parses_two_and_four_digit_color_components908–914 ↗
fn parses_two_and_four_digit_color_components()
作用:测试颜色分量既支持两位十六进制,也支持四位十六进制。
数据流:进去的是 rgb 和 rgba 示例字符串。测试调用 parse_osc_rgb,确认 00/80/ff 和 ffff/8000/0000 都能转成正确 RGB。出来是测试结果。
调用关系:它覆盖 parse_osc_rgb 和 parse_osc_component,保证不同精度的终端颜色回复不会被误拒。
调用图:外部调用 1 个(assert_eq!)。
tests::parses_default_colors_from_one_buffer917–936 ↗
fn parses_default_colors_from_one_buffer()
作用:测试默认前景色和背景色能从同一段缓冲区里解析出来,而且顺序可以不同。
数据流:进去的是包含槽位 10 和 11 的模拟字节,有前景在前、背景在前等情况。测试调用 parse_default_colors 并比较结果;如果只有一项则期望 None。出来是测试结果。
调用关系:它保护 parse_default_colors 的核心行为:必须成对拿到颜色,并且不依赖回复顺序。
调用图:外部调用 1 个(assert_eq!)。
tests::ignores_malformed_or_partial_default_color_responses939–952 ↗
fn ignores_malformed_or_partial_default_color_responses()
作用:测试格式错误或没收完整的颜色回复不会被当成有效默认颜色。
数据流:进去的是几段坏数据,比如颜色文本不合法、分量数量不对、缺少结束符。测试调用 parse_default_colors,期望全部返回 None。出来是测试通过或失败。
调用关系:它让解析器保持保守,避免把脏数据变成错误主题颜色。
调用图:外部调用 1 个(assert_eq!)。
tui/src/tui/keyboard_modes.rs源码 ↗
终端程序想读懂复杂按键,不能只靠普通字符流,还要临时打开一些“增强键盘上报”模式。这个文件就是开关这些模式的地方。它先判断当前环境是否适合开启:比如用户设置了环境变量要求关闭,或者在 WSL(Windows 里的 Linux 环境)加 VS Code 终端里运行,可能会有兼容问题,就会自动避开。真正开启时,它用 crossterm(Rust 的终端控制库)向终端写入 ANSI 转义序列,也就是一串终端能看懂的控制字符。tmux(终端复用器,像一个终端里的多窗口工具)比较特殊,只有确认它支持 csi-u 格式时才额外打开 modifyOtherKeys。退出或恢复时,它会弹出之前压入的键盘设置,并发送更强的重置命令,避免父 shell 继承这些奇怪的按键模式。
keyboard_enhancement_disabled18–23 ↗
fn keyboard_enhancement_disabled() -> bool
作用:判断这次运行是否应该关闭键盘增强功能。它把用户显式设置、WSL 环境、VS Code 终端兼容性这些因素合在一起看。
数据流:进去的是当前进程的环境变量和运行环境信息 → 它读取 CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT,并询问是否在 WSL、是否在 VS Code 终端里 → 出来一个 true 或 false,true 表示后面不要打开增强键盘模式。
调用关系:初始化和 enable_keyboard_enhancement 会先问它一句“能不能开”。它自己把具体判断交给 running_in_wsl、running_in_vscode_terminal 和 keyboard_enhancement_disabled_for。
调用图:调用 3 个内部函数(keyboard_enhancement_disabled_for, running_in_vscode_terminal, running_in_wsl);被 2 处调用(init, enable_keyboard_enhancement);外部调用 1 个(var)。
keyboard_enhancement_disabled_for25–38 ↗
fn keyboard_enhancement_disabled_for(
disable_env: Option<&str>,
is_wsl: bool,
is_vscode_terminal: bool,
) -> bool
作用:把“是否禁用键盘增强”的规则集中成一个纯判断。这样真实环境和测试都能用同一套规则。
数据流:进去的是可选的环境变量文本、是否 WSL、是否 VS Code 终端 → 它先用 parse_bool_env 看用户有没有明确说开或关 → 如果没有明确设置,就在“WSL + VS Code 终端”时自动禁用。
调用关系:keyboard_enhancement_disabled 收集完环境信息后调用它。它只负责判断规则,解析布尔值的细节交给 parse_bool_env。
调用图:调用 1 个内部函数(parse_bool_env);被 1 处调用(keyboard_enhancement_disabled)。
parse_bool_env40–50 ↗
fn parse_bool_env(value: Option<&str>) -> Option<bool>
作用:把环境变量里的常见开关写法转成真假值。比如 1、true、yes 表示开,0、false、no 表示关。
数据流:进去的是一个可能不存在的字符串 → 它先去掉前后空白,再不区分大小写地识别常见写法 → 出来 Some(true)、Some(false),或者 None 表示看不懂。
调用关系:它被 keyboard_enhancement_disabled_for 用来判断用户是否明确覆盖自动检测结果。
调用图:被 1 处调用(keyboard_enhancement_disabled_for)。
running_in_wsl52–62 ↗
fn running_in_wsl() -> bool
作用:判断程序是不是跑在 WSL 里。WSL 是 Windows 上的 Linux 子系统,终端行为有时和普通 Linux 不一样。
数据流:进去的是当前操作系统条件 → 在 Linux 上它调用 clipboard_paste::is_probably_wsl 做实际探测;非 Linux 上直接返回 false → 出来一个是否 WSL 的布尔值。
调用关系:keyboard_enhancement_disabled 会用它来决定是否进入 WSL 相关的兼容性分支。
调用图:调用 1 个内部函数(is_probably_wsl);被 1 处调用(keyboard_enhancement_disabled)。
running_in_vscode_terminal64–69 ↗
fn running_in_vscode_terminal() -> bool
作用:判断当前终端是不是 VS Code 内置终端。它不只看 Linux 这边的变量,还会在 WSL 场景下尝试看 Windows 那边的变量。
数据流:进去的是当前环境中的 TERM_PROGRAM,以及 windows_term_program 读到的 Windows 侧 TERM_PROGRAM → 它把两边结果交给 vscode_terminal_detected → 出来 true 表示检测到 VS Code 终端。
调用关系:keyboard_enhancement_disabled 调用它来决定是否自动关闭键盘增强。它内部会调用 windows_term_program 和 vscode_terminal_detected;调用图里也把它标成与自身检测流程相关。
调用图:调用 2 个内部函数(vscode_terminal_detected, windows_term_program);被 2 处调用(keyboard_enhancement_disabled, running_in_vscode_terminal);外部调用 1 个(var)。
vscode_terminal_detected71–76 ↗
fn vscode_terminal_detected(
linux_term_program: Option<&str>,
windows_term_program: Option<&str>,
) -> bool
作用:把 Linux 侧和 Windows 侧的 TERM_PROGRAM 放在一起判断,只要有一边写着 vscode,就认为是 VS Code 终端。
数据流:进去的是两个可选字符串 → 它分别交给 term_program_is_vscode 检查 → 出来一个布尔值,表示是否命中 VS Code。
调用关系:running_in_vscode_terminal 收集到两边信息后调用它。它把单个字符串的比较交给 term_program_is_vscode。
调用图:调用 1 个内部函数(term_program_is_vscode);被 1 处调用(running_in_vscode_terminal)。
term_program_is_vscode78–80 ↗
fn term_program_is_vscode(value: Option<&str>) -> bool
作用:判断一个 TERM_PROGRAM 值是不是 vscode。它忽略大小写,避免 VSCode、vscode 这种写法差异带来误判。
数据流:进去的是一个可能没有值的字符串 → 如果有值,就和 vscode 做不区分大小写的比较 → 出来 true 或 false。
调用关系:vscode_terminal_detected 用它分别检查 Linux 侧和 Windows 侧的终端标识。
调用图:被 1 处调用(vscode_terminal_detected)。
windows_term_program82–96 ↗
fn windows_term_program() -> Option<String>
作用:在 Linux 上尝试读取 Windows 那边的 TERM_PROGRAM,并把结果缓存起来。缓存的意思是第一次查完后记住,后面不用反复启动 cmd.exe。
数据流:进去的是当前平台条件 → Linux 上通过 OnceLock(一种只初始化一次的保险箱)保存读到的结果;非 Linux 上直接没有结果 → 出来一个可选字符串。
调用关系:running_in_vscode_terminal 会调用它补充 Windows 侧信息。Linux 下真正读取 Windows 环境的活由 read_windows_term_program 完成。
调用图:被 1 处调用(running_in_vscode_terminal);外部调用 1 个(new)。
read_windows_term_program99–119 ↗
fn read_windows_term_program() -> Option<String>
作用:在 WSL/Linux 场景下通过 cmd.exe 去问 Windows:TERM_PROGRAM 是什么。这样能发现 Linux 环境变量里看不到的 VS Code 终端信息。
数据流:进去的是系统上可用的 cmd.exe → 它运行 set TERM_PROGRAM,忽略标准输入和错误输出,读取命令输出 → 如果找到 TERM_PROGRAM=... 且值不空,就返回这个值,否则返回 None。
调用关系:它是 windows_term_program 背后的底层读取动作。为了避免频繁启动外部命令,外层会缓存它的结果。
调用图:外部调用 3 个(from_utf8_lossy, new, null)。
enable_keyboard_enhancement121–139 ↗
fn enable_keyboard_enhancement()
作用:真正打开 TUI 需要的增强键盘上报模式。没有它,程序可能分不清一些特殊键和组合键。
数据流:进去的是当前终端和环境状态 → 它先问 keyboard_enhancement_disabled 是否应该跳过;如果可以开启,就向 stdout 写入禁用旧 modifyOtherKeys、压入 crossterm 键盘增强标志等控制命令;如果 tmux 条件满足,再额外打开 modifyOtherKeys 模式 → 没有返回值,但会改变终端的键盘上报方式。
调用关系:set_modes 会在接管终端时调用它。它把是否禁用交给 keyboard_enhancement_disabled,把 tmux 特殊判断交给 tmux_should_enable_modify_other_keys,最后通过 execute! 把命令写给终端。
调用图:调用 2 个内部函数(keyboard_enhancement_disabled, tmux_should_enable_modify_other_keys);被 1 处调用(set_modes);外部调用 1 个(execute!)。
running_in_tmux_session141–146 ↗
fn running_in_tmux_session() -> bool
作用:判断当前是不是在 tmux 会话里。tmux 会夹在程序和真实终端之间,所以键盘上报要特别谨慎。
数据流:进去的是当前环境变量 TMUX 和 TMUX_PANE → 它把这两个值交给 tmux_session_detected → 出来 true 表示看起来在 tmux 中。
调用关系:tmux_should_enable_modify_other_keys 会调用它作为是否考虑 tmux 特殊设置的第一步。
调用图:调用 1 个内部函数(tmux_session_detected);被 1 处调用(tmux_should_enable_modify_other_keys);外部调用 1 个(var)。
tmux_session_detected148–150 ↗
fn tmux_session_detected(tmux: Option<&str>, tmux_pane: Option<&str>) -> bool
作用:用最简单的规则判断 tmux:TMUX 或 TMUX_PANE 只要有一个存在,就认为在 tmux 里。
数据流:进去的是两个可选环境变量值 → 它检查任意一个是否存在 → 出来是否检测到 tmux。
调用关系:running_in_tmux_session 读取环境变量后调用它。测试也用它验证 tmux 判断规则。
调用图:被 1 处调用(running_in_tmux_session)。
tmux_should_enable_modify_other_keys152–157 ↗
fn tmux_should_enable_modify_other_keys() -> bool
作用:决定在 tmux 里是否要打开 modifyOtherKeys。modifyOtherKeys 是一种让终端更详细报告组合键的模式,但旧 tmux 支持不好,不能乱开。
数据流:进去的是当前 tmux 环境和 tmux 配置 → 它先判断是否在 tmux,再读取 extended-keys-format → 交给 tmux_should_enable_modify_other_keys_for 做最终判断 → 出来 true 才会允许打开额外模式。
调用关系:enable_keyboard_enhancement 在基础键盘增强开启后调用它。它把环境检测交给 running_in_tmux_session,把配置读取交给 read_tmux_extended_keys_format,把规则判断交给 tmux_should_enable_modify_other_keys_for。
调用图:调用 3 个内部函数(read_tmux_extended_keys_format, running_in_tmux_session, tmux_should_enable_modify_other_keys_for);被 1 处调用(enable_keyboard_enhancement)。
tmux_should_enable_modify_other_keys_for159–167 ↗
fn tmux_should_enable_modify_other_keys_for(
running_in_tmux_session: bool,
extended_keys_format: Option<&str>,
) -> bool
作用:执行 tmux 下是否打开 modifyOtherKeys 的核心规则:必须真的在 tmux 中,并且 tmux 明确说格式是 csi-u。
数据流:进去的是“是否在 tmux”和 extended-keys-format 的值 → 它只接受 Some("csi-u") 这种确认过的格式 → 出来 true 或 false。
调用关系:tmux_should_enable_modify_other_keys 收集好现场信息后调用它。这样规则本身容易测试,不需要真的启动 tmux。
调用图:被 1 处调用(tmux_should_enable_modify_other_keys);外部调用 1 个(matches!)。
read_tmux_extended_keys_format169–195 ↗
fn read_tmux_extended_keys_format() -> Option<String>
作用:询问 tmux 当前使用哪种扩展按键格式。这个答案决定程序能不能安全打开额外的组合键上报。
数据流:进去的是可执行的 tmux 命令 → 它依次尝试 display-message 和 show-options 两种问法,忽略输入和错误输出 → 如果命令成功并返回非空文本,就返回这个格式名;都失败就返回 None。
调用关系:tmux_should_enable_modify_other_keys 调用它来拿 tmux 的真实配置,再把结果交给规则函数判断。
调用图:被 1 处调用(tmux_should_enable_modify_other_keys);外部调用 3 个(from_utf8, new, null)。
restore_keyboard_enhancement_stack197–203 ↗
fn restore_keyboard_enhancement_stack()
作用:在 TUI 暂时放开终端或恢复普通模式时,把之前压入的键盘增强设置弹出来。像把借来的设置还回去。
数据流:进去的是当前终端输出流 → 它向 stdout 写入 PopKeyboardEnhancementFlags 和 DisableModifyOtherKeys → 没有返回值,但会让终端回退一层键盘设置并关闭 modifyOtherKeys。
调用关系:restore_common 会在恢复公共终端状态时调用它。它通过 execute! 直接把恢复命令发给终端。
调用图:被 1 处调用(restore_common);外部调用 1 个(execute!)。
reset_keyboard_reporting_after_exit205–212 ↗
fn reset_keyboard_reporting_after_exit()
作用:进程退出后做更强的键盘上报重置,防止用户的 shell 继续处在增强按键模式里。
数据流:进去的是当前终端输出流 → 它写入弹出增强标志、全量重置增强标志、关闭 modifyOtherKeys 三个命令 → 没有返回值,但会尽量把键盘报告恢复到干净状态。
调用关系:restore_common 会在退出清理时调用它。它比 restore_keyboard_enhancement_stack 更强,因为退出时要保护父 shell。
调用图:被 1 处调用(restore_common);外部调用 1 个(execute!)。
ResetKeyboardEnhancementFlags::write_ansi218–220 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:把“重置所有键盘增强标志”的终端控制码写出来。ANSI 控制码就是终端能看懂的特殊文本命令。
数据流:进去的是一个可写的文本缓冲区 → 它写入 ESC[<u 对应的控制序列 → 输出缓冲区多了一段重置命令。
调用关系:execute! 在发送 ResetKeyboardEnhancementFlags 时会调用它,把这个自定义命令变成实际写给终端的字节。
调用图:外部调用 1 个(write_str)。
ResetKeyboardEnhancementFlags::execute_winapi223–228 ↗
fn execute_winapi(&self) -> std::io::Result<()>
作用:说明这个重置命令不支持老式 Windows API。它不是静默假装成功,而是明确返回“不支持”。
数据流:进去的是 Windows 旧 API 执行请求 → 它创建一个 Unsupported 错误 → 出来是失败结果。
调用关系:这是 crossterm Command 接口在 Windows 下需要的实现。正常 ANSI 路径会用 write_ansi;旧 Windows API 路径会得到这个错误。
调用图:外部调用 1 个(new)。
ResetKeyboardEnhancementFlags::is_ansi_code_supported231–233 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉 crossterm:在老式 Windows API 路径下,这个命令不能用 ANSI 方式兜底。
数据流:进去没有额外数据 → 它直接返回 false → 调用方知道这个命令在该路径不受支持。
调用关系:这是 ResetKeyboardEnhancementFlags 作为 crossterm Command 的一部分,配合 Windows 相关执行逻辑使用。
EnableModifyOtherKeys::write_ansi240–242 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:写出打开 modifyOtherKeys 模式 2 的终端控制码。这个模式让终端更详细地报告带 Ctrl、Alt 等修饰键的按键。
数据流:进去的是可写文本缓冲区 → 它写入 ESC[>4;2m 对应的控制序列 → 缓冲区里出现开启命令。
调用关系:enable_keyboard_enhancement 在 tmux 条件合适时会通过 execute! 发送 EnableModifyOtherKeys,最终调用这个方法写出控制码。
调用图:外部调用 1 个(write_str)。
EnableModifyOtherKeys::execute_winapi245–250 ↗
fn execute_winapi(&self) -> std::io::Result<()>
作用:说明打开 modifyOtherKeys 不支持老式 Windows API。这样程序不会误以为已经成功开启。
数据流:进去的是 Windows 旧 API 执行请求 → 它创建 Unsupported 错误 → 出来是失败结果。
调用关系:这是 EnableModifyOtherKeys 实现 crossterm Command 所需的 Windows 分支;常规终端会走 ANSI 写入。
调用图:外部调用 1 个(new)。
EnableModifyOtherKeys::is_ansi_code_supported253–255 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉 crossterm,这个命令在老式 Windows API 路径下不支持 ANSI 代码。
数据流:进去没有额外数据 → 直接返回 false → 调用方据此知道不能在该路径执行。
调用关系:它和 EnableModifyOtherKeys::execute_winapi 一起描述 Windows 旧终端能力边界。
DisableModifyOtherKeys::write_ansi262–264 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:写出关闭 modifyOtherKeys 的终端控制码。它用于进入前先清理旧状态,也用于退出时恢复终端。
数据流:进去的是可写文本缓冲区 → 它写入 ESC[>4;0m 对应的控制序列 → 缓冲区里出现关闭命令。
调用关系:enable_keyboard_enhancement、restore_keyboard_enhancement_stack 和 reset_keyboard_reporting_after_exit 都会通过 execute! 发送这个命令。
调用图:外部调用 1 个(write_str)。
DisableModifyOtherKeys::execute_winapi267–272 ↗
fn execute_winapi(&self) -> std::io::Result<()>
作用:说明关闭 modifyOtherKeys 不支持老式 Windows API。这个命令依赖终端理解 ANSI 控制序列。
数据流:进去的是 Windows 旧 API 执行请求 → 它创建 Unsupported 错误 → 出来是失败结果。
调用关系:这是 DisableModifyOtherKeys 作为 crossterm Command 的 Windows 分支实现;正常情况下走 write_ansi。
调用图:外部调用 1 个(new)。
DisableModifyOtherKeys::is_ansi_code_supported275–277 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉 crossterm,在老式 Windows API 路径下这个命令不支持 ANSI 执行。
数据流:进去没有额外数据 → 直接返回 false → 调用方知道不能靠该路径完成关闭操作。
调用关系:它配合 DisableModifyOtherKeys::execute_winapi,明确这个自定义终端命令的兼容范围。
tests::ansi_for293–297 ↗
fn ansi_for(command: impl Command) -> String
作用:测试用的小工具:把一个终端命令转成它会写出的 ANSI 字符串,方便断言是否正确。
数据流:进去的是一个实现了 Command 的命令对象 → 它创建空字符串,让命令把 ANSI 文本写进去 → 出来是这段 ANSI 文本。
调用关系:后面的测试用它检查 ResetKeyboardEnhancementFlags、EnableModifyOtherKeys、DisableModifyOtherKeys 写出的控制码是否符合预期。
调用图:外部调用 2 个(new, write_ansi)。
tests::keyboard_enhancement_env_flag_parses_common_values300–309 ↗
fn keyboard_enhancement_env_flag_parses_common_values()
作用:测试环境变量开关的常见写法都能被正确识别。这样用户写 true、YES、0 等都不会产生意外。
数据流:进去的是一组示例字符串和空值 → 它调用 parse_bool_env 并用断言比较结果 → 测试通过表示解析规则稳定。
调用关系:它专门保护 parse_bool_env,间接保证 keyboard_enhancement_disabled_for 能正确听懂用户配置。
调用图:外部调用 1 个(assert_eq!)。
tests::keyboard_enhancement_auto_disables_for_vscode_in_wsl312–316 ↗
fn keyboard_enhancement_auto_disables_for_vscode_in_wsl()
作用:测试在 WSL 加 VS Code 终端的组合下,会自动关闭键盘增强。这个组合被认为有兼容风险。
数据流:进去的是没有环境变量覆盖、is_wsl 为 true、is_vscode_terminal 为 true 的模拟条件 → 它调用 keyboard_enhancement_disabled_for → 断言结果必须是 true。
调用关系:它保护自动禁用规则,确保真实的 keyboard_enhancement_disabled 在这种环境下会选择保守做法。
调用图:外部调用 1 个(assert!)。
tests::keyboard_enhancement_auto_disable_requires_wsl_and_vscode319–326 ↗
fn keyboard_enhancement_auto_disable_requires_wsl_and_vscode()
作用:测试自动禁用必须同时满足 WSL 和 VS Code 终端两个条件。只有其中一个不够。
数据流:进去的是两组模拟条件:只有 WSL、只有 VS Code → 它调用 keyboard_enhancement_disabled_for → 断言两种情况都不自动禁用。
调用关系:它防止自动禁用规则过宽,避免普通 WSL 或普通 VS Code 终端被不必要地降级。
调用图:外部调用 1 个(assert!)。
tests::keyboard_enhancement_env_flag_overrides_auto_detection329–340 ↗
fn keyboard_enhancement_env_flag_overrides_auto_detection()
作用:测试用户环境变量的显式设置优先于自动检测。用户说开就开,用户说关就关。
数据流:进去的是带 0 或 1 的环境变量值,以及不同的模拟环境 → 它调用 keyboard_enhancement_disabled_for → 断言 0 会取消自动禁用,1 会强制禁用。
调用关系:它保护 keyboard_enhancement_disabled_for 的优先级规则,保证用户配置比环境猜测更有权威。
调用图:外部调用 1 个(assert!)。
tests::vscode_terminal_detection_uses_linux_and_windows_term_program343–359 ↗
fn vscode_terminal_detection_uses_linux_and_windows_term_program()
作用:测试 VS Code 终端检测会同时看 Linux 侧和 Windows 侧的 TERM_PROGRAM。特别是 WSL 里,Windows 侧信息可能更可靠。
数据流:进去的是几组 Linux/Windows TERM_PROGRAM 示例 → 它调用 vscode_terminal_detected → 断言 vscode 能命中,WindowsTerminal 或空值不能命中。
调用关系:它保护 vscode_terminal_detected,进而保护 running_in_vscode_terminal 的判断准确性。
调用图:外部调用 1 个(assert!)。
tests::tmux_session_detection_accepts_tmux_or_tmux_pane362–371 ↗
fn tmux_session_detection_accepts_tmux_or_tmux_pane()
作用:测试 tmux 检测只要看到 TMUX 或 TMUX_PANE 任意一个变量就算命中。
数据流:进去的是几组 TMUX 和 TMUX_PANE 示例 → 它调用 tmux_session_detected → 断言有任意一个值时为 true,两个都没有时为 false。
调用关系:它保护 running_in_tmux_session 使用的核心判断规则,避免 tmux 场景漏检。
调用图:外部调用 1 个(assert!)。
tests::tmux_modify_other_keys_only_requests_confirmed_csi_u_format374–394 ↗
fn tmux_modify_other_keys_only_requests_confirmed_csi_u_format()
作用:测试 tmux 下只有确认 extended-keys-format 是 csi-u 时才打开 modifyOtherKeys。这样可以避开旧 tmux 或不兼容格式。
数据流:进去的是多组“是否在 tmux”和格式值 → 它调用 tmux_should_enable_modify_other_keys_for → 断言只有 true 加 csi-u 这一组允许开启。
调用关系:它保护 tmux_should_enable_modify_other_keys_for,确保 enable_keyboard_enhancement 不会在不安全的 tmux 环境里乱发开启命令。
调用图:外部调用 1 个(assert!)。
tests::reset_keyboard_enhancement_flags_clears_all_pushed_levels397–399 ↗
fn reset_keyboard_enhancement_flags_clears_all_pushed_levels()
作用:测试重置键盘增强标志的命令会写出预期控制码。
数据流:进去的是 ResetKeyboardEnhancementFlags 命令 → 它用 ansi_for 转成字符串 → 断言结果等于 ESC[<u 对应的序列。
调用关系:它保护 ResetKeyboardEnhancementFlags::write_ansi,确保退出清理时发给终端的重置命令没有写错。
调用图:外部调用 1 个(assert_eq!)。
tests::enable_modify_other_keys_requests_xterm_keyboard_reporting402–404 ↗
fn enable_modify_other_keys_requests_xterm_keyboard_reporting()
作用:测试开启 modifyOtherKeys 的命令会写出正确控制码。
数据流:进去的是 EnableModifyOtherKeys 命令 → 它用 ansi_for 得到 ANSI 字符串 → 断言结果等于 ESC[>4;2m 对应的序列。
调用关系:它保护 EnableModifyOtherKeys::write_ansi,确保 tmux 条件满足时真正发送的是模式 2 开启命令。
调用图:外部调用 1 个(assert_eq!)。
tests::disable_modify_other_keys_resets_xterm_keyboard_reporting407–409 ↗
fn disable_modify_other_keys_resets_xterm_keyboard_reporting()
作用:测试关闭 modifyOtherKeys 的命令会写出正确控制码。
数据流:进去的是 DisableModifyOtherKeys 命令 → 它用 ansi_for 得到 ANSI 字符串 → 断言结果等于 ESC[>4;0m 对应的序列。
调用关系:它保护 DisableModifyOtherKeys::write_ansi,确保进入前清理和退出恢复都能正确关闭该模式。
调用图:外部调用 1 个(assert_eq!)。
tui/src/tui/terminal_stderr.rs源码 ↗
有些 macOS 系统库会直接往 stderr(标准错误输出,通常就是终端里的错误信息通道)写东西,而且绕过这个项目自己的界面绘制器。TUI 正在显示时,这些文字会像有人拿笔乱写在屏幕上一样,把输入框或界面弄乱。这个文件的做法是:如果发现 stdout 和 stderr 指向同一个终端,就在 TUI 占用终端期间,把 stderr 这个文件描述符(系统给打开文件或终端的编号)临时改到 /dev/null(像黑洞,写进去就消失)。需要临时放开终端时再恢复,回来时再压住,结束时彻底恢复。它用互斥锁(一把锁,防止两个任务同时改同一份状态)保护全局状态,避免重复安装或恢复出错。非 macOS 上基本是空操作。
TerminalStderrGuard::install45–54 ↗
fn install() -> io::Result<Self>
作用:这是外部开始使用这套保护机制时调用的入口。它会判断当前情况是否真的需要压住 stderr;不需要时就返回一个不工作的保护对象。
数据流:输入是当前进程的 stdout、stderr 状态和操作系统信息。它在 macOS 上先检查 stderr 是否和 stdout 指向同一个终端;如果是,就安装真正的抑制;如果不是,直接产出一个 inactive 的 TerminalStderrGuard,不改任何东西。
调用关系:TUI 初始化时会调用它;测试里的 tests::preserves_stderr_when_already_redirected 也用它确认 stderr 已经被重定向时不会乱改。它把“是否需要处理”的判断交给 stderr_targets_stdout_terminal,把真正动手的部分交给 TerminalStderrGuard::install_suppression。
调用图:调用 1 个内部函数(stderr_targets_stdout_terminal);被 2 处调用(init, preserves_stderr_when_already_redirected);外部调用 1 个(install_suppression)。
TerminalStderrGuard::install_suppression57–68 ↗
fn install_suppression() -> io::Result<Self>
作用:这个函数真正打开“消音”模式。它会把 stderr 临时转到 /dev/null,并记录原来的 stderr,方便之后恢复。
数据流:输入是全局的 stderr 状态。它先拿锁,确认当前没有另一个 TUI 已经占用这套保护;然后调用 suppress_locked 保存原 stderr 并重定向到 /dev/null;最后把 owner_active 标成 true,返回一个 active 的 TerminalStderrGuard。
调用关系:TerminalStderrGuard::install 在需要保护时会用它;测试 tests::suppresses_stderr_only_while_terminal_is_owned 也直接调用它来验证行为。它依赖 lock_state 保证状态安全,再把底层文件描述符操作交给 suppress_locked。
调用图:调用 2 个内部函数(lock_state, suppress_locked);被 1 处调用(suppresses_stderr_only_while_terminal_is_owned);外部调用 1 个(new)。
TerminalStderrGuard::drop72–77 ↗
fn drop(&mut self)
作用:这是保护对象被销毁时自动运行的收尾动作。它保证调用者忘了手动结束时,stderr 也会尽量恢复正常。
数据流:输入是这个 guard 自己的 active 标记。若 active 为 true,它调用 finish 恢复 stderr,并把 active 改成 false;若本来就不 active,则什么也不做。
调用关系:Rust 在 TerminalStderrGuard 生命周期结束时自动调用它。它把实际恢复工作交给 finish,所以这是一道保险,防止 TUI 退出后 stderr 还被留在 /dev/null。
调用图:调用 1 个内部函数(finish)。
pause81–91 ↗
fn pause() -> io::Result<()>
作用:当 TUI 临时把终端还给别的代码用时,这个函数恢复 stderr。这样暂停期间的错误信息能正常显示出来。
数据流:输入是全局状态。它在 macOS 上拿到锁,如果发现当前 TUI 仍是 stderr 保护的拥有者,就调用 restore_locked 把 stderr 指回原来的地方;最后返回成功或系统错误。
调用关系:with_restored 和 suspend_process 这类“临时离开 TUI”的流程会调用它,测试 tests::suppresses_stderr_only_while_terminal_is_owned 也验证它。它和 resume 成对出现:pause 放开,resume 再压住。
调用图:调用 2 个内部函数(lock_state, restore_locked);被 3 处调用(with_restored, suspend_process, suppresses_stderr_only_while_terminal_is_owned)。
resume94–104 ↗
fn resume() -> io::Result<()>
作用:当 TUI 重新拿回终端时,这个函数再次压住 stderr。它防止恢复界面后又被系统库的错误输出打乱。
数据流:输入是全局状态。它拿锁后检查 owner_active;如果 TUI 会话仍然有效,就调用 suppress_locked 再次把 stderr 重定向到 /dev/null;如果没有有效会话,就不动。
调用关系:with_restored 和 suspend_process 在临时放开终端后会调用它,测试也会调用。它接在 pause 之后,把 stderr 从“可见”切回“隐藏”。
调用图:调用 2 个内部函数(lock_state, suppress_locked);被 3 处调用(with_restored, suspend_process, suppresses_stderr_only_while_terminal_is_owned)。
finish107–118 ↗
fn finish() -> io::Result<()>
作用:这是 TUI 会话真正结束时的永久恢复。它把 stderr 放回原处,并标记这次保护已经结束。
数据流:输入是全局状态。它拿锁后,如果 owner_active 为 true,就调用 restore_locked 恢复原 stderr,然后把 owner_active 改成 false;如果已经结束,就什么也不做。
调用关系:restore_after_exit 和 TerminalStderrGuard::drop 会用它,测试也直接验证它。它是整套机制的最终收尾点,确保程序退出 TUI 后错误输出不会继续被吞掉。
调用图:调用 2 个内部函数(lock_state, restore_locked);被 3 处调用(restore_after_exit, drop, suppresses_stderr_only_while_terminal_is_owned)。
lock_state121–125 ↗
fn lock_state() -> io::Result<MutexGuard<'static, StderrState>>
作用:这个函数负责拿到全局 stderr 状态的锁。它让多个地方不能同时改 stderr 的保存和恢复信息。
数据流:输入是全局的 STDERR_STATE 互斥锁。它尝试加锁;成功就返回可以修改状态的 MutexGuard;如果锁已经中毒,也就是之前有线程在持锁时崩了,就返回一个错误。
调用关系:TerminalStderrGuard::install_suppression、pause、resume、finish 都先调用它。它是所有状态修改前的门卫,保证后面的 suppress_locked 和 restore_locked 不会互相踩踏。
调用图:被 4 处调用(install_suppression, finish, pause, resume)。
stderr_targets_stdout_terminal128–146 ↗
fn stderr_targets_stdout_terminal() -> bool
作用:这个函数判断 stderr 和 stdout 是否都连着同一个终端。只有这种情况才需要隐藏 stderr,因为它会直接污染 TUI 所在的屏幕区域。
数据流:输入是当前进程的 stdout 和 stderr。它先确认两者都是终端,再用 fstat 读取它们在系统里的设备号和节点号;如果两者完全相同,就返回 true,否则返回 false。
调用关系:TerminalStderrGuard::install 会先问它“现在需要保护吗”。它不修改任何东西,只做判断,避免在 stderr 已经被重定向到文件或管道时错误地吞掉输出。
调用图:被 1 处调用(install);外部调用 4 个(uninit, stderr, stdout, fstat)。
suppress_locked149–168 ↗
fn suppress_locked(state: &mut StderrState) -> io::Result<()>
作用:这个函数在已经拿到锁的前提下,真正把 stderr 静音。它会先保存原 stderr,再把 stderr 指向 /dev/null。
数据流:输入是可修改的 StderrState。若里面已经保存过 stderr,说明已经静音,直接返回;否则它用 dup 复制当前 stderr,打开 /dev/null,再用 dup2 把系统的 stderr 编号改到 /dev/null,最后把保存的原 stderr 放进 state.saved_stderr。
调用关系:TerminalStderrGuard::install_suppression 和 resume 会调用它。调用者负责先通过 lock_state 拿锁;它只处理底层文件描述符的保存和改向。
调用图:被 2 处调用(install_suppression, resume);外部调用 5 个(new, from_raw_fd, last_os_error, dup, dup2)。
restore_locked171–182 ↗
fn restore_locked(state: &mut StderrState) -> io::Result<()>
作用:这个函数在已经拿到锁的前提下,把 stderr 从 /dev/null 恢复回原来的位置。
数据流:输入是可修改的 StderrState。它检查 saved_stderr 里有没有保存的原 stderr;如果没有,直接返回;如果有,就用 dup2 把当前 stderr 编号改回保存的目标,然后清空 saved_stderr。
调用关系:pause 和 finish 会调用它。pause 用它做临时恢复,finish 用它做最终恢复;调用者负责决定恢复后 owner_active 是否还保持为 true。
调用图:被 2 处调用(finish, pause);外部调用 2 个(last_os_error, dup2)。
tests::CapturedStderr::start207–220 ↗
fn start(file: &File) -> std::io::Result<Self>
作用:这是测试用的小工具,用来把 stderr 暂时改到一个测试文件里。这样测试就能检查哪些错误输出真的被写出来了。
数据流:输入是一个文件。它先复制当前 stderr 以便之后恢复,然后用 dup2 把 stderr 指向传入的文件,最后返回 CapturedStderr,里面保存着原 stderr。
调用关系:两个测试都会用它开始捕获 stderr。它和 tests::CapturedStderr::drop 配合,像测试里的临时录音机:start 开始录,drop 负责把麦克风接回原处。
调用图:外部调用 5 个(as_raw_fd, from_raw_fd, last_os_error, dup, dup2)。
tests::CapturedStderr::drop224–227 ↗
fn drop(&mut self)
作用:这是测试捕获工具的自动清理。测试结束或对象被丢弃时,它把 stderr 恢复到捕获前的状态。
数据流:输入是 CapturedStderr 里保存的原 stderr。它用 dup2 把系统 stderr 编号重新指向这个保存的目标;它忽略恢复失败的错误,因为这是析构清理阶段。
调用关系:tests::CapturedStderr::start 创建的对象离开作用域或被显式 drop 时会调用它。它保证测试不会把 stderr 重定向状态泄漏到后面的测试。
调用图:外部调用 2 个(as_raw_fd, dup2)。
tests::write_stderr230–234 ↗
fn write_stderr(message: &str) -> std::io::Result<()>
作用:这是测试里统一写 stderr 的辅助函数。它把一段文字写到当前 stderr,并立刻刷新,方便测试准确判断是否可见。
数据流:输入是一段字符串。它锁住 stderr,把字符串转成字节写进去,再 flush 刷新;结果是这段文字要么进入捕获文件,要么被 /dev/null 吞掉,取决于当前 stderr 指向哪里。
调用关系:两个测试都会借它制造 stderr 输出。它本身不关心保护机制,只负责发出测试信号,让后面的断言能看到真实效果。
调用图:外部调用 1 个(stderr)。
tests::suppresses_stderr_only_while_terminal_is_owned238–257 ↗
fn suppresses_stderr_only_while_terminal_is_owned() -> std::io::Result<()>
作用:这个测试验证核心行为:TUI 占用终端时 stderr 会被隐藏,暂停和结束后 stderr 又能正常出现。
数据流:它先创建临时文件并把 stderr 捕获进去;安装抑制后写一行应被隐藏的文字;pause 后写一行应可见的文字;resume 后再写一行应隐藏的文字;finish 后写一行应可见的文字。最后读取文件,确认只留下暂停期间和结束后的两行。
调用关系:它直接覆盖 TerminalStderrGuard::install_suppression、pause、resume、finish 和 tests::write_stderr 的配合效果。因为 stderr 是全局资源,测试用 serial 串行运行,避免和别的测试互相干扰。
调用图:调用 4 个内部函数(install_suppression, finish, pause, resume);外部调用 5 个(new, assert_eq!, tempfile, start, write_stderr)。
tests::preserves_stderr_when_already_redirected261–274 ↗
fn preserves_stderr_when_already_redirected() -> std::io::Result<()>
作用:这个测试验证一个重要边界:如果 stderr 已经被重定向到文件,普通 install 不应该再把它吞掉。
数据流:它创建临时文件并让 stderr 指向该文件,然后调用 TerminalStderrGuard::install。因为此时 stderr 不再是和 stdout 同一个终端,安装应返回 inactive guard;随后写入的文字应该正常进入文件。最后读取文件并断言内容存在。
调用关系:它主要验证 TerminalStderrGuard::install 和 stderr_targets_stdout_terminal 的判断不会误伤已重定向的 stderr。这个测试防止日志、脚本管道或测试捕获输出被错误丢弃。
调用图:调用 1 个内部函数(install);外部调用 5 个(new, assert_eq!, tempfile, start, write_stderr)。
tui/src/tui/job_control.rs源码 ↗
在命令行里,Ctrl+Z 会把正在跑的程序“挂起”,像把一本书临时合上,之后可以用 fg 再翻回来。对普通程序这很简单,但对 TUI(全屏文字界面)很麻烦,因为它可能用了备用屏幕、隐藏/移动光标、特殊键盘输入模式。这个文件专门记住“暂停前屏幕处于什么状态”,暂停时先把终端尽量还给 shell,恢复后再决定是重新进入备用屏幕,还是把普通行内界面的视口对齐到原来的光标位置。SuspendContext 像一个小记事本,记录暂停后该怎么恢复,以及光标大概在哪一行。PreparedResumeAction 则像一张恢复工单,等下一次绘制时真正把终端调回正确状态。这里还会处理 SIGTSTP(系统发给程序的暂停信号),并在恢复后重新开启程序需要的终端模式。
SuspendContext::new50–55 ↗
fn new() -> Self
作用:创建一个新的暂停/恢复上下文,也就是准备好一块共享的小记事本,用来记录将来暂停后该怎么恢复,以及光标所在的行。
数据流:进去时不需要外部输入 → 它创建一个空的“待恢复动作”记录,并把缓存的光标行设为 0 → 出来一个 SuspendContext,之后事件流和绘制流程都可以拿它来共享暂停状态。
调用关系:它在创建 TUI 状态或事件流时被调用,为后面的 Ctrl+Z 处理提前准备好状态容器。后续 SuspendContext::suspend 会往里面写恢复意图,SuspendContext::prepare_resume_action 会把这些意图取出来。
调用图:被 2 处调用(new, make_stream);外部调用 3 个(new, new, new)。
SuspendContext::suspend63–98 ↗
fn suspend(&self, alt_screen_active: &Arc<AtomicBool>) -> Result<()>
作用:真正处理用户按 Ctrl+Z 的那一刻:先把终端收拾到 shell 能接管的样子,再让进程暂停;等用户 fg 回来后,再重新整理终端状态。
数据流:进去的是当前是否在备用屏幕里的标记 alt_screen_active,以及它自己保存的光标行 → 如果正在备用屏幕,就退出备用屏幕并记下“回来后要恢复备用屏幕”;否则记下“回来后对齐普通视口”。然后把光标移到合适位置,调用 suspend_process 发送暂停信号。恢复后,它重新应用原始输入模式,尝试询问终端当前光标位置,更新缓存的光标行,最后清掉残留输入 → 出来时程序已经从暂停中回来,内部已经记录好下一次绘制该怎么修复屏幕。
调用关系:它由按键事件处理流程 map_crossterm_event 在识别到暂停键时调用。它自己会调用 set_resume_action 记下恢复计划,调用 suspend_process 真正暂停进程,恢复后还会调用 set_cursor_y 更新光标位置。之后绘制流程会通过 prepare_resume_action 接着完成屏幕恢复。
调用图:调用 4 个内部函数(cursor_position, set_cursor_y, set_resume_action, suspend_process);被 1 处调用(map_crossterm_event);外部调用 5 个(execute!, flush_terminal_input_buffer, reapply_raw_mode_after_resume, debug!, trace!)。
SuspendContext::prepare_resume_action104–126 ↗
fn prepare_resume_action(
&self,
alt_saved_viewport: &mut Option<Rect>,
) -> Option<PreparedResumeAction>
作用:把之前暂停时记下的“恢复计划”取出来,并整理成绘制阶段可以直接执行的恢复动作。
数据流:进去的是可能保存着备用屏幕视口的 alt_saved_viewport,以及 SuspendContext 里等待处理的恢复意图 → 它先取走并清空恢复意图。如果需要对齐普通界面,就用缓存的光标行做一个新的视口位置;如果需要恢复备用屏幕,就把保存的视口行号更新成当前光标行 → 出来一个 PreparedResumeAction;如果没有待恢复事项,就返回空。
调用关系:它在 draw 和 draw_with_resize_reflow 这些绘制流程里被调用。它不会立刻改终端,而是把要做的事包装成 PreparedResumeAction,交给后面的 PreparedResumeAction::apply 在安全的绘制时机执行。
调用图:调用 2 个内部函数(cursor_y, take_resume_action);被 2 处调用(draw, draw_with_resize_reflow);外部调用 2 个(new, RealignViewport)。
SuspendContext::set_cursor_y132–134 ↗
fn set_cursor_y(&self, value: u16)
作用:更新“暂停时应该把光标放在哪一行”的缓存值。这样用户突然按 Ctrl+Z 时,程序知道把光标摆到一个比较合理的位置。
数据流:进去的是一个行号 value → 它把这个行号写进原子数值里;原子数值可以理解成多处代码同时读写也比较安全的小格子 → 出来没有返回值,但 SuspendContext 内部记住了新的光标行。
调用关系:正常绘制时可以更新这个值;在 SuspendContext::suspend 恢复后,它也会根据终端探测到的实际光标位置再次调用它。之后 prepare_resume_action 会通过 cursor_y 读取这个值来安排视口。
调用图:被 1 处调用(suspend)。
SuspendContext::cursor_y136–138 ↗
fn cursor_y(&self) -> u16
作用:读取当前缓存的光标行号,供恢复屏幕时决定视口应该放到哪里。
数据流:进去不需要额外输入 → 它从内部保存的原子数值里读出光标行 → 出来一个 u16 行号,也就是一个非负的小整数。
调用关系:它主要被 SuspendContext::prepare_resume_action 使用。prepare_resume_action 需要知道暂停回来后光标在哪一行,才能生成 RealignViewport 这样的恢复动作。
调用图:被 1 处调用(prepare_resume_action)。
SuspendContext::set_resume_action141–146 ↗
fn set_resume_action(&self, value: ResumeAction)
作用:记录一次暂停之后应该走哪条恢复路线:是恢复备用屏幕,还是对齐普通行内界面。
数据流:进去的是一个 ResumeAction,也就是恢复方式 → 它给内部的互斥锁加锁;互斥锁可以理解成一把锁,防止两个任务同时改同一份记录。然后把恢复方式写进去 → 出来没有返回值,但内部多了一条“待恢复任务”。
调用关系:它只由 SuspendContext::suspend 调用。suspend 会根据当时是否处于备用屏幕,决定写入 RestoreAlt 或 RealignInline。之后 prepare_resume_action 会通过 take_resume_action 把这条记录取走。
调用图:被 1 处调用(suspend)。
SuspendContext::take_resume_action149–154 ↗
fn take_resume_action(&self) -> Option<ResumeAction>
作用:取出并清空之前记录的恢复方式,确保同一次暂停只恢复一次,不会重复执行。
数据流:进去不需要额外输入 → 它锁住内部记录,把里面的 ResumeAction 拿出来,同时把原位置清空 → 出来可能是一个恢复方式,也可能是什么都没有。
调用关系:它由 SuspendContext::prepare_resume_action 调用。这个设计让恢复动作被“消费”掉:绘制流程拿到一次恢复工单后,后续绘制不会一直重复同样的恢复。
调用图:被 1 处调用(prepare_resume_action)。
PreparedResumeAction::apply181–197 ↗
fn apply(self, terminal: &mut Terminal) -> Result<()>
作用:把准备好的恢复工单真正应用到终端上,让屏幕回到程序需要的状态。
数据流:进去的是一个 PreparedResumeAction 和可修改的 Terminal → 如果动作是 RealignViewport,它就调整终端视口位置;如果动作是 RestoreAltScreen,它就重新进入备用屏幕,开启备用滚动,读取终端大小,重置视口并清屏 → 出来是成功或错误结果,同时终端显示状态已经被修改。
调用关系:它接在 SuspendContext::prepare_resume_action 后面使用。prepare_resume_action 负责决定“要做什么”,PreparedResumeAction::apply 负责在绘制阶段真正操作 Terminal,比如 set_viewport_area、size、clear 和进入备用屏幕。
调用图:调用 3 个内部函数(clear, set_viewport_area, size);外部调用 2 个(new, execute!)。
suspend_process201–211 ↗
fn suspend_process() -> Result<()>
作用:执行系统层面的暂停动作:先把终端模式还原给 shell,再发送 SIGTSTP 暂停信号;等恢复后,再把程序需要的终端模式设回来。
数据流:进去不需要业务输入 → 它先调用 restore 还原终端,暂停 stderr 相关输出通道,然后用 libc::kill 给当前进程组发送 SIGTSTP;SIGTSTP 是 Unix 系统里的“请暂停这个任务”信号。用户用 fg 恢复后,它恢复 stderr 通道并重新设置终端模式 → 出来是成功或错误结果,程序已经经历了一次暂停和恢复。
调用关系:它由 SuspendContext::suspend 调用,是整个 Ctrl+Z 流程里真正把进程交给操作系统暂停的底层步骤。SuspendContext::suspend 在它前后负责记录恢复计划、移动光标、重新探测光标和清理输入。
调用图:调用 2 个内部函数(pause, resume);被 1 处调用(suspend);外部调用 3 个(kill, restore, set_modes)。
tui/src/notifications/mod.rs源码 ↗
终端程序想提醒用户时,不能假设所有终端都支持同一种桌面通知。这个文件把两种通知后端包成一个统一入口:OSC 9(一种终端控制序列,能让部分现代终端弹系统通知)和 BEL(传统响铃字符,像按了一下终端的铃)。如果用户明确指定用哪种方式,就直接照做;如果选择 Auto,程序会先识别当前终端,再判断它是否属于已知支持 OSC 9 的终端,比如 iTerm2、Kitty、WezTerm 等。支持就用 OSC 9,不支持就退回 BEL。这样上层代码不用关心终端差异,只要调用 notify 发消息即可。文件末尾的测试则保证:手动选择不会选错,自动判断里的支持名单也不会被意外改坏。
DesktopNotificationBackend::for_method20–32 ↗
fn for_method(method: NotificationMethod) -> Self
作用:根据用户选择的通知方式,创建真正要使用的通知后端。如果用户选的是自动,它会看看当前终端支不支持 OSC 9,再决定用高级通知还是退回响铃。
数据流:输入是一种通知设置:自动、OSC 9 或 BEL。函数会在自动模式下读取当前终端信息,再交给 supports_osc9 判断;如果适合 OSC 9,就创建 Osc9Backend,否则创建 BelBackend。输出是一个 DesktopNotificationBackend,里面已经装好了后面发通知要用的具体工具。
调用关系:它是实际选择后端的核心函数,由 detect_backend 调用。它会向 terminal_info 要当前终端资料,向 supports_osc9 问这个终端是否支持 OSC 9,并在需要时调用 Osc9Backend::new 来准备 OSC 9 后端;BEL 后端则直接使用。
调用图:调用 2 个内部函数(new, supports_osc9);被 1 处调用(detect_backend);外部调用 3 个(Bel, Osc9, terminal_info)。
DesktopNotificationBackend::method34–39 ↗
fn method(&self) -> NotificationMethod
作用:告诉别人当前这个通知后端对应哪种通知方式。有人想显示或记录“现在用的是 OSC 9 还是 BEL”时会用它。
数据流:输入是已经创建好的 DesktopNotificationBackend。函数只看它里面包的是 Osc9 还是 Bel,然后返回对应的 NotificationMethod。它不发送通知,也不修改任何状态。
调用关系:它位于选择完成之后,用来把内部后端反查成外部能理解的配置值。它不依赖其他函数,也不把工作继续交给别的函数。
DesktopNotificationBackend::notify41–46 ↗
fn notify(&mut self, message: &str) -> io::Result<()>
作用:真正发出一条通知。上层代码不用知道背后是 OSC 9 还是 BEL,只要把消息交给它。
数据流:输入是一段要提醒用户看的文字,以及当前后端自身。函数根据后端类型,把这段文字转交给 Osc9Backend 或 BelBackend 的 notify。输出是一个 io::Result,表示写通知这件事成功了,还是遇到了输入输出错误;同时底层可能向终端写入控制字符或响铃字符。
调用关系:它是统一的发送入口。前面由 for_method 或 detect_backend 选好后端,后面发通知时就调用它;它再把具体发送动作分派给 bel 或 osc9 子模块里的实现。
detect_backend49–51 ↗
fn detect_backend(method: NotificationMethod) -> DesktopNotificationBackend
作用:这是给外部代码用的简洁入口:传入通知设置,拿到可用的通知后端。它把“怎么选择”的细节藏起来。
数据流:输入是用户或配置给出的 NotificationMethod。函数直接调用 DesktopNotificationBackend::for_method,让它完成判断和创建。输出是已经选好的 DesktopNotificationBackend。
调用关系:它被更上层的初始化代码和通知设置更新流程使用,比如 new 和 set_notification_settings。它自己不做复杂判断,只是把请求转给 for_method,方便其他地方不用直接碰枚举的内部构造。
调用图:调用 1 个内部函数(for_method);被 2 处调用(new, set_notification_settings)。
supports_osc953–62 ↗
fn supports_osc9(terminal: &TerminalInfo) -> bool
作用:判断某个终端是否在“支持 OSC 9 桌面通知”的名单里。自动选择通知方式时,这个判断决定能不能用更好的通知方案。
数据流:输入是一份 TerminalInfo,也就是当前终端的识别结果。函数只查看其中的终端名称,并和一组已知支持 OSC 9 的名字比较。输出是 true 或 false:true 表示可以用 OSC 9,false 表示应该用 BEL 这类保底方式。
调用关系:它被 DesktopNotificationBackend::for_method 在 Auto 模式下调用。它不创建后端,只负责回答“这个终端行不行”,让 for_method 根据答案继续选择 Osc9Backend 或 BelBackend。
调用图:被 1 处调用(for_method);外部调用 1 个(matches!)。
tests::test_terminal73–81 ↗
fn test_terminal(name: TerminalName) -> TerminalInfo
作用:给测试快速造一份假的终端信息。测试只关心终端名字,所以其他字段都留空。
数据流:输入是一个 TerminalName。函数把这个名字放进 TerminalInfo,其他像版本、TERM 环境变量、多路复用器信息都设为 None。输出是一份可交给 supports_osc9 使用的测试用终端资料。
调用关系:它只在本文件测试里使用,帮助 supports_osc9_for_supported_terminals 和 supports_osc9_for_unsupported_terminals 构造样本,避免每个测试都重复写一整块 TerminalInfo。
tests::selects_osc9_method84–89 ↗
fn selects_osc9_method()
作用:确认用户明确选择 OSC 9 时,系统真的会选 OSC 9 后端。这样可以防止手动配置被自动判断逻辑误改。
数据流:测试输入是 NotificationMethod::Osc9。它调用 detect_backend 得到后端,然后用断言检查结果是不是 DesktopNotificationBackend::Osc9。测试本身不返回业务结果,只是在不符合预期时让测试失败。
调用关系:它验证 detect_backend 这条对外入口在手动选择 OSC 9 时的行为。虽然调用图只标出断言宏,但这个测试的意义是保护 detect_backend 到 for_method 的选择链路。
调用图:外部调用 1 个(assert!)。
tests::selects_bel_method92–97 ↗
fn selects_bel_method()
作用:确认用户明确选择 BEL 时,系统真的会选 BEL 后端。BEL 是保底方案,这个测试保证它不会被错误替换成别的方式。
数据流:测试输入是 NotificationMethod::Bel。它通过 detect_backend 得到后端,再检查后端是不是 DesktopNotificationBackend::Bel。符合就通过,不符合就测试失败。
调用关系:它和 selects_osc9_method 成对出现,用来确认 detect_backend 对两种手动配置都尊重用户选择。
调用图:外部调用 1 个(assert!)。
tests::supports_osc9_for_supported_terminals100–113 ↗
fn supports_osc9_for_supported_terminals()
作用:确认支持名单里的终端都会被判断为支持 OSC 9。这样新增或整理代码时,不会不小心把某个支持的终端踢出去。
数据流:测试准备了一组应该支持 OSC 9 的终端名字。每个名字都会被 test_terminal 包成 TerminalInfo,再交给 supports_osc9。输出不是普通返回值,而是一组断言:每个结果都必须是 true。
调用关系:它直接验证 supports_osc9 的正向名单。这个测试保护 Auto 模式下 for_method 的选择依据,确保这些终端会走 OSC 9 后端。
调用图:外部调用 1 个(assert!)。
tests::supports_osc9_for_unsupported_terminals116–134 ↗
fn supports_osc9_for_unsupported_terminals()
作用:确认不在支持名单里的终端不会被误判为支持 OSC 9。这样可以避免程序向不懂 OSC 9 的终端发送无效控制序列。
数据流:测试准备了一组不应该支持 OSC 9 的终端名字。每个名字都会被 test_terminal 做成 TerminalInfo,然后交给 supports_osc9。每次得到的结果都必须等于 false,否则测试失败。
调用关系:它直接验证 supports_osc9 的反向名单。这个测试保护 Auto 模式的保守退路:不确定支持时,就让 for_method 选择 BEL,而不是冒险使用 OSC 9。
调用图:外部调用 1 个(assert_eq!)。
tui/src/notifications/osc9.rs源码 ↗
有些终端支持 OSC 9,也就是一种藏在输出文字里的控制指令,可以让终端弹出桌面通知。这个文件就是通知出口:外部只要调用 Osc9Backend::notify,传入一句消息,它就会往标准输出写入对应的终端控制字符。麻烦点在于 tmux 像一个“中间转接盒”,普通控制指令可能被它吃掉,所以 Osc9Backend::new 会先探测当前是不是在 tmux 里;如果是,就用 DCS passthrough(一层给 tmux 看的转发包装)把 OSC 9 包起来。PostNotification 是真正会写控制字符的命令对象。它还能把消息里的 ESC 字符加倍转义,防止在 tmux 包装里把格式弄乱。简单说,这个文件保证“任务完成了,通知我一下”这件事,在普通终端和 tmux 里都尽量能正常工作。
Osc9Backend::default16–18 ↗
fn default() -> Self
作用:提供一个默认的通知后端,方便别的代码不用关心怎么初始化。它的结果和直接调用 Osc9Backend::new 一样。
数据流:进去没有额外参数 → 它把创建工作交给 Osc9Backend::new,让那里去检测终端环境 → 出来一个可以发送 OSC 9 通知的 Osc9Backend。
调用关系:它服务于 Rust 的 Default 习惯用法:当别的地方需要“给我一个默认通知器”时会走这里;真正判断是不是 tmux 的活儿交给 Osc9Backend::new。
调用图:外部调用 1 个(new)。
Osc9Backend::new22–26 ↗
fn new() -> Self
作用:创建通知后端,并记住当前终端外面是不是套着 tmux。这个判断决定之后发通知时要不要加一层 tmux 专用包装。
数据流:进去没有业务参数 → 它读取当前终端信息,查看 multiplexer(终端复用器,也就是 tmux 这类中间层)是不是 Tmux → 出来一个 Osc9Backend,里面保存 dcs_passthrough 这个开关。
调用关系:Osc9Backend::default 会把创建工作交给它;后续 Osc9Backend::notify 会使用它保存的开关,决定 PostNotification 写出哪种控制字符。
调用图:被 1 处调用(for_method);外部调用 1 个(matches!)。
Osc9Backend::notify28–36 ↗
fn notify(&mut self, message: &str) -> io::Result<()>
作用:真正发送一条桌面通知。调用者只需要给它一句消息,它负责把消息写成终端能识别的 OSC 9 指令并输出。
数据流:进去是一段 message 字符串,同时读取自己保存的 dcs_passthrough 开关 → 它构造一个 PostNotification 命令,并通过 execute! 写到标准输出 stdout → 出来是 io::Result,表示写出成功或失败;成功时终端会收到通知指令。
调用关系:这是外部最常用的入口。它不自己拼控制字符,而是把格式化工作交给 PostNotification;execute! 会触发 PostNotification::write_ansi 把命令写成 ANSI 控制序列。
调用图:外部调用 1 个(execute!)。
PostNotification::write_ansi47–54 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:把通知命令翻译成 ANSI 控制序列。ANSI 控制序列就是终端看得懂、但普通人看起来像乱码的特殊字符命令。
数据流:进去是一个可写入的字符串缓冲区 f,以及 PostNotification 里的 message 和 dcs_passthrough → 如果不在 tmux 包装模式,就写入普通 OSC 9:ESC ] 9 ; 消息 BEL;如果需要 tmux 转发,就先转义消息里的 ESC,再写入带 DCS 包装的版本 → 出来是格式化结果,并且 f 里多了一串终端控制字符。
调用关系:Osc9Backend::notify 通过 crossterm 的 execute! 间接用到它。它在 tmux 模式下会调用 escape_tmux_dcs_passthrough_payload,确保消息内容不会破坏外层包装。
调用图:调用 1 个内部函数(escape_tmux_dcs_passthrough_payload);外部调用 1 个(write!)。
PostNotification::execute_winapi57–61 ↗
fn execute_winapi(&self) -> io::Result<()>
作用:在 Windows 上明确拒绝用 WinAPI 方式执行这个命令。WinAPI 是 Windows 的系统接口,但这里的通知命令应该走 ANSI 终端控制字符。
数据流:进去是 PostNotification 自身 → 它不尝试发送通知,而是直接构造一个错误,说明不该用 WinAPI 执行 → 出来是失败的 io::Result。
调用关系:这是给 crossterm 命令接口补齐的 Windows 分支。真正可用的路径是 ANSI,所以它和 PostNotification::is_ansi_code_supported 配合,告诉框架该走控制字符输出。
调用图:外部调用 1 个(other)。
PostNotification::is_ansi_code_supported64–66 ↗
fn is_ansi_code_supported(&self) -> bool
作用:在 Windows 上声明这个命令支持 ANSI 控制字符。也就是说,虽然不用 WinAPI,但可以通过终端转义序列发送。
数据流:进去是 PostNotification 自身 → 它不读取消息内容,只返回 true → 出来告诉调用框架“可以用 ANSI 方式执行”。
调用关系:它服务于 crossterm 的 Windows 执行判断。PostNotification::execute_winapi 拒绝 WinAPI,这个函数则给出替代路线:使用 PostNotification::write_ansi。
escape_tmux_dcs_passthrough_payload69–71 ↗
fn escape_tmux_dcs_passthrough_payload(message: &str) -> String
作用:把消息里原本的 ESC 字符转义成两个 ESC,避免它在 tmux 的 DCS passthrough 包装里被误当成控制边界。
数据流:进去是一段 message 字符串 → 它扫描字符串,把每个 ESC 字符替换成两个 ESC → 出来是一段更安全的字符串,适合放进 tmux 转发包装里。
调用关系:它只在 PostNotification::write_ansi 的 tmux 分支里使用。可以把它理解成给包裹里的内容“加防撞垫”,防止特殊字符撞坏外层格式。
调用图:被 1 处调用(write_ansi)。
tests::post_notification_writes_plain_osc9_sequence81–93 ↗
fn post_notification_writes_plain_osc9_sequence()
作用:测试普通终端场景下,通知命令会写出正确的 OSC 9 控制序列。它防止以后改代码时把最基础的通知格式弄错。
数据流:进去是测试里手工创建的空字符串缓冲区和一条 message 为 hello、dcs_passthrough 为 false 的命令 → 调用 write_ansi 把命令写进缓冲区 → 最后用 assert_eq! 比较结果,确认输出正好是普通 OSC 9 格式。
调用关系:这是 PostNotification::write_ansi 的普通路径测试。它不经过 Osc9Backend::notify,而是直接检查底层格式化是否正确。
调用图:外部调用 2 个(new, assert_eq!)。
tests::post_notification_writes_tmux_dcs_wrapped_osc9_sequence96–108 ↗
fn post_notification_writes_tmux_dcs_wrapped_osc9_sequence()
作用:测试 tmux 场景下,通知命令会加上 DCS passthrough 外包装。它保证通知能被 tmux 转发到真正的终端。
数据流:进去是空字符串缓冲区和一条 message 为 done、dcs_passthrough 为 true 的命令 → 调用 write_ansi 写出 tmux 包装版控制序列 → 用 assert_eq! 确认结果包含 tmux 前缀、内部 OSC 9 指令和结束标记。
调用关系:这是 PostNotification::write_ansi 的 tmux 路径测试。它验证 Osc9Backend::new 一旦判断需要 dcs_passthrough,后面的输出格式确实是 tmux 能理解的。
调用图:外部调用 2 个(new, assert_eq!)。
tests::post_notification_escapes_escape_bytes_inside_tmux_payload111–126 ↗
fn post_notification_escapes_escape_bytes_inside_tmux_payload()
作用:测试消息内容里如果本身带 ESC 字符,在 tmux 包装里会被正确转义。这个测试防止特殊消息破坏控制序列格式。
数据流:进去是空字符串缓冲区和一条带有 ESC 字符的危险样例消息,且 dcs_passthrough 为 true → 调用 write_ansi,它会先通过 escape_tmux_dcs_passthrough_payload 把 ESC 加倍 → 最后断言输出里内部 ESC 已经变成安全形式。
调用关系:这是 PostNotification::write_ansi 和 escape_tmux_dcs_passthrough_payload 的配合测试。它覆盖的是少见但很重要的边角情况:消息内容本身含有终端控制字符。
调用图:外部调用 2 个(new, assert_eq!)。
tui/src/notifications/bel.rs源码 ↗
这个文件像是在程序里放了一只很小的铃铛。程序想提醒用户时,不是弹窗口,也不是连系统通知接口,而是向终端写入一个特殊字符 BEL,也就是 ASCII 里的响铃字符。很多终端看到这个字符后,会发出提示音,或者把它转换成桌面通知。BelBackend 是这个通知后端的入口,外面只要调用 notify,就会把“响铃命令”送到标准输出。PostNotification 则是具体的命令,它告诉 crossterm(一个跨平台控制终端的库)该往终端写什么。这里有个重要点:传进来的消息内容没有被使用,因为 BEL 通知本身只负责“叮一下”,不显示文字。在 Windows 上也特别说明了:不要走 WinAPI(Windows 系统接口)那套方式,而是仍然走 ANSI 控制字符这条路。
BelBackend::notify12–14 ↗
fn notify(&mut self, _message: &str) -> io::Result<()>
作用:这个函数在程序需要提醒用户时调用。它不关心通知文字是什么,只负责让终端响铃或触发终端支持的通知。
数据流:进去的是一段消息文字,但这里没有读取它;函数创建一个 PostNotification 命令,并通过标准输出发送给终端;出来的是一个输入输出结果,成功表示命令写出去了,失败表示写终端时出错了。
调用关系:它是这个文件对外最常用的入口。上层通知系统需要发提醒时会调用它;它把真正写终端的工作交给 crossterm 的 execute!,而 execute! 会进一步使用 PostNotification 这个命令来生成要写出的内容。
调用图:外部调用 1 个(execute!)。
PostNotification::write_ansi22–24 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:这个函数告诉终端命令的实际内容是什么:写入 BEL 字符,也就是“响铃”控制符。它是让通知真正发生的关键一步。
数据流:进去的是一个可写入文字的目标;函数往里面写入特殊字符“\x07”;出来的是格式化写入结果,成功表示 BEL 字符已经准备好写给终端,失败表示写入目标拒绝了这次写入。
调用关系:它不是通常由业务代码直接调用,而是由 crossterm 在执行 PostNotification 命令时调用。BelBackend::notify 触发 execute!,execute! 再需要这个函数提供 ANSI 控制字符。
调用图:外部调用 1 个(write!)。
PostNotification::execute_winapi27–31 ↗
fn execute_winapi(&self) -> io::Result<()>
作用:这个函数只在 Windows 编译时存在,用来明确拒绝通过 Windows 原生接口执行这个通知命令。它的意思是:这个命令只能按 ANSI 控制字符的方式用。
数据流:进去的是这个命令本身,没有额外输入;函数不做通知,而是直接返回一个错误,说明不应该用 WinAPI 执行;出来的是失败结果,不会改动外部状态。
调用关系:它属于 crossterm 命令接口在 Windows 上要求提供的一部分。正常流程应该走 PostNotification::write_ansi;如果有人或库试图走 WinAPI 路线,这里会立刻报错,避免产生误解。
调用图:外部调用 1 个(other)。
PostNotification::is_ansi_code_supported34–36 ↗
fn is_ansi_code_supported(&self) -> bool
作用:这个函数只在 Windows 编译时存在,用来告诉终端库:这个命令支持 ANSI 控制字符方式。简单说,就是允许用“写特殊字符”的办法来执行它。
数据流:进去的是这个命令本身;函数不读取外部信息,直接返回 true;出来的结果表示 ANSI 方式可用,不修改任何东西。
调用关系:它配合 Windows 下的 crossterm 判断执行路线。因为 PostNotification::execute_winapi 会拒绝 WinAPI,所以这里明确告诉库应该使用 PostNotification::write_ansi 这条路。
tui/src/terminal_title.rs源码 ↗
终端改标题不是普通打印文字,而是发送一段特殊的转义序列(可以理解成“给终端看的暗号”)。如果把用户内容、模型输出或路径原样塞进去,里面的控制字符可能打断这段暗号,甚至让标题看起来和真实内容不一致。这个文件做了两件事:一是把标题里的危险字符、看不见的排版字符、多余空白去掉,并限制长度;二是把安全后的标题写到标准输出,也就是当前终端。它不会尝试读取或恢复旧标题,因为不同终端做法不统一。上层代码决定什么时候改标题、空标题该不该清空;这里只负责“安全地写出去”。
set_terminal_title56–68 ↗
fn set_terminal_title(title: &str) -> io::Result<SetTerminalTitleResult>
作用:把一段标题文字安全地设置到终端窗口标题上。它会先清洗文字,避免把危险或看不见的控制内容写进终端指令里。
数据流:进去的是调用者给的一段标题字符串;它先检查标准输出是不是终端,如果不是,就当作已经处理完,不真的写;如果是终端,就交给 sanitize_terminal_title 清洗。清洗后如果没有任何可见内容,就返回 NoVisibleContent;否则用 execute! 把 SetWindowTitle 写到 stdout,最后返回 Applied。
调用关系:这是外部最常用的入口。上层想改标题时会调用它;它自己不判断标题该不该改,只负责清洗后发送。真正的文字清洗交给 sanitize_terminal_title,真正的终端编码由 SetWindowTitle::write_ansi 在 execute! 执行时完成。
调用图:调用 1 个内部函数(sanitize_terminal_title);外部调用 2 个(execute!, stdout)。
clear_terminal_title74–80 ↗
fn clear_terminal_title() -> io::Result<()>
作用:把终端标题清空。注意它只是清掉当前可见标题,不会恢复运行这个程序之前的旧标题。
数据流:进去没有业务数据;它先看 stdout 是否真的是终端。如果不是终端,就什么也不做直接成功;如果是终端,就发送一个空字符串版本的 SetWindowTitle,让终端标题变空。
调用关系:上层决定需要清标题时会调用它。它不像 set_terminal_title 那样清洗文本,因为它写出去的就是空标题;实际写入仍然通过 execute! 和 SetWindowTitle 这套终端命令路径完成。
调用图:外部调用 2 个(execute!, stdout)。
SetWindowTitle::write_ansi86–91 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result
作用:把标题包装成终端认识的 ANSI 转义序列。ANSI 转义序列可以理解成夹在普通输出里的“控制命令”。
数据流:进去的是 SetWindowTitle 里保存的标题字符串和一个可写入文本的目标;它把标题拼成形如“ESC ] 0;标题 BEL”的格式写进去;出来是写入成功或格式化失败的结果。
调用关系:它是 crossterm 的 Command 接口要求实现的核心方法。set_terminal_title 和 clear_terminal_title 通过 execute! 发送 SetWindowTitle 时,底层会走到这里,把标题变成真正发给终端的字节样式。
调用图:外部调用 1 个(write!)。
SetWindowTitle::execute_winapi94–98 ↗
fn execute_winapi(&self) -> io::Result<()>
作用:在 Windows 平台上明确拒绝用 WinAPI 方式设置标题。这里要求走 ANSI 转义序列这条路,避免两套机制行为不一致。
数据流:进去的是当前 SetWindowTitle 命令;它不读取标题,也不尝试调用 Windows API,而是直接返回一个错误,说明不该用这种方式执行。
调用关系:这是为 crossterm 的 Command 接口在 Windows 上补齐的方法。正常流程希望 execute! 使用 ANSI 路径;如果有人试图走 WinAPI 路径,这个函数会及时拦住。
调用图:外部调用 1 个(other)。
SetWindowTitle::is_ansi_code_supported101–103 ↗
fn is_ansi_code_supported(&self) -> bool
作用:告诉命令执行框架:这个设置标题的命令支持 ANSI 转义序列。这样 Windows 上也可以走统一的终端控制写法。
数据流:进去没有额外数据;它固定返回 true,表示 ANSI 写法可用;不改动任何状态。
调用关系:这是 Windows 条件编译下给 crossterm Command 接口看的小开关。它配合 execute_winapi,表达的意思是:不要走 WinAPI,请走 ANSI。
sanitize_terminal_title111–146 ↗
fn sanitize_terminal_title(title: &str) -> String
作用:把不可信的标题文字整理成安全、短、好读的一行。它会去掉控制字符、隐藏字符,把连续空白压成一个空格,并限制最大长度。
数据流:进去是一段原始标题;它逐个字符检查:空白先暂存成一个待写空格,危险字符交给 is_disallowed_terminal_title_char 判断并丢弃,普通可见字符才写入结果;写够最大字符数就停止。出来是一段清洗后的标题字符串,可能为空。
调用关系:set_terminal_title 在真正写终端前一定会调用它。测试函数也直接调用它,检查空白折叠、隐藏字符剔除、长度截断这些规则是否稳定。它把“某个字符该不该丢”的判断交给 is_disallowed_terminal_title_char。
调用图:调用 1 个内部函数(is_disallowed_terminal_title_char);被 5 处调用(set_terminal_title, sanitizes_terminal_title, strips_invisible_format_chars_from_terminal_title, truncates_terminal_title, truncation_prefers_visible_char_over_pending_space);外部调用 1 个(new)。
is_disallowed_terminal_title_char154–177 ↗
fn is_disallowed_terminal_title_char(ch: char) -> bool
作用:判断某个字符是否不适合出现在终端标题里。主要拦截控制字符,以及一些肉眼看不见、却能改变文字显示方向或隐藏内容的特殊字符。
数据流:进去是一个字符;它先看是不是控制字符,是就返回 true;否则用 matches! 对照一组明确列出的 Unicode 字符范围。出来是 true 或 false,表示这个字符要不要被清洗掉。
调用关系:它是 sanitize_terminal_title 的过滤器。清洗函数每遇到一个非空白字符,都会问它“这个能不能留”,这样危险字符列表集中在一个地方,方便理解和维护。
调用图:被 1 处调用(sanitize_terminal_title);外部调用 1 个(matches!)。
tests::sanitizes_terminal_title188–192 ↗
fn sanitizes_terminal_title()
作用:测试标题清洗能不能处理常见脏输入,比如前后空格、制表符、换行和终端控制字符。
数据流:进去是一段故意混入空白和控制字符的示例标题;它调用 sanitize_terminal_title;出来的结果必须等于“Project | Working | Thread”,否则测试失败。
调用关系:这是 sanitize_terminal_title 的基础安全测试。它不参与程序运行时流程,只在测试时验证清洗规则没有被改坏。
调用图:调用 1 个内部函数(sanitize_terminal_title);外部调用 1 个(assert_eq!)。
tests::strips_invisible_format_chars_from_terminal_title195–200 ↗
fn strips_invisible_format_chars_from_terminal_title()
作用:测试那些看不见但会影响文字显示的格式字符会被删除。这样标题不会被做成肉眼误导人的样子。
数据流:进去是一段夹杂双向文字控制符、零宽字符等特殊字符的标题;它调用 sanitize_terminal_title;出来必须是正常可读的“Project Title”。
调用关系:这个测试专门保护 is_disallowed_terminal_title_char 和 sanitize_terminal_title 的配合效果。它确保类似 Trojan Source 的隐藏排版问题不会进入终端标题。
调用图:调用 1 个内部函数(sanitize_terminal_title);外部调用 1 个(assert_eq!)。
tests::truncates_terminal_title203–207 ↗
fn truncates_terminal_title()
作用:测试标题过长时会被截断到规定上限。这样终端标签页不会被超长文字撑得难看,也避免发送太长的控制序列。
数据流:进去是一串比 MAX_TERMINAL_TITLE_CHARS 更长的 a;它调用 sanitize_terminal_title;出来的字符串长度必须刚好等于最大限制。
调用关系:这个测试守住长度限制这条规则。它直接验证 sanitize_terminal_title 的截断行为。
调用图:调用 1 个内部函数(sanitize_terminal_title);外部调用 1 个(assert_eq!)。
tests::truncation_prefers_visible_char_over_pending_space210–215 ↗
fn truncation_prefers_visible_char_over_pending_space()
作用:测试快到长度上限时,如果面临“补一个空格”和“保留后面的可见字符”的选择,会优先保留可见字符。
数据流:进去的是一段接近最大长度的 a,后面跟着空格和 b;它调用 sanitize_terminal_title;出来长度达到上限,而且最后一个字符必须是 b,不是空格。
调用关系:这个测试覆盖一个容易忽略的边角情况。它保证 sanitize_terminal_title 截断时不会把宝贵的位置浪费在末尾空格上。
调用图:调用 1 个内部函数(sanitize_terminal_title);外部调用 2 个(assert_eq!, format!)。
tests::writes_osc_title_with_bel_terminator218–224 ↗
fn writes_osc_title_with_bel_terminator()
作用:测试 SetWindowTitle 写出的终端标题命令格式是否正确,尤其是结尾用 BEL 字符结束。
数据流:进去是标题 hello 和一个空字符串作为输出缓冲区;它调用 SetWindowTitle 的 write_ansi 把控制序列写进去;出来的缓冲区必须等于预期的 OSC 标题序列。
调用关系:这个测试保护 SetWindowTitle::write_ansi 的底层协议格式。set_terminal_title 和 clear_terminal_title 最终都依赖这个格式来让终端真正改标题。
调用图:外部调用 3 个(new, assert_eq!, new)。
cli/src/doctor/title.rs源码 ↗
终端标题就是窗口最上面那行名字,里面可以放“当前项目”“运行状态”“模型名”等信息。这个文件的作用像一张体检单:不直接改标题,而是在 doctor 检查时把标题配置看一遍,给出清楚的结果。它先判断用户有没有配置标题项目:没配就用默认的“活动状态 + 项目名”,配了空列表就等于关闭,配了内容就逐个识别。遇到不认识的名字,它不会让程序崩掉,而是把这些名字列出来并给出警告。项目名也不是随便取的:优先用 Git 仓库根目录,其次用项目配置目录,最后才用当前目录。为了标题别太长,它还会按“用户看见的字符”截短,避免把 emoji 或组合字符切坏。
terminal_title_check30–36 ↗
fn terminal_title_check(config: &Config) -> DoctorCheck
作用:这是外部调用这项检查的入口。它从完整配置里拿出终端标题相关信息,并准备好当前目录和项目根目录,再生成检查结果。
数据流:进去的是 Config,也就是程序当前的配置和工作目录 → 它读取标题配置、当前目录,并调用 terminal_title_project_root 找项目根目录 → 出来的是一个 DoctorCheck,里面写着终端标题检查的状态、摘要和细节。
调用关系:doctor 系统需要检查终端标题时会调用它。它自己不做所有判断,而是先把真实配置整理成 TerminalTitleInputs,再把主要工作交给 terminal_title_check_from_inputs。
调用图:调用 2 个内部函数(terminal_title_check_from_inputs, terminal_title_project_root)。
terminal_title_check_from_inputs38–106 ↗
fn terminal_title_check_from_inputs(inputs: TerminalTitleInputs) -> DoctorCheck
作用:这是生成终端标题体检报告的核心函数。它判断标题配置来自默认值、用户配置还是被关闭,并把发现的问题写进检查结果。
数据流:进去的是整理好的 TerminalTitleInputs:标题项目列表、当前目录、可能的项目根目录 → 它解析项目名,找出无效项,判断是否启用了活动状态和项目名,并补充项目名来源和值 → 出来的是 DoctorCheck;如果配置里有不认识的项目,还会附上一条警告问题和修复建议。
调用关系:terminal_title_check 会把真实配置交给它;测试也直接调用它来验证各种场景。它会调用 parse_terminal_title_items 识别配置项,调用 project_title_selected 决定是否需要项目名,再调用 project_title_candidate 算出项目标题内容。
调用图:调用 5 个内部函数(new, new, parse_terminal_title_items, project_title_candidate, project_title_selected);被 8 处调用(terminal_title_check, terminal_title_omits_project_when_project_item_is_not_selected, terminal_title_project_value_uses_tui_truncation_shape, terminal_title_reports_default_items_and_git_project_name, terminal_title_reports_disabled_configuration, terminal_title_reports_project_config_fallback, terminal_title_warns_for_invalid_configured_items, terminal_title_warns_when_all_configured_items_are_invalid);外部调用 3 个(new, format!, vec!)。
parse_terminal_title_items108–123 ↗
fn parse_terminal_title_items(items: Vec<String>) -> (Vec<String>, Vec<String>)
作用:这个函数把用户写的标题项目列表翻译成程序统一认识的名字。写错的项目会被单独挑出来,方便给用户报错。
数据流:进去的是一串用户配置的字符串 → 它逐个交给 terminal_title_item_id 判断是否是合法项目;合法的变成标准名字,不合法的收集起来,并且重复的错误只报一次 → 出来的是两个列表:可用项目列表和无效项目列表。
调用关系:terminal_title_check_from_inputs 在处理用户自定义配置时会调用它。它负责把杂乱输入清洗成后面检查逻辑能直接使用的稳定格式。
调用图:调用 1 个内部函数(terminal_title_item_id);被 1 处调用(terminal_title_check_from_inputs);外部调用 3 个(new, new, format!)。
terminal_title_item_id125–150 ↗
fn terminal_title_item_id(item: &str) -> Option<&'static str>
作用:这个函数像一本小字典,说明终端标题支持哪些项目名,也处理一些旧叫法或别名。比如用户写 project,会被当成 project-name。
数据流:进去的是一个项目名字符串 → 它用固定匹配表查这个名字是否被支持,并把别名换成标准名字 → 出来的是标准项目名,或者 None 表示不认识。
调用关系:parse_terminal_title_items 会反复调用它来判断每个配置项。它是“哪些标题项目合法”的集中定义处。
调用图:被 1 处调用(parse_terminal_title_items)。
activity_enabled152–156 ↗
fn activity_enabled(items: &[String]) -> bool
作用:这个函数判断终端标题里是否会显示“活动状态”。活动状态可以理解成一个提示,告诉用户程序现在是不是在忙。
数据流:进去的是已经解析好的标题项目列表 → 它检查里面有没有 activity 或 spinner → 出来的是 true 或 false,不改动任何数据。
调用关系:terminal_title_check_from_inputs 用它来写入检查细节里的“terminal title activity”。它不依赖别的本文件函数,是一个很小的判断工具。
project_title_selected158–162 ↗
fn project_title_selected(items: &[String]) -> bool
作用:这个函数判断终端标题里是否需要显示项目名。只有需要项目名时,后面才会去计算项目名来源和值。
数据流:进去的是标题项目列表 → 它查里面有没有 project-name 或 project → 出来的是 true 或 false。
调用关系:terminal_title_check_from_inputs 会调用它来决定要不要继续调用 project_title_candidate。这样可以避免在不显示项目名时多做无关工作。
调用图:被 1 处调用(terminal_title_check_from_inputs)。
terminal_title_project_root164–189 ↗
fn terminal_title_project_root(config: &Config, cwd: &Path) -> Option<ProjectTitleRoot>
作用:这个函数负责找“项目根目录”,也就是终端标题里的项目名应该以哪个文件夹为准。它优先相信 Git 仓库根目录,其次才看项目配置。
数据流:进去的是完整配置和当前目录 → 它先调用 get_git_repo_root 看当前目录是否在 Git 仓库里;如果找不到,再翻配置层,寻找项目配置所在的 .codex 文件夹的上一级目录 → 出来的是带来源说明的 ProjectTitleRoot,或者 None。
调用关系:terminal_title_check 会在正式生成检查报告前调用它。它把“项目到底是哪一个文件夹”这个问题提前解决,供 terminal_title_check_from_inputs 后续展示。
调用图:被 1 处调用(terminal_title_check);外部调用 1 个(get_git_repo_root)。
project_title_candidate191–202 ↗
fn project_title_candidate(
project_root: Option<ProjectTitleRoot>,
cwd: &Path,
) -> (&'static str, Option<String>)
作用:这个函数算出标题里应该显示的项目名文字,以及这个名字是从哪里来的。它保证即使没有项目根目录,也能退回到当前目录。
数据流:进去的是可能存在的项目根目录和当前目录 → 如果有项目根目录,就取它的显示名;如果没有,就取当前目录的显示名;然后调用 truncate_title_part 截短 → 出来的是来源标签和可显示的项目名。
调用关系:terminal_title_check_from_inputs 在确认标题需要项目名后调用它。它会把取名字的细节交给 path_display_name,把截短交给 truncate_title_part。
调用图:调用 2 个内部函数(path_display_name, truncate_title_part);被 1 处调用(terminal_title_check_from_inputs)。
path_display_name204–208 ↗
fn path_display_name(path: &Path) -> String
作用:这个函数把一个路径变成适合放在标题里的短名字。一般只取最后一级文件夹名,而不是整条很长的路径。
数据流:进去的是一个路径 → 它先尝试取路径最后一段,比如 /home/me/project 里的 project;如果取不到,就退回显示完整路径 → 出来的是字符串形式的名字。
调用关系:project_title_candidate 会调用它来得到项目根目录或当前目录的显示名。它让标题更像人看的名字,而不是机器用的完整路径。
调用图:被 1 处调用(project_title_candidate);外部调用 1 个(file_name)。
truncate_title_part210–226 ↗
fn truncate_title_part(value: String) -> String
作用:这个函数把过长的标题片段截短,避免终端标题太长不好看。它按“用户实际看到的字符”来切,不容易把 emoji 这类字符切坏。
数据流:进去的是一段标题文字 → 它最多保留 24 个可见字符;如果原文超过限制,就保留前面一部分并加上 ... → 出来的是适合放进标题的短字符串。
调用关系:project_title_candidate 会调用它处理项目名。它依赖 unicode_segmentation 提供的字符分割能力,保证截断更符合人的阅读习惯。
调用图:被 1 处调用(project_title_candidate)。
tests::terminal_title_reports_default_items_and_git_project_name235–261 ↗
fn terminal_title_reports_default_items_and_git_project_name()
作用:这个测试确认:用户没有配置终端标题时,会使用默认项目,并且项目名可以来自 Git 仓库根目录。
数据流:进去的是测试手写的输入:没有配置、当前目录在 /repo/subdir、项目根目录是 /repo → 它调用 terminal_title_check_from_inputs 生成检查结果 → 然后断言摘要、默认项目列表、项目来源和项目值都符合预期。
调用关系:它直接测试 terminal_title_check_from_inputs 的默认路径。这个测试保护默认行为,防止以后改代码时误把默认标题内容或 Git 项目名显示改坏。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 3 个(from, assert!, assert_eq!)。
tests::terminal_title_reports_disabled_configuration264–288 ↗
fn terminal_title_reports_disabled_configuration()
作用:这个测试确认:用户把标题项目配置成空列表时,系统会把它当成关闭终端标题,而不是当成错误。
数据流:进去的是测试输入:configured_items 是空 Vec、当前目录是 /workspace、没有项目根目录 → 它调用 terminal_title_check_from_inputs → 出来的检查结果应显示 disabled、items 为 none、activity 为 false,并且不出现项目相关细节。
调用关系:它直接覆盖 terminal_title_check_from_inputs 的“关闭配置”分支。这样可以保证空列表这个特殊含义不会被误解成默认配置。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 4 个(from, new, assert!, assert_eq!)。
tests::terminal_title_reports_project_config_fallback291–312 ↗
fn terminal_title_reports_project_config_fallback()
作用:这个测试确认:当项目名来自项目配置而不是 Git 时,检查结果会正确说明来源和值。
数据流:进去的是测试输入:标题项目写 project,项目根目录来源标为 project config → 它调用 terminal_title_check_from_inputs → 输出应说明标题是 configured,项目来源是 project config,项目值是 project。
调用关系:它测试 terminal_title_check_from_inputs 与 project_title_candidate 配合时的项目配置兜底场景。这样能防止非 Git 项目的标题显示丢失。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 4 个(from, assert!, assert_eq!, vec!)。
tests::terminal_title_omits_project_when_project_item_is_not_selected315–332 ↗
fn terminal_title_omits_project_when_project_item_is_not_selected()
作用:这个测试确认:如果标题项目里没有选择项目名,就算知道项目根目录,也不会在检查细节里写项目名。
数据流:进去的是测试输入:只配置 model,同时提供一个项目根目录 → 它调用 terminal_title_check_from_inputs → 输出摘要应正常,但 details 里不应出现以 terminal title project 开头的内容。
调用关系:它验证 project_title_selected 这个判断确实生效。它防止检查报告显示用户没有要求显示的信息。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 4 个(from, assert!, assert_eq!, vec!)。
tests::terminal_title_warns_for_invalid_configured_items335–366 ↗
fn terminal_title_warns_for_invalid_configured_items()
作用:这个测试确认:配置里混有合法项和非法项时,合法项会保留,非法项会触发警告,而且重复错误只报一次。
数据流:进去的是测试输入:project、bogus、activity、bogus → 它调用 terminal_title_check_from_inputs → 输出状态应为 Warning,摘要说明有 invalid items,细节里保留 project-name 和 activity,并只列出一次 "bogus",同时 issues 数量为 1。
调用关系:它覆盖 parse_terminal_title_items 和 terminal_title_check_from_inputs 的错误处理配合。它保证用户写错配置时能看到明确提醒,而不是静默忽略。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 4 个(from, assert!, assert_eq!, vec!)。
tests::terminal_title_warns_when_all_configured_items_are_invalid369–387 ↗
fn terminal_title_warns_when_all_configured_items_are_invalid()
作用:这个测试确认:如果用户配置的标题项目全都不认识,检查结果会显示没有可用项目,并给出警告。
数据流:进去的是测试输入:只有 bogus,一个非法项目 → 它调用 terminal_title_check_from_inputs → 输出状态应为 Warning,items 应为 none,并列出 "bogus" 这个无效项。
调用关系:它测试最糟糕的配置错误场景。这样可以保证即使没有任何合法标题项目,doctor 检查仍然给出可理解的结果。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 4 个(from, assert!, assert_eq!, vec!)。
tests::terminal_title_project_value_uses_tui_truncation_shape390–405 ↗
fn terminal_title_project_value_uses_tui_truncation_shape()
作用:这个测试确认:项目名太长时,会按终端界面使用的样式截短,并在末尾加省略号。
数据流:进去的是测试输入:项目目录名是很长的 abcdefghijklmnopqrstuvwxyz → 它调用 terminal_title_check_from_inputs → 输出细节里的项目值应变成 abcdefghijklmnopqrstu...。
调用关系:它保护 truncate_title_part 的显示规则。这个测试让以后修改标题逻辑时,不会意外改变用户看到的截断效果。
调用图:调用 1 个内部函数(terminal_title_check_from_inputs);外部调用 3 个(from, assert!, vec!)。
宠物与辅助终端界面
这些文件提供宠物渲染子系统,以及 TUI 运行后使用的其他面向终端的辅助界面。
tui/src/pets/mod.rs源码 ↗
这个文件解决的是:终端里的小宠物要从哪里来、能不能先准备好、最后怎么安全地画出来。内置宠物像应用自带贴纸,需要先放进 CODEX_HOME 缓存;自定义宠物则是用户自己的本地文件,这里不会乱动。绘图时,它会根据终端支持的图片协议来输出内容:Kitty 协议是一种终端图片控制码,Sixel 也是一种老牌终端图片格式。它还记住上一次画图用的协议和清理区域,避免旧图片残留,像擦黑板一样先擦掉该擦的地方,再把新图放上去。两个公开绘图入口分别服务于常驻宠物和宠物选择器预览,底层都交给同一个渲染函数处理。错误也分得很清楚:是终端写入失败,还是图片素材找不到或读不了。
ensure_builtin_pack_for_pet60–68 ↗
fn ensure_builtin_pack_for_pet(
pet_id: &str,
codex_home: &std::path::Path,
) -> Result<()>
作用:这个函数在使用某个内置宠物前,先确认它的图片包已经在本地缓存里。这样第一次预览或选择内置宠物时,不会等到真正画图才发现素材缺失。
数据流:进去的是宠物编号和 CODEX_HOME 目录位置 → 它先问目录表这个编号是不是内置宠物,如果是,就让素材包模块把对应资源准备到本地 → 出来是成功或失败;自定义宠物不会被改动。
调用关系:它会先调用 catalog::builtin_pet 判断宠物是不是内置的,再调用 asset_pack::ensure_builtin_pet 做实际缓存准备。调用方通常会在加载预览或保存选择前用它,先把“素材边界”的问题处理掉。
调用图:调用 2 个内部函数(ensure_builtin_pet, builtin_pet)。
PetImageRenderError::fmt77–82 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:这个函数把宠物图片渲染错误变成人能看懂的文字。它会说明失败是因为终端写入出错,还是因为宠物图片素材不可用。
数据流:进去的是一个错误对象和一个文字输出目标 → 它根据错误类型拼出不同提示语 → 出来是一段可显示、可记录日志的错误文字。
调用关系:它是标准错误显示流程的一部分。外层代码或测试打印 PetImageRenderError 时,会通过它得到清楚的说明;内部用 write! 把文字写到格式化器里。
调用图:外部调用 1 个(write!)。
PetImageRenderError::source86–91 ↗
fn source(&self) -> Option<&(dyn std::error::Error + 'static)>
作用:这个函数让外层代码能继续追到更底层的真正原因。比如表面上是“图片素材不可用”,底下可能是某个文件读不到。
数据流:进去的是当前渲染错误 → 它查看里面包着的是终端 IO 错误,还是素材错误 → 出来是可选的底层错误引用,方便日志或调试继续展开。
调用关系:它是 Rust 标准错误链的一部分。测试会确认这类错误不是只有一句空话,而是能拿到原始原因。
PetImageRenderError::from95–97 ↗
fn from(err: std::io::Error) -> Self
作用:这个函数把普通的终端写入错误自动包装成宠物图片渲染错误。这样写终端失败时,可以直接用统一的错误类型往外传。
数据流:进去的是 std::io::Error,也就是读写设备时常见的错误 → 它把它放进 PetImageRenderError::Terminal 这一类 → 出来是统一的宠物渲染错误。
调用关系:render_pet_image 里很多写入终端的操作都可能失败,这个转换让问号操作符可以自然地把底层错误交给上层。
调用图:外部调用 1 个(Terminal)。
render_ambient_pet_image100–106 ↗
fn render_ambient_pet_image(
writer: &mut impl Write,
state: &mut PetImageRenderState,
request: Option<AmbientPetDraw>,
) -> std::result::Result<(), PetImageRenderError>
作用:这个函数专门画主界面里的常驻宠物,也可以在没有绘图请求时清掉它。它给这只常驻宠物使用固定的图片编号,避免和别的图片混在一起。
数据流:进去的是终端输出对象、渲染状态、以及可选的绘图请求 → 它把这些交给通用绘图函数,并指定常驻宠物的图片 ID 0xC0DE → 出来是成功或渲染错误,同时状态可能记录了上一次绘图信息。
调用关系:它是外部界面代码画 ambient pet 的入口,内部只做一层包装,把实际工作交给 render_pet_image。测试和 clear_ambient_pet_image 都会通过它验证绘制、清除和报错行为。
调用图:调用 1 个内部函数(render_pet_image);被 8 处调用(ambient_pet_image_restores_cursor_after_drawing, kitty_local_file_pet_image_uses_file_reference_without_inline_payload, kitty_pet_image_clear_deletes_without_moving_cursor, missing_frame_is_an_asset_error, sixel_pet_image_clear_erases_last_drawn_area, sixel_pet_image_clears_cell_area_before_redrawing, writer_failure_is_a_terminal_error, clear_ambient_pet_image)。
render_pet_picker_preview_image108–114 ↗
fn render_pet_picker_preview_image(
writer: &mut impl Write,
state: &mut PetImageRenderState,
request: Option<AmbientPetDraw>,
) -> std::result::Result<(), PetImageRenderError>
作用:这个函数专门画“选择宠物”弹窗里的预览图。它和常驻宠物用同一套绘图逻辑,但用不同图片编号,防止两张图互相覆盖。
数据流:进去的是终端输出对象、预览图自己的渲染状态、以及可选绘图请求 → 它调用通用绘图函数,并指定预览图的图片 ID 0xC0DF → 出来是成功或失败,状态会记录清理所需的信息。
调用关系:它是宠物选择器预览区域的出口。实际图片协议处理仍然交给 render_pet_image,所以预览和常驻宠物的清理规则保持一致。
调用图:调用 1 个内部函数(render_pet_image)。
render_pet_image122–207 ↗
fn render_pet_image(
writer: &mut impl Write,
state: &mut PetImageRenderState,
image_id: u32,
request: Option<AmbientPetDraw>,
) -> std::result::Result<(), PetImageRenderError>
作用:这是这个文件里真正干活的绘图函数。它负责把一帧宠物图片变成终端能理解的控制码或字节,并且在重画或清除时处理旧图残留。
数据流:进去的是输出目标、渲染状态、图片编号、以及可选绘图请求 → 如果请求为空,它会删除 Kitty 图片或擦掉上次 Sixel 占过的区域;如果有请求,它会按 Kitty、Kitty 本地文件、Sixel 三种协议准备图片内容,移动光标到指定位置写入,再恢复光标 → 出来是成功或错误,并更新上次使用的协议和 Sixel 清理区域。
调用关系:render_ambient_pet_image 和 render_pet_picker_preview_image 都把活交给它。它会调用 image_protocol 里的 Kitty 发送函数或 Sixel 帧定位函数,也会调用 clear_sixel_area 擦区域,并用 is_kitty_protocol 判断是否需要先删除旧 Kitty 图。
调用图:调用 6 个内部函数(from, clear_sixel_area, kitty_transmit_png_file_with_id, kitty_transmit_png_with_id, sixel_frame, is_kitty_protocol);被 2 处调用(render_ambient_pet_image, render_pet_picker_preview_image);外部调用 8 个(flush, write_all, matches!, queue!, read, Bytes, Text, write!)。
is_kitty_protocol214–219 ↗
fn is_kitty_protocol(protocol: image_protocol::ImageProtocol) -> bool
作用:这个小函数判断当前图片协议是不是 Kitty 系列。这里把普通 Kitty 和 Kitty 本地文件引用都算作同一类,因为清除旧图时它们都要走 Kitty 的删除命令。
数据流:进去的是一个图片协议枚举值 → 它检查这个值是不是 Kitty 或 KittyLocalFile → 出来是 true 或 false。
调用关系:render_pet_image 在绘制新图和清除旧图前会用它做判断。它的作用像一个简单的分类标签,避免主流程里重复写判断条件。
调用图:被 1 处调用(render_pet_image);外部调用 1 个(matches!)。
SixelClearArea::from230–237 ↗
fn from(request: &AmbientPetDraw) -> Self
作用:这个函数从一次绘图请求里算出 Sixel 图片需要擦掉的终端格子范围。Sixel 不像 Kitty 那样可以直接按图片 ID 删除,所以要记住该擦哪块地方。
数据流:进去的是 AmbientPetDraw 绘图请求,里面有 x、y、行列数和清理起点 → 它取出横坐标、清理顶部、底部和宽度,底部用 y 加 rows 算出 → 出来是一个 SixelClearArea。
调用关系:render_pet_image 在处理 Sixel 绘图时会调用它。算出的区域随后交给 clear_sixel_area,用空格把旧图覆盖掉。
调用图:被 1 处调用(render_pet_image)。
clear_sixel_area240–250 ↗
fn clear_sixel_area(writer: &mut impl Write, area: SixelClearArea) -> std::io::Result<()>
作用:这个函数用空格擦掉 Sixel 图片曾经占用的终端区域。可以把它理解成拿橡皮擦把旧贴图覆盖掉。
数据流:进去的是终端输出对象和要清理的矩形区域 → 它按行移动光标到区域左侧,然后写入一排空格 → 出来是成功或终端写入错误;终端屏幕上的那块区域会被清空。
调用关系:render_pet_image 在重画 Sixel 或清除 Sixel 时会调用它。它只负责擦格子,不决定什么时候擦,这个决定由上层渲染流程做。
调用图:被 1 处调用(render_pet_image);外部调用 2 个(queue!, write!)。
tests::ambient_pet_image_restores_cursor_after_drawing262–290 ↗
fn ambient_pet_image_restores_cursor_after_drawing()
作用:这个测试确认画常驻宠物时不会把用户的光标位置弄乱。也就是说,宠物画完后,光标要回到原来的地方。
数据流:进去的是测试临时创建的一张假 PNG 和一份 Kitty 绘图请求 → 它调用 render_ambient_pet_image,把输出收集成字符串检查顺序 → 出来是断言结果:必须先保存光标、移动到宠物位置、写图片、再恢复光标。
调用关系:它由测试框架运行,直接调用 render_ambient_pet_image。这个测试保护的是 render_pet_image 里保存和恢复光标的关键行为。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 7 个(new, from_utf8, new, assert!, write, tempdir, default)。
tests::kitty_pet_image_clear_deletes_without_moving_cursor293–320 ↗
fn kitty_pet_image_clear_deletes_without_moving_cursor()
作用:这个测试确认清除 Kitty 宠物图时,只发送删除图片命令,不额外移动光标。这样清除宠物不会打扰用户正在输入的位置。
数据流:进去的是一张假 PNG 和 Kitty 绘图请求 → 它先画一次,再传入空请求表示清除 → 出来是断言结果:输出里有 Kitty 删除命令,但没有保存光标、移动光标、恢复光标这些动作。
调用关系:它通过 render_ambient_pet_image 走完整流程。它验证 render_pet_image 在 request 为 None 且上次协议是 Kitty 时的特殊清理分支。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 7 个(new, from_utf8, new, assert!, write, tempdir, default)。
tests::kitty_local_file_pet_image_uses_file_reference_without_inline_payload323–349 ↗
fn kitty_local_file_pet_image_uses_file_reference_without_inline_payload()
作用:这个测试确认 Kitty 本地文件模式不会把 PNG 内容直接塞进终端输出,而是告诉终端去引用文件。这样可以减少输出量,也符合本地文件协议的做法。
数据流:进去的是假 PNG 文件和 KittyLocalFile 绘图请求 → 它调用 render_ambient_pet_image 并读取输出文本 → 出来是断言结果:有删除旧图命令、有移动位置、有文件引用发送命令,但没有 PNG 的内联编码内容。
调用关系:它调用 render_ambient_pet_image,间接覆盖 render_pet_image 里 KittyLocalFile 分支,也验证 image_protocol::kitty_transmit_png_file_with_id 的输出被正确使用。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 7 个(new, from_utf8, new, assert!, write, tempdir, default)。
tests::sixel_pet_image_clears_cell_area_before_redrawing352–380 ↗
fn sixel_pet_image_clears_cell_area_before_redrawing()
作用:这个测试确认画 Sixel 宠物前,会先把它要占的终端格子擦干净。否则旧图的像素或字符可能残留在新图周围。
数据流:进去的是假 PNG、假 Sixel 文件和一份 Sixel 绘图请求 → 它调用 render_ambient_pet_image 收集输出 → 出来是断言结果:输出先包含多行空格清理,再移动到绘图位置写入 Sixel 内容,最后恢复光标。
调用关系:它通过 render_ambient_pet_image 触发 render_pet_image 的 Sixel 分支。这个分支会调用 SixelClearArea::from 和 clear_sixel_area。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 7 个(from_utf8, new, assert!, create_dir, write, tempdir, default)。
tests::sixel_pet_image_clear_erases_last_drawn_area383–415 ↗
fn sixel_pet_image_clear_erases_last_drawn_area()
作用:这个测试确认 Sixel 宠物被清除时,会擦掉上一次画过的区域,而不是发送 Kitty 删除命令。因为 Sixel 没有这里使用的按图片 ID 删除机制。
数据流:进去的是假 PNG、假 Sixel 文件和 Sixel 绘图请求 → 它先画一次,再传入空请求清除 → 出来是断言结果:输出包含保存光标、擦区域、恢复光标,不包含 Kitty 删除命令,也不再写入图片内容。
调用关系:它调用 render_ambient_pet_image,验证 render_pet_image 依靠 PetImageRenderState 记住上次 Sixel 清理区域,并在清除时交给 clear_sixel_area。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 7 个(from_utf8, new, assert!, create_dir, write, tempdir, default)。
tests::missing_frame_is_an_asset_error418–438 ↗
fn missing_frame_is_an_asset_error()
作用:这个测试确认如果宠物图片文件不存在,错误会被归为“素材错误”,而不是误报成终端坏了。
数据流:进去的是一个指向不存在 PNG 的 Kitty 绘图请求 → 它调用 render_ambient_pet_image 并故意拿到错误 → 出来是断言结果:错误类型必须是 PetImageRenderError::Asset,并且能追到源错误。
调用关系:它通过 render_ambient_pet_image 触发 render_pet_image 读取或编码图片时的失败路径。这个测试保护错误分类的准确性。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 5 个(new, new, assert!, tempdir, default)。
tests::writer_failure_is_a_terminal_error441–467 ↗
fn writer_failure_is_a_terminal_error()
作用:这个测试确认如果写终端本身失败,错误会被归为“终端错误”。比如管道断了、输出设备不可写,就应该让调用方知道问题在输出端。
数据流:进去的是一个故意写入失败的 FailingWriter,以及记录过 Kitty 协议的渲染状态 → 它调用 render_ambient_pet_image 尝试清除图片 → 出来是断言结果:错误类型必须是 PetImageRenderError::Terminal,并且带有底层原因。
调用关系:它直接测试 render_ambient_pet_image 到 render_pet_image 的写入失败路径。内部依靠 PetImageRenderError::from 把 std::io::Error 转成统一的终端错误。
调用图:调用 1 个内部函数(render_ambient_pet_image);外部调用 2 个(default, assert!)。