TUI 动画、运动、终端媒体和临时进度输出
这一阶段是给终端界面“加动态”的幕后工具箱,不是主流程本身,而是让等待、检查、引导这些时刻更好看也更稳。motion 像总开关,统一决定能不能用闪烁、发光等动效,用户不想看动画时就换成安静可读的显示。shimmer 负责文字扫光,让“正在运行”更醒目。ascii_animation 负责字符小动画该播哪一帧,frames 则把动画素材提前装进程序里。doctor 的 progress 只在合适时临时显示检查进度,避免污染最终报告。
动效协调
这些文件定义感知动效的展示层,从集中的减少动效策略到 shimmer 文本效果。
tui/src/motion.rs源码 ↗
这个文件解决的是“终端界面要不要动起来”的问题。有些人喜欢加载时的小圆点闪动、文字发光;但也有人会因为动画分心、不适,或者环境不支持漂亮颜色。所以这里定义了 MotionMode,也就是动画模式:Animated 表示开动画,Reduced 表示减少动画。外面的界面不用自己判断,只要把用户设置传进来,这里就给出合适结果。比如 activity_indicator 会在开动画时显示会变化的小圆点,关动画时可以选择完全隐藏,或显示一个暗一点的静态圆点。shimmer_text 会在开动画时生成带“流光”效果的文字,关动画时就退回普通文字。文件里还带了测试,专门防止别的地方绕过这个统一入口直接调用底层动画函数。可以把它理解成一个“动效总开关和转换器”:所有灯光效果都先经过它,才能保证全屋的开关规则一致。
MotionMode::from_animations_enabled20–26 ↗
fn from_animations_enabled(animations_enabled: bool) -> Self
作用:把一个简单的“动画开没开”的布尔值,转换成这个文件内部使用的动画模式。这样外面的代码不用记 enum(枚举,一组选项)怎么选,只要传 true 或 false。
数据流:输入是 animations_enabled,true 表示用户允许动画,false 表示不允许。函数只做一次直接翻译:true 变成 MotionMode::Animated,false 变成 MotionMode::Reduced。输出是后续渲染界面时会使用的 MotionMode,不改动别的东西。
调用关系:它是外部设置进入动效系统的入口之一。activity_marker、push_running_hook_header、display_lines 和 render 等界面渲染代码会先用它把配置变成 MotionMode,然后再把这个模式交给 activity_indicator 或 shimmer_text 这类函数使用。
调用图:被 5 处调用(activity_marker, push_running_hook_header, display_lines, display_lines, render)。
activity_indicator35–47 ↗
fn activity_indicator(
start_time: Option<Instant>,
motion_mode: MotionMode,
reduced_motion_indicator: ReducedMotionIndicator,
) -> Option<Span<'static>>
作用:生成一个“正在忙”的小标记,比如加载中的圆点。它会根据动画模式决定:显示会动的标记、显示静态标记,还是干脆不显示。
数据流:输入包括开始时间 start_time、动画模式 motion_mode,以及减少动画时该怎么替代的 reduced_motion_indicator。函数先看 motion_mode:如果允许动画,就把开始时间交给 animated_activity_indicator 生成动态小圆点;如果是减少动画,就按要求返回 None 表示不显示,或返回一个暗色的静态圆点。输出是一个可显示的 Span(终端里一小段带样式的文字),也可能什么都没有。
调用关系:它位于界面渲染和底层动画之间。activity_marker、push_running_hook_header 和 render 这些地方需要显示“正在做事”的提示时会调用它;它自己只在动画模式下继续调用 animated_activity_indicator,把真正的动态效果封装起来。
调用图:调用 1 个内部函数(animated_activity_indicator);被 3 处调用(activity_marker, push_running_hook_header, render)。
shimmer_text49–60 ↗
fn shimmer_text(text: &str, motion_mode: MotionMode) -> Vec<Span<'static>>
作用:把一段普通文字变成界面上可显示的文字;如果允许动画,就做成流光效果;如果减少动画,就保持普通文本。它让调用方不用关心“流光动画该不该出现”。
数据流:输入是一段 text 和一个 motion_mode。函数先看是否开动画:开了就调用 shimmer_spans,把文字拆成带动态颜色效果的若干 Span;没开就检查文字是否为空,空字符串返回空列表,非空字符串返回只包含普通文字的一项列表。输出是一组 Span,供终端界面直接绘制。
调用关系:它是所有“发光文字”效果的统一入口。render、push_running_hook_header、render_continue_in_browser、render_device_code_login 等界面代码需要强调某段文字时会调用它;它在动画模式下才把工作交给底层的 shimmer_spans。
调用图:调用 1 个内部函数(shimmer_spans);被 5 处调用(render, push_running_hook_header, render_continue_in_browser, render_device_code_login, render);外部调用 2 个(new, vec!)。
animated_activity_indicator62–76 ↗
fn animated_activity_indicator(start_time: Option<Instant>) -> Span<'static>
作用:生成真正会变化的忙碌小圆点。它会根据终端是否支持高级颜色,选择更漂亮的流光效果,或者退回简单的亮暗闪烁。
数据流:输入是可选的 start_time。函数先算从开始到现在过了多久;再检查标准输出是否支持 16m 真彩色(也就是很细腻的颜色显示)。如果支持,就用 shimmer_spans 给圆点做流光效果;如果不支持,就按时间每 600 毫秒切换一次,在实心圆点和暗色空心圆点之间来回变。输出是一个 Span,用来显示当前这一刻的小圆点样子。
调用关系:它是 activity_indicator 背后的动画实现,外部代码不直接碰它。activity_indicator 在 MotionMode::Animated 时调用它,这样动态细节被藏在一个地方,也方便减少动画模式绕开这些变化效果。
调用图:调用 1 个内部函数(shimmer_spans);被 1 处调用(activity_indicator);外部调用 1 个(on_cached)。
tests::reduced_motion_activity_indicator_uses_explicit_fallback89–106 ↗
fn reduced_motion_activity_indicator_uses_explicit_fallback()
作用:这是一个测试,确认减少动画模式下的小标记不会偷偷动起来,而是严格按调用方指定的替代方案显示或隐藏。
数据流:测试输入是两组固定调用:一种要求隐藏,另一种要求显示静态圆点。它调用 activity_indicator 后,用断言比较结果:隐藏时应得到 None,静态圆点时应得到暗色的“•”。输出不是业务数据,而是测试通过或失败;失败说明减少动画的承诺被破坏了。
调用关系:它直接检验 activity_indicator 在 MotionMode::Reduced 下的行为。这个测试帮助维护者放心修改动画代码,因为一旦减少动画模式不再使用明确 fallback,测试就会报错。
调用图:外部调用 1 个(assert_eq!)。
tests::reduced_motion_shimmer_text_is_plain_text109–118 ↗
fn reduced_motion_shimmer_text_is_plain_text()
作用:这是一个测试,确认减少动画模式下的流光文字会变回普通文字,不会保留动态效果。
数据流:测试把“Loading”和空字符串分别传给 shimmer_text,并指定 MotionMode::Reduced。函数返回后,测试检查非空文字应变成普通 Span 列表,空文字应变成空列表。输出是测试是否通过;失败表示减少动画时文字显示不再朴素可靠。
调用关系:它专门守住 shimmer_text 的降级规则。render 等界面代码依赖这个规则来尊重用户关闭动画的设置,所以这个测试是防止回归的小护栏。
调用图:外部调用 1 个(assert_eq!)。
tests::animation_primitives_are_only_used_by_motion_module121–167 ↗
fn animation_primitives_are_only_used_by_motion_module()
作用:这是一个“规则检查”测试,防止项目里其他文件直接调用底层动画函数。它确保所有动画都必须经过 motion.rs 这个统一入口。
数据流:测试先准备两个正则表达式(用来在文本里找特定写法的匹配规则),分别查找 direct spinner 调用和 shimmer_spans 调用。然后它找到 TUI 源码目录,收集所有 Rust 文件,跳过允许直接使用动画的 motion.rs 和 shimmer.rs。接着逐行读取其他文件,如果发现直接调用,就把文件名和行号记进 violations。最后断言 violations 为空;不为空就让测试失败,并打印违规位置。
调用关系:它把这个文件的设计原则变成自动检查。它会调用 collect_rust_files 收集源码文件,也会调用 animation_primitive_allowlisted_path 判断哪些文件例外;目的就是保证 render 等界面代码只能通过 motion.rs 使用动画能力。
调用图:外部调用 8 个(new, assert!, find_resource!, format!, read_to_string, new, animation_primitive_allowlisted_path, collect_rust_files)。
tests::collect_rust_files169–179 ↗
fn collect_rust_files(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()>
作用:这是测试用的小工具,用来从一个目录开始,把里面所有 Rust 源文件都找出来,包括子目录里的文件。
数据流:输入是一个目录路径 dir,以及一个可追加的文件列表 files。函数读取目录内容;遇到子目录就递归进入继续找,遇到扩展名是 .rs 的文件就放进 files。输出是一个 std::io::Result,表示查找过程是否成功;同时 files 会被填入找到的 Rust 文件路径。
调用关系:它服务于 tests::animation_primitives_are_only_used_by_motion_module。那个测试需要扫描整个源码树,于是把“走遍目录、收集 .rs 文件”的体力活交给这个函数。
调用图:外部调用 2 个(read_dir, collect_rust_files)。
tests::animation_primitive_allowlisted_path181–183 ↗
fn animation_primitive_allowlisted_path(relative_path: &str) -> bool
作用:这是测试用的判断函数,用来说明哪些文件允许直接碰底层动画函数。现在只有 motion.rs 和 shimmer.rs 在白名单里。
数据流:输入是一个相对路径字符串 relative_path。函数用 matches! 判断它是不是 motion.rs 或 shimmer.rs;是就返回 true,不是就返回 false。它不读取文件,也不改动任何状态。
调用关系:它被 tests::animation_primitives_are_only_used_by_motion_module 调用,用来跳过允许直接使用动画原语的文件。这样扫描测试既严格,又不会把真正定义动画能力的文件误报成违规。
调用图:外部调用 1 个(matches!)。
tui/src/shimmer.rs源码 ↗
终端不像网页那样天然有动画,所以这个文件把动画拆成一串带样式的字符。它先记住程序启动的时间,再根据“现在过了多久”算出亮光扫到文字的哪个位置。每个字符都会按离亮光中心的远近得到不同亮度:靠近就更亮、更粗,远离就更暗。它还会检查当前终端是否支持真彩色(也就是能显示完整 RGB 颜色)。支持的话,就把前景色和背景色混合出细腻的渐变;不支持的话,就退回到加粗、变暗这类基础样式。可以把它想成给每个字单独贴一张小标签,标签上写着“这个字现在该多亮”。
elapsed_since_start16–19 ↗
fn elapsed_since_start() -> Duration
作用:这个函数告诉别人:程序从第一次需要这个动画计时开始,已经过去了多久。这样扫光动画每次重画时都能接着走,而不是从头乱跳。
数据流:进去时不需要外部参数;它会读取一个全局保存的开始时间。如果这是第一次调用,就把当前时间记下来;之后每次都用当前时间减去这个开始时间。出来的是一个时间长度,表示已经过去多久,同时它可能在第一次调用时初始化那份开始时间。
调用关系:它是 shimmer_spans 的计时器。shimmer_spans 每次要画扫光文字时都会问它“现在动画走到哪一刻了”,然后再据此决定亮光位置。
调用图:被 1 处调用(shimmer_spans)。
shimmer_spans21–69 ↗
fn shimmer_spans(text: &str) -> Vec<Span<'static>>
作用:这个函数把一整段文字变成一组带样式的字符片段,用来显示会发亮扫过的文字。外部界面想显示“正在工作”的闪光文本时,就会用它。
数据流:进去的是一段普通字符串。它先把字符串拆成一个个字符;如果没有字符,就直接返回空列表。然后它读取动画已经运行的时间,算出亮光当前扫到哪里;再检查终端是否支持真彩色。接着它读取默认前景色和背景色,并为每个字符计算亮度。如果支持 RGB 颜色,就调用 blend 把颜色混合成渐变;如果不支持,就交给 color_for_level 选一个简单样式。最后出来的是一串 Span,也就是终端界面库能直接绘制的“带样式文字片段”。
调用关系:它是这个文件的核心出口,被 animated_activity_indicator 和 shimmer_text 这类界面代码调用。它自己会向 elapsed_since_start 要时间,向 default_fg 和 default_bg 要终端默认颜色,必要时用 blend 混色,或者用 color_for_level 做低配终端的样式兜底。
调用图:调用 5 个内部函数(blend, color_for_level, elapsed_since_start, default_bg, default_fg);被 2 处调用(animated_activity_indicator, shimmer_text);外部调用 6 个(styled, default, new, with_capacity, Rgb, on_cached)。
color_for_level71–80 ↗
fn color_for_level(intensity: f32) -> Style
作用:这个函数是在终端不支持真彩色时用的备用方案。它把“亮度强弱”翻译成变暗、普通、加粗三种简单效果,让扫光在老旧或能力有限的终端里也能看出来。
数据流:进去的是一个 0 到 1 左右的亮度数值。它根据数值分三档:很低就返回变暗样式,中等就返回普通样式,较高就返回加粗样式。出来的是一个 Style,也就是终端绘制字符时使用的样式说明。
调用关系:它只被 shimmer_spans 调用。当 shimmer_spans 发现终端不能显示完整 RGB 颜色时,就把每个字符算出的亮度交给它,让它选择最合适的基础样式。
调用图:被 1 处调用(shimmer_spans);外部调用 1 个(default)。
ASCII 动画素材与驱动器
这些文件提供内置帧数据和可复用驱动器,用于将这些帧转换为定时终端动画。
tui/src/ascii_animation.rs源码 ↗
终端界面不会自己“播放视频”,它只能一帧一帧重画文字。这个文件里的 AsciiAnimation 就像一个小放映员:它记住动画从什么时候开始、每隔多久换一张图、当前用哪一组图案。界面渲染时会问它“现在该显示哪一帧”,它根据已经过去的时间算出答案;渲染完还会让它预约下一次刷新,避免动画卡住。它还支持多套动画图案,用户按快捷键时可以随机换一套,并立刻要求重画。一个重要细节是:创建时必须至少给一套动画,否则程序会直接报错;如果传入的动画编号太大,它会自动压到最后一套,避免越界。文件末尾还有一个小测试,保证默认换帧间隔不是 0。
AsciiAnimation::new21–23 ↗
fn new(request_frame: FrameRequester) -> Self
作用:创建一个使用默认动画素材的 ASCII 动画播放器。外部只要给它一个“请求刷新画面”的工具,它就能开始按默认配置工作。
数据流:进去的是 FrameRequester,也就是一个可以通知终端界面重新绘制的对象 → 函数把它和默认动画集合 ALL_VARIANTS、默认第 0 套动画一起交给更通用的创建函数 → 出来的是一个准备好的 AsciiAnimation。
调用关系:它是最省事的入口,适合不需要自定义动画素材的地方使用。它自己不做复杂初始化,而是把活儿交给 AsciiAnimation::with_variants,保证默认创建和自定义创建走同一套规则。
调用图:被 1 处调用(new);外部调用 1 个(with_variants)。
AsciiAnimation::with_variants25–42 ↗
fn with_variants(
request_frame: FrameRequester,
variants: &'static [&'static [&'static str]],
variant_idx: usize,
) -> Self
作用:用指定的动画素材和起始动画编号创建播放器。它适合测试或特殊界面使用,因为调用者可以指定有哪些动画、先播哪一套。
数据流:进去的是刷新请求器、一组动画变体、以及想使用的变体编号 → 它先确认动画列表不能为空,再把过大的编号压到合法范围内,并记录当前时间作为动画起点 → 出来的是一个包含刷新方式、动画素材、当前变体、换帧间隔和开始时间的 AsciiAnimation。
调用关系:AsciiAnimation::new 会调用它来完成默认初始化;一些和快捷键切换动画有关的测试也会直接用它搭建场景。它是所有 AsciiAnimation 实例真正成形的地方。
调用图:被 2 处调用(ctrl_dot_changes_animation_variant, ctrl_shift_dot_changes_animation_variant);外部调用 2 个(now, assert!)。
AsciiAnimation::schedule_next_frame44–63 ↗
fn schedule_next_frame(&self)
作用:预约下一次界面刷新,让动画按节奏继续动。没有它,画面可能会停在当前字符图上,不会自动换到下一帧。
数据流:进去的是当前动画对象里保存的开始时间和每帧间隔 → 它算出现在距离下一次整齐换帧还差多少毫秒;如果间隔是 0,就马上请求刷新;如果时间能安全转换成毫秒数,就预约延迟刷新;如果数字异常大转换失败,就退回到马上刷新 → 结果是 FrameRequester 收到一次“现在或稍后重画”的请求。
调用关系:界面渲染函数 render_ref 在画完当前帧后会用它安排下一次绘制。它会调用 FrameRequester 的 schedule_frame 或 schedule_frame_in,把具体“什么时候重画”的工作交给界面刷新系统。
调用图:调用 2 个内部函数(schedule_frame, schedule_frame_in);被 1 处调用(render_ref);外部调用 4 个(as_millis, from_millis, elapsed, try_from)。
AsciiAnimation::current_frame65–77 ↗
fn current_frame(&self) -> &'static str
作用:告诉界面现在应该显示哪一张 ASCII 图。它把真实流逝的时间换算成动画帧编号。
数据流:进去的是动画的开始时间、每帧间隔、当前选中的动画素材 → 它先取出当前变体的所有帧;如果没有帧就返回空字符串;如果换帧间隔是 0,就固定返回第一帧;否则按经过的毫秒数除以每帧时长,再对帧数取余,得到循环播放中的当前帧 → 出来的是一段要显示在终端里的静态字符串。
调用关系:render_ref 在绘制界面时会调用它拿到要画的内容。它内部会调用 AsciiAnimation::frames 取当前变体的帧列表,然后自己完成时间到帧编号的换算。
调用图:调用 1 个内部函数(frames);被 1 处调用(render_ref);外部调用 2 个(as_millis, elapsed)。
AsciiAnimation::pick_random_variant79–91 ↗
fn pick_random_variant(&mut self) -> bool
作用:随机换一套动画图案,并告诉界面马上刷新。用户触发切换动画时会用到它。
数据流:进去的是当前动画对象,以及它保存的所有动画变体和当前编号 → 如果只有一套或没有可换的第二套,就什么也不改并返回 false;如果有多套,它会随机挑一个和当前不同的编号,更新当前变体,然后请求立刻重画 → 出来的是 true 或 false,表示是否真的换了动画,同时对象内部的 variant_idx 可能被改掉。
调用关系:键盘事件处理函数 handle_key_event 会在用户按相关快捷键时调用它。它完成选择后调用 FrameRequester 的 schedule_frame,让新的动画变体不用等下一轮自然刷新就能显示出来。
调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(handle_key_event);外部调用 1 个(rng)。
AsciiAnimation::frames93–95 ↗
fn frames(&self) -> &'static [&'static str]
作用:取出当前正在使用的那套动画帧。它是一个小助手,让其他函数不用自己关心变体编号怎么对应到素材列表。
数据流:进去的是动画对象里保存的 variants 和 variant_idx → 它用当前编号从动画素材数组里取出对应的一组字符串帧 → 出来的是当前动画变体的帧列表引用,不复制内容,也不修改状态。
调用关系:AsciiAnimation::current_frame 会调用它先拿到当前变体的全部帧,然后再根据时间选择其中一帧。它把“选哪套素材”这件事集中在一个地方,减少重复代码。
调用图:被 1 处调用(current_frame)。
tests::frame_tick_must_be_nonzero103–105 ↗
fn frame_tick_must_be_nonzero()
作用:确认默认的动画换帧间隔不是 0。这样可以防止默认动画因为配置错误而失去正常的按时间换帧行为。
数据流:进去的是常量 FRAME_TICK_DEFAULT → 测试把它转换成毫秒数,并检查这个数大于 0 → 如果条件成立,测试通过;如果不是,测试失败,提醒开发者默认配置有问题。
调用关系:这是测试阶段运行的小检查,不参与正常界面播放。它用 assert! 做断言,守住 AsciiAnimation::current_frame 和 AsciiAnimation::schedule_next_frame 所依赖的默认时间设置。
调用图:外部调用 1 个(assert!)。
tui/src/frames.rs源码 ↗
这个文件像是一本“翻页小动画”的画册。每种动画样式都有 36 张文字图片,也就是 36 个帧;程序快速一张张显示,就会看起来像在动。这里用 Rust 的 include_str!(编译时把文本文件内容塞进程序里)把 frames 目录下的每个 frame_*.txt 读进来,变成常量数组。frames_for! 是一个宏,可以理解成“批量生成重复代码的小模具”,避免每种动画都手写 36 次路径。文件还把所有动画样式收进 ALL_VARIANTS,方便别的地方统一挑选。最后的 FRAME_TICK_DEFAULT 规定默认每 80 毫秒换一帧,也就是动画播放速度。它没有运行时函数,主要提供现成素材和默认节奏。
临时进度输出
此文件应用终端能力检查,为 doctor 运行选择并渲染临时进度报告。
cli/src/doctor/progress.rs源码 ↗
doctor 命令一边检查系统状态,一边最后要打印一份报告。这里最重要的问题是:进度提示不能混进 stdout(标准输出,通常放最终结果),否则 JSON 报告会变成坏格式,重定向到文件的人类报告也会被插入奇怪文字。所以这个文件把进度提示统一放到 stderr(标准错误输出,常用来放提示信息),而且只在真正的交互式终端里显示。它先用 doctor_progress 选择一种实现:如果是 JSON、不是终端、或终端太简陋,就用 QuietProgress,什么都不写;如果适合显示,就用 StderrProgress,在同一行反复刷新“正在检查”。StderrProgress 里面有互斥锁(一把锁,防止两个任务同时改同一份状态),记住自己是否写过一行,最后 settle 会把临时行擦掉,好让正式报告干干净净地出现。
doctor_progress25–35 ↗
fn doctor_progress(json: bool) -> std::sync::Arc<dyn DoctorProgress>
作用:根据当前输出方式,挑一个合适的进度提示器。它的核心目标是:能显示时给人一点反馈,不能显示时绝不破坏最终报告。
数据流:输入是 json 这个布尔值,另外它会读取 TERM 环境变量和 stderr 是否是终端。它把这些信息交给 should_show_progress 判断;如果可以显示,就创建 StderrProgress;否则创建 QuietProgress。输出是一份可共享的 DoctorProgress 对象,后续检查流程拿它来接收开始、心跳、结束和收尾事件。
调用关系:build_report 会调用它来拿到本次 doctor 报告构建时使用的进度通道。它自己不做具体显示,而是先调用 should_show_progress 做决定,再把后面的工作交给 QuietProgress 或 StderrProgress。
调用图:调用 1 个内部函数(should_show_progress);被 1 处调用(build_report);外部调用 4 个(default, stderr, var, new)。
should_show_progress37–39 ↗
fn should_show_progress(json: bool, term: Option<&str>, stderr_is_tty: bool) -> bool
作用:用一个很小的规则判断要不要显示临时进度。它保护 JSON 输出和非交互环境,避免把提示文字写到不该写的地方。
数据流:输入是三个条件:是否在输出 JSON、终端类型 TERM 是什么、stderr 是否真的是终端。它只在“不是 JSON、stderr 是终端、TERM 不是 dumb”三件事同时满足时返回 true,否则返回 false。
调用关系:doctor_progress 会先问它“现在适不适合显示进度”。测试函数也直接检查它的几种典型判断,确保这条规则不会被改坏。
调用图:被 1 处调用(doctor_progress)。
QuietProgress::begin44–44 ↗
fn begin(&self, _label: &'static str)
作用:表示某项检查开始了,但安静模式下什么也不显示。它用于那些必须保持输出干净的场景。
数据流:输入是检查项目的名字,但函数故意不读取、不打印、不保存。调用前后没有任何可见变化,结果就是保持沉默。
调用关系:当 doctor_progress 选择 QuietProgress 后,检查流程发来的 begin 事件会落到这里。它不再把活儿交给任何显示函数,因为它的任务就是不打扰最终输出。
QuietProgress::heartbeat46–46 ↗
fn heartbeat(&self, _label: &'static str, _elapsed: Duration)
作用:表示某项检查还在继续,但安静模式下不显示“还在检查”。它避免长时间检查时的提示污染 JSON 或文件输出。
数据流:输入是检查项目名字和已经耗费的时间,但函数全部忽略。调用之后不会写终端,也不会记录状态。
调用关系:当检查流程给 QuietProgress 发送心跳事件时会到这里。和 QuietProgress 的其他方法一样,它是一个安全的空操作。
QuietProgress::finish48–48 ↗
fn finish(&self, _label: &'static str, _status: CheckStatus)
作用:表示某项检查结束了,但安静模式下不打印完成状态。最终结果会由正式报告统一展示。
数据流:输入是检查项目名字和检查状态,但这里不使用它们。它不产生输出,也不改变内部状态。
调用关系:检查流程在单项检查完成时可能通过 DoctorProgress 调到这里。它不显示状态,是因为最终报告才是状态的权威来源。
QuietProgress::settle50–50 ↗
fn settle(&self)
作用:安静模式下的收尾动作。因为之前什么都没写,所以这里也不需要清理。
数据流:没有输入,也不读取任何状态。调用前后完全一样,没有输出、没有副作用。
调用关系:在最终报告打印前,流程可能会要求进度器 settle 一下。QuietProgress 收到这个要求时直接什么都不做。
StderrProgress::render64–72 ↗
fn render(&self, message: String)
作用:把一条临时进度消息写到 stderr 的同一行上。可以把它想成用橡皮擦掉旧提示,再写上新提示。
数据流:输入是一段已经拼好的消息文字。它先拿互斥锁保护内部状态,然后锁住 stderr,写入回车和清行控制符,再写消息并刷新,让用户马上看到;最后记录“我写过一行了”。如果锁状态失败,它会直接放弃,避免程序因为提示信息出错而崩掉。
调用关系:StderrProgress::begin 和 StderrProgress::heartbeat 会调用它来实际刷新终端文字。它是 StderrProgress 里真正负责把提示送到终端的底层小工具。
StderrProgress::begin76–78 ↗
fn begin(&self, label: &'static str)
作用:在某项检查刚开始时显示“Checking 某项……”。它让用户知道程序没有卡住,而是在做哪一步。
数据流:输入是检查项目的名字。它把名字拼成一条人能看懂的英文提示,然后交给 render 写到 stderr 的临时行上。返回时没有正式结果,但终端上的进度提示被更新了。
调用关系:检查流程通过 DoctorProgress 发出 begin 事件时会用到它。它自己只负责组织文字,实际写终端的工作交给 StderrProgress::render。
StderrProgress::heartbeat80–82 ↗
fn heartbeat(&self, label: &'static str, elapsed: Duration)
作用:在检查耗时较久时显示“还在检查某项,已经多少秒”。它给等待中的用户一个活着的信号。
数据流:输入是检查项目名字和已经过去的时间。它取出秒数,拼成一条“Still checking...”消息,再交给 render 刷新到 stderr。调用后用户看到的临时提示会被更新。
调用关系:检查流程在等待期间发心跳事件时会用到它。它依赖 StderrProgress::render 来做真正的终端刷新。
StderrProgress::finish84–84 ↗
fn finish(&self, _label: &'static str, _status: CheckStatus)
作用:表示某项检查结束,但这里故意不显示完成信息。这样可以避免进度提示和最后的正式报告重复或冲突。
数据流:输入是检查项目名字和状态,但函数不使用它们。它不写 stderr,也不改状态,最终状态留给正式报告展示。
调用关系:检查流程完成单项检查时可能调用它。StderrProgress 只在开始和等待时显示临时提示,完成结果则交给报告部分统一打印。
StderrProgress::settle86–97 ↗
fn settle(&self)
作用:在最终报告打印前,把之前显示过的临时进度行擦干净。这样屏幕上不会残留“Checking...”之类的半成品提示。
数据流:它先拿互斥锁读取内部状态。如果之前没写过临时行,就直接结束;如果写过,就锁住 stderr,写入回车和清行控制符并刷新,然后把 wrote_line 改回 false。结果是终端临时行被清空,内部也记为已清理。
调用关系:报告流程在要输出正式报告前会让进度器 settle。它不调用 render,而是自己直接写清行控制符,因为这一步不是显示新消息,而是清场。
调用图:外部调用 2 个(stderr, write!)。
tests::progress_is_quiet_for_json105–111 ↗
fn progress_is_quiet_for_json()
作用:验证 JSON 输出时一定不能显示进度。因为只要混入提示文字,JSON 就可能无法被机器读取。
数据流:测试把 json 设为 true,同时模拟一个正常终端。它调用 should_show_progress,期望结果是 false;如果不是 false,断言会失败。
调用关系:这是 should_show_progress 的安全网之一,专门守住 JSON 输出必须安静这条规则。
调用图:外部调用 1 个(assert!)。
tests::progress_is_quiet_for_non_tty114–120 ↗
fn progress_is_quiet_for_non_tty()
作用:验证 stderr 不是交互式终端时不显示进度。比如输出被重定向或在自动化环境里运行时,就不该塞入动态提示。
数据流:测试把 json 设为 false,TERM 设为普通终端名,但 stderr_is_tty 设为 false。它调用 should_show_progress,期望得到 false。
调用关系:这个测试覆盖 doctor_progress 选择 QuietProgress 的一个重要条件:没有真实终端时保持安静。
调用图:外部调用 1 个(assert!)。
tests::progress_is_quiet_for_dumb_terminal123–129 ↗
fn progress_is_quiet_for_dumb_terminal()
作用:验证 TERM 是 dumb 时不显示进度。dumb 终端通常不支持清行、回车刷新这些小技巧,强行显示会很乱。
数据流:测试传入 json 为 false、stderr 是终端,但 TERM 是 dumb。它调用 should_show_progress,期望返回 false。
调用关系:这个测试保护 should_show_progress 里的终端能力判断,避免在简陋终端上输出控制字符造成乱码。
调用图:外部调用 1 个(assert!)。
tests::progress_is_shown_for_human_tty_output132–138 ↗
fn progress_is_shown_for_human_tty_output()
作用:验证普通人类终端下会显示进度。这样长时间检查时用户能看到程序仍在工作。
数据流:测试传入 json 为 false、TERM 是 xterm-256color、stderr 是终端。它调用 should_show_progress,期望返回 true。
调用关系:这个测试确认正向场景没有被误关掉:当 doctor_progress 面对正常交互终端和人类输出时,应该选择可显示的进度方案。
调用图:外部调用 1 个(assert!)。