Codex 系统手册

交互式事件分发

stage-10.196 个文件

这一阶段是终端界面的“总调度室”,属于用户正在操作时的主循环。键盘、粘贴、窗口变化和重画信号先被 event_stream 统一收进一条事件流;暂停界面时还会放开键盘输入,免得抢走 vim 之类程序的按键。之后 App 总分发决定交给谁:输入框处理打字和斜杠命令,弹窗接管确认和选择,聊天区接后台消息,线程路由切到正确对话,mention 搜索弹出候选。各小工具按需插进来,办完再退场。

子阶段

本阶段的文件1

交互式事件分发

tui/src/tui/event_stream.rs源码 ↗
io_transportmain loop / pause-resume / tests

TUI 就是终端里的图形界面。这个文件像一个“前台接待员”:一边听真实终端输入,一边听内部的重画通知,然后把它们整理成统一的 TuiEvent。这里最重要的设计是 EventBroker。它把 crossterm 的输入流包起来,让所有界面共用同一个输入来源;需要暂停时,它会直接丢掉底层输入流,而不是只是不读。原因是 crossterm 可能在后台线程继续读 stdin,导致别的程序拿不到输入。TuiEventStream 则是给界面用的事件流,它会轮流检查“用户输入”和“重画信号”,避免其中一边饿死。它还会过滤掉不关心的事件,比如鼠标事件;焦点回来时会重新查询终端颜色;在 Unix 上按挂起键时,会暂停输入、挂起进程、再恢复输入。文件末尾的测试用假的事件源模拟终端,验证这些细节不会坏。

函数细节26
EventBrokerState::active_event_source_mut65–77 ↗
fn active_event_source_mut(&mut self) -> Option<&mut S>

作用:拿到底层终端事件来源。如果还没启动,就现场创建一个;如果已经暂停,就明确告诉调用者现在没有输入来源。

数据流:进去的是 EventBroker 当前保存的状态:Paused、Start 或 Running。它看到 Start 就创建默认事件源并切到 Running;看到 Running 就把里面的事件源借出去;看到 Paused 就返回空。出来的是一个可继续读取终端事件的对象,或者表示暂停的 None,同时它可能改变 broker 的内部状态。

调用关系:这是 EventBroker 的核心小开关。TuiEventStream::poll_crossterm_event 读取真实输入前会靠它拿事件源;测试里的 tests::FakeEventSourceHandle::send 也靠它找到假的事件源,把模拟输入塞进去。

调用图:外部调用 3 个(default, Running, unreachable!)。

EventBroker::new81–87 ↗
fn new() -> Self

作用:创建一个新的事件中转站 EventBroker。它一开始不马上打开终端输入,而是标记为 Start,等第一次真正读取时再打开。

数据流:进去没有业务输入。它创建一个 watch 通道,watch 通道可以理解成“状态变化通知器”,用来告诉等待中的事件流:输入已经恢复了。出来的是一个带锁的 broker,内部状态是 Start,并保存了恢复通知的发送端。

调用关系:创建 TUI 事件系统时会用它;测试的 setup 也用它搭建假环境。之后 TuiEventStream::new 会拿着这个 broker 来共享同一个终端输入来源。

调用图:被 2 处调用(new, setup);外部调用 2 个(new, channel)。

EventBroker::pause_events90–96 ↗
fn pause_events(&self)

作用:暂停读取终端输入,并且真正丢掉底层输入流。这样 TUI 暂停或让出终端时,不会继续偷偷读键盘。

数据流:进去的是当前 broker。它先拿到互斥锁,互斥锁就是一把防止多人同时改同一份状态的锁;然后把状态改成 Paused。出来没有返回值,但底层事件源会被释放。

调用关系:当程序需要把终端交给别的东西,或者 Unix 上处理挂起键时,会调用它。之后 TuiEventStream::poll_crossterm_event 看到 Paused,就会等待恢复通知。

EventBroker::resume_events99–106 ↗
fn resume_events(&self)

