TUI 展示模型、样式和轻量视图辅助
这一阶段不是主流程干活儿,而是终端界面背后的“装配台”。底层文字能排好后,这里负责把它变得好看、好懂、好操作:颜色和样式保证不同背景下看得清,统一宽度和可渲染零件让文字、空白、横竖布局像积木一样拼起来。页脚、弹窗、选项卡、选择列表和滚动状态负责提示用户按什么、当前选哪项。状态、目标、技能、迁移清单、连接信息和历史记录则把机器数据整理成人能读的短句,并避免警告刷屏或敏感地址泄露。
样式与布局原语
这些文件定义共享的颜色、样式、布局和可渲染基础,供更高层的 TUI 展示辅助工具构建使用。
tui/src/color.rs源码 ↗
终端界面经常要根据背景色自动挑文字色、阴影色、分隔线色。这个文件就像一个“调色小盒子”,输入颜色的 RGB 数值,也就是红、绿、蓝三个 0 到 255 的数字,然后给出简单可靠的判断或新颜色。is_light 用人眼对红绿蓝敏感度不同的经验公式,判断背景偏亮还是偏暗。blend 按透明度把前景色和背景色揉在一起,做出半透明效果。perceptual_distance 更讲究一些:它先把普通 RGB 颜色换到更接近人眼感受的 Lab 颜色空间,再算距离,用来衡量两种颜色看起来有多接近。这样界面组件不必各自乱算颜色,可以共用同一套直觉一致的规则。
is_light1–5 ↗
fn is_light(bg: (u8, u8, u8)) -> bool
作用:判断一个 RGB 背景色是不是“亮色”。有人会用它来决定文字、边框或覆盖层该用深色还是浅色,避免浅底浅字、深底深字这种看不清的情况。
数据流:进去的是一个背景色,包含红、绿、蓝三个数字。它按人眼对绿色最敏感、对蓝色较不敏感的比例算出一个亮度值;如果这个值大于 128,就输出 true,表示偏亮,否则输出 false,表示偏暗。它不改动任何外部数据。
调用关系:它是很多界面配色决策的第一步。from_parts、diff_theme_for_bg、adaptive_default_theme_selection 会用它判断主题背景的明暗;dense_row_background_style、transcript_loading_overlay_style、user_message_bg 这类具体界面样式也会用它来决定在当前背景上该怎么配色。
调用图:被 6 处调用(from_parts, diff_theme_for_bg, adaptive_default_theme_selection, dense_row_background_style, transcript_loading_overlay_style, user_message_bg)。
blend7–12 ↗
fn blend(fg: (u8, u8, u8), bg: (u8, u8, u8), alpha: f32) -> (u8, u8, u8)
作用:把两种 RGB 颜色按一个透明度比例混合成一种新颜色。它常用来做淡淡的背景、覆盖层、闪光效果或分隔线,让界面颜色不要太突兀。
数据流:进去的是前景色、背景色,以及 alpha 透明度。alpha 越接近 1,新颜色越像前景色;越接近 0,新颜色越像背景色。它分别计算红、绿、蓝三个通道的加权平均值,最后输出混合后的 RGB 颜色。它只返回结果,不保存状态。
调用关系:它被多个界面绘制环节当作统一的调色方法使用。from_parts 会在组装主题时用它,dense_row_background_style 和 transcript_loading_overlay_style 会用它做背景层次,shimmer_spans 会用它做闪动文字效果,table_separator_style_for 会用它调表格分隔线,user_message_bg 会用它生成用户消息背景。
调用图:被 6 处调用(from_parts, dense_row_background_style, transcript_loading_overlay_style, shimmer_spans, table_separator_style_for, user_message_bg)。
perceptual_distance16–75 ↗
fn perceptual_distance(a: (u8, u8, u8), b: (u8, u8, u8)) -> f32
作用:估算两种颜色在人眼看来相差多远。它不是简单比较 RGB 数字,而是尽量按人的视觉感受来算,所以更适合判断颜色是否太接近、是否容易混淆。
数据流:进去的是两个 RGB 颜色。它先把每个颜色从 sRGB 转成线性 RGB,意思是去掉屏幕显示里的非线性影响;再转成 XYZ,再转成 Lab,Lab 是一种更接近人眼感知的颜色表示。最后它计算两个 Lab 点之间的直线距离,并输出一个浮点数;数字越大,说明看起来越不一样。它不修改外部状态。
调用关系:从提供的调用关系看,它没有被列出的其他函数直接调用,更像是给配色或主题逻辑预留的通用测距工具。需要判断两个颜色是否足够有区别时,别的代码可以直接调用它,而它内部自己完成颜色空间转换和距离计算。
tui/src/style.rs源码 ↗
终端不像网页那样可控:有人用白底,有人用黑底;有的终端能显示真彩色,有的只能显示很少的颜色。这个文件就是为了解决“同一套界面在不同终端里别糊成一片”的问题。它会先查看终端的默认前景色、背景色和颜色能力,再决定该用什么颜色。比如用户写的消息会加一层很淡的背景,好像给纸条垫了一张浅色便签;表格里的分隔线会被调得很低调,能看见但不抢正文;当前选中或活跃的控件会用醒目的青色,并加粗。遇到拿不到终端颜色信息的情况,它不会硬猜,而是退回到安全的默认样式,避免显示得很怪。
user_message_style17–19 ↗
fn user_message_style() -> Style
作用:给“用户自己发的消息”生成一套显示样式。调用者不用关心当前终端背景是什么,这个函数会自己去拿。
数据流:进去时没有参数 → 它读取终端默认背景色,然后把这个背景色交给 user_message_style_for 去决定具体样式 → 出来的是一个 Style,也就是终端界面绘制文字时用的颜色和背景设置。
调用关系:它是给渲染代码用的便捷入口。多个 render 相关流程在画用户消息时会调用它;它自己不直接算颜色,而是先问 default_bg,再把活交给 user_message_style_for。
调用图:调用 2 个内部函数(user_message_style_for, default_bg);被 7 处调用(render, render_with_mask_and_textarea_right_reserve, render, render, render, render_menu_surface, render)。
proposed_plan_style21–23 ↗
fn proposed_plan_style() -> Style
作用:给“系统提出的计划内容”生成显示样式。它让计划块和普通文本区分开,但风格和用户消息保持一致。
数据流:进去时没有参数 → 它读取终端默认背景色,再交给 proposed_plan_style_for → 出来的是用于计划内容的 Style。
调用关系:它主要在 render_display_lines 画展示行时被用到。它负责提供统一入口,真正按背景色决定样式的工作交给 proposed_plan_style_for。
调用图:调用 2 个内部函数(proposed_plan_style_for, default_bg);被 1 处调用(render_display_lines)。
table_separator_style26–28 ↗
fn table_separator_style() -> Style
作用:给 Markdown 表格里的分隔线生成低调的样式。目标是让线看得见,但不要比单元格内容还抢眼。
数据流:进去时没有参数 → 它读取终端默认前景色、默认背景色和终端颜色能力 → 把这些信息交给 table_separator_style_for → 出来的是分隔线该用的 Style。
调用关系:render_table_lines 在画表格线时会用它。它像一个包装函数,把当前终端环境收集齐,再交给 table_separator_style_for 做具体判断。
调用图:调用 4 个内部函数(table_separator_style_for, default_bg, default_fg, stdout_color_level);被 1 处调用(render_table_lines)。
accent_style31–33 ↗
fn accent_style() -> Style
作用:生成界面里“当前选中、当前活跃”的强调样式。比如选中的行、标签、快捷键提示等都可以用它保持统一视觉。
数据流:进去时没有参数 → 它读取终端默认背景色 → 交给 accent_style_for 判断该用哪种青色以及是否加粗 → 出来的是强调用的 Style。
调用关系:它被选中行、事件表、标签页、快捷键提示等多处界面组件使用。它让这些地方不用各自决定颜色,而是统一通过 accent_style_for 得到同一种强调风格。
调用图:调用 2 个内部函数(accent_style_for, default_bg);被 7 处调用(event_table_lines, selected_event_rows_use_the_shared_accent_style, selected_rows_use_the_shared_accent_style, tab_unit, keymap_debug_hint_line, keymap_picker_hint_line, keymap_row_prefix)。
user_message_style_for36–41 ↗
fn user_message_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style
作用:根据给定的终端背景色,生成用户消息的样式。它适合测试或已有背景色信息的地方使用。
数据流:进去的是一个可选的 RGB 背景色,也就是红绿蓝三个数;如果有背景色,它会调用 user_message_bg 算出一层适合叠加的消息背景;如果没有背景色,就返回默认样式 → 出来的是用户消息的 Style。
调用关系:user_message_style 会调用它。它是“知道背景色以后怎么做”的核心步骤,把具体背景颜色计算交给 user_message_bg。
调用图:调用 1 个内部函数(user_message_bg);被 1 处调用(user_message_style);外部调用 1 个(default)。
proposed_plan_style_for43–48 ↗
fn proposed_plan_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style
作用:根据给定的终端背景色,生成计划内容的样式。现在它和用户消息使用同一套背景规则。
数据流:进去的是一个可选的 RGB 背景色 → 如果有背景色,就调用 proposed_plan_bg 算出背景色并放进 Style;如果没有,就返回默认 Style → 出来的是计划内容的显示样式。
调用关系:proposed_plan_style 会调用它。它把“计划块要用什么底色”的决定交给 proposed_plan_bg,从而和用户消息背景规则保持一致。
调用图:调用 1 个内部函数(proposed_plan_bg);被 1 处调用(proposed_plan_style);外部调用 1 个(default)。
accent_style_for51–57 ↗
fn accent_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style
作用:根据终端背景色选择强调色。白底终端上用更深的青色,黑底或未知背景上用普通青色,并且都会加粗。
数据流:进去的是一个可选的 RGB 背景色 → 它判断背景是不是偏亮;如果偏亮,就把预设的深青色转换成终端最合适的颜色;否则用标准青色 → 出来的是带前景色和加粗效果的 Style。
调用关系:accent_style 会在正常界面绘制时调用它,测试函数 tests::accent_style_uses_darker_cyan_on_light_backgrounds 也会直接检查它。它依赖 best_color 把 RGB 颜色换成当前终端能显示得最接近的颜色。
调用图:调用 1 个内部函数(best_color);被 2 处调用(accent_style, accent_style_uses_darker_cyan_on_light_backgrounds);外部调用 1 个(default)。
table_separator_style_for59–73 ↗
fn table_separator_style_for(
terminal_fg: Option<(u8, u8, u8)>,
terminal_bg: Option<(u8, u8, u8)>,
color_level: StdoutColorLevel,
) -> Style
作用:根据终端前景色、背景色和颜色能力,算出表格分隔线该用什么颜色。它的重点是“低对比但仍可见”。
数据流:进去的是可选的前景 RGB、可选的背景 RGB,以及终端颜色级别 → 如果前景或背景缺失,就返回变暗的默认样式;如果信息齐全,就把前景色按很低比例混到背景色里,得到一条淡淡的线;真彩色终端直接用这个 RGB,256 色终端选最接近的颜色,颜色能力太弱时退回变暗样式 → 出来的是表格分隔线的 Style。
调用关系:table_separator_style 在实际画表格时调用它,多个测试也直接调用它检查深色和浅色背景下的结果。它把颜色混合交给 blend,把颜色适配交给 rgb_color 或 best_color。
调用图:调用 3 个内部函数(blend, best_color, rgb_color);被 3 处调用(table_separator_style, table_separator_blends_toward_dark_background, table_separator_blends_toward_light_background);外部调用 1 个(default)。
user_message_bg76–83 ↗
fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color
作用:为用户消息算一个合适的背景色。它不是随便挑颜色,而是在当前终端背景上轻轻叠一层,让消息区域有区别但不刺眼。
数据流:进去的是终端背景 RGB → 它先判断背景偏亮还是偏暗;亮背景上叠一点黑,暗背景上叠一点白;然后把混合后的 RGB 转成终端最适合显示的颜色 → 出来的是一个 Color。
调用关系:user_message_style_for 会用它给用户消息设置背景,proposed_plan_bg 也复用它。它依赖 is_light 判断明暗,依赖 blend 混色,依赖 best_color 适配终端调色板。
调用图:调用 3 个内部函数(blend, is_light, best_color);被 2 处调用(proposed_plan_bg, user_message_style_for)。
proposed_plan_bg86–88 ↗
fn proposed_plan_bg(terminal_bg: (u8, u8, u8)) -> Color
作用:为计划内容算背景色。它目前直接沿用用户消息的背景算法,让两类内容在视觉上保持一致。
数据流:进去的是终端背景 RGB → 它把这个背景色直接交给 user_message_bg → 出来的是计划内容要用的 Color。
调用关系:proposed_plan_style_for 会调用它。它本身不另起一套规则,而是复用 user_message_bg,避免用户消息和计划块的底色逻辑分叉。
调用图:调用 1 个内部函数(user_message_bg);被 1 处调用(proposed_plan_style_for)。
tests::accent_style_uses_darker_cyan_on_light_backgrounds97–102 ↗
fn accent_style_uses_darker_cyan_on_light_backgrounds()
作用:测试白色背景下强调色是否会换成更深的青色,并且保持加粗。这样能防止白底终端上普通青色太浅看不清。
数据流:进去的是测试里写死的白色背景 → 它调用 accent_style_for 得到样式 → 检查前景色是不是预期的深青色,并检查加粗标记是否存在。
调用关系:这是 accent_style_for 的保护性测试。以后有人改强调色逻辑时,如果破坏了白底可读性,这个测试会提醒。
调用图:调用 1 个内部函数(accent_style_for);外部调用 2 个(assert!, assert_eq!)。
tests::accent_style_uses_cyan_on_dark_or_unknown_backgrounds105–110 ↗
fn accent_style_uses_cyan_on_dark_or_unknown_backgrounds()
作用:测试深色背景或不知道背景时,强调样式是否使用普通青色加粗。这样能保证常见黑底终端表现稳定。
数据流:进去的是测试里构造的黑色背景和空背景信息 → 它准备一个“普通青色加粗”的预期样式 → 分别检查 accent_style_for 在这两种情况下是否返回同样结果。
调用关系:它直接验证 accent_style_for 的默认路线。和白底测试配合,覆盖了强调色最重要的两种选择。
调用图:外部调用 2 个(default, assert_eq!)。
tests::table_separator_blends_toward_dark_background113–121 ↗
fn table_separator_blends_toward_dark_background()
作用:测试深色背景下表格分隔线是否被调成接近背景的深灰色。这样线不会亮得抢走内容注意力。
数据流:进去的是白色前景、黑色背景和真彩色能力 → 它调用 table_separator_style_for → 检查得到的前景色是否是预期的深灰 RGB。
调用关系:它直接检查 table_separator_style_for 的混色结果。这个测试保护“表格线要低调”的设计。
调用图:调用 1 个内部函数(table_separator_style_for);外部调用 1 个(assert_eq!)。
tests::table_separator_blends_toward_light_background124–132 ↗
fn table_separator_blends_toward_light_background()
作用:测试浅色背景下表格分隔线是否被调成接近背景的浅灰色。这样白底终端上表格线不会太黑太重。
数据流:进去的是黑色前景、白色背景和真彩色能力 → 它调用 table_separator_style_for → 检查得到的前景色是否是预期的浅灰 RGB。
调用关系:它和深色背景测试成对出现,确保 table_separator_style_for 在亮底和暗底上都能把线调得合适。
调用图:调用 1 个内部函数(table_separator_style_for);外部调用 1 个(assert_eq!)。
tui/src/ui_consts.rs源码 ↗
这个文件很小,但作用很实际:它规定了终端用户界面左侧要预留多少列空间。终端里的“列”可以理解成一个字符占的横向位置。比如聊天输入区左边可能有边框和空格,状态提示行前面也要留空,历史消息自动换行时也要把这个左侧前缀算进去。如果每个地方都自己写一个数字,改界面时很容易漏改,最后就会出现文字对不齐、换行位置不一致的问题。这里用 LIVE_PREFIX_COLS 统一表示左侧前缀宽度,目前是 2 列;FOOTER_INDENT_COLS 则把同一个值转换成另一种数字类型,方便页脚等地方使用。它就像一把统一的尺子,让不同界面部件按同一个标准排版。
tui/src/render/renderable.rs源码 ↗
终端界面不是随便往屏幕上写字,它要先知道每块内容占多大,再把内容画到对应区域里。这个文件就像一套“积木接口”。Renderable 是所有可显示内容共同遵守的规矩:给它一块矩形区域和画布,它就把自己画进去;给它宽度,它就报出需要的高度。RenderableItem 解决“这个积木是我拥有的,还是只是借来看一眼”的问题。ColumnRenderable 把孩子从上到下排,RowRenderable 从左到右排,FlexRenderable 会把剩余高度按比例分给可伸缩的孩子,InsetRenderable 给内容四周加内边距。它还让普通字符串、Span、Line、Paragraph、Option、Arc 都能直接当作可渲染内容使用。这样别的界面代码不用关心具体类型,只要按同一套规则组合和绘制即可。
Renderable::cursor_pos17–19 ↗
fn cursor_pos(&self, _area: Rect) -> Option<(u16, u16)>
作用:这是可显示内容的默认光标位置。大多数内容不需要光标,所以默认回答“没有光标”。
数据流:进去的是一块屏幕区域 → 默认不检查也不计算 → 出来是 None,意思是不要求移动终端光标。
调用关系:各种容器在寻找光标时会问孩子这个问题;如果某个具体控件需要光标,就会覆盖这个默认做法。
Renderable::cursor_style20–22 ↗
fn cursor_style(&self, _area: Rect) -> SetCursorStyle
作用:这是可显示内容的默认光标样式。没有特殊要求时,就使用终端用户原本的光标形状。
数据流:进去的是一块屏幕区域 → 默认不看区域内容 → 出来是 SetCursorStyle::DefaultUserShape。
调用关系:当容器找到某个孩子有光标时,会继续问它光标样式;没有覆盖实现的控件就会用这个默认值。
RenderableItem::render31–36 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把 RenderableItem 里面真正的内容画出来,不管它是自己拥有的,还是借来的。
数据流:进去的是显示区域和画布 → 判断内部是 Owned 还是 Borrowed → 把同一块区域和画布转交给内部孩子绘制,画布被改写。
调用关系:它被各种父级 render 流程调用,用来隐藏“拥有/借用”的差别,让外层只当它是一个普通可画对象。
调用图:被 1 处调用(render)。
RenderableItem::desired_height38–43 ↗
fn desired_height(&self, width: u16) -> u16
作用:询问 RenderableItem 里面的内容在某个宽度下想占多高。
数据流:进去的是可用宽度 → 找到内部真正的孩子 → 返回孩子报出的高度。
调用关系:列、行、弹性布局在分配空间前会靠它估算每个孩子需要多少行。
调用图:被 1 处调用(desired_height)。
RenderableItem::cursor_pos45–50 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:询问内部内容有没有光标位置。
数据流:进去的是孩子所在区域 → 转交给 Owned 或 Borrowed 的内部孩子 → 返回孩子给出的坐标,或者没有。
调用关系:父容器在从孩子里寻找光标时会调用它,这样不用关心孩子是盒装的还是借用的。
调用图:被 1 处调用(cursor_pos)。
RenderableItem::cursor_style52–57 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:询问内部内容希望用什么光标样式。
数据流:进去的是孩子所在区域 → 转交给内部孩子 → 返回孩子要求的光标形状。
调用关系:父容器先找到有光标的孩子,再通过它取得光标样式。
调用图:被 1 处调用(cursor_style)。
RenderableItem::from61–63 ↗
fn from(value: Box<dyn Renderable + 'a>) -> Self
作用:把一个装在 Box 里的可显示内容包装成 RenderableItem。
数据流:进去的是 Box<dyn Renderable>,也就是一个堆上的可显示对象 → 标记为 Owned → 出来是 RenderableItem::Owned。
调用关系:很多构造函数接收 Into<RenderableItem>,这个转换让调用者可以直接传 Box,不必手写包装代码。
调用图:外部调用 1 个(Owned)。
Box::from70–72 ↗
fn from(value: R) -> Self
作用:把任意实现了 Renderable 的具体对象装进 Box,变成统一类型。
数据流:进去的是一个具体可显示对象 → 用 Box::new 放到堆上,并擦掉具体类型差异 → 出来是 Box<dyn Renderable>。
调用关系:ColumnRenderable::push、RowRenderable::push 等地方依赖这种转换,方便把不同种类的界面零件放进同一个列表。
调用图:外部调用 1 个(new)。
str::render83–85 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让字符串切片也能直接画到终端区域里。
数据流:进去的是字符串、显示区域和画布 → 调用 ratatui 的现成文字绘制能力 → 画布上对应区域出现这段文字。
调用关系:有了它,界面代码可以把普通 &str 当作 Renderable 使用,不需要先包成专门控件。
str::desired_height86–88 ↗
fn desired_height(&self, _width: u16) -> u16
作用:说明一段普通字符串默认只占一行高。
数据流:进去的是宽度,但这里不按宽度重新计算 → 直接返回 1。
调用关系:父布局用它估算字符串占用空间;这是一种简单约定,适合单行文字。
String::render92–94 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让拥有所有权的 String 也能直接画出来。
数据流:进去的是 String、区域和画布 → 调用 ratatui 的文字绘制 → 把文字写入画布。
调用关系:和 &str::render 作用类似,只是服务于 String 类型,方便动态生成的文字直接参与布局。
String::desired_height95–97 ↗
fn desired_height(&self, _width: u16) -> u16
作用:说明 String 默认按一行内容处理。
数据流:进去的是宽度 → 不做额外换行计算 → 返回高度 1。
调用关系:父布局在安排 String 时会用这个高度,保持简单文字的空间估算一致。
Span::render101–103 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让带样式的一小段文字 Span 可以作为可显示内容绘制。
数据流:进去的是 Span、区域和画布 → 调用 ratatui 的绘制方法 → 把带颜色或样式的文字写到画布。
调用关系:界面里需要局部高亮或变色文字时,可以直接把 Span 放进这些布局容器。
Span::desired_height104–106 ↗
fn desired_height(&self, _width: u16) -> u16
作用:说明一个 Span 默认占一行。
数据流:进去的是宽度 → 直接返回 1 → 不额外计算多行。
调用关系:父布局据此给样式文字分配一行空间。
Line::render110–112 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让 ratatui 的一整行文字 Line 可以按 Renderable 规则画出来。
数据流:进去的是 Line、区域和画布 → 调用 WidgetRef::render_ref 这个 ratatui 的引用绘制接口 → 画布被写入这一行内容。
调用关系:它把外部库的 Line 接到本文件的统一渲染接口上,供列、行等容器调用。
调用图:外部调用 1 个(render_ref)。
Line::desired_height113–115 ↗
fn desired_height(&self, _width: u16) -> u16
作用:说明一条 Line 占一行高。
数据流:进去的是宽度 → 返回 1。
调用关系:布局容器用这个结果给 Line 留出一行。
Paragraph::render119–121 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让段落 Paragraph 可以直接画进指定区域。
数据流:进去的是 Paragraph、区域和画布 → 调用 ratatui 的段落绘制 → 画布中出现可能包含换行和自动折行的文本。
调用关系:它把更复杂的文本块接入 Renderable,供父级布局统一处理。
Paragraph::desired_height122–124 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算段落在给定宽度下会占多少行。
数据流:进去的是可用宽度 → 调用 Paragraph 的 line_count 计算折行后的行数 → 返回这个行数作为高度。
调用关系:ColumnRenderable 和 FlexRenderable 这类容器会用它提前知道段落需要多少空间。
Option::render128–132 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让“可能有、也可能没有”的内容也能安全参与绘制。
数据流:进去的是 Option、区域和画布 → 如果里面是 Some,就让内部内容绘制;如果是 None,什么也不画。
调用关系:界面中有些块只在特定状态出现,这个实现让调用者不用到处写 if 判断。
Option::desired_height134–140 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算可选内容需要的高度;没有内容时就不占空间。
数据流:进去的是宽度 → Some 时返回内部内容高度,None 时返回 0。
调用关系:父布局可以直接包含 Option,隐藏内容会自然变成零高度。
Option::cursor_pos142–145 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:从可选内容里寻找光标位置。
数据流:进去的是区域 → Some 时询问内部内容,None 时返回 None。
调用关系:父容器查找光标时不用特别处理可选内容,这里会自动跳过不存在的部分。
Option::cursor_style147–152 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:从可选内容里取得光标样式;没有内容时使用默认样式。
数据流:进去的是区域 → Some 时返回内部内容的样式,None 时返回 DefaultUserShape。
调用关系:它配合 Option::cursor_pos 使用,让可隐藏控件不会破坏整体光标设置。
Arc::render156–158 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:让 Arc 包着的共享内容也能直接绘制。Arc 是一种可多人共享同一对象的智能指针。
数据流:进去的是 Arc、区域和画布 → 取出里面的真实对象引用 → 调用它的 render,画布被更新。
调用关系:当多个地方共享同一个可显示对象时,这个实现让它仍然能放进统一渲染流程。
Arc::desired_height159–161 ↗
fn desired_height(&self, width: u16) -> u16
作用:询问 Arc 内部共享对象想要的高度。
数据流:进去的是宽度 → 取内部对象 → 返回内部对象计算出的高度。
调用关系:父布局不需要知道对象是否被 Arc 包着,照常询问高度即可。
Arc::cursor_pos162–164 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:询问 Arc 内部共享对象的光标位置。
数据流:进去的是区域 → 转问内部对象 → 返回内部对象给出的坐标或 None。
调用关系:用于共享控件参与整体光标查找。
Arc::cursor_style165–167 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:询问 Arc 内部共享对象希望使用的光标样式。
数据流:进去的是区域 → 转问内部对象 → 返回它的光标样式。
调用关系:父容器处理 Arc 内容时和处理普通内容一样。
ColumnRenderable::render175–185 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把多个孩子从上到下依次画出来,像竖着堆积木。
数据流:进去的是整列区域和画布 → 从顶部 y 坐标开始,按每个孩子想要的高度切出小区域,并裁剪到父区域内 → 非空区域交给孩子绘制,画布逐段更新。
调用关系:很多界面头部、菜单、弹窗会创建 ColumnRenderable;渲染时它负责把各个竖向块摆好。
调用图:外部调用 1 个(new)。
ColumnRenderable::desired_height187–192 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算整列内容总共想要多高。
数据流:进去的是宽度 → 逐个询问孩子在这个宽度下的高度 → 把所有高度相加后返回。
调用关系:外层布局用它判断这一整列需要多少屏幕行。
ColumnRenderable::cursor_pos198–211 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:在竖排孩子里找到第一个声明光标位置的孩子。
数据流:进去的是整列区域 → 按渲染时相同的方式给每个孩子算区域 → 找到第一个非空且有光标的孩子,返回它给出的坐标;都没有就返回 None。
调用关系:整体界面设置光标时会沿着容器往下找;这里负责在竖向布局中继续查找。
调用图:外部调用 1 个(new)。
ColumnRenderable::cursor_style213–224 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:在竖排孩子里找到有光标的那个孩子,并采用它的光标样式。
数据流:进去的是整列区域 → 按孩子高度逐个切区域 → 谁有光标,就返回谁的样式;找不到就返回默认样式。
调用关系:它和 ColumnRenderable::cursor_pos 配套,让光标位置和形状来自同一个孩子。
调用图:外部调用 1 个(new)。
ColumnRenderable::new228–230 ↗
fn new() -> Self
作用:创建一个空的竖向布局容器。
数据流:没有输入 → 建一个空 children 列表 → 返回 ColumnRenderable。
调用关系:很多界面构建函数会先调用它,再用 push 往里面加内容。
调用图:被 31 处调用(new, reset_confirmation_header, settings_header, build, new, connectors_loading_popup_params, connectors_popup_params, model_menu_header, open_reasoning_popup, marketplace_add_error_popup_params (+15 more));外部调用 1 个(vec!)。
ColumnRenderable::with232–240 ↗
fn with(children: I) -> Self
作用:一次性用一批孩子创建竖向布局。
数据流:进去的是一组可转换成 RenderableItem 的孩子 → 逐个转换并收集进列表 → 返回装好孩子的 ColumnRenderable。
调用关系:构建标题、选项列表、确认弹窗等固定结构时常用它,省去一条条 push。
调用图:被 7 处调用(build_options, build_header, feedback_upload_consent_params, new, open_full_access_confirmation, open_world_writable_warning_confirmation, from);外部调用 1 个(into_iter)。
ColumnRenderable::push242–244 ↗
fn push(&mut self, child: impl Into<Box<dyn Renderable + 'a>>)
作用:往竖向布局末尾追加一个孩子。
数据流:进去的是一个可显示对象 → 转成 Box<dyn Renderable>,再包装为 Owned → 加到 children 列表末尾。
调用关系:渲染多行文本、Markdown 内容、菜单时会逐步调用它拼出一整列。
调用图:被 3 处调用(render_lines, render_markdown_content, render_menu);外部调用 2 个(into, Owned)。
FlexRenderable::new261–263 ↗
fn new() -> Self
作用:创建一个空的弹性竖向布局容器。
数据流:没有输入 → 建一个空 children 列表 → 返回 FlexRenderable。
调用关系:需要按剩余空间分配高度的界面会先创建它,再加入固定高度或弹性孩子。
调用图:被 4 处调用(as_renderable_with_composer_right_reserve, as_renderable, flex_redistributes_space_unused_by_short_children, flex_reserves_non_flex_space_before_flexible_children);外部调用 1 个(vec!)。
FlexRenderable::push265–270 ↗
fn push(&mut self, flex: i32, child: impl Into<RenderableItem<'a>>)
作用:给弹性布局追加一个孩子,并指定它的 flex 值。flex 可以理解成“分剩余空间时的份数”。
数据流:进去的是 flex 数值和孩子 → 孩子转成 RenderableItem → 存成 FlexChild 放进列表。
调用关系:调用者用它声明哪些块固定占高,哪些块按比例分享剩余高度。
调用图:外部调用 1 个(into)。
FlexRenderable::allocate275–338 ↗
fn allocate(&self, area: Rect) -> Vec<Rect>
作用:给 FlexRenderable 的每个孩子计算最终分到的矩形区域。
数据流:进去的是父区域 → 先给 flex<=0 的孩子按需要高度分空间,再把剩余高度按 flex 比例分给弹性孩子;如果某个弹性孩子用不了那么多,会把多余空间再分给别人 → 出来是一组和孩子一一对应的 Rect。
调用关系:render、desired_height、cursor_pos、cursor_style 都调用它,保证绘制、测量和找光标用的是同一套分配结果。
调用图:被 4 处调用(cursor_pos, cursor_style, desired_height, render);外部调用 5 个(new, new, with_capacity, from, vec!)。
FlexRenderable::render342–349 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:按弹性分配结果把每个孩子画出来。
数据流:进去的是父区域和画布 → 调用 allocate 算出每个孩子的区域 → 逐个让孩子在自己的区域里绘制。
调用关系:这是 FlexRenderable 真正参与屏幕绘制的入口,核心空间计算交给 allocate。
调用图:调用 1 个内部函数(allocate)。
FlexRenderable::desired_height351–356 ↗
FlexRenderable::cursor_pos358–363 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:在弹性布局的孩子里寻找光标位置。
数据流:进去的是父区域 → 先 allocate 得到每个孩子的实际区域 → 按顺序询问孩子,返回第一个光标位置。
调用关系:它确保找光标时使用的区域和实际绘制时一致。
调用图:调用 1 个内部函数(allocate)。
FlexRenderable::cursor_style365–376 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:找到弹性布局中拥有光标的孩子,并使用它的光标样式。
数据流:进去的是父区域 → allocate 算区域 → 找到第一个有光标的孩子,再问它样式 → 找不到则返回默认光标样式。
调用关系:它和 FlexRenderable::cursor_pos 配套,避免光标形状来自错误的孩子。
调用图:调用 1 个内部函数(allocate)。
RowRenderable::render384–395 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把多个孩子从左到右画出来,像横着摆盒子。
数据流:进去的是整行区域和画布 → 从左侧 x 坐标开始,按每个孩子指定宽度切区域,并限制不超过剩余宽度 → 区域为空就停止,否则让孩子绘制。
调用关系:需要左右并排显示内容时会用它,比如一行里有标签和选项。
调用图:外部调用 1 个(new)。
RowRenderable::desired_height396–411 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算横向布局需要的高度,也就是所有可见孩子里最高的那个高度。
数据流:进去的是总宽度 → 按孩子宽度逐段分配可用宽度 → 询问每个可见孩子高度,取最大值返回。
调用关系:外层竖向布局用它给这一整行留出足够高度。
RowRenderable::cursor_pos413–426 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:在横向排列的孩子中寻找光标位置。
数据流:进去的是整行区域 → 按指定宽度给每个孩子算区域 → 找到第一个有光标的孩子并返回位置;没有就返回 None。
调用关系:整体界面查光标时,遇到 RowRenderable 就由它继续在左右孩子里找。
调用图:外部调用 1 个(new)。
RowRenderable::cursor_style428–439 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:在横向孩子里找到有光标的那个,并返回它的光标样式。
数据流:进去的是整行区域 → 逐个计算孩子区域 → 谁有光标就返回谁的样式;都没有就返回默认样式。
调用关系:它和 RowRenderable::cursor_pos 使用同样的区域划分,保持光标行为一致。
调用图:外部调用 1 个(new)。
RowRenderable::new443–445 ↗
fn new() -> Self
作用:创建一个空的横向布局容器。
数据流:没有输入 → 建一个空 children 列表 → 返回 RowRenderable。
调用关系:需要组装一行选项或左右分栏时,调用者会先创建它。
调用图:被 1 处调用(selection_option_row_with_dim);外部调用 1 个(vec!)。
RowRenderable::push447–450 ↗
fn push(&mut self, width: u16, child: impl Into<Box<dyn Renderable>>)
作用:往横向布局末尾追加一个指定宽度的孩子。
数据流:进去的是宽度和可显示对象 → 对象装进 Box 并标记为 Owned → 连同宽度一起放入 children 列表。
调用关系:调用者通过多次 push 定义一行里每一段占多宽、显示什么。
调用图:外部调用 2 个(into, Owned)。
InsetRenderable::render459–461 ↗
InsetRenderable::desired_height462–467 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算带内边距内容的总高度。
数据流:进去的是外层可用宽度 → 扣掉左右内边距后询问孩子高度 → 再加上上下内边距,返回总高度。
调用关系:父布局看到的是包含空白边在内的整体尺寸,而不是只看孩子本身。
调用图:调用 1 个内部函数(desired_height)。
InsetRenderable::cursor_pos468–470 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>
作用:在带内边距的内容里查找光标位置。
数据流:进去的是外层区域 → 先缩成内层区域 → 把内层区域交给孩子查光标 → 返回孩子的结果。
调用关系:它保证光标查询和实际绘制一样,都发生在扣掉内边距后的区域里。
调用图:调用 1 个内部函数(cursor_pos);外部调用 1 个(inset)。
InsetRenderable::cursor_style472–474 ↗
fn cursor_style(&self, area: Rect) -> SetCursorStyle
作用:取得带内边距内容中孩子的光标样式。
数据流:进去的是外层区域 → 缩小为内层区域 → 向孩子询问光标样式 → 返回该样式。
调用关系:它把光标样式请求透传给被包住的孩子。
调用图:调用 1 个内部函数(cursor_style);外部调用 1 个(inset)。
InsetRenderable::new478–483 ↗
fn new(child: impl Into<RenderableItem<'a>>, insets: Insets) -> Self
作用:创建一个带内边距的可显示包装器。
数据流:进去的是孩子和 Insets(上下左右留白数) → 把孩子转成 RenderableItem,并保存留白设置 → 返回 InsetRenderable。
调用关系:插入单元格、实时尾部显示等地方会用它给内容加空白边。
调用图:被 3 处调用(from, insert_cell, live_tail_renderable);外部调用 1 个(into)。
R::inset494–498 ↗
fn inset(self, insets: Insets) -> RenderableItem<'a>
作用:给任何 Renderable 快速套上一层内边距,是一个方便写法。
数据流:进去的是某个可显示对象和 Insets → 先把原对象包装成 RenderableItem → 再创建 InsetRenderable 并包装成 Owned → 返回新的 RenderableItem。
调用关系:这是 RenderableExt 提供的扩展方法,让调用者可以写得更像“这个内容加一点边距”,不用手动创建 InsetRenderable。
调用图:外部调用 2 个(new, Owned)。
按键提示与页脚展示
此组涵盖按键绑定显示逻辑,以及将底部窗格状态转换为紧凑用户指引的页脚/标题构建器。
tui/src/key_hint.rs源码 ↗
这个文件解决的是终端按键“不讲普通话”的问题。比如有的终端把 Shift+a 报成大写 A,有的把 Ctrl+j 报成一个看不见的控制字符。这里的 KeyBinding 就像一张标准化的门票,把“键本身”和“ctrl、shift、alt 这些修饰键”放在一起。真正比较按键时,它会先把各种终端报法整理成统一格式,再判断是不是同一个快捷键。它还区分“用户正在打字”和“用户按了命令快捷键”,这样搜索框里输入 j 不会被误当成向下移动。最后,它能把快捷键变成界面上的提示文字,比如“ctrl + k”或“⌥ + ↑”,并加上淡色样式显示。
KeyBinding::new50–52 ↗
fn new(key: KeyCode, modifiers: KeyModifiers) -> Self
作用:创建一个快捷键对象,把一个具体按键和 ctrl、shift、alt 这类修饰键绑在一起。别人定义快捷键时会用它当基础积木。
数据流:进去的是一个键和一组修饰键 → 它原样放进 KeyBinding 里 → 出来的是一个可以被匹配、显示、保存的快捷键值,不改动外部状态。
调用关系:plain、alt、shift、ctrl、ctrl_alt 这些方便函数都把活交给它;配置解析 parse_keybinding 和部分测试也会直接用它来造出指定快捷键。
调用图:被 7 处调用(alt, ctrl, ctrl_alt, plain, shift, shift_letter_binding_preserves_other_modifiers_with_uppercase_compat, parse_keybinding)。
KeyBinding::from_event54–57 ↗
fn from_event(event: KeyEvent) -> Self
作用:把终端收到的一次真实按键事件转换成项目内部统一使用的快捷键格式。这样后面不用直接面对各家终端的差异。
数据流:进去的是 KeyEvent,也就是终端报上来的按键信息 → 它调用 normalize_key_parts 先做标准化 → 出来的是一个 KeyBinding,代表这次按键在本程序眼里的标准样子。
调用关系:handle_key_event 在处理用户按键时会用它;key_event_to_config_key_spec 在把按键写成配置格式时也会用它。它把标准化工作交给 normalize_key_parts。
调用图:调用 1 个内部函数(normalize_key_parts);被 2 处调用(handle_key_event, key_event_to_config_key_spec)。
KeyBinding::is_press59–63 ↗
fn is_press(&self, event: KeyEvent) -> bool
作用:判断某个真实按键事件是否触发了这个快捷键。它不仅看键和修饰键,还只接受“按下”和“长按重复”,不接受“松开”。
数据流:进去的是一个 KeyEvent → 它把自己保存的快捷键和事件里的按键都交给 normalize_key_parts 统一口径 → 如果两边一致,并且事件是按下或重复,就返回 true,否则返回 false。
调用关系:这是快捷键匹配的核心判断。列表匹配函数 KeyBinding::is_pressed 会逐个调用它;其他界面代码也依赖它来决定用户是不是按了某个命令键。
调用图:调用 1 个内部函数(normalize_key_parts)。
KeyBinding::parts65–67 ↗
fn parts(&self) -> (KeyCode, KeyModifiers)
作用:把一个快捷键拆回“键”和“修饰键”两部分。需要把快捷键转成配置文字时会用到。
数据流:进去的是一个 KeyBinding 自身 → 它读取内部保存的 key 和 modifiers → 出来的是这两个值组成的一对数据,不做转换,也不改状态。
调用关系:binding_to_config_key_spec 会调用它,把内部快捷键拆开后再写成配置里可读的格式。
调用图:被 1 处调用(binding_to_config_key_spec)。
KeyBinding::display_label69–83 ↗
fn display_label(&self) -> String
作用:把快捷键变成给人看的短标签,比如 enter、space、↑、ctrl + k。它用于界面底部或提示区展示快捷键。
数据流:进去的是一个 KeyBinding → 它先用 modifiers_to_string 拼出 ctrl、shift、alt 前缀,再把特殊键换成更友好的名字 → 出来的是一段可显示的字符串。
调用关系:Span::from 会调用它,把这段文字包装成带样式的界面文本。它自己把修饰键文字生成交给 modifiers_to_string。
调用图:调用 1 个内部函数(modifiers_to_string);被 1 处调用(from);外部调用 2 个(to_string, format!)。
normalize_key_parts86–103 ↗
fn normalize_key_parts(
key: KeyCode,
mut modifiers: KeyModifiers,
) -> (KeyCode, KeyModifiers)
作用:把不同终端对同一个按键的不同报法统一起来。比如大写 A 会被看作 shift+a,某些不可见控制字符会被看作 ctrl+对应字母。
数据流:进去的是一个键和一组修饰键 → 如果是普通字符,它会检查是不是控制字符或大写字母,并补上 CONTROL 或 SHIFT → 出来的是标准化后的键和修饰键;非字符键会原样返回。
调用关系:KeyBinding::from_event、KeyBinding::is_press 和 key_parts_to_config_key_spec 都依赖它统一口径。遇到 C0 控制字符时,它把识别工作交给 c0_control_char_to_ctrl_char。
调用图:调用 1 个内部函数(c0_control_char_to_ctrl_char);被 3 处调用(from_event, is_press, key_parts_to_config_key_spec);外部调用 3 个(Char, insert, is_empty)。
c0_control_char_to_ctrl_char105–113 ↗
fn c0_control_char_to_ctrl_char(ch: char) -> Option<char>
作用:把终端里那些看不见的老式控制字符翻译成对应的 Ctrl 快捷键字符。比如换行控制字符可以对应 ctrl+j。
数据流:进去的是一个字符 → 它看这个字符的数字编码是否落在支持的控制字符范围内 → 如果能识别,就返回对应的普通字符;不能识别就返回空值。
调用关系:它只被 normalize_key_parts 使用,是标准化 Ctrl 快捷键兼容性的底层小翻译器。
调用图:被 1 处调用(normalize_key_parts);外部调用 2 个(from_u32, from)。
KeyBinding::is_pressed126–128 ↗
fn is_pressed(&self, event: KeyEvent) -> bool
作用:判断一组候选快捷键里,有没有任何一个匹配当前按键事件。适合一个动作有多个快捷键入口的情况。
数据流:进去的是一组 KeyBinding 和一个 KeyEvent → 它逐个检查每个绑定是否匹配这次事件 → 只要有一个匹配就返回 true,全部不匹配才返回 false。
调用关系:历史搜索相关的 is_history_search_forward_key 和 is_history_search_key 会用它判断用户是否按了搜索快捷键。它内部依赖每个 KeyBinding 的 is_press 判断。
调用图:被 2 处调用(is_history_search_forward_key, is_history_search_key)。
is_plain_text_key_event139–150 ↗
fn is_plain_text_key_event(event: KeyEvent) -> bool
作用:判断一次按键是不是“用户在输入普通文字”。这能防止搜索框把普通字母误当成导航快捷键。
数据流:进去的是一个 KeyEvent → 它检查这个键是不是可打印字符,并且没有 ctrl 或 alt → 如果是普通输入就返回 true,否则返回 false;它不改动事件本身。
调用关系:多个 handle_key_event 和 handle_key 会在处理输入前调用它,用来决定这次按键该进搜索文字,还是该当命令快捷键处理。
调用图:被 4 处调用(handle_key_event, handle_key_event, handle_key_event, handle_key);外部调用 1 个(matches!)。
plain152–154 ↗
fn plain(key: KeyCode) -> KeyBinding
作用:快速创建一个“不带 ctrl、shift、alt”的普通快捷键。这样定义快捷键时不用每次都写一长串修饰键参数。
数据流:进去的是一个键 → 它把修饰键固定为 NONE,然后调用 KeyBinding::new → 出来的是普通按键绑定。
调用关系:很多默认绑定、页脚提示、历史搜索提示和渲染代码都会用它。它本质上是 KeyBinding::new 的简写版本。
调用图:调用 1 个内部函数(new);被 14 处调用(footer_props, new_with_config, footer_insert_newline_key, history_search_action_key_span, default_bindings, esc_hint_line, new, render_one_pending_steer_with_remapped_interrupt_binding, footer_tips, skills_toggle_hint_line (+4 more))。
alt156–158 ↗
fn alt(key: KeyCode) -> KeyBinding
作用:快速创建一个 Alt 组合键。比如 alt+上箭头这类快捷键可以用它清楚表达。
数据流:进去的是一个键 → 它加上 ALT 修饰键并调用 KeyBinding::new → 出来的是 Alt 快捷键绑定。
调用关系:默认快捷键、终端相关编辑快捷键、切换 agent 的快捷键等会用它。它把真正构造工作交给 KeyBinding::new。
调用图:调用 1 个内部函数(new);被 6 处调用(default_bindings, new, queued_message_edit_binding_for_terminal, alt_up_edits_most_recent_queued_message, next_agent_shortcut, previous_agent_shortcut)。
shift160–162 ↗
fn shift(key: KeyCode) -> KeyBinding
作用:快速创建一个 Shift 组合键。它让 shift+字母、shift+方向键这类绑定写起来更直观。
数据流:进去的是一个键 → 它加上 SHIFT 修饰键并调用 KeyBinding::new → 出来的是 Shift 快捷键绑定。
调用关系:页脚提示、快照提示、消息渲染和相关测试会用它。真正的对象创建仍由 KeyBinding::new 完成。
调用图:调用 1 个内部函数(new);被 6 处调用(footer_insert_newline_key, footer_snapshots, render_one_message_with_shift_left_binding, queued_message_edit_binding_for_terminal, shift_letter_binding_does_not_match_plain_lowercase_or_other_uppercase, shifted_letter_binding_matches_uppercase_char_events)。
ctrl164–166 ↗
fn ctrl(key: KeyCode) -> KeyBinding
作用:快速创建一个 Ctrl 组合键。这是终端程序里最常见的快捷键写法之一。
数据流:进去的是一个键 → 它加上 CONTROL 修饰键并调用 KeyBinding::new → 出来的是 Ctrl 快捷键绑定。
调用关系:Ctrl+C、默认快捷键、页脚状态、粘贴图片快捷键、输入处理和多处测试都会用它。它是 KeyBinding::new 的常用包装。
调用图:调用 1 个内部函数(new);被 16 处调用(on_ctrl_c, new_with_config, base_footer_mode_tracks_empty_state_after_quit_hint_expires, default_bindings, footer_snapshots, footer_status_line_truncates_to_keep_mode_indicator, paste_image_shortcut_prefers_ctrl_alt_v_under_wsl, handle_key_event, on_ctrl_c, on_ctrl_d (+6 more))。
ctrl_alt168–170 ↗
fn ctrl_alt(key: KeyCode) -> KeyBinding
作用:快速创建一个同时按 Ctrl 和 Alt 的组合键。适合需要更复杂、不容易误触的快捷键。
数据流:进去的是一个键 → 它把 CONTROL 和 ALT 合在一起,再调用 KeyBinding::new → 出来的是 Ctrl+Alt 快捷键绑定。
调用关系:粘贴图片快捷键相关逻辑和测试会用它,尤其是在需要区分普通 Ctrl 或普通 Alt 的地方。
调用图:调用 1 个内部函数(new);被 1 处调用(paste_image_shortcut_prefers_ctrl_alt_v_under_wsl)。
modifiers_to_string172–184 ↗
fn modifiers_to_string(modifiers: KeyModifiers) -> String
作用:把 ctrl、shift、alt 这些修饰键变成可读文字前缀。它负责决定提示里写成“ctrl + ”还是 Mac 风格的“⌥ + ”。
数据流:进去的是一组修饰键 → 它按 CONTROL、SHIFT、ALT 的顺序检查并拼接文字 → 出来的是一段前缀字符串,比如“ctrl + shift + ”。
调用关系:KeyBinding::display_label 调用它来生成快捷键显示文字的前半段。
调用图:被 1 处调用(display_label);外部调用 2 个(contains, new)。
Span::from192–194 ↗
fn from(binding: &KeyBinding) -> Self
作用:把 KeyBinding 转成 ratatui 的 Span,也就是终端界面里一段带样式的文字。这样快捷键可以直接放进 UI 组件显示。
数据流:进去的是一个 KeyBinding 或它的引用 → 它先拿 display_label 生成文字,再用 key_hint_style 取样式 → 出来的是一段带淡色样式的 Span。
调用关系:界面提示渲染需要把快捷键显示成文本时会走到这里。它把文字内容交给 display_label,把样式交给 key_hint_style。
调用图:调用 2 个内部函数(display_label, key_hint_style);外部调用 1 个(styled)。
key_hint_style197–199 ↗
has_ctrl_or_alt201–203 ↗
fn has_ctrl_or_alt(mods: KeyModifiers) -> bool
作用:判断修饰键里是否真的包含 Ctrl 或 Alt。它还会避开 Windows 上的 AltGr,因为 AltGr 常用于输入特殊字符,不应该误判成命令快捷键。
数据流:进去的是一组修饰键 → 它检查是否包含 CONTROL 或 ALT,并调用 is_altgr 排除 AltGr 情况 → 出来的是布尔值,表示这次按键更像快捷命令还是普通输入辅助。
调用关系:handle_key_event、handle_input_basic_with_time、handle_history_search_key、handle_key_event_at 等输入处理流程会用它。它把平台差异判断交给 is_altgr。
调用图:调用 1 个内部函数(is_altgr);被 4 处调用(handle_key_event, handle_input_basic_with_time, handle_history_search_key, handle_key_event_at);外部调用 1 个(contains)。
is_altgr213–215 ↗
fn is_altgr(_mods: KeyModifiers) -> bool
作用:判断一组修饰键是不是 AltGr。AltGr 是一些键盘上用来输入特殊字符的键,在 Windows 上通常表现得像 Ctrl+Alt。
数据流:进去的是修饰键集合 → 在 Windows 上检查是否同时有 ALT 和 CONTROL;在非 Windows 上固定认为不是 AltGr → 出来的是 true 或 false。
调用关系:has_ctrl_or_alt 会调用它,避免把 AltGr 输入误当成快捷命令;input_with_keymap 也会用它理解按键映射里的平台差异。
调用图:被 2 处调用(input_with_keymap, has_ctrl_or_alt);外部调用 1 个(contains)。
tests::is_press_accepts_press_and_repeat_but_rejects_release222–239 ↗
tests::keybinding_list_ext_matches_any_binding242–248 ↗
tests::shifted_letter_binding_matches_uppercase_char_events251–257 ↗
fn shifted_letter_binding_matches_uppercase_char_events()
作用:测试 shift+a 能兼容终端报出来的大写 A。这样用户在不同终端里按 Shift 字母都能触发同一个快捷键。
数据流:进去的是 shift+a 绑定和几种 A/a 事件 → 它检查显式 Shift+a、无 Shift 的大写 A、带 Shift 的大写 A → 断言都能匹配。
调用关系:它调用 shift 创建绑定,间接验证 normalize_key_parts 对大写字母的标准化。
tests::shift_letter_binding_preserves_other_modifiers_with_uppercase_compat260–267 ↗
fn shift_letter_binding_preserves_other_modifiers_with_uppercase_compat()
作用:测试大写字母兼容时不会丢掉 Ctrl 这类其他修饰键。否则 Ctrl+Shift+i 可能被错误识别。
数据流:进去的是 Ctrl+Shift+i 绑定和一个 Ctrl+I 形式的大写事件 → 标准化后应该补出 Shift,同时保留 Ctrl → 断言两者匹配。
调用关系:它直接调用 KeyBinding::new 构造复杂绑定,用来验证 normalize_key_parts 的细节不会破坏组合键。
tests::shift_letter_binding_does_not_match_plain_lowercase_or_other_uppercase270–275 ↗
fn shift_letter_binding_does_not_match_plain_lowercase_or_other_uppercase()
作用:测试 shift+o 不会误匹配普通小写 o,也不会误匹配别的大写字母。它防止快捷键匹配太宽松。
数据流:进去的是 shift+o 绑定、普通 o 事件和大写 P 事件 → 它逐个匹配 → 断言这些错误输入都不能触发。
调用关系:它调用 shift 创建绑定,补充验证 KeyBinding::is_press 和 normalize_key_parts 的边界。
tests::ctrl_letter_binding_matches_c0_control_char_events278–283 ↗
fn ctrl_letter_binding_matches_c0_control_char_events()
作用:测试 Ctrl 字母可以匹配终端报出的 C0 控制字符。C0 控制字符是早期终端使用的一组不可见字符。
数据流:进去的是 ctrl+p 绑定,以及一个裸的控制字符事件和一个带 Alt 的控制字符事件 → 它检查裸控制字符能匹配,带 Alt 的不能匹配 → 得到兼容但不乱匹配的结果。
调用关系:它调用 ctrl 创建绑定,验证 c0_control_char_to_ctrl_char 通过 normalize_key_parts 发挥作用。
tests::ctrl_bindings_match_all_supported_c0_control_char_events286–333 ↗
fn ctrl_bindings_match_all_supported_c0_control_char_events()
作用:测试所有被支持的 C0 控制字符都能对应到正确的 Ctrl 快捷键。它像一张对照表检查翻译没有漏项。
数据流:进去的是一批“Ctrl 字符”和“原始控制字符”的配对 → 每一对都检查裸控制字符能匹配,同时带 Alt 的不能匹配 → 如果有一项不对,测试失败。
调用关系:它集中验证 normalize_key_parts 和 c0_control_char_to_ctrl_char 的完整映射范围。
调用图:外部调用 1 个(assert!)。
tests::ctrl_binding_does_not_match_ambiguous_c0_escape_or_delete336–345 ↗
fn ctrl_binding_does_not_match_ambiguous_c0_escape_or_delete()
作用:测试 Escape 和 Delete 这类容易产生歧义的控制字符不会被当成 Ctrl+[ 或 Ctrl+?。这样可以避免误触发危险或奇怪的快捷键。
数据流:进去的是 ctrl+[、ctrl+? 绑定,以及 ESC、DEL 原始字符事件 → 它尝试匹配 → 断言都不能匹配。
调用关系:它验证 c0_control_char_to_ctrl_char 有意不支持这些含糊字符,保证 normalize_key_parts 不过度翻译。
调用图:外部调用 1 个(assert!)。
tests::history_search_ctrl_bindings_match_c0_control_char_events348–357 ↗
fn history_search_ctrl_bindings_match_c0_control_char_events()
作用:测试历史搜索常用的 Ctrl+R 和 Ctrl+S 在控制字符报法下也能识别。这样历史搜索快捷键在更多终端中可用。
数据流:进去的是 ctrl+r、ctrl+s 绑定和对应的原始控制字符事件 → 它做匹配检查 → 断言两者都能正常触发。
调用关系:它关注历史搜索场景,实际验证 ctrl 快捷键兼容逻辑能服务上层搜索功能。
调用图:外部调用 1 个(assert!)。
tests::ctrl_alt_sets_both_modifiers360–368 ↗
fn ctrl_alt_sets_both_modifiers()
作用:测试 ctrl_alt 创建的绑定确实同时带有 Ctrl 和 Alt。它保证这个快捷函数没有漏掉任何一个修饰键。
数据流:进去的是 ctrl_alt(v) → 它取出 parts → 断言结果是字符 v 加 CONTROL 和 ALT 两个修饰键。
调用关系:它验证 ctrl_alt 这个构造快捷方式,间接确认 KeyBinding::parts 能正确读回内部数据。
调用图:外部调用 1 个(assert_eq!)。
tests::has_ctrl_or_alt_checks_supported_modifier_combinations371–380 ↗
fn has_ctrl_or_alt_checks_supported_modifier_combinations()
作用:测试 has_ctrl_or_alt 对常见修饰键组合的判断。它还覆盖 Windows 和非 Windows 对 Ctrl+Alt 的不同解释。
数据流:进去的是 NONE、CONTROL、ALT、CONTROL|ALT 等修饰键组合 → 它调用 has_ctrl_or_alt → 断言普通 Ctrl/Alt 会被识别,而 Windows 上 Ctrl+Alt 会按 AltGr 规则特殊处理。
调用关系:它验证 has_ctrl_or_alt 和 is_altgr 配合后的平台差异行为,避免输入处理把普通打字误判成快捷命令。
调用图:外部调用 1 个(assert!)。
tui/src/bottom_pane/action_required_title.rs源码 ↗
这个文件解决的是“标题栏怎么显示得清楚又不乱”的问题。程序底部面板有时要提醒用户:现在有事情需要你处理。标题不能只是硬写一句话,因为后面可能还要带上当前模型、会话状态、路径之类的信息;但有些信息又不该显示,比如加载中的转圈标记,或者调用方明确说要排除的项目。这里就像在做一块门牌:先放上固定的警示标题,再按顺序挑选能显示的内容,用“ | ”隔开。核心函数会遍历传进来的标题项目,跳过不需要的,向外部给的取值函数询问每个项目应该显示成什么文字,最后拼成一整行字符串。
build_action_required_title_text5–25 ↗
fn build_action_required_title_text(
prefix: &str,
items: I,
excluded_items: &[TerminalTitleItem],
mut value_for: F,
) -> String
作用:这个函数把“需要处理”的标题拼成最终要显示的一行文字。调用者可以决定有哪些标题项目、哪些不要显示,以及每个项目具体显示什么内容。
数据流:进去的是一个固定前缀、一串候选标题项目、一个排除名单,以及一个把项目变成文字的函数。它先把前缀放进结果里,然后逐个检查项目:如果是转圈加载标记,或者在排除名单里,就跳过;否则就请传入的函数给出显示文字,拿到文字就追加进去。最后它用“ | ”把所有片段连起来,输出一整条标题字符串,不会改动外部数据。
调用关系:它是这个小文件里的核心拼装工具。需要显示“Action Required”标题的界面代码会在准备标题时调用它;它自己不决定每个项目的内容,而是把这部分交给调用者传入的取值函数。内部只用到列表创建和排除名单检查,也就是先准备零件,再筛掉不该上牌子的零件。
弹出与选择视图辅助工具
这些文件提供轻量级选择式底部窗格视图使用的可复用状态、常量、行模型、标签页和编号行辅助工具。
tui/src/bottom_pane/popup_consts.rs源码 ↗
底部面板里会有很多小弹窗,比如选择项、确认框、搜索提示等。这个文件就像一张统一的弹窗小抄:规定弹窗最多别显示超过 8 行,避免界面太挤;还提供通用的底部提示语,比如“按 Enter 确认,按 Esc 返回”。它用 ratatui 的 Line(一行可带样式的终端文字)来拼出提示内容,并把按键显示交给 key_hint 这类工具,让按键看起来和整个界面风格一致。更灵活的地方是,它不只支持固定的 Enter/Esc,也能根据当前列表的按键设置来生成提示。这样用户改了快捷键,弹窗提示也能跟着变,不会出现“提示写的是 A,实际要按 B”的尴尬。
standard_popup_hint_line16–24 ↗
fn standard_popup_hint_line() -> Line<'static>
作用:生成最常见的弹窗底部提示:按 Enter 确认,按 Esc 返回。弹窗如果没有特殊快捷键需求,就可以直接用它,省得每个地方重复写同一句话。
数据流:它不需要外部输入。进去的是代码里固定好的 Enter 和 Esc 两个按键 → 它把普通文字和按键提示拼成一行终端文字 → 出来的是一个 Line,可以直接放到弹窗底部显示;它不修改任何状态。
调用关系:很多弹窗和选择界面在渲染底部说明时会用到它,例如确认替换目标、标准弹窗提示、反馈相关弹窗、选择视图和搜索行测试等。它自己只做简单拼装,把文字片段交给 Line::from 和向量构造来组成最终显示内容。
调用图:被 18 处调用(show_replace_thread_goal_confirmation, apply_standard_popup_hint, render, render, feedback_disabled_params, feedback_upload_consent_params, make_selection_view, renders_search_query_line_when_enabled, snapshot_footer_note_wraps, footer_hint (+8 more));外部调用 2 个(from, vec!)。
standard_popup_hint_line_for_keymap26–33 ↗
fn standard_popup_hint_line_for_keymap(list_keymap: &ListKeymap) -> Line<'static>
作用:根据当前列表的按键配置,生成“确认/返回”的提示文字。它的用处是:如果用户或界面使用的不是固定 Enter/Esc,也能显示正确的按键。
数据流:进去的是一个 ListKeymap,也就是列表操作的按键表 → 它从“确认”和“取消”这两类动作里取出主要按键 → 再交给 accept_cancel_hint_line 拼成一句人能读懂的提示 → 出来的是一行可显示在弹窗底部的 Line。
调用关系:它处在“按键配置”和“最终提示文字”之间。需要按当前快捷键显示提示的地方,比如选择视图参数,会调用它;调用图里标准弹窗提示相关流程也会走到它。它把取主要按键的活交给 primary_binding,把拼句子的活交给 accept_cancel_hint_line。
调用图:调用 2 个内部函数(accept_cancel_hint_line, primary_binding);被 2 处调用(standard_popup_hint_line, selection_view_params)。
accept_cancel_hint_line35–61 ↗
fn accept_cancel_hint_line(
accept: Option<KeyBinding>,
accept_label: &'static str,
cancel: Option<KeyBinding>,
cancel_label: &'static str,
) -> Line<'static>
作用:把“确认键”和“取消键”组合成一条底部提示。它还能处理某个键不存在的情况,比如只显示确认,或只显示返回,避免界面上出现空的、错的提示。
数据流:进去的是可选的确认按键、确认说明文字、可选的取消按键、取消说明文字 → 它判断两个按键是否存在:两个都有就写成“按 A 做某事或按 B 做某事”,只有一个就只写一个,都没有就返回空行 → 出来的是一条 Line;它只生成文字,不改变外部数据。
调用关系:它是最底层的提示拼装工。standard_popup_hint_line_for_keymap 会先找出当前按键,再把结果交给它;approval_footer_hint 这类需要自定义确认/取消文案的地方也会直接用它。它不关心弹窗是什么,只负责把给定按键和说明拼成用户看得懂的一句话。
调用图:被 2 处调用(approval_footer_hint, standard_popup_hint_line_for_keymap);外部调用 2 个(from, vec!)。
tui/src/bottom_pane/scroll_state.rs源码 ↗
可以把它想成一个小本子,专门记两件事:当前选中第几行,以及列表窗口从第几行开始显示。终端界面常常只能显示一小段列表,比如总共有 100 行,但屏幕只露出 10 行。用户按上下键、翻页键、Home/End 键时,程序既要改“选中行”,又要保证这行还在看得见的范围里。ScrollState 就把这套规则集中起来:空列表时清空选择;普通上下移动会从头尾循环;翻页不会循环,只会停在边界;跳到顶部或底部后会自动调整滚动位置。它不保存列表内容,也不保存列表长度,而是每次由调用者传入当前行数和可见行数。这样过滤、分页、窗口变高变矮时都能复用,但调用者必须传最新的长度,否则选中位置可能对不上。
ScrollState::new21–26 ↗
fn new() -> Self
作用:新建一个干净的滚动状态。刚创建时没有选中任何行,滚动位置也在最顶部。
数据流:进去不需要任何输入 → 它生成一个 ScrollState,把 selected_idx 设成 None,把 scroll_top 设成 0 → 出来的是一份初始状态,供列表开始使用。
调用关系:很多界面状态初始化时会调用它,比如新建面板、打开事件、返回事件列表等场景。它是这些列表拥有滚动和选择能力的起点,后续再交给移动、翻页、可见性调整等函数继续更新。
调用图:被 17 处调用(action_state, new, new, new, from_entry, open_selected_event, return_to_events, new, new, open_reset_confirmation (+7 more))。
ScrollState::reset29–32 ↗
fn reset(&mut self)
作用:把列表的选择和滚动位置恢复到初始状态。常用于列表内容彻底换了,旧的选中行已经不可信的时候。
数据流:进去的是当前这份 ScrollState → 它不看列表长度,直接把 selected_idx 清成 None,把 scroll_top 改回 0 → 出来后等于“没有选中,画面回到开头”。
调用关系:当输入框文字变化、空提示被设置、切换标签页时,上层会调用它。意思是:旧列表上下文已经变了,先清空状态,再由后续逻辑按新内容重新选择或显示。
调用图:被 3 处调用(on_composer_text_change, set_empty_prompt, switch_tab)。
ScrollState::clamp_selection35–40 ↗
fn clamp_selection(&mut self, len: usize)
作用:把当前选中行修正到合法范围内。比如列表过滤后变短了,原来选第 20 行可能已经不存在,这个函数会把它拉回到最后一行以内。
数据流:进去的是当前状态和当前列表长度 len → 它先调用 clear_if_empty 检查列表是不是空;如果空就清空选择和滚动;如果不空,就把没有选择的情况变成选第 0 行,把越界的选择压到 len - 1 → 出来后 selected_idx 一定不会指向不存在的行。
调用关系:过滤结果变化、匹配项变化、上层自己的 clamp_selection 流程会用它。它把“列表长度变化后的安全修正”集中处理,并把空列表这个特殊情况交给 clear_if_empty。
调用图:调用 1 个内部函数(clear_if_empty);被 7 处调用(on_composer_text_change, set_matches, apply_filter, clamp_selection, apply_filter, clamp_selection, apply_filter)。
ScrollState::move_up_wrap43–52 ↗
fn move_up_wrap(&mut self, len: usize)
作用:让选中行向上移动一格,并且支持从第一行绕到最后一行。就像菜单里按上键,到了顶部再按会跳到底部。
数据流:进去的是当前状态和列表长度 len → 它先用 clear_if_empty 处理空列表;如果不空,已有选择且不在第一行就减 1,在第一行就变成 len - 1,没有选择就选第 0 行 → 出来后 selected_idx 指向新的上一项,但 scroll_top 不会在这里自动改。
调用关系:很多上层的 move_up 或跳过禁用项的 skip_disabled_up 会调用它。它只负责算“该选哪一行”,至于是否需要滚动到看得见,通常由调用方之后再调用 ensure_visible。
调用图:调用 1 个内部函数(clear_if_empty);被 10 处调用(move_up, move_up, move_up, move_up, move_up, skip_disabled_up, move_up, move_up, move_up, move_up)。
ScrollState::move_down_wrap55–63 ↗
fn move_down_wrap(&mut self, len: usize)
作用:让选中行向下移动一格,并且支持从最后一行绕回第一行。它对应用户在列表里按下方向键的常见行为。
数据流:进去的是当前状态和列表长度 len → 它先用 clear_if_empty 判断空列表;如果不空,已有选择且下面还有行就加 1,已经在最后一行或还没选择时就选第 0 行 → 出来后 selected_idx 指向新的下一项,滚动位置本身不在这里调整。
调用关系:多个界面列表的 move_down,以及跳过禁用项的 skip_disabled_down,会借它完成基础移动。之后调用方通常会再安排 ensure_visible,让新选中的行显示在窗口里。
调用图:调用 1 个内部函数(clear_if_empty);被 10 处调用(move_down, move_down, move_down, move_down, move_down, skip_disabled_down, move_down, move_down, move_down, move_down)。
ScrollState::page_up_clamped70–78 ↗
fn page_up_clamped(&mut self, len: usize, visible_rows: usize)
作用:向上翻一页选择,但不会从顶部绕到底部。连续按 PageUp 时,最后会停在第一行,符合多数终端列表的习惯。
数据流:进去的是当前状态、列表长度 len、当前能看见的行数 visible_rows → 它先处理空列表;把翻页步长设为至少 1 行;从当前选中行往上减这个步长,减过头就停在 0;最后调用 ensure_visible 调整滚动窗口 → 出来后选中行上移一页,并且仍然看得见。
调用关系:上层的 page_up 动作会调用它。它内部先交给 clear_if_empty 排除空列表,再交给 ensure_visible 修正显示窗口,所以调用方不用自己重复处理翻页后的可见性。
调用图:调用 2 个内部函数(clear_if_empty, ensure_visible);被 5 处调用(page_up, page_up, page_up, page_up, page_up)。
ScrollState::page_down_clamped85–93 ↗
fn page_down_clamped(&mut self, len: usize, visible_rows: usize)
作用:向下翻一页选择,但不会从底部绕回顶部。连续按 PageDown 时,最后会停在最后一行。
数据流:进去的是当前状态、列表长度 len、可见行数 visible_rows → 它先用 clear_if_empty 处理空列表;把步长设为至少 1;从当前选中行往下加这个步长,加过头就压到 len - 1;最后调用 ensure_visible → 出来后选中行下移一页,滚动窗口也跟着挪到能看见它的位置。
调用关系:各个列表的 page_down 动作会用它。它把“翻页选择”和“保持可见”打包完成,内部依赖 clear_if_empty 和 ensure_visible,减少上层界面代码出错。
调用图:调用 2 个内部函数(clear_if_empty, ensure_visible);被 5 处调用(page_down, page_down, page_down, page_down, page_down)。
ScrollState::jump_top96–102 ↗
fn jump_top(&mut self, len: usize, visible_rows: usize)
作用:直接选中第一行,并把显示窗口调整到能看到第一行。它通常对应 Home 键或“跳到顶部”的命令。
数据流:进去的是当前状态、列表长度 len、可见行数 visible_rows → 它先检查空列表;如果有内容,就把 selected_idx 设为 0;再调用 ensure_visible → 出来后选择落在第一行,scroll_top 通常也回到 0。
调用关系:多个上层 jump_top 动作会调用它。它先靠 clear_if_empty 避免空列表出错,再靠 ensure_visible 完成滚动修正,是“跳到开头”这个用户动作的底层实现。
调用图:调用 2 个内部函数(clear_if_empty, ensure_visible);被 5 处调用(jump_top, jump_top, jump_top, jump_top, jump_top)。
ScrollState::jump_bottom105–111 ↗
fn jump_bottom(&mut self, len: usize, visible_rows: usize)
作用:直接选中最后一行,并把显示窗口滚到能看到它。它通常对应 End 键或“跳到底部”的命令。
数据流:进去的是当前状态、列表长度 len、可见行数 visible_rows → 它先处理空列表;如果有内容,就把 selected_idx 设为 len - 1;随后调用 ensure_visible → 出来后选择落在最后一行,scroll_top 会被调整到列表尾部附近。
调用关系:多个上层 jump_bottom 动作会调用它。它和 jump_top 成对出现,内部同样把空列表检查交给 clear_if_empty,把画面滚动交给 ensure_visible。
调用图:调用 2 个内部函数(clear_if_empty, ensure_visible);被 5 处调用(jump_bottom, jump_bottom, jump_bottom, jump_bottom, jump_bottom)。
ScrollState::clear_if_empty113–120 ↗
fn clear_if_empty(&mut self, len: usize) -> bool
作用:专门处理“列表一行都没有”的情况。它像一道安全门,防止其他函数在空列表里还试图选第 0 行或最后一行。
数据流:进去的是当前状态和列表长度 len → 如果 len 不是 0,它什么也不改并返回 false;如果 len 是 0,它把 selected_idx 清成 None,把 scroll_top 设为 0,并返回 true → 出来时调用者能知道是否已经因为空列表而提前处理完。
调用关系:clamp_selection、上下移动、翻页、跳顶、跳底都会先调用它。它是这些操作共同的防护点,让每个动作都不用重复写一遍空列表处理。
调用图:被 7 处调用(clamp_selection, jump_bottom, jump_top, move_down_wrap, move_up_wrap, page_down_clamped, page_up_clamped)。
ScrollState::ensure_visible124–141 ↗
fn ensure_visible(&mut self, len: usize, visible_rows: usize)
作用:调整滚动起点,保证当前选中的行在屏幕可见范围内。简单说,就是“选到哪,窗口就跟到哪,但只在必要时移动”。
数据流:进去的是当前状态、列表长度 len、可见行数 visible_rows → 如果列表为空或一行都显示不了,就把 scroll_top 归零;如果有选中行且它在当前窗口上方,就把窗口顶端挪到它;如果它在窗口下方,就把窗口往下挪到刚好包含它;如果已经看得见,就不动 → 出来后 scroll_top 与 selected_idx 匹配,选中行不会藏在屏幕外。
调用关系:它被很多地方调用:上下移动后、输入变化后、匹配项变化后、翻页和跳转内部都会用到。它是显示层最关键的“别让光标跑出视野”规则。
调用图:被 33 处调用(move_down, move_up, on_composer_text_change, move_down, move_up, move_down, move_up, set_matches, move_down, move_up (+15 more))。
tui/src/bottom_pane/selection_popup_common.rs源码 ↗
这个文件服务的是终端用户界面,也就是在命令行窗口里画出来的界面。它把一个个可选项整理成“可显示的行”,再根据窗口宽度、滚动位置、当前选中项来决定显示哪几行、每行怎么换行、说明文字从哪一列开始。可以把它想成菜单的排版师:先给菜单铺一层统一背景和内边距,再量文字长度,决定左右两栏宽度,最后把选中项染成强调色、禁用项变灰。它还特别处理窄窗口、空列表、长描述、模糊搜索命中的加粗字符等情况。重要的是,测量高度和真正绘制用的是同一套规则,否则弹窗可能预留空间不够,文字会被截掉。
ColumnWidthConfig::new65–70 ↗
fn new(mode: ColumnWidthMode, name_column_width: Option<usize>) -> Self
作用:创建一份列宽配置,告诉菜单名称栏和说明栏该怎么分配宽度。调用者需要稳定排版时会用它,比如让所有行按固定比例或固定参考宽度对齐。
数据流:输入一个列宽模式和一个可选的名称栏宽度 → 把它们放进 ColumnWidthConfig 这个小配置对象里 → 返回配置本身,不画界面,也不改别的状态。
调用关系:它通常在上层计算高度或渲染菜单前被调用。后面的 render 和 desired_height 这类流程会把这个配置传给真正测量和绘制行的函数。
调用图:被 2 处调用(desired_height, render)。
wrap_styled_line113–122 ↗
fn wrap_styled_line(line: &'a Line<'a>, width: u16) -> Vec<Line<'a>>
作用:把一行带样式的文字按指定宽度换成多行,同时保留颜色、加粗等样式。它解决的是长提示文字在窄窗口里溢出的问题。
数据流:输入一行带样式文字和目标宽度 → 把宽度至少修正为 1,避免极窄窗口出错 → 调用项目里的自动换行工具 → 输出多行仍然带样式的文字。
调用关系:它被高度计算和未回答确认框布局使用。上层先用它预估文字会占几行,再决定弹窗需要多高。
调用图:调用 1 个内部函数(new);被 2 处调用(desired_height, unanswered_confirmation_layout);外部调用 1 个(from)。
line_to_owned124–137 ↗
fn line_to_owned(line: Line<'_>) -> Line<'static>
作用:把一行可能借用外部字符串的文字,转换成完全自己拥有内容的文字行。这样返回出去以后,不会因为原始临时文本消失而失效。
数据流:输入一行 Line → 复制它的整体样式、对齐方式和每个文字片段 → 输出生命周期更独立的 Line,不改变原来的行。
调用关系:它是换行流程里的收尾小工具,主要配合 wrap_standard_row 使用。标准换行得到的行可能引用原始内容,这个函数把它们变成安全可保存、可继续加工的结果。
compute_desc_col139–203 ↗
fn compute_desc_col(
rows_all: &[GenericDisplayRow],
start_idx: usize,
visible_items: usize,
content_width: u16,
column_width: ColumnWidthConfig,
) -> usize
作用:计算说明文字应该从第几列开始。它让“名称”和“说明”像表格一样对齐,而不是每一行都乱跳。
数据流:输入全部行、当前起始行、可见数量、可用宽度和列宽配置 → 根据固定比例、可见行最长名称,或全部行最长名称来估算名称栏宽度 → 输出说明栏开始的列号。
调用关系:它是排版核心,被渲染、测量高度、单行渲染以及滚动修正流程反复使用。后续 build_full_line 和换行函数会按照这个列号插入空格或拆分两栏。
调用图:被 4 处调用(adjust_start_for_wrapped_selection_visibility, measure_rows_height_inner, render_rows_inner, render_rows_single_line_with_col_width_mode);外部调用 1 个(iter)。
wrap_indent206–216 ↗
fn wrap_indent(row: &GenericDisplayRow, desc_col: usize, max_width: u16) -> usize
作用:决定某一行换到第二行以后要缩进多少空格。这样长说明换行后能对齐到说明栏,看起来不会乱。
数据流:输入一条显示行、说明栏列号和最大宽度 → 优先使用行自己指定的缩进;没有指定时,有说明或禁用原因就缩到说明栏,否则不缩进 → 输出不超过最大宽度的缩进数。
调用关系:它被 wrap_standard_row 调用。标准换行会拿这个缩进数来设置后续行的开头空格。
调用图:被 1 处调用(wrap_standard_row)。
should_wrap_name_in_column218–228 ↗
fn should_wrap_name_in_column(row: &GenericDisplayRow) -> bool
作用:判断一条行是否适合用“两栏分别换行”的特殊排版。这个规则只给比较朴素的选项用,避免带快捷键、标签或搜索高亮的复杂行被拆坏。
数据流:输入一条显示行 → 检查它是否有自定义缩进、有说明、没有禁用原因、没有匹配高亮、没有快捷键、没有分类标签、没有前缀样式 → 输出 true 或 false。
调用关系:它被 wrap_row_lines 先调用,像一个分岔口。结果为 true 时走 wrap_two_column_row,否则走通用的 wrap_standard_row。
调用图:被 1 处调用(wrap_row_lines)。
wrap_two_column_row230–288 ↗
fn wrap_two_column_row(row: &GenericDisplayRow, desc_col: usize, width: u16) -> Vec<Line<'static>>
作用:把名称和说明当成左右两栏分别换行。适合那种“左边是较长选项名,右边是解释”的菜单行。
数据流:输入一条行、说明栏起始列和总宽度 → 算出左栏宽度、右栏宽度,分别对名称和说明做文字换行 → 再把同一序号的左右两行拼起来 → 输出多行可直接渲染的文字;如果宽度太窄无法分栏,就返回空列表让外层改走普通方式。
调用关系:wrap_row_lines 会在规则允许时调用它。测试 one_cell_width_falls_back_without_panic_for_wrapped_two_column_rows 专门验证极窄宽度下它不会崩溃,而是安全返回空结果。
调用图:被 2 处调用(one_cell_width_falls_back_without_panic_for_wrapped_two_column_rows, wrap_row_lines);外部调用 5 个(from, new, with_capacity, new, wrap)。
wrap_standard_row290–303 ↗
fn wrap_standard_row(row: &GenericDisplayRow, desc_col: usize, width: u16) -> Vec<Line<'static>>
作用:用普通方式把一条菜单行组装完整并按宽度换行。它适合大多数带样式、快捷键、禁用说明或搜索高亮的行。
数据流:输入一条行、说明栏列号和总宽度 → 先调用 build_full_line 拼出完整显示文本 → 再用 wrap_indent 决定后续行缩进 → 调用项目的换行工具 → 输出拥有自己内容的多行文字。
调用关系:它是 wrap_row_lines 的默认路线。复杂行不走两栏分别换行时,都会交给它处理。
调用图:调用 3 个内部函数(build_full_line, wrap_indent, new);被 1 处调用(wrap_row_lines);外部调用 1 个(from)。
wrap_row_lines305–314 ↗
fn wrap_row_lines(row: &GenericDisplayRow, desc_col: usize, width: u16) -> Vec<Line<'static>>
作用:统一决定一条菜单行到底怎么换行。调用者不用关心特殊两栏和普通换行的区别,只拿到可画的多行文本。
数据流:输入一条行、说明栏列号和宽度 → 先判断能不能用两栏换行;能且成功就返回两栏结果 → 否则返回标准换行结果。
调用关系:它被渲染、测量高度、检查选中项是否可见这些流程共用。这样“算高度”和“真正画出来”尽量保持一致。
调用图:调用 3 个内部函数(should_wrap_name_in_column, wrap_standard_row, wrap_two_column_row);被 3 处调用(is_selected_visible_in_wrapped_viewport, measure_rows_height_inner, render_rows_inner)。
apply_row_state_style316–331 ↗
fn apply_row_state_style(lines: &mut [Line<'static>], selected: bool, is_disabled: bool)
作用:给已经排好的行套上状态样式:选中项变成强调色,禁用项变灰。它让用户一眼知道当前焦点在哪、哪些项不能选。
数据流:输入一组已换好的文字行,以及是否选中、是否禁用两个标记 → 遍历每个文字片段并修改样式 → 不返回新值,而是直接改传进来的行。
调用关系:render_rows_inner 在每条行准备渲染前调用它。它只管样式,不管换行和位置。
调用图:被 1 处调用(render_rows_inner);外部调用 1 个(iter_mut)。
compute_item_window_start333–354 ↗
fn compute_item_window_start(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_items: usize,
) -> usize
作用:计算列表应该从第几个选项开始显示。它保证滚动位置合理,并尽量让当前选中项留在可见范围内。
数据流:输入全部行、滚动状态和最多显示多少项 → 先以 scroll_top 作为起点,再根据 selected_idx 往前或往后调整 → 输出最终起始下标。
调用关系:adjust_start_for_wrapped_selection_visibility 会先调用它得到基础起点。之后如果多行换行导致选中项仍被挤出视野,再进一步修正。
调用图:被 1 处调用(adjust_start_for_wrapped_selection_visibility);外部调用 2 个(is_empty, len)。
is_selected_visible_in_wrapped_viewport356–387 ↗
fn is_selected_visible_in_wrapped_viewport(
rows_all: &[GenericDisplayRow],
start_idx: usize,
max_items: usize,
selected_idx: usize,
desc_col: usize,
width: u16,
viewport_h
作用:检查在“每个选项可能占多行”的情况下,当前选中项是否真的看得见。普通按条数计算不够,因为一条长说明可能占掉好几行。
数据流:输入全部行、起始下标、最多条数、选中下标、说明栏列、宽度和视口高度 → 从起点开始逐条换行并累加占用行数 → 如果在超出视口前遇到选中项就返回 true,否则返回 false。
调用关系:它被 adjust_start_for_wrapped_selection_visibility 调用。内部会用 wrap_row_lines 来模拟真实渲染时每一项会占几行。
调用图:调用 1 个内部函数(wrap_row_lines);被 1 处调用(adjust_start_for_wrapped_selection_visibility);外部调用 1 个(iter)。
adjust_start_for_wrapped_selection_visibility389–425 ↗
fn adjust_start_for_wrapped_selection_visibility(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_items: usize,
desc_measure_items: usize,
width: u16,
viewport_height:
作用:在有自动换行时修正列表起点,确保选中项没有被长行挤出可见区域。它解决的是“按条目看似可见,按终端行数却被挡住”的问题。
数据流:输入行数据、滚动状态、最大项数、测量项数、宽度、高度和列宽配置 → 先算基础起点 → 如果有选中项,就不断向下移动起点,并每次重新计算说明栏和可见性 → 输出最终起点。
调用关系:render_rows_inner 在真正画列表前调用它。它会串起 compute_item_window_start、compute_desc_col 和 is_selected_visible_in_wrapped_viewport。
调用图:调用 3 个内部函数(compute_desc_col, compute_item_window_start, is_selected_visible_in_wrapped_viewport);被 1 处调用(render_rows_inner)。
build_full_line430–511 ↗
fn build_full_line(row: &GenericDisplayRow, desc_col: usize) -> Line<'static>
作用:把一条选项拼成一整行可显示文字:名称、快捷键、说明、分类标签、禁用提示都在这里合起来。它还会处理搜索命中字符加粗和名称过长时的省略号。
数据流:输入一条 GenericDisplayRow 和说明栏起始列 → 合并说明与禁用原因,按宽度限制名称,给匹配字符加粗,必要时加省略号,再插入空格让说明对齐 → 输出一行带样式的 Line。
调用关系:wrap_standard_row 会先用它生成完整行再换行;render_rows_single_line_with_col_width_mode 会用它生成单行后再截断。它是“原始行数据”变成“屏幕文字”的关键步骤。
调用图:被 2 处调用(render_rows_single_line_with_col_width_mode, wrap_standard_row);外部调用 4 个(from, width, with_capacity, format!)。
render_rows_inner517–596 ↗
fn render_rows_inner(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
column_width: ColumnWidthC
作用:真正把可换行的选择列表画到终端缓冲区里。它处理空列表、滚动、列对齐、换行、选中样式和禁用样式。
数据流:输入绘制区域、缓冲区、全部行、滚动状态、最大结果数、空提示和列宽配置 → 如果没有行就画空提示;否则算可显示窗口和说明栏,逐行换行、套样式、写入缓冲区 → 返回实际画了多少终端行。
调用关系:render_rows 和 render_rows_with_col_width_mode 都只是外层入口,最后会把活交给它。它内部调用滚动修正、列宽计算、行换行和状态样式函数。
调用图:调用 4 个内部函数(adjust_start_for_wrapped_selection_visibility, apply_row_state_style, compute_desc_col, wrap_row_lines);被 2 处调用(render_rows, render_rows_with_col_width_mode);外部调用 5 个(from, is_empty, iter, len, from)。
render_rows606–623 ↗
fn render_rows(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
) -> u16
作用:用默认列宽规则画一个可换行的选择列表。多数弹窗不需要特殊列宽时会直接用它。
数据流:输入区域、缓冲区、行数据、滚动状态、最多显示数和空提示 → 补上默认 ColumnWidthConfig → 交给 render_rows_inner → 返回实际渲染的行数。
调用关系:它被多个弹窗渲染入口调用,也被测试用来检查选中项样式。它是最常用的高层渲染入口。
调用图:调用 1 个内部函数(render_rows_inner);被 8 处调用(render, render, render_ref, render_input, render, render_unanswered_confirmation, render_rows_bottom_aligned, selected_rows_use_the_shared_accent_style);外部调用 1 个(default)。
render_rows_with_col_width_mode631–649 ↗
fn render_rows_with_col_width_mode(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
column_width
作用:按调用者指定的列宽规则画可换行列表。需要固定列宽或跨滚动保持列对齐时会用它。
数据流:输入区域、缓冲区、行数据、滚动状态、最大结果数、空提示和列宽配置 → 不改配置,直接转交给 render_rows_inner → 返回实际画出的终端行数。
调用关系:它是 render_rows 的可配置版本,被需要明确列宽模式的上层 render 或 render_ref 使用。
调用图:调用 1 个内部函数(render_rows_inner);被 2 处调用(render_ref, render)。
render_rows_single_line656–673 ↗
fn render_rows_single_line(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
) -> u16
作用:把选择列表按“每个选项只占一行”的方式画出来。适合信息密集的菜单,避免长说明让列表高度忽上忽下。
数据流:输入区域、缓冲区、行数据、滚动状态、最大结果数和空提示 → 使用默认列宽配置 → 调用单行可配置版本 → 返回实际画出的行数。
调用关系:它被一些弹窗的 render 或 render_ref 调用。真正工作由 render_rows_single_line_with_col_width_mode 完成。
调用图:调用 1 个内部函数(render_rows_single_line_with_col_width_mode);被 3 处调用(render, render_ref, render);外部调用 1 个(default)。
render_rows_single_line_with_col_width_mode677–751 ↗
fn render_rows_single_line_with_col_width_mode(
area: Rect,
buf: &mut Buffer,
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
empty_message: &str,
作用:按指定列宽规则画单行列表,并把超出宽度的文字用省略号截掉。这样列表始终整齐,不会换成多行。
数据流:输入区域、缓冲区、行数据、滚动状态、最大结果数、空提示和列宽配置 → 处理空列表和可见窗口 → 计算说明栏 → 为每条可见行构建完整文字、套选中或禁用样式、超宽截断 → 写入缓冲区并返回画出的行数。
调用关系:render_rows_single_line 会调用它作为默认入口;一些需要直接控制列宽的 render 也会调用它。它依赖 build_full_line 和 truncate_line_with_ellipsis_if_overflow。
调用图:调用 3 个内部函数(build_full_line, compute_desc_col, truncate_line_with_ellipsis_if_overflow);被 2 处调用(render, render_rows_single_line);外部调用 5 个(from, is_empty, iter, len, from)。
measure_rows_height761–774 ↗
fn measure_rows_height(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
width: u16,
) -> u16
作用:在真正绘制前,估算默认换行规则下列表需要占多少终端行。外层弹窗靠它决定自己要多高。
数据流:输入全部行、滚动状态、最大结果数和可用宽度 → 使用默认列宽配置 → 调用内部测量函数 → 返回需要的高度。
调用关系:它被多处 desired_height、render 前准备和选项高度计算调用。它应该和 render_rows 搭配使用,避免测出来的高度和画出来的不一致。
调用图:调用 1 个内部函数(measure_rows_height_inner);被 9 处调用(action_rows_height, desired_height, render, options_required_height, desired_height, render, options_preferred_height, options_required_height, unanswered_confirmation_height);外部调用 1 个(default)。
measure_rows_height_with_col_width_mode779–787 ↗
fn measure_rows_height_with_col_width_mode(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
width: u16,
column_width: ColumnWidthConfig,
) -> u16
作用:用指定列宽规则估算列表高度。需要固定列宽或共享列宽的弹窗,用它来和对应渲染方式保持一致。
数据流:输入行数据、滚动状态、最大结果数、宽度和列宽配置 → 直接交给内部测量函数 → 返回预计需要的终端行数。
调用关系:它是 render_rows_with_col_width_mode 的测量伙伴,被 calculate_required_height、desired_height 和 render 等上层流程使用。
调用图:调用 1 个内部函数(measure_rows_height_inner);被 3 处调用(calculate_required_height, desired_height, render)。
measure_rows_height_inner789–835 ↗
fn measure_rows_height_inner(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
width: u16,
column_width: ColumnWidthConfig,
) -> u16
作用:实际执行列表高度测量。它按同样的列对齐和换行规则,算出这些选项画出来会占多少行。
数据流:输入全部行、滚动状态、最大结果数、宽度和列宽配置 → 空列表返回 1 行占位;否则计算起点、说明栏位置,并逐条调用 wrap_row_lines 统计换行数量 → 输出至少 1 的总高度。
调用关系:measure_rows_height 和 measure_rows_height_with_col_width_mode 都调用它。它和渲染流程共享 compute_desc_col 与 wrap_row_lines,以减少“测量”和“绘制”不一致。
调用图:调用 2 个内部函数(compute_desc_col, wrap_row_lines);被 2 处调用(measure_rows_height, measure_rows_height_with_col_width_mode);外部调用 3 个(is_empty, iter, len)。
tests::one_cell_width_falls_back_without_panic_for_wrapped_two_column_rows846–856 ↗
fn one_cell_width_falls_back_without_panic_for_wrapped_two_column_rows()
作用:测试极窄宽度下,两栏换行不会让程序崩溃。终端窗口可能被缩到很窄,这种边界情况必须安全。
数据流:构造一条有长名称、长说明和缩进的测试行 → 用宽度 1 调用 wrap_two_column_row → 检查结果是空列表,表示函数让外层回退到别的排版方式。
调用关系:它直接验证 wrap_two_column_row 的防护逻辑。这个测试保证未来改排版时不会重新引入窄窗口崩溃。
调用图:调用 1 个内部函数(wrap_two_column_row);外部调用 2 个(default, assert_eq!)。
tui/src/bottom_pane/selection_tabs.rs源码 ↗
底部面板可能有多个分类标签,比如像浏览器标签页一样让用户在几组内容之间切换。这个文件做的事就是:先把每个标签变成可显示的文字片段,再按终端窗口宽度把它们排成一行或多行,最后画到屏幕缓冲区里。屏幕缓冲区可以理解成“先在草稿纸上画好,再一次性显示”。当前选中的标签会加上方括号并使用强调颜色,没选中的标签会变暗。它还提供了计算标签栏需要多高的函数,这样外层布局能提前给它留够空间,不会把其他内容挤坏。
tab_bar_height23–31 ↗
fn tab_bar_height(tabs: &[SelectionTab], active_idx: usize, width: u16) -> u16
作用:这个函数用来算标签栏需要占几行高度。外层界面在排版前会问它“你要多高”,这样才能给标签栏留出合适空间。
数据流:输入是一组标签、当前选中的标签位置、可用宽度。它先看有没有标签,没有就直接返回 0;有标签时,就让 tab_bar_lines 按宽度排好行,再把行数转换成界面高度返回。它不画任何东西,也不改动数据。
调用关系:它是在真正绘制之前被外层布局使用的。desired_height 和 render 会调用它来确认底部标签栏占多少高度;具体怎么换行这件事,它交给 tab_bar_lines 来算。
调用图:调用 1 个内部函数(tab_bar_lines);被 2 处调用(desired_height, render);外部调用 1 个(is_empty)。
render_tab_bar33–54 ↗
fn render_tab_bar(
tabs: &[SelectionTab],
active_idx: usize,
area: Rect,
buf: &mut Buffer,
)
作用:这个函数真正把标签栏画到终端界面上。它负责把已经排好行的标签文字放进指定区域里。
数据流:输入是一组标签、当前选中的位置、要绘制的矩形区域,以及屏幕缓冲区。它调用 tab_bar_lines 先生成一行行文字,然后按区域高度最多画这么多行,把每一行写进缓冲区对应的位置。结果是缓冲区被更新,用户稍后能在屏幕上看到标签栏。
调用关系:它由外层 render 流程调用,是标签栏从“计算结果”变成“屏幕内容”的一步。它不自己决定每个标签长什么样,而是依赖 tab_bar_lines 生成可绘制的行。
调用图:调用 1 个内部函数(tab_bar_lines);被 1 处调用(render)。
tab_bar_lines56–93 ↗
fn tab_bar_lines(tabs: &[SelectionTab], active_idx: usize, width: u16) -> Vec<Line<'static>>
作用:这个函数负责把所有标签排成一行或多行。它像排队一样往一行里塞标签,塞不下就换下一行。
数据流:输入是标签列表、当前选中项的位置、可用宽度。它先处理空列表;然后逐个标签调用 tab_unit 生成显示片段,计算这一小段文字有多宽,再判断当前行还能不能放下。放得下就加进去,放不下就把当前行收起来并开新行。最后输出多行可绘制的 Line。
调用关系:它是这个文件的核心排版步骤。tab_bar_height 用它来数需要几行,render_tab_bar 用它来拿到要画的内容;每个单独标签的样式则交给 tab_unit 处理。
调用图:调用 1 个内部函数(tab_unit);被 2 处调用(render_tab_bar, tab_bar_height);外部调用 4 个(from, new, is_empty, iter)。
tab_unit95–106 ↗
fn tab_unit(label: &str, active: bool) -> Vec<Span<'static>>
作用:这个函数把一个标签名字变成可显示的文字片段,并决定它是高亮还是变暗。它让用户一眼看出当前选中的是哪个标签。
数据流:输入是标签文字和一个表示是否选中的布尔值。若是当前选中,它会取强调样式,把标签包在方括号里并统一上色;若不是选中,就把标签文字变成暗色。输出是一组 Span,也就是带样式的小段文字。
调用关系:它只处理单个标签的外观,被 tab_bar_lines 在排列每个标签时调用。高亮颜色来自 accent_style,所以整个界面的强调色能保持一致。
调用图:调用 1 个内部函数(accent_style);被 1 处调用(tab_bar_lines);外部调用 1 个(vec!)。
tui/src/selection_list.rs源码 ↗
在终端界面里,列表不能只把文字打印出来,还要告诉用户“现在选中的是哪一行”。这个文件就像菜单项的排版小工:先给每一项加上编号前缀,比如“› 1. ”或“ 2. ”;如果这一项被选中,就加一个箭头“›”并染成青色;如果需要变淡,就用暗淡样式;最后把前缀和正文拼成同一行。它还会计算前缀在屏幕上实际占多宽,因为中文、符号和普通字母在终端里宽度可能不一样。正文部分会自动换行,但不随便裁掉空格。这样,上层的菜单或选择器只要交给它序号、文字和选中状态,就能得到一块可以直接显示的界面元素。
selection_option_row10–16 ↗
fn selection_option_row(
index: usize,
label: String,
is_selected: bool,
) -> Box<dyn Renderable>
作用:这是最常用的入口,用来生成一行普通的选择项。调用者只要告诉它第几个、显示什么文字、是不是当前选中,它就返回一个可以画到终端上的行。
数据流:进去的是序号、标签文字、是否选中这三个信息 → 它不自己排版,而是把这些信息转交给更完整的版本,并固定“不变暗”这个选项 → 出来的是一个可渲染对象,也就是后续界面系统能拿去显示的一行内容。
调用关系:它是给菜单和引用选择界面使用的简化按钮。render_ref、render_menu 等地方需要画列表项时会调用它;它再把实际工作交给 selection_option_row_with_dim,因为后者支持更多样式选择。
调用图:调用 1 个内部函数(selection_option_row_with_dim);被 4 处调用(render_ref, render_menu, render_ref, render_ref)。
selection_option_row_with_dim18–46 ↗
fn selection_option_row_with_dim(
index: usize,
label: String,
is_selected: bool,
dim: bool,
) -> Box<dyn Renderable>
作用:这是实际制作列表行的函数,支持选中高亮,也支持把未选中的项显示得更淡。它负责把箭头、编号、文字、颜色和换行规则组合成一行终端界面。
数据流:进去的是序号、标签文字、是否选中、是否变暗 → 它先决定前缀:选中就用带箭头的“› n. ”,否则用普通缩进的“ n. ”;再决定样式:选中用青色,变暗用暗色,普通项用默认样式;接着计算前缀实际宽度,创建一行容器,把前缀和可换行的正文放进去 → 出来的是一个可渲染对象,里面已经带好排版和样式。
调用关系:它是这个文件里的核心工人。selection_option_row 会调用它来处理普通情况;render_ref 等更细的界面渲染流程也可以直接调用它,以便控制某些选项是否变暗。它内部会创建 RowRenderable 和 Paragraph 这些界面零件,把最终显示效果拼装好。
调用图:调用 1 个内部函数(new);被 2 处调用(render_ref, selection_option_row);外部调用 4 个(new, default, width, format!)。
紧凑显示模型整形
此组汇集小型格式化和标签辅助工具,将原始领域/配置数据转换为面向 TUI 的简洁显示文本和模型。
tui/src/chatwidget/warnings.rs源码 ↗
聊天界面可能会收到很多警告消息。其中有一种比较特殊:系统找不到某个模型的元数据,于是改用备用元数据。这个警告很重要,但如果同一个模型一直触发,就会像收银台反复播同一句提示音一样烦人。这个文件用一个小状态本记录已经提醒过哪些模型。WarningDisplayState 里面有一个 HashSet(可以理解成“不重复名单”),专门存放已经显示过这类警告的模型名字。收到新消息时,它先判断这条消息是不是那种固定格式的“备用模型元数据”警告;如果不是,就正常显示;如果是,就取出模型名,看看名单里有没有。没有就加入名单并显示,有就不再显示。这样既不会漏掉普通警告,也能让重复的模型元数据警告安静下来。
WarningDisplayState::should_display13–16 ↗
fn should_display(&mut self, message: &str) -> bool
作用:判断一条警告消息现在该不该显示。普通警告会直接放行;特定的“模型元数据缺失”警告只在同一个模型第一次出现时放行,后面重复出现就拦住。
数据流:输入是一条警告文字,以及这个 WarningDisplayState 里已经记录过的模型名单。它先把文字交给 fallback_model_metadata_warning_slug,看看能不能从中认出模型名。认不出来时,结果就是“应该显示”。认出来时,它把模型名尝试放进 HashSet 这份不重复名单:如果是新名字,就放进去并返回“应该显示”;如果名单里早就有了,就返回“不显示”。这个过程会改动内部名单,因为它会记住第一次见到的模型。
调用关系:它是外部聊天界面判断警告是否展示时会调用的入口。它自己不解析固定文字格式,而是把这一步交给 fallback_model_metadata_warning_slug;拿到解析结果后,再负责做“第一次显示、之后静音”的决定。
调用图:调用 1 个内部函数(fallback_model_metadata_warning_slug)。
fallback_model_metadata_warning_slug19–23 ↗
fn fallback_model_metadata_warning_slug(message: &str) -> Option<&str>
作用:从一条固定格式的警告文字里抠出模型名字。只有文字正好长得像“Model metadata for 某模型 not found...”这种格式时,它才会返回中间的模型名。
数据流:输入是一整条消息字符串。它先检查开头是不是预设的前缀,再检查结尾是不是预设的后缀;两头都对上,剩下夹在中间的部分就是模型名,并作为结果返回。只要开头或结尾有一个对不上,就返回空,表示这不是它认识的那种警告。
调用关系:它只被 WarningDisplayState::should_display 使用,位置很像一个小筛子:先筛出“这是不是备用模型元数据警告”,再把模型名交回去,让上层决定是否重复显示。
调用图:被 1 处调用(should_display)。
tui/src/external_agent_config_migration_model.rs源码 ↗
用户迁移旧工具配置时,后台拿到的是一串项目数据,里面有设置、插件、命令、聊天记录等。如果直接展示,用户会很难判断该选什么。这个文件专门把这些原始项目变成适合界面显示的“模型”:先按项目有没有工作目录、是不是聊天记录,把它们分成“Tools & setup”“Projects”“Chat sessions”等组;再给每个具体项目起一个清楚的名字,比如把配置迁移显示成“Settings”;最后,如果项目里有更细的内容,还会生成一句简短详情,比如“3 agents: a, b, c”。这里的“模型”不是机器学习模型,而是界面要用的一份整理好的数据。这样终端界面不用懂每种迁移项目的细节,只要拿这些分组、标签和说明直接画出来就行。
external_agent_config_migration_groups12–76 ↗
fn external_agent_config_migration_groups(
items: &[ExternalAgentConfigMigrationItem],
) -> Vec<ExternalAgentConfigMigrationGroupModel>
作用:把一长串待迁移项目分成几个用户容易理解的大类。这样界面可以按组展示,而不是把所有东西混在一起。
数据流:输入是一组迁移项目。函数逐个查看每个项目:没有工作目录且不是聊天记录的,放进“Tools & setup”;有工作目录且不是聊天记录的,放进“Projects”;类型是聊天记录的,放进“Chat sessions”。之后它会生成一组分组对象,每个对象带有显示标题、说明文字,以及原始项目在列表里的位置编号。
调用关系:它会在迁移界面模型创建时被 new 调用,用来先搭好整个选择列表的骨架。它自己主要做分类和计数,使用迭代工具逐项查看数据,并在需要显示项目数量时拼出标题文字。
external_agent_config_migration_item_label78–92 ↗
fn external_agent_config_migration_item_label(
item: &ExternalAgentConfigMigrationItem,
) -> &'static str
作用:给单个迁移项目取一个用户看得懂的标题。比如内部类型叫 Config,界面上会显示成“Settings (settings.json -> config.toml)”。
数据流:输入是一个迁移项目。函数只看它的项目类型,然后从固定的一组文案里选出对应标签。输出是一段静态文字,不会改动输入数据。
调用关系:它通常被界面展示代码用来给每一行迁移项目贴标题。它不再调用别的本文件函数,属于一个简单但很关键的“翻译表”:把程序内部分类翻译成用户语言。
external_agent_config_migration_item_detail94–135 ↗
fn external_agent_config_migration_item_detail(
item: &ExternalAgentConfigMigrationItem,
) -> Option<String>
作用:给某些迁移项目补上一句更具体的说明,比如有几个 MCP 服务器、几个命令、最近聊天记录叫什么。没有可展示细节的项目就不返回说明。
数据流:输入是一个迁移项目。函数先看项目里有没有 details,也就是更细的内容;没有就直接返回空。然后按项目类型挑出对应的名单和数量,比如服务器名、agent 名、hook 名、命令名或聊天标题,并交给 format_counted_details 变成一句短说明。输出是可选的字符串;有内容就返回说明,没有就返回空。
调用关系:它会在 build_customize_render_lines 生成自定义迁移界面的显示行时被调用。它负责决定“这个项目需不需要副标题”,真正把数量和名字拼成句子的活交给 format_counted_details。
调用图:调用 1 个内部函数(format_counted_details);被 1 处调用(build_customize_render_lines)。
format_counted_details137–147 ↗
fn format_counted_details(
noun: &str,
count: usize,
names: impl Iterator<Item = &'a str>,
) -> String
作用:把“数量 + 名字列表”整理成一句短小的详情文字。比如把 3 个命令和它们的名字变成“3 slash commands: foo, bar, baz”。
数据流:输入是一个名词、一个数量,以及一串名字。函数先根据数量决定英文名词要不要加复数 s,然后最多取前 4 个名字。如果没有名字,就只输出数量和名词;如果有名字,就输出数量、名词和这些名字组成的逗号列表。
调用关系:它只被 external_agent_config_migration_item_detail 调用,是一个小的格式化帮手。上层函数负责判断项目类型和拿到名单,它负责把名单压缩成界面上好读的一句话。
调用图:被 1 处调用(external_agent_config_migration_item_detail);外部调用 3 个(is_empty, take, format!)。
tui/src/goal_display.rs源码 ↗
这个文件专门服务于终端界面里的 /goal 功能。用户设置一个目标后,系统会记录目标内容、状态、花了多久、用了多少 token。token 可以理解成模型读写文字时消耗的“计量单位”。这个文件做的事,就是把这些数字和枚举值翻译成清楚的提示,比如把 5400 秒显示成 “1h 30m”,把状态显示成 “paused”,把目标摘要拼成一整句。它还定义了 /goal 命令的用法提示,方便用户输错时看到说明。这里的逻辑不负责真正执行目标,也不负责保存数据,只负责“怎么说给人看”。下面的测试会检查时间格式和摘要格式,防止以后改代码时把界面文字弄坏。
format_goal_elapsed_seconds7–31 ↗
fn format_goal_elapsed_seconds(seconds: i64) -> String
作用:把一段秒数变成短小好读的时间文字。比如 90 分钟会变成 “1h 30m”,一天以上会显示成“几天几小时几分钟”。
数据流:输入是一串秒数。函数先把负数当成 0,避免显示奇怪的负时间;然后按秒、分钟、小时、天逐级换算;最后输出一个字符串,用在界面上展示已经花了多久。它不修改任何外部数据。
调用关系:它是目标用时显示的基础小工具。调用图里显示,active_goal_usage 和 completed_goal_usage 会用它来把正在进行或已完成目标的用时变成人能读的格式。goal_usage_summary 里也用同样思路来拼摘要。
调用图:被 2 处调用(active_goal_usage, completed_goal_usage);外部调用 1 个(format!)。
goal_status_label33–42 ↗
fn goal_status_label(status: ThreadGoalStatus) -> &'static str
作用:把目标状态这种程序里的固定选项,翻译成界面上能直接显示的英文标签。比如 Active 变成 “active”,BudgetLimited 变成 “limited by budget”。
数据流:输入是一个 ThreadGoalStatus,也就是目标当前处于哪种状态。函数根据状态逐一匹配,返回对应的静态文字。它只做翻译,不读取文件,也不改变目标本身。
调用关系:它位于“数据状态”和“界面文字”之间,像一个小词典。别的界面代码需要展示目标状态时,可以直接用它,避免每个地方都自己写一套状态名称。
goal_usage_summary44–60 ↗
fn goal_usage_summary(goal: &ThreadGoal) -> String
作用:把一个目标的关键信息拼成一句摘要,方便界面或提示消息直接展示。摘要里一定有目标内容,有用时就显示用时,有 token 预算就显示已用和预算。
数据流:输入是一个 ThreadGoal,里面包含目标文字、已用时间、token 预算和已用 token。函数先放入 “Objective: ...”,如果已用时间大于 0,就调用时间格式化逻辑生成 “Time: ...”;如果设置了 token_budget,就用紧凑格式显示 token 数,比如 K 这种缩写。最后把这些片段用空格连成一个字符串返回,不修改原来的目标。
调用关系:它是设置或更新目标状态时生成说明文字的出口。调用图里显示,set_thread_goal_draft 和 set_thread_goal_status 会调用它,把目标草稿或目标状态变化转成用户能看到的摘要。它内部会借助 format_goal_elapsed_seconds 和 format_tokens_compact 让数字更好读。
调用图:被 2 处调用(set_thread_goal_draft, set_thread_goal_status);外部调用 2 个(format!, vec!)。
tests::format_goal_elapsed_seconds_is_compact70–85 ↗
fn format_goal_elapsed_seconds_is_compact()
作用:这是测试,用来确认时间显示一直保持短小、正确。它覆盖了秒、分钟、小时和天这些边界情况。
数据流:测试输入一组固定秒数,比如 0、59、60、一天、接近三天。每次调用 format_goal_elapsed_seconds,再用断言比较实际结果和预期文字。如果结果不同,测试就失败,提醒开发者格式被改坏了。
调用关系:它专门保护 format_goal_elapsed_seconds。平时不会在正常程序里运行,只会在执行测试时运行,帮助开发者放心改代码。
调用图:外部调用 1 个(assert_eq!)。
tests::test_thread_goal87–99 ↗
fn test_thread_goal(token_budget: Option<i64>, tokens_used: i64) -> ThreadGoal
作用:这是测试用的小工厂函数,用来快速造出一个 ThreadGoal 测试对象。这样测试不用每次手写一大段重复字段。
数据流:输入是 token 预算和已用 token 数。函数把这些值塞进一个固定内容的 ThreadGoal 里,同时设置固定线程 ID、目标文字、状态、用时和时间戳。输出是一个完整的 ThreadGoal,供测试继续使用。
调用关系:它服务于测试函数 tests::goal_usage_summary_formats_time_and_budgeted_tokens。它不参与真实运行流程,只是让测试更短、更清楚。
tests::goal_usage_summary_formats_time_and_budgeted_tokens102–110 ↗
fn goal_usage_summary_formats_time_and_budgeted_tokens()
作用:这是测试,用来确认目标摘要会正确显示目标文字、用时,以及带预算的 token 用量。
数据流:测试先用 tests::test_thread_goal 造一个带 token 预算的目标,再把它交给 goal_usage_summary。函数返回摘要字符串后,测试用断言检查它是否正好等于预期结果。如果 token 压缩格式、时间格式或拼接文字变了,这个测试会立刻发现。
调用关系:它保护 goal_usage_summary 的对外显示效果。它会间接覆盖时间格式化和 token 紧凑显示,确保用户看到的目标摘要稳定、可读。
调用图:外部调用 1 个(assert_eq!)。
tui/src/skills_helpers.rs源码 ↗
这个文件像是技能列表的“前台整理员”。后台给来的技能资料可能有好几种名字和说明:有的带专门给人看的显示名,有的名字里写着“插件名:技能名”,有的只有原始名称。这里先挑最适合展示给人的名字;再挑最短、最适合列表里看的说明;名字太长时会用统一规则截短,避免界面被撑乱。它还提供搜索匹配:先拿用户输入去匹配显示名,如果没匹配上,再去匹配真实技能名。匹配显示名时会返回命中的字符位置,方便界面把命中的字高亮;如果只是在真实技能名里匹配到,就只返回分数,不返回高亮位置,因为屏幕上显示的可能不是那个真实名字。
skill_display_name8–25 ↗
fn skill_display_name(skill: &SkillMetadata) -> String
作用:给一个技能挑一个最适合显示给人的名字。它优先用技能接口里专门写好的显示名;如果没有,就把“插件名:技能名”改成更好读的“技能名(插件名)”。
数据流:进去的是一份技能资料。它先查看里面有没有面向用户的显示名;有就直接拿出来。没有的话,它检查原始名字是不是“插件名:技能名”这种格式,并且两边都不空;是的话就拼成更友好的文字。再不行,就原样返回技能的名字。出来的是一个新的字符串,不会改动原来的技能资料。
调用关系:它在技能要出现在界面上时被使用。mention_items 和 skill_candidate 会调用它,把后台的技能名称变成用户更容易看懂的名称;需要拼接“技能名(插件名)”时,它会用到格式化字符串的能力。
调用图:被 2 处调用(mention_items, skill_candidate);外部调用 1 个(format!)。
skill_description27–34 ↗
fn skill_description(skill: &SkillMetadata) -> &str
作用:给一个技能挑一段最适合在界面里显示的短说明。它会尽量选短的、面向用户的说明,实在没有才用完整描述。
数据流:进去的是一份技能资料。它先看接口信息里有没有短说明;没有就看技能本身有没有短说明;还没有就退回到完整描述。出来的是一段文本引用,也就是直接指向原资料里的说明文字,不额外复制,也不修改资料。
调用关系:它在界面需要显示技能简介时被 optional_skill_description 调用。它的作用是把“到底该展示哪段说明”这个选择规则集中到一个地方,避免别的代码各自猜。
调用图:被 1 处调用(optional_skill_description)。
truncate_skill_name36–38 ↗
fn truncate_skill_name(name: &str) -> String
作用:把技能名截到界面允许的长度,防止名字太长把列表挤乱。可以把它理解成给名称套了一个固定宽度的标签框。
数据流:进去的是一个技能名字文本。它把这个文本交给通用的 truncate_text,并指定最多保留 21 个字符这个规则。出来的是截短后的字符串;如果原本不长,就基本保持原样。
调用关系:它是技能名专用的截短入口,本身不重复造轮子,而是把实际截短工作交给 truncate_text。这样别的地方只要调用它,就不用记住技能名应该截到多长。
调用图:调用 1 个内部函数(truncate_text)。
match_skill40–54 ↗
fn match_skill(
filter: &str,
display_name: &str,
skill_name: &str,
) -> Option<(Option<Vec<usize>>, i32)>
作用:判断用户输入的过滤词能不能匹配某个技能,并给出匹配分数。它优先匹配屏幕上看到的显示名,必要时再匹配内部真实技能名。
数据流:进去的是用户输入的过滤词、技能的显示名、技能的真实名。它先用模糊匹配(一种不要求完全连续、完全一样的搜索方式)匹配显示名;成功时返回命中的字符位置和分数。失败后,如果显示名和真实名不同,就再匹配真实名;成功时只返回分数,不返回显示名里的高亮位置。两个名字都不匹配时,返回空结果。
调用关系:它在 apply_filter 过滤技能列表时被调用。实际的相似度计算交给外部的 fuzzy_match;这个函数负责安排匹配顺序,并把结果整理成界面能用的形式,比如是否能高亮命中的字符。
调用图:被 1 处调用(apply_filter);外部调用 1 个(fuzzy_match)。
tui/src/status/format.rs源码 ↗
这个文件像状态页里的“小排版员”。它先根据所有标签算出最宽的标签有多宽,再统一给每一行补空格,让“值”都从同一个位置开始。这里用的是 Unicode 显示宽度,也就是文字在终端里真正占几格,而不是简单数几个字母;这对中文、全角符号特别重要。FieldFormatter 是主要工具,负责生成带缩进、标签、冒号和值的终端行。push_label 用来收集标签并去重,避免同一个标签重复出现。line_display_width 和 truncate_line_to_width 则负责测量一整行的显示宽度,以及在屏幕太窄时把行剪短,同时保留原来的样式。整体上,它不决定显示什么内容,只保证内容显示得对齐、好看、不越界。
FieldFormatter::from_labels18–36 ↗
fn from_labels(labels: impl IntoIterator<Item = S>) -> Self
作用:根据一组标签创建一个排版器,让后面所有“标签: 值”的行都能对齐。比如标签有长有短,它会先找出最长的那个,作为统一排版标准。
数据流:输入是一批标签文字 → 它逐个计算这些标签在终端里真正占的宽度,找出最大值,再算出值应该从第几格开始 → 输出一个 FieldFormatter,里面记好了缩进、标签宽度和值的起始位置。
调用关系:它在 display_lines 准备生成状态显示内容时被调用,是排版流程的起点。它自己只借助外部的迭代和宽度计算工具,不生成具体行,后续的 line、full_spans 等函数会用它算好的排版参数。
调用图:被 1 处调用(display_lines);外部调用 2 个(into_iter, width)。
FieldFormatter::line38–44 ↗
fn line(
&self,
label: &'static str,
value_spans: Vec<Span<'static>>,
) -> Line<'static>
作用:把一个标签和一组已经带样式的值片段,拼成终端界面里的一整行。调用者不用关心前面要补多少空格,只要给标签和值就行。
数据流:输入是标签和若干值片段 → 它先调用 full_spans 拼出完整的片段列表,再把这些片段包装成 ratatui 的 Line,也就是终端 UI 库能显示的一行 → 输出一行可直接显示的内容。
调用关系:它被 rate_limit_lines 用来生成限流信息的普通行。它把真正拼片段的工作交给 full_spans,自己主要负责把结果变成界面组件能用的 Line。
调用图:调用 1 个内部函数(full_spans);被 1 处调用(rate_limit_lines);外部调用 1 个(from)。
FieldFormatter::continuation46–51 ↗
fn continuation(&self, mut spans: Vec<Span<'static>>) -> Line<'static>
作用:生成一条“续行”,也就是没有标签、但值要和上一行的值部分对齐的行。常见场景是某个值太长,需要下一行继续显示。
数据流:输入是一组要继续显示的值片段 → 它先在最前面加上一段和“值起始位置”一样宽的空白缩进,并把这段空白做成较淡的样式 → 输出一条从值列开始的终端行。
调用关系:它依赖 FieldFormatter 创建时算好的 value_indent。调用图里没有显示当前上游调用者,但它和 line 是一组配套工具:line 负责第一行,continuation 负责后续对齐的行。
调用图:外部调用 3 个(from, from, with_capacity)。
FieldFormatter::value_width53–55 ↗
fn value_width(&self, available_inner_width: usize) -> usize
作用:算出在当前屏幕宽度下,真正留给“值”的空间还有多少。这样后面的代码可以决定值能不能放下,或者需要换行、裁剪。
数据流:输入是可用的内部宽度 → 它减去标签、冒号、缩进这些固定占用的位置;如果空间不够,就安全地返回 0,而不是出现负数错误 → 输出值区域可用的宽度。
调用关系:它被 rate_limit_row_lines 用来排版单条限流信息。这个函数不生成文字,只提供宽度判断,让上层排版逻辑知道值最多能占几格。
调用图:被 1 处调用(rate_limit_row_lines)。
FieldFormatter::full_spans57–66 ↗
fn full_spans(
&self,
label: &str,
mut value_spans: Vec<Span<'static>>,
) -> Vec<Span<'static>>
作用:把标签部分和值部分合在一起,形成一行所需的全部文字片段。它保留值片段原有的颜色和样式,只在前面补上格式化好的标签。
数据流:输入是标签和一组值片段 → 它先调用 label_span 做出带缩进、冒号和补空格的标签片段,再把值片段接在后面 → 输出完整的一行片段列表。
调用关系:它被 FieldFormatter::line 和 rate_limit_row_lines 使用,是“拼装整行”的核心步骤。它把标签制作交给 label_span,自己负责把标签和值接起来。
调用图:调用 1 个内部函数(label_span);被 2 处调用(rate_limit_row_lines, line);外部调用 1 个(with_capacity)。
FieldFormatter::label_span68–82 ↗
fn label_span(&self, label: &str) -> Span<'static>
作用:制作一行最前面的标签部分,比如“ 名称: ”这种带缩进、冒号和补空格的前缀。它会把标签显示成较淡的样式,让值更突出。
数据流:输入是一个标签文字 → 它把固定缩进、标签、冒号放进字符串,再根据标签实际显示宽度补足空格 → 输出一个带暗色样式的 Span,也就是 ratatui 里一小段有样式的文字。
调用关系:它只被 full_spans 调用,是内部小零件。full_spans 需要先有一个对齐好的标签片段,才好把后面的值片段接上。
调用图:被 1 处调用(full_spans);外部调用 3 个(from, with_capacity, width)。
push_label85–93 ↗
fn push_label(labels: &mut Vec<String>, seen: &mut BTreeSet<String>, label: &str)
作用:把标签加入列表,同时保证同一个标签只加一次。它避免后面根据标签算宽度时被重复标签干扰,也让标签收集结果更干净。
数据流:输入是标签列表、已见过标签的集合,以及一个新标签 → 它先检查这个标签是否已经出现过;出现过就什么也不做,没出现过就复制一份,放进集合和列表 → 输出没有返回值,但会更新传入的列表和集合。
调用关系:它被 collect_rate_limit_labels 和 display_lines 调用,属于准备排版数据的早期步骤。后面 FieldFormatter::from_labels 会拿这些整理好的标签来计算统一宽度。
调用图:被 2 处调用(collect_rate_limit_labels, display_lines);外部调用 2 个(contains, insert)。
line_display_width95–99 ↗
fn line_display_width(line: &Line<'static>) -> usize
作用:计算一整行在终端里实际会占多少格。它不是简单数字符,因为中文、全角符号和某些特殊字符占的格数可能不同。
数据流:输入是一条 ratatui 的 Line → 它遍历行里的每个文字片段,分别计算显示宽度,再把宽度加起来 → 输出这条行的总显示宽度。
调用关系:它被 rate_limit_row_lines 用来判断排好的行是否放得下。它不改变行内容,只提供一个准确的宽度数字,帮助上层决定是否需要进一步处理。
调用图:被 1 处调用(rate_limit_row_lines);外部调用 1 个(iter)。
truncate_line_to_width101–147 ↗
fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static>
作用:把一条终端行裁剪到指定宽度以内,同时尽量保留原来的文字样式。它用于防止内容太长,把界面撑破或显示错位。
数据流:输入是一条 Line 和最大允许宽度 → 如果宽度是 0,就返回空行;否则它按片段逐段累加显示宽度,能完整放下的就保留,放不下的那段再按字符逐个裁剪,并且每段都保留原样式 → 输出一条不超过指定宽度的新 Line。
调用关系:调用图里没有显示当前上游调用者,但它明显是给状态界面排版时兜底用的工具。它依赖 Unicode 宽度计算来避免把中文或宽字符按普通字符数错误裁剪。
tui/src/status/helpers.rs源码 ↗
状态栏空间很小,但要展示的信息很多:当前模型、推理设置、加载了哪些 agents 文件、账号套餐、token 用量、当前目录、限制重置时间等。这个文件就像一个“文字整理员”:拿到程序内部的原始数据后,改写成适合终端看的短句。比如把 15300 个 token 显示成 15.3K,把用户目录下的路径改成以 ~ 开头,把太长的路径从中间截短,把 Team 套餐显示成更面向用户的 Business。它还会处理一些边角情况,比如没有 agents 文件时显示 <none>,负数 token 当作 0,今天的重置时间只显示几点,非今天才补上日期。文件底部的测试用临时目录和套餐样例,确认这些展示规则不会被改坏。
normalize_agents_display_path12–14 ↗
fn normalize_agents_display_path(path: &Path) -> String
作用:把 agents 文件路径整理成更正常、更适合显示的字符串。它会去掉路径里多余的绕路写法,比如把类似 a/../b 这种形式简化掉。
数据流:进去的是一个文件路径 → 它交给路径简化工具处理,再转成可显示的文字 → 出来的是一段路径字符串,不改动原路径。
调用关系:它是 compose_agents_summary 的小帮手。当 agents 路径不能简单地显示成文件名或相对路径时,compose_agents_summary 会把路径交给它做最后的清理。
调用图:被 1 处调用(compose_agents_summary);外部调用 1 个(simplified)。
compose_model_display16–34 ↗
fn compose_model_display(
model_name: &str,
entries: &[(&str, String)],
) -> (String, Vec<String>)
作用:把模型名称和模型附加设置整理成状态栏可显示的内容。比如把“reasoning effort”变成“reasoning high”,把摘要关闭显示成“summaries off”。
数据流:进去的是模型名,以及一组设置项名称和值 → 它挑出推理强度和推理摘要这两类跟显示有关的信息,并统一大小写和措辞 → 出来的是模型名本身,以及一组补充说明文字。
调用关系:它通常在状态栏对象创建时被调用,用来把模型配置变成最终 UI 要显示的短文本。它不再调用项目里的复杂流程,只做文字拼装。
compose_agents_summary36–80 ↗
fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String
作用:把当前生效的 agents 文件列表合成一行摘要。agents 文件可以理解为给助手补充规则或说明的文件,这里要告诉用户到底用了哪些。
数据流:进去的是配置对象和一串绝对路径 → 它根据当前工作目录判断每个路径该怎么显示:同目录只显示文件名,上级目录用 ../ 表示,其他地方显示简化后的完整路径 → 出来的是用逗号分隔的一行文字;如果没有路径,就出来 <none>。
调用关系:它是状态栏展示 agents 来源的核心函数。测试 tests::compose_agents_summary_orders_global_before_project_agents 会直接调用它,确认多个 agents 文件的显示顺序和格式符合预期;它内部会在需要时调用 normalize_agents_display_path 清理路径。
调用图:调用 1 个内部函数(normalize_agents_display_path);被 1 处调用(compose_agents_summary_orders_global_before_project_agents);外部调用 2 个(new, format!)。
compose_account_display82–86 ↗
fn compose_account_display(
account_display: Option<&StatusAccountDisplay>,
) -> Option<StatusAccountDisplay>
作用:把账号显示信息复制一份给状态栏使用。它的作用很简单:有账号信息就原样带过去,没有就保持没有。
数据流:进去的是一个可能存在的账号显示对象 → 它对里面的内容做克隆,也就是复制一份 → 出来的是新的可选账号显示对象,不修改原来的数据。
调用关系:它通常在状态栏创建时被调用,作为账号信息进入 UI 显示流程前的一道轻量转换。它不把工作交给其他项目函数。
调用图:被 1 处调用(new)。
plan_type_display_name88–98 ↗
fn plan_type_display_name(plan_type: PlanType) -> String
作用:把内部的套餐类型改成用户更容易理解的名字。比如内部的 Team 类套餐统一显示成 Business,Business 类套餐显示成 Enterprise。
数据流:进去的是一个套餐枚举值,也就是程序内部用来代表套餐种类的固定选项 → 它先判断是不是团队类或企业类,再处理 ProLite 这种特殊名字,最后把普通枚举名转成首字母大写的文字 → 出来的是给用户看的套餐名称字符串。
调用关系:它服务于账号和状态展示。它会调用 PlanType 自带的判断方法识别套餐大类,也会调用 title_case 把普通枚举名变成人话;测试 tests::plan_type_display_name_remaps_display_labels 用一组套餐样例检查这些映射。
调用图:调用 1 个内部函数(title_case);外部调用 3 个(is_business_like, is_team_like, format!)。
format_tokens_compact100–139 ↗
fn format_tokens_compact(value: i64) -> String
作用:把很大的 token 数量压缩成短格式。token 可以粗略理解为模型读写文字时计数用的小单位,状态栏不能显示一长串数字,所以要变成 1.2K、3.4M 这种形式。
数据流:进去的是一个整数 token 数 → 它先把负数当作 0,再按大小换算成 K、M、B、T,并控制小数位,去掉没用的 0 → 出来的是短数字字符串,比如 999、1K、12.5M。
调用关系:它会被 context_window_spans 和 token_usage_spans 使用,也就是在展示上下文窗口大小和 token 用量时,把数字变得紧凑好读。
调用图:被 2 处调用(context_window_spans, token_usage_spans);外部调用 1 个(format!)。
format_directory_display141–162 ↗
fn format_directory_display(directory: &Path, max_width: Option<usize>) -> String
作用:把目录路径改成适合状态栏显示的样子。它会尽量用 ~ 表示用户主目录,还能在宽度不够时把路径从中间截短。
数据流:进去的是目录路径,以及一个可选的最大显示宽度 → 它先尝试把路径改成相对用户主目录的写法;如果给了宽度限制,就计算这段文字在终端里占多宽,太长则调用截断工具处理 → 出来的是最终路径文字。
调用关系:它会被 display_lines 调用,用在真正生成状态栏显示行的时候。它依赖 relativize_to_home 判断路径能不能写成 ~ 开头,也依赖 center_truncate_path 做中间截断。
调用图:调用 2 个内部函数(relativize_to_home, center_truncate_path);被 1 处调用(display_lines);外部调用 4 个(display, new, width, format!)。
format_reset_timestamp164–171 ↗
fn format_reset_timestamp(dt: DateTime<Local>, captured_at: DateTime<Local>) -> String
作用:把某个重置时间显示成简短时间文字。今天的时间只显示几点,跨天才加上日期,避免状态栏啰嗦。
数据流:进去的是重置时间和捕获当前状态的时间 → 它比较两者是不是同一天,然后格式化小时分钟;如果不是同一天,再补上日期和月份 → 出来的是类似 14:30 或 14:30 on 7 Jul 的字符串。
调用关系:它用于展示限制或额度什么时候重置。它只做时间格式化,不负责计算重置时间本身。
调用图:外部调用 3 个(date_naive, format, format!)。
title_case173–183 ↗
fn title_case(s: &str) -> String
作用:把一个单词改成首字母大写、其余字母小写的显示形式。它是给套餐名这类短标签做最后美化的小工具。
数据流:进去的是一段字符串 → 如果为空就直接返回空;否则取第一个字符转成大写,把后面的部分转成小写 → 出来的是整理后的字符串。
调用关系:它被 plan_type_display_name 调用,用来处理那些没有特殊映射规则的套餐类型名称。
调用图:被 1 处调用(plan_type_display_name);外部调用 1 个(new)。
tests::test_config193–200 ↗
async fn test_config(codex_home: &TempDir, cwd: &TempDir) -> Config
作用:为测试临时造一个配置对象。这样测试不用依赖用户电脑上的真实配置和真实目录。
数据流:进去的是两个临时目录:一个当作 Codex 的家目录,一个当作当前工作目录 → 它用配置构建器填入这些路径并异步生成配置 → 出来的是测试可用的 Config。
调用关系:它是下面几个 agents 路径显示测试的准备步骤。那些测试先调用它拿到干净配置,再检查 compose_agents_summary 的输出。
tests::plan_type_display_name_remaps_display_labels203–222 ↗
fn plan_type_display_name_remaps_display_labels()
作用:检查套餐类型显示名是否符合产品想给用户看的叫法。尤其是 Team 显示成 Business、Business 显示成 Enterprise 这些容易改错的规则。
数据流:进去的是测试里写死的一组套餐类型和期望文字 → 它逐个调用 plan_type_display_name 对比结果 → 如果有一个不一致,测试失败;否则说明映射规则还正确。
调用关系:它直接保护 plan_type_display_name。以后有人修改套餐枚举或显示规则时,这个测试会提醒哪些用户可见名称被影响了。
调用图:外部调用 1 个(assert_eq!)。
tests::compose_agents_summary_includes_global_agents_path225–235 ↗
async fn compose_agents_summary_includes_global_agents_path()
作用:检查全局 agents 文件路径会出现在摘要里。全局文件不在当前项目目录时,也不能被漏掉。
数据流:进去的是测试创建的临时 Codex 家目录和当前目录 → 它构造一个全局 agents 文件路径,生成测试配置,再调用 compose_agents_summary → 最后断言输出等于这个路径应有的显示形式。
调用关系:它用 tests::test_config 准备配置,并通过断言保护 compose_agents_summary 对全局 agents 文件的显示行为。
调用图:外部调用 3 个(new, assert_eq!, test_config)。
tests::compose_agents_summary_names_global_agents_override238–248 ↗
async fn compose_agents_summary_names_global_agents_override()
作用:检查全局 agents 覆盖文件也能按路径显示出来。也就是说,不同名字的全局 agents 文件不应该被特殊规则误处理掉。
数据流:进去的是临时目录和一个 override.md 路径 → 它生成配置,调用 compose_agents_summary 处理这个路径 → 出来没有业务返回值,但测试会确认摘要文字就是预期的路径显示。
调用关系:它和上一个测试类似,都依赖 tests::test_config,并共同覆盖 compose_agents_summary 对非项目目录 agents 文件的处理。
调用图:外部调用 3 个(new, assert_eq!, test_config)。
tests::compose_agents_summary_orders_global_before_project_agents251–273 ↗
async fn compose_agents_summary_orders_global_before_project_agents()
作用:检查多个 agents 文件显示时,传入的顺序会被保留下来,尤其是全局文件排在项目文件前面。这样用户看到的摘要顺序和实际使用顺序一致。
数据流:进去的是临时创建的全局路径和项目路径 → 它生成配置,把两个路径传给 compose_agents_summary,再把返回的一行摘要按逗号拆开 → 最后确认第一项是全局路径,第二项是项目文件,并且没有多余项。
调用关系:它直接调用 compose_agents_summary,是这个文件里对 agents 摘要整体行为检查最完整的测试之一;它也使用 tests::test_config 准备隔离的测试环境。
调用图:调用 1 个内部函数(compose_agents_summary);外部调用 4 个(new, assert!, assert_eq!, test_config)。
tui/src/status/remote_connection.rs源码 ↗
这个文件解决的是 TUI(文字界面)里“远程连接状态怎么显示”的问题。程序可能直接内嵌运行,也可能连本机后台服务,或者连远程服务器。内嵌运行时没有远程连接,所以不显示状态;一旦是通过 WebSocket(网页常用的一种长连接)或 Unix Socket(一种本机进程间通信文件)连接,就要显示地址和版本。这里最重要的一点是安全:WebSocket 地址里可能带用户名、密码、token、网页片段等信息,不能原样展示。它会先解析地址,再删掉这些敏感部分,只留下干净的主机和端口。最后产出一个简单的 RemoteConnectionStatus,里面只有 address 和 version,方便上层界面直接拿来显示。
remote_connection_status_value11–34 ↗
fn remote_connection_status_value(
app_server_target: &AppServerTarget,
server_version: Option<&str>,
) -> Option<RemoteConnectionStatus>
作用:这个函数把当前应用服务器的连接目标,转换成一条可以显示给用户看的连接状态。有人会用它来告诉用户:现在连的是哪里,以及对方服务器是什么版本。
数据流:进去的是 app_server_target,也就是服务器目标,以及可选的 server_version。它先判断是不是内嵌运行:如果是,就直接返回 None,表示不用显示远程连接状态;如果是本机后台服务或远程服务,就取出连接端点。端点如果是 WebSocket,它会交给 sanitized_websocket_display_address 清理地址;如果是 Unix Socket,它会拼成 unix://路径。版本号如果有,就加上 v 前缀;如果没有,就写成 unknown。最后出来的是 Some(RemoteConnectionStatus),里面放着干净地址和版本文字。
调用关系:它是在上层 run 流程需要刷新或展示状态时被调用的。它自己负责判断连接类型,并在遇到 WebSocket 地址时把清理工作交给 sanitized_websocket_display_address;格式化 Unix Socket 地址和版本文字则在本函数里完成。
调用图:调用 1 个内部函数(sanitized_websocket_display_address);被 1 处调用(run);外部调用 1 个(format!)。
sanitized_websocket_display_address36–43 ↗
fn sanitized_websocket_display_address(raw: &str) -> Option<String>
作用:这个函数专门把 WebSocket 地址变成适合公开显示的样子。它会去掉用户名、密码、查询参数和片段,避免把 token 或秘密信息露出来。
数据流:进去的是一段原始 WebSocket 地址字符串。它先用 Url::parse 尝试把字符串解析成真正的网址;如果解析失败,就返回 None。解析成功后,它把用户名清空,把密码删掉,把问号后面的查询参数删掉,也把井号后的片段删掉。最后出来的是一个清理后的地址字符串。
调用关系:它只被 remote_connection_status_value 调用,是那个函数里处理 WebSocket 地址的安全小助手。remote_connection_status_value 负责决定何时需要显示地址,而它负责保证显示出来的地址不带敏感信息。
调用图:被 1 处调用(remote_connection_status_value);外部调用 1 个(parse)。
tests::remote_connection_status_value_formats_display_value51–85 ↗
fn remote_connection_status_value_formats_display_value() -> color_eyre::Result<()>
作用:这是一个测试,用来确认连接状态显示得对,尤其是不会把 WebSocket 地址里的秘密信息显示出来。
数据流:它构造了三种情况:内嵌服务器、带用户名密码和 token 的 WebSocket 远程服务器、本机 Unix Socket 后台服务。然后分别调用 remote_connection_status_value,并用 assert_eq! 比较实际结果和预期结果。测试通过时,说明内嵌模式不会显示连接状态,WebSocket 地址会被清理,Unix Socket 地址和未知版本也会按规则显示。
调用关系:它属于测试模块,只在跑测试时活跃。它通过构造不同的 AppServerTarget 来验证 remote_connection_status_value 的行为,间接也验证了 sanitized_websocket_display_address 的清理效果。
调用图:调用 1 个内部函数(relative_to_current_dir);外部调用 1 个(assert_eq!)。
专用视觉组件
这些文件提供用于图表调色板的专用展示辅助工具,以及 TUI 中其他位置使用的可复用历史单元组合。
tui/src/chatwidget/tokens/chart/palette.rs源码 ↗
这个文件像是图表的“配色师”和“符号选择员”。终端不都一样:有的能显示真彩色,有的只能显示很少几种颜色。如果硬用同一套颜色,图表可能糊成一片,或者空格、活动格分不清。这里的 TokenActivityPalette 会先看当前终端能显示多少颜色,再看主题给的前景色、背景色和强调色。条件够好时,它会把主题强调色和背景色混合,做出 5 档深浅,用颜色表示活动强度;条件不好时,它退回到更稳的方案:空格用暗淡样式,活动格用主题强调样式,并用空心方块和实心方块来区分。它还单独照顾柱状图:有值的地方画满块,没值的地方画空白,让柱子的轮廓更清楚。
TokenActivityPalette::current40–47 ↗
fn current() -> Self
作用:按当前终端和当前主题生成一套可用的图表颜色表。图表真正绘制前会用它来拿到“此刻最适合”的样式。
数据流:进去时不需要外部参数;它会读取默认前景色、默认背景色、终端颜色能力,以及主题里的活动强调样式。然后把这些信息交给配色构造过程。出来的是一个 TokenActivityPalette,里面已经装好不同强度等级该用的颜色和符号策略。
调用关系:它是外部绘图流程最常用的入口,chart_lines 在准备画活动图时会调用它。它自己不细算颜色,而是先通过 theme_activity_style 找主题强调色,再把终端信息交给 TokenActivityPalette::from_parts 做真正的组装。
调用图:调用 4 个内部函数(theme_activity_style, default_bg, default_fg, stdout_color_level);被 1 处调用(chart_lines);外部调用 1 个(from_parts)。
TokenActivityPalette::from_parts49–91 ↗
fn from_parts(
default_fg: Option<(u8, u8, u8)>,
default_bg: Option<(u8, u8, u8)>,
color_level: StdoutColorLevel,
active_style: Style,
) -> Self
作用:根据给定的前景色、背景色、终端颜色能力和主题强调样式,拼出一套图表调色板。它是这个文件里最核心的判断点:能用渐变就用渐变,不能用就安全降级。
数据流:输入是默认前景色、默认背景色、终端颜色级别,以及活动样式。它先从活动样式里取出 RGB 颜色;如果缺少必要颜色,或者终端只支持很少颜色,就改用 fallback。否则它根据背景明暗选择空格的透明混合比例,再把前景色或强调色与背景色混合成 5 档颜色,并转换成当前终端最接近能显示的颜色。输出是带有 5 档格子样式、柱状图样式、以及“使用颜色表达强度”标记的 TokenActivityPalette。
调用关系:它被 TokenActivityPalette::current 间接用来生成真实运行时的调色板,也被多组测试直接调用,专门检查低色终端、缺失颜色、非 RGB 主题色、亮背景和暗背景等情况。它会调用 activity_anchor_rgb 取主题颜色,用 blend 混色,用 is_light 判断背景明暗,再用 best_color_for_level 适配终端能力。
调用图:调用 4 个内部函数(activity_anchor_rgb, blend, is_light, best_color_for_level);被 5 处调用(ansi16_palette_uses_theme_accent_without_green_fallback, missing_terminal_colors_use_theme_accent_fallback, non_rgb_theme_accent_remains_active_fallback, truecolor_palette_blends_empty_cell_for_light_background, truecolor_palette_blends_theme_accent_against_dark_background);外部调用 3 个(default, matches!, from_fn)。
TokenActivityPalette::fallback93–106 ↗
fn fallback(active_style: Style) -> Self
作用:生成一套保底调色板,用在颜色信息不全或终端颜色能力太弱的时候。它保证图表不会因为颜色不可靠而看不懂。
数据流:输入是主题里的活动样式。它把空格样式设成默认的暗淡样式,把所有非空活动等级都设成同一个活动样式,柱状图也用这个活动样式,并标记为不依赖颜色渐变。输出是一套简单但清楚的 TokenActivityPalette。
调用关系:TokenActivityPalette::from_parts 在发现无法安全做彩色渐变时会走到这里。它不再调用复杂的颜色计算,只用最基本的默认样式来保底。
调用图:外部调用 1 个(default)。
TokenActivityPalette::for_level108–110 ↗
fn for_level(&self, level: usize) -> Style
作用:根据活动强度等级取对应的格子样式。绘图代码用它来决定某个小格子该显示成什么颜色或效果。
数据流:输入是一个等级数字。它把数字限制在最多第 4 档,避免越界,然后从内部 5 档样式表里取出对应 Style。输出是这个等级该用的终端样式,不会改动调色板本身。
调用关系:legend_line 会用它来画图例,TokenActivityPalette::for_bar_level 也会借它处理 0 级柱状图。它处在绘制阶段的取样式环节,是调色板对外提供样式的简单窗口。
调用图:被 2 处调用(legend_line, for_bar_level)。
TokenActivityPalette::for_bar_level112–118 ↗
fn for_bar_level(&self, level: usize) -> Style
作用:给柱状图取样式。它把“没有高度”和“有高度”分开处理,让空白柱格和填充柱格看起来更像真正的柱状图。
数据流:输入是柱状图某一格的等级。等级为 0 时,它复用 for_level(0) 的空格样式;等级大于 0 时,它统一返回柱状图专用的 bar_style。输出是这一格柱状图该用的 Style。
调用关系:它站在柱状图绘制时的样式选择位置。它会把 0 级情况交给 TokenActivityPalette::for_level,非 0 情况直接使用自己保存的柱状图样式。
调用图:调用 1 个内部函数(for_level)。
TokenActivityPalette::glyph125–134 ↗
fn glyph(&self, view: TokenActivityView, level: usize) -> &'static str
作用:决定某个格子实际画哪个字符,比如空心方块、实心方块、满块或空格。颜色负责“好看”,字符负责“看得懂”。
数据流:输入是图表视图类型和活动等级。若不是每日网格视图,它把 0 级画成空格,把有值的格子画成满块,形成柱状图轮廓。若是每日网格视图,在真彩色方案下所有格子都用实心方块,靠颜色分强弱;在低色方案下,0 级用空心方块,有活动用实心方块。输出是一个固定的字符字符串,不改动任何状态。
调用关系:legend_line 会调用它来画图例和说明。它使用 TokenActivityPalette 里保存的 uses_color 标记,配合 TokenActivityView 判断当前是网格图还是柱状图。
调用图:被 1 处调用(legend_line)。
theme_activity_style137–141 ↗
fn theme_activity_style() -> Style
作用:从当前主题里挑一个适合表示“活动”的强调样式。它让图表颜色尽量跟整个界面主题保持一致。
数据流:输入不需要参数;它会按几个主题作用域名称去找前景色样式。找到了就使用这个样式,找不到就用系统的默认强调样式。最后把样式加粗。输出是一个用于活动格子的 Style。
调用关系:TokenActivityPalette::current 在创建当前调色板前会调用它。它把主题查询工作交给 foreground_style_for_scopes,如果主题里没有匹配项,就退到 accent_style。
调用图:调用 1 个内部函数(foreground_style_for_scopes);被 1 处调用(current)。
activity_anchor_rgb143–148 ↗
fn activity_anchor_rgb(style: Style) -> Option<(u8, u8, u8)>
作用:从一个样式里取出 RGB 颜色,作为活动渐变的“锚点色”。只有拿到明确的红绿蓝数值,后面才好和背景混合出渐变。
数据流:输入是一个 Style。它查看这个样式的前景色;如果前景色是 RGB 格式,就取出红、绿、蓝三个数字并返回;如果没有前景色,或不是 RGB 颜色,就返回空值。它不修改输入样式。
调用关系:TokenActivityPalette::from_parts 会先调用它确认主题强调色能不能参与混色。拿不到 RGB 时,from_parts 就不会硬算渐变,而是改走 fallback,避免生成不可靠的颜色。
调用图:被 1 处调用(from_parts)。
tui/src/history_cell/base.rs源码 ↗
终端界面里的历史记录不是简单把字符串往屏幕上一扔。它要考虑屏幕宽度、换行、前缀缩进、网页链接是否可点击,以及导出 transcript(会话记录)时要不要保留链接信息。这个文件就是这些“记录块”的基础积木。PlainHistoryCell 保存几行普通文字;WebHyperlinkHistoryCell 也保存文字,但显示链接版本时会把网址标出来;PrefixedWrappedHistoryCell 像给一段话贴上标签,第一行用一个前缀,后续换行用另一个前缀;CompositeHistoryCell 则像文件夹,把多个记录块合成一个,中间自动加空行分隔。它们都实现 HistoryCell 这个统一接口,所以外面的渲染代码不用关心具体是哪一种格子,只要问它“按这个宽度给我显示行”“给我原始纯文本行”就行。
PlainHistoryCell::new11–13 ↗
fn new(lines: Vec<Line<'static>>) -> Self
作用:创建一个普通历史记录格子,里面就是一组已经准备好的显示行。别人想把简单文本塞进历史记录时,会用它包装一下。
数据流:进去的是多行 Line 文本,也就是终端界面可显示的一行一行内容 → 函数把这些行原样放进 PlainHistoryCell 的 lines 字段 → 出来的是一个普通记录格子,之后可以被渲染或导出。
调用关系:它是很多地方放入普通历史消息的入口,比如 plain_line_cell、handle_permissions_decision、add_plain_history_lines、rename_confirmation_cell 和一些测试都会调用它。它本身不再把工作交给别人,只负责把现成的行装盒。
调用图:被 16 处调用(plain_line_cell, handle_permissions_decision, add_plain_history_lines, rename_confirmation_cell, emit_forked_thread_event, completed_token_activity_refresh_waits_for_active_history_cell, pending_token_activity_refresh_keeps_composer_visible_in_short_viewport, startup_reset_hint_waits_for_active_output_snapshot, new_token_activity_output, new_debug_config_output (+6 more))。
PlainHistoryCell::display_lines17–19 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>
作用:把普通历史记录格子的内容拿出来给屏幕显示。因为内容已经排好了,所以它不管屏幕宽度,直接返回副本。
数据流:进去的是调用者给的宽度,但这里不会使用 → 函数复制自己保存的 lines → 出来的是可显示的行列表,原来的格子不变。
调用关系:渲染或转换文本时会通过 HistoryCell 接口调用它,例如 cell_to_text 会用它拿到要显示的内容。它是最简单的显示路径,不做换行、不加链接。
调用图:被 1 处调用(cell_to_text)。
PlainHistoryCell::raw_lines21–23 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:取出普通格子的“纯文本版本”。这用于导出或比较内容时去掉界面样式,只保留文字本身。
数据流:进去不需要额外参数 → 函数复制保存的行,并交给 plain_lines 去做去样式化处理 → 出来的是更干净的原始行列表,格子内容不被改动。
调用关系:它是 HistoryCell 统一接口的一部分。外部在需要原始会话记录,而不是带样式的终端显示内容时,会走到这里。
WebHyperlinkHistoryCell::new32–34 ↗
fn new(lines: Vec<Line<'static>>) -> Self
作用:创建一个可能包含网页地址的历史记录格子。它适合放反馈成功信息这类文字,因为里面的 URL 后面可以被识别成可点击链接。
数据流:进去的是多行显示文本 → 函数把这些行保存到 WebHyperlinkHistoryCell 里 → 出来的是一个支持链接标注的历史记录格子。
调用关系:feedback_success_cell 会调用它来生成带网页链接能力的消息。它只是建好容器,真正识别网址的活儿留给 display_hyperlink_lines。
调用图:被 1 处调用(feedback_success_cell)。
WebHyperlinkHistoryCell::display_lines38–40 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>
作用:按普通方式显示这个带链接能力的格子。也就是说,即使内容里有网址,这个函数也只返回普通文本行。
数据流:进去的是宽度参数,但这里不会使用 → 函数复制保存的 lines → 出来的是普通显示行,不包含额外的链接标注。
调用关系:当调用方只需要普通终端显示,不需要可点击链接信息时,会通过 HistoryCell 接口走这条路。链接相关的工作由 display_hyperlink_lines 处理。
WebHyperlinkHistoryCell::display_hyperlink_lines42–44 ↗
fn display_hyperlink_lines(&self, _width: u16) -> Vec<HyperlinkLine>
作用:把内容里的网页地址识别出来,并生成带链接信息的显示行。这样终端或 transcript 可以知道哪些文字应该当成网页链接。
数据流:进去的是宽度参数,但这里不按宽度重排文本 → 函数复制保存的行,再交给 annotate_web_urls 扫描网址并加上链接标记 → 出来的是 HyperlinkLine 列表,也就是带“这里是链接”说明的行。
调用关系:它直接调用 terminal_hyperlinks 里的 annotate_web_urls 来做网址识别。WebHyperlinkHistoryCell::transcript_hyperlink_lines 也会转头调用它,保证显示用链接和导出用链接一致。
调用图:调用 1 个内部函数(annotate_web_urls);被 1 处调用(transcript_hyperlink_lines)。
WebHyperlinkHistoryCell::transcript_hyperlink_lines46–48 ↗
fn transcript_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine>
作用:为会话导出提供带网页链接的版本。它避免导出 transcript 时把可点击网址信息丢掉。
数据流:进去的是导出时可用的宽度 → 函数直接调用 display_hyperlink_lines,用同一套网址标注规则处理 → 出来的是带链接信息的导出行。
调用关系:它把工作交给 WebHyperlinkHistoryCell::display_hyperlink_lines,所以显示和 transcript 导出不会各搞一套规则。
调用图:调用 1 个内部函数(display_hyperlink_lines)。
WebHyperlinkHistoryCell::raw_lines50–52 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:取出这个带链接格子的纯文本内容。需要只看文字、不看链接标记和样式时会用它。
数据流:进去不需要额外参数 → 函数复制保存的行,并用 plain_lines 去掉样式信息 → 出来的是普通原始行列表,链接标注不会出现在结果里。
调用关系:它是 HistoryCell 接口里的原始内容出口。外部做纯文本 transcript、测试断言或日志时,可以通过统一接口拿到这里的结果。
PrefixedWrappedHistoryCell::new62–72 ↗
fn new(
text: impl Into<Text<'static>>,
initial_prefix: impl Into<Line<'static>>,
subsequent_prefix: impl Into<Line<'static>>,
) -> Self
作用:创建一个“带前缀、会自动换行”的历史记录格子。它适合显示警告、审批结果、提示等需要左边有标签或缩进的长文本。
数据流:进去的是正文 text、第一行前缀 initial_prefix、后续行前缀 subsequent_prefix → 函数把它们转换成内部统一的 Text 和 Line 类型后保存 → 出来的是一个之后能按屏幕宽度重新排版的格子。
调用关系:很多构造特定消息的函数会调用它,比如 new_approval_decision_cell、new_warning_event,以及 guardian 审批通过、拒绝、超时相关的消息。它负责存材料,真正按宽度换行由 display_lines 完成。
调用图:被 11 处调用(new_approval_decision_cell, new_guardian_approved_action_request, new_guardian_denied_action_request, new_guardian_denied_patch_request, new_guardian_timed_out_action_request, new_guardian_timed_out_patch_request, new_warning_event, display_lines, prefixed_wrapped_history_cell_does_not_split_url_like_token, prefixed_wrapped_history_cell_height_matches_wrapped_rendering (+1 more));外部调用 1 个(into)。
PrefixedWrappedHistoryCell::display_lines76–84 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>
作用:把带前缀的长文本按当前终端宽度排成多行。第一行用第一种前缀,换出来的后续行用另一种前缀,这样看起来整齐。
数据流:进去的是屏幕宽度 width → 如果宽度是 0,就直接返回空列表,避免没空间还硬排版;否则创建换行选项 RtOptions,放入第一行缩进和后续缩进 → 再调用 adaptive_wrap_lines 按宽度切行 → 出来的是排好版的显示行。
调用关系:渲染带前缀消息时会通过 HistoryCell 接口调用它。它内部会新建换行配置,并使用 adaptive_wrap_lines 做真正的折行,就像把一段长话按纸张宽度重新断行。
PrefixedWrappedHistoryCell::raw_lines86–88 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:取出带前缀换行格子的原始正文,不带显示时加上的前缀缩进。这样导出纯文本时不会把界面装饰混进正文。
数据流:进去不需要额外参数 → 函数复制 text 里的原始行,再交给 plain_lines 去掉样式 → 出来的是纯正文行列表,显示用前缀不会被加进去。
调用关系:它属于 HistoryCell 的原始内容出口。和 display_lines 不同,它不做屏幕换行,也不贴前缀,适合 transcript 或内部检查使用。
调用图:外部调用 1 个(clone)。
CompositeHistoryCell::new96–98 ↗
fn new(parts: Vec<Box<dyn HistoryCell>>) -> Self
作用:创建一个组合历史记录格子,把多个小格子打包成一个大格子。需要把几段不同类型的输出当成一条历史内容展示时会用它。
数据流:进去的是多个 Box<dyn HistoryCell>,可以理解成装着不同格子的盒子,具体类型可以不一样 → 函数把它们保存到 parts 里 → 出来的是一个组合格子,之后会逐个询问子格子该怎么显示。
调用关系:new_token_activity_output、new_unified_exec_processes_output、new_status_output_with_rate_limits_handle 等会调用它,把多个输出片段拼成一个整体。一些测试也用它确认子格子的网页链接不会丢。
调用图:被 4 处调用(new_token_activity_output, new_unified_exec_processes_output, composite_cell_preserves_child_web_links, new_status_output_with_rate_limits_handle)。
CompositeHistoryCell::display_lines102–116 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>
作用:把组合格子里的每个子格子都转成普通显示行,然后拼在一起。不同子格子之间会自动插入一个空行,让内容不挤成一团。
数据流:进去的是屏幕宽度 width → 函数从空列表开始,依次调用每个 part 的 display_lines(width) → 如果某个子格子有内容,且它不是第一个有内容的部分,就先插入空行,再追加它的行 → 出来的是合并后的普通显示行列表。
调用关系:外部渲染组合内容时会通过 HistoryCell 接口调用它。它不关心子格子具体是什么类型,只把宽度传下去,让每个子格子自己决定怎么显示。
CompositeHistoryCell::display_hyperlink_lines118–132 ↗
fn display_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine>
作用:把组合格子里的每个子格子都转成“可能带链接”的显示行,再拼接起来。这样组合内容里如果有网页链接,也不会因为拼装而失效。
数据流:进去的是屏幕宽度 width → 函数依次调用每个子格子的 display_hyperlink_lines(width) → 对非空部分,在前后两块之间加一个空的 HyperlinkLine 当分隔 → 出来的是合并后的带链接显示行列表。
调用关系:当终端或上层代码想显示可点击链接版本时会走这里。它把真正识别链接的工作交给各个子格子,例如 WebHyperlinkHistoryCell 会继续调用网址标注函数。
CompositeHistoryCell::transcript_hyperlink_lines134–148 ↗
fn transcript_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine>
作用:为导出的会话记录拼出一个带链接信息的组合版本。它保证多个子内容合并后,导出时仍然有清楚的空行分隔和链接标记。
数据流:进去的是导出时的宽度 width → 函数依次调用每个子格子的 transcript_hyperlink_lines(width) → 非空子块之间插入空行 → 出来的是适合 transcript 使用的带链接行列表。
调用关系:会话导出需要组合内容时会通过 HistoryCell 接口走这里。它和 display_hyperlink_lines 很像,但调用的是子格子的 transcript 专用出口,给子格子机会区分“屏幕显示”和“导出记录”。