作用:恢复终端输入。它不会立刻强行读取,而是把状态改成“下次读取时重新创建事件源”,并唤醒正在等的人。

数据流:进去的是当前 broker。它把内部状态设为 Start,然后通过 watch 通道发一个空通知。出来没有业务返回值,但等待中的事件流会被唤醒,下一次轮询会重新打开底层输入流。

调用关系:pause_events 之后通常会调用它。TuiEventStream::poll_crossterm_event 在暂停或等待输入时会监听这个恢复通知,所以恢复后不会一直睡着。

调用图:外部调用 1 个(send)。

EventBroker::resume_events_rx112–114 ↗
fn resume_events_rx(&self) -> watch::Receiver<()>

作用:给调用者一个“恢复事件”的订阅器。谁拿到它,谁就能在 broker 恢复输入时被叫醒。

数据流:进去的是 broker 自己。它从内部 watch 发送端派生出一个接收端。出来的是 watch::Receiver,后续可以变成流来等待 resume_events 的通知。

调用关系:TuiEventStream::new 会调用它,并把接收端包装成 resume_stream。poll_crossterm_event 在暂停或 pending 时会轮询这个流。

调用图:外部调用 1 个(subscribe)。

CrosstermEventSource::default121–123 ↗
fn default() -> Self

作用:创建真实的 crossterm 终端事件源。crossterm 是一个 Rust 终端库,负责读键盘、窗口大小、粘贴等终端事件。

数据流:进去没有输入。它调用 crossterm 的 EventStream::new,得到一个能持续产出终端事件的流。出来的是 CrosstermEventSource,把这个真实流包在里面。

调用关系:EventBrokerState::active_event_source_mut 在 Start 状态下会通过默认构造创建它。生产环境里,broker 默认用的就是这个事件源。

调用图:外部调用 1 个(new)。

CrosstermEventSource::poll_next127–129 ↗
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<EventResult>>

作用:从真实 crossterm 事件流里取下一个终端事件。它只是薄薄地转交请求,让外层不用直接依赖 crossterm 的具体类型。

数据流:进去的是异步轮询上下文和这个事件源。它把轮询请求转给内部 crossterm EventStream。出来可能是一个成功的终端事件、一个错误、结束信号,或者 Pending,Pending 表示现在还没有事件。

调用关系:TuiEventStream::poll_crossterm_event 通过 EventSource 接口间接调用它。测试时会换成 FakeEventSource::poll_next,以便不用真的敲键盘。

调用图:外部调用 1 个(new)。

TuiEventStream::new152–171 ↗
fn new(
        broker: Arc<EventBroker<S>>,
        draw_rx: broadcast::Receiver<()>,
        terminal_focused: Arc<AtomicBool>,
        #[cfg(unix)] suspend_context: crate::tui::job_control::Suspend

作用:创建一个给 TUI 使用的统一事件流。它把共享的终端输入 broker、重画通知、焦点状态等零件装配到一起。

数据流:进去的是共享 broker、一个 draw 广播接收器、终端是否聚焦的共享标记,以及 Unix 下的挂起相关上下文。它把 draw 接收器包装成 BroadcastStream,把 broker 的恢复通知包装成 WatchStream,并记录这些状态。出来的是一个 TuiEventStream,可以被异步地不断读取 TuiEvent。

调用关系:界面代码创建事件流时会用它;测试辅助函数 tests::make_stream 也用它造出可控的测试流。之后异步运行时会不断调用 TuiEventStream::poll_next。

调用图:被 2 处调用(event_stream, make_stream);外部调用 2 个(new, from_changes)。

TuiEventStream::poll_crossterm_event178–222 ↗
fn poll_crossterm_event(&mut self, cx: &mut Context<'_>) -> Poll<Option<TuiEvent>>

作用:读取下一条真实终端输入,并把它转换成 TUI 看得懂的事件。它会跳过不关心的输入,还会在暂停时让出 stdin。

数据流:进去的是当前事件流和异步轮询上下文。它先通过 broker 拿到底层事件源;如果 broker 暂停,就等待恢复通知;如果读到终端事件,就交给 map_crossterm_event 翻译;如果读到错误或结束,就重置 broker 状态并结束。出来可能是一个 TuiEvent、结束信号,或者 Pending。

调用关系:TuiEventStream::poll_next 会调用它,并和 poll_draw_event 轮流检查。它是终端输入进入 TUI 的主要通道,也负责在 pause/resume 时正确配合 EventBroker。

调用图:被 1 处调用(poll_next);外部调用 2 个(new, Ready)。

TuiEventStream::poll_draw_event225–234 ↗
fn poll_draw_event(&mut self, cx: &mut Context<'_>) -> Poll<Option<TuiEvent>>

作用:读取程序内部发出的“需要重画界面”通知,并把它变成 TuiEvent::Draw。

数据流:进去的是当前事件流和轮询上下文。它检查 draw 广播流;收到正常通知就产出 Draw;如果因为接收太慢漏掉了一些通知,也仍然产出一个 Draw,因为重画一次通常就够了。出来是 Draw、结束信号,或 Pending。

调用关系:TuiEventStream::poll_next 会和 poll_crossterm_event 交替调用它。这样用户输入和界面刷新都能及时被处理。

调用图:被 1 处调用(poll_next);外部调用 2 个(new, Ready)。

TuiEventStream::map_crossterm_event237–269 ↗
fn map_crossterm_event(&mut self, event: Event) -> Option<TuiEvent>

作用:把 crossterm 的原始事件翻译成项目自己的 TuiEvent。它也会顺手处理焦点变化、粘贴、窗口变化和 Unix 挂起键这些特殊情况。

数据流:进去的是一个原始终端事件。键盘事件会变成 TuiEvent::Key;窗口大小变化会变成 Resize;粘贴会变成 Paste;焦点回来会把 terminal_focused 设为 true、重新查询终端默认颜色,并要求重画;焦点离开只更新状态、不产生界面事件;不关心的事件返回 None。Unix 上如果按了挂起键,它会暂停输入、挂起 TUI、恢复输入,然后要求重画。

调用关系:poll_crossterm_event 读到每个原始事件后都会交给它。它把外部库的事件格式隔离在这里,让 TUI 其他部分只面对项目自己的 TuiEvent。

调用图:调用 1 个内部函数(suspend);外部调用 4 个(requery_default_colors, warn!, Key, Paste)。

TuiEventStream::poll_next277–299 ↗
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>

作用:这是 TuiEventStream 作为异步 Stream 的入口。外面的界面循环每次想要下一个事件,最终都会走到这里。

数据流:进去的是事件流和轮询上下文。它用 poll_draw_first 这个小开关在“先查重画”和“先查输入”之间来回切换;如果某边已经有事件,就立刻返回;两边都没有就返回 Pending。出来是一个 TuiEvent、结束信号,或暂时没消息。

调用关系:它把 poll_crossterm_event 和 poll_draw_event 串起来,并用轮流优先的方式避免某一类事件一直抢先。TUI 主循环通常消费的就是这个 Stream。

调用图:调用 2 个内部函数(poll_crossterm_event, poll_draw_event);外部调用 1 个(Ready)。

tests::FakeEventSource::new329–332 ↗
fn new() -> Self

作用:创建测试用的假终端事件源。测试可以往它里面塞事件,不需要真的操作终端。

数据流:进去没有输入。它创建一个无界 mpsc 通道,mpsc 通道可以理解成“多人可发、一个人接收”的消息管道;发送端留给测试塞事件,接收端留给事件流读取。出来的是 FakeEventSource。

调用关系:tests::FakeEventSource::default 和 tests::setup 会用它。它替代真实 CrosstermEventSource,让测试可预测、可重复。

调用图:外部调用 1 个(unbounded_channel)。

tests::FakeEventSource::default336–338 ↗
fn default() -> Self

作用:给假事件源提供默认创建方式,方便 EventBroker 在需要时自动造一个。

数据流:进去没有输入。它直接调用 tests::FakeEventSource::new。出来是一个带通道的假事件源。

调用关系:EventBrokerState::active_event_source_mut 要求事件源能 Default,所以测试版事件源也实现这个函数。

调用图:外部调用 1 个(new)。

tests::FakeEventSourceHandle::new342–344 ↗
fn new(broker: Arc<EventBroker<FakeEventSource>>) -> Self

作用:创建一个测试控制手柄。测试用它向 broker 里的假事件源发送模拟终端事件。

数据流:进去的是共享的假 broker。它把 broker 保存进 handle。出来的是 FakeEventSourceHandle。

调用关系:tests::setup 会调用它,把 handle 交给各个测试。测试再通过 handle.send 模拟键盘、窗口变化、错误等输入。

tests::FakeEventSourceHandle::send346–356 ↗
fn send(&self, event: EventResult)

作用:往假事件源里塞一条模拟事件。这样测试可以像“假装用户按键”一样驱动事件流。

数据流:进去的是一个 EventResult,可能是成功事件,也可能是错误。它先锁住 broker 状态,找到当前活跃的假事件源;如果 broker 暂停,就什么也不做;如果找到了,就把事件发进假事件源的通道。出来没有返回值,但事件可能已经等待被 stream 读走。

调用关系:多个测试会用它喂入 Key、Resize、FocusLost 或错误。TuiEventStream::poll_crossterm_event 后续通过 FakeEventSource::poll_next 读到这些事件。

tests::FakeEventSource::poll_next360–362 ↗
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<EventResult>>

作用:让假事件源看起来像真实终端事件源一样,可以被异步轮询。

数据流:进去的是假事件源和轮询上下文。它从内部 mpsc 接收端尝试取一条测试塞进去的事件。出来是事件、结束信号,或 Pending。

调用关系:测试版 TuiEventStream 通过 EventSource 接口调用它。这样生产代码的读取逻辑不用改,测试只替换输入来源。

调用图:外部调用 1 个(new)。

tests::make_stream365–379 ↗
fn make_stream(
        broker: Arc<EventBroker<FakeEventSource>>,
        draw_rx: broadcast::Receiver<()>,
        terminal_focused: Arc<AtomicBool>,
    ) -> TuiEventStream<FakeEventSource>

作用:测试辅助函数,用一组测试零件快速拼出 TuiEventStream。它减少每个测试里的重复搭建代码。

数据流:进去的是假 broker、draw 接收器和焦点状态。它补上 Unix 下需要的挂起上下文和备用屏幕状态,然后调用 TuiEventStream::new。出来的是使用 FakeEventSource 的事件流。

调用关系:各个异步测试都会调用它。它把测试环境和真实 TuiEventStream 构造逻辑连接起来。

调用图:调用 2 个内部函数(new, new);外部调用 2 个(new, new)。

tests::setup389–398 ↗
fn setup() -> SetupState

作用:搭建一整套测试环境:假事件源、broker、发送手柄、重画通道和焦点状态。

数据流:进去没有输入。它创建 FakeEventSource 和 EventBroker,把 broker 状态手动设成 Running;再创建 draw 广播通道和 terminal_focused 标记。出来是一组测试会反复使用的对象。

调用关系:几乎所有测试开头都调用它。它保证每个测试都有干净、可控的事件流环境。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, new, channel, Running, new, new)。

tests::key_event_skips_unmapped401–418 ↗
async fn key_event_skips_unmapped()

作用:验证无用事件会被跳过。例子里 FocusLost 不会直接产出 TuiEvent,后面的按键才会被返回。

数据流:进去是测试环境。它先发送 FocusLost,再发送字符 a 的按键事件;然后从 stream 取下一条事件。结果应该是 Key(a),同时测试会确认不是别的事件。

调用关系:这个测试覆盖 TuiEventStream::poll_crossterm_event 和 map_crossterm_event 的配合,确保过滤逻辑不会把真正的按键挡住。

调用图:外部调用 7 个(Char, new, assert_eq!, panic!, Key, make_stream, setup)。

tests::draw_and_key_events_yield_both421–448 ↗
async fn draw_and_key_events_yield_both()

作用:验证重画事件和按键事件都会被送出来,不会一个把另一个挤掉。

数据流:进去是测试环境。它先发一个 draw 通知,再发一个按键 a;然后连续取两次 stream。出来的两条事件必须包含 Draw 和 Key(a),顺序不强求。

调用关系:这个测试主要检查 TuiEventStream::poll_next 的轮询策略,以及 poll_draw_event 和 poll_crossterm_event 能否一起工作。

调用图:外部调用 8 个(Char, new, assert!, assert_eq!, panic!, Key, make_stream, setup)。

tests::lagged_draw_maps_to_draw451–461 ↗
async fn lagged_draw_maps_to_draw()

作用:验证 draw 广播接收器即使漏掉通知,也仍然会触发一次重画。

数据流:进去是测试环境。它让 draw 通道容量很小,并连续发送通知,故意制造 Lagged,也就是接收者落后了。然后读取 stream,结果应该仍是 TuiEvent::Draw。

调用关系:这个测试覆盖 TuiEventStream::poll_draw_event 的容错行为。它保证界面刷新信号即使被压缩,也不会完全丢失。

调用图:外部调用 3 个(assert!, make_stream, setup)。

tests::resize_event_maps_to_resize464–472 ↗
async fn resize_event_maps_to_resize()

作用:验证终端窗口大小变化会被翻译成 TuiEvent::Resize。

数据流:进去是测试环境。它发送一个 Resize(80, 24) 原始事件,然后从 stream 读取。结果应该是 Resize 事件。

调用关系:这个测试直接检查 map_crossterm_event 对窗口变化事件的翻译是否正确。

调用图:外部调用 4 个(assert!, Resize, make_stream, setup)。

tests::error_or_eof_ends_stream475–483 ↗
async fn error_or_eof_ends_stream()

作用:验证底层事件源报错时,TUI 事件流会结束,而不是卡住或产出假事件。

数据流:进去是测试环境。它发送一个模拟 IO 错误 boom,然后读取 stream。结果应该是 None,表示这个事件流结束了。

调用关系:这个测试覆盖 poll_crossterm_event 里处理错误和结束的分支,确保 broker 会把底层源重置并向上报告结束。

调用图:外部调用 4 个(assert!, other, make_stream, setup)。

tests::resume_wakes_paused_stream486–507 ↗
async fn resume_wakes_paused_stream()

作用:验证事件流在 broker 已经暂停时等待,恢复后能被唤醒并读到新按键。

数据流:进去是测试环境。测试先暂停 broker,再启动一个任务等待 stream.next;随后调用 resume_events,并发送按键 r。结果应该在超时时间内读到 Key(r)。

调用关系:这个测试检查 EventBroker::pause_events、resume_events、resume_events_rx 和 TuiEventStream::poll_crossterm_event 的配合,确保暂停状态不会让事件流永久睡死。

调用图:外部调用 11 个(from_millis, Char, new, assert_eq!, panic!, Key, spawn, yield_now, timeout, make_stream (+1 more))。

tests::resume_wakes_pending_stream510–530 ↗
async fn resume_wakes_pending_stream()

作用:验证事件流已经在等待 stdin 时,如果发生暂停再恢复,也能被恢复通知唤醒。

数据流:进去是测试环境。测试先让 stream.next 进入等待状态,然后调用 pause_events 和 resume_events,再发送按键 p。结果应该及时读到 Key(p)。

调用关系:这个测试覆盖更隐蔽的情况:不是一开始就暂停,而是读取已经 Pending 后才暂停恢复。它保证 poll_crossterm_event 在等待输入时也会监听恢复通知。

调用图:外部调用 11 个(from_millis, Char, new, assert_eq!, panic!, Key, spawn, yield_now, timeout, make_stream (+1 more))。