交互模式:复制模式的文本选择与控制模式的协议
前置知识
- ›本系列第 1-5 篇文章
- ›了解 vi/emacs 模态编辑的基本概念
交互模式:复制模式的文本选择与控制模式的协议
在前几篇文章中,我们将窗格视为被动的终端显示单元。但 tmux 的窗格可以切换到交互模式,完全接管显示输出和输入处理。其中最重要的是复制模式——一个约 6200 行的模块,实现了可滚动的历史记录浏览、带增量高亮的正则表达式搜索,以及矩形区域选择,是整个代码库中最复杂的子系统之一。
本文还将介绍控制模式(tmux 提供给 iTerm2 等 GUI 终端使用的机器可读协议)、驱动四种选择界面的共享模式树框架,以及弹出层叠加系统。
复制模式:架构与屏幕叠加
复制模式(实现于 window-copy.c)可通过按下 Ctrl-b [ 或鼠标滚动触发历史浏览来激活。它会创建一个独立的屏幕,叠加在窗格的基础屏幕之上。
该模式的数据结构(window-copy.c 内部)维护以下状态:
- 后备网格:进入模式时对完整滚动历史记录所做的快照
- 视图屏幕:从后备网格中截取的窗口大小的显示切片
- 光标位置(cx, cy):相对于视图的坐标
- 滚动偏移量(oy):记录用户向上滚动的距离
- 搜索状态:包括搜索字符串、正则标志和匹配高亮标记
- 选择状态:包括选择类型(普通、行、块/矩形)、锚点和当前范围
flowchart TD
subgraph "Pane in copy mode"
BS["Backing Screen<br/>(snapshot of grid + history)"]
VS["View Screen<br/>(window-sized slice)"]
C["Cursor (cx, cy)"]
S["Scroll offset (oy)"]
end
subgraph "Rendering"
VS --> R["window_copy_write_line()"]
R --> SW["screen_write_* functions"]
SW --> T["tty output to client"]
end
BS -->|"offset by oy"| VS
C -->|"relative to view"| VS
复制模式初始化时,会对窗格的网格(包含所有滚动历史)做一次完整快照。这份拷贝完全独立于活跃窗格——用户在浏览历史记录时,窗格仍可继续接收输出。每当用户滚动或移动光标,视图屏幕会根据当前偏移量从后备网格读取对应单元格并重新渲染。
该模式通过 window_copy_mode 和 window_copy_mode_view 两个入口接入窗口模式的虚函数表(vtable,详见第 2 篇)。init 回调负责创建后备屏幕,key 回调处理所有用户交互,free 回调则负责清理模式专属的状态。
复制模式:搜索、选择与粘贴
搜索是复制模式中最复杂的功能。前向和后向搜索均支持字面字符串和正则表达式两种方式。搜索的执行流程如下:
- 编译搜索字符串(如启用则按正则处理)
window_copy_search_marks()扫描后备网格并标记所有匹配位置- 标记数据按行存储为数组,每个单元格带有一个位标志,表示是否属于某个匹配
- 当前匹配项与其他匹配项使用不同的高亮样式加以区分
- 增量搜索会随用户输入实时更新标记
sequenceDiagram
participant User
participant CopyMode as window-copy.c
participant Grid as Backing Grid
participant Screen as View Screen
User->>CopyMode: Press / (search forward)
CopyMode->>CopyMode: Enter search prompt
User->>CopyMode: Type "error"
CopyMode->>Grid: window_copy_search_marks()
Grid-->>CopyMode: Mark array (matches at positions)
CopyMode->>Screen: Redraw with highlighted marks
CopyMode->>CopyMode: Jump cursor to first match
User->>CopyMode: Press n (next match)
CopyMode->>CopyMode: Advance to next mark
CopyMode->>Screen: Redraw, highlight current match
选择共有三种类型:
- 普通模式:选择连续的字符范围
- 行模式:选择整行
- 块/矩形模式:选择矩形区域(不受换行符约束)
三种模式均以锚点(选择开始时设定)和当前位置(光标所在处)来确定选区范围。被选中的文本在重绘时会以反色或高亮颜色显示。
用户执行复制操作时(vi 模式下按 Enter,emacs 模式下按 M-w),所选文本会从后备网格中提取,存入由 paste.c 管理的粘贴缓冲区,并可选择通过 OSC 52 发送至系统剪贴板。
复制模式的按键绑定通过专用的键表(copy-mode-vi 和 copy-mode-emacs)分发,而非常规的前缀键表。window_copy_key_table() 回调返回对应的键表名称,第 3 篇中介绍的按键处理管道据此查找绑定关系。复制模式内部的命令通过 window_copy_command() 回调分发,可处理 send-keys -X search-forward 等操作。
模式树框架
tmux 的四种交互模式——tree(会话/窗口/窗格选择器)、buffer(粘贴缓冲区浏览器)、client(客户端选择器)和 customize(选项编辑器)——共享一套实现于 mode-tree.c 的通用框架。
classDiagram
class mode_tree_data {
+draw()
+search()
+filter()
+menu()
+sort_list
}
class window_tree_mode {
build_cb: window_tree_build()
draw_cb: window_tree_draw()
}
class window_buffer_mode {
build_cb: window_buffer_build()
draw_cb: window_buffer_draw()
}
class window_client_mode {
build_cb: window_client_build()
draw_cb: window_client_draw()
}
class window_customize_mode {
build_cb: window_customize_build()
draw_cb: window_customize_draw()
}
mode_tree_data <|-- window_tree_mode
mode_tree_data <|-- window_buffer_mode
mode_tree_data <|-- window_client_mode
mode_tree_data <|-- window_customize_mode
该框架提供以下能力:
- 树形渲染:以缩进列表展示可展开/折叠的节点
- 过滤:通过搜索栏按模式过滤可见项
- 排序:通过按键绑定切换多种排序条件
- 标记:为多个条目打标记,支持批量操作
- 预览:显示当前选中项详细信息的预览面板
各模式通过提供回调函数接入框架:构建树(从数据填充节点)、绘制每一行(格式化显示内容),以及处理菜单操作。以 window-tree.c 为例,它构建了一棵三级树(会话 → 窗口 → 窗格),并允许用户在其中导航、切换或关闭条目。
这是 C 语言中"框架 + 回调"模式的经典实践。共享框架负责处理所有通用交互(滚动、展开/折叠、过滤、输入处理),而各模式专属的回调则负责数据填充和格式化。
控制模式:机器可读协议
控制模式(-C 标志)是 tmux 面向 GUI 终端集成的协议接口。tmux 不再向终端渲染画面,而是发送结构化的文本通知,供程序解析使用。macOS 上的 iTerm2 正是通过这一机制实现其 tmux 集成功能的。
协议定义于 control.c#L29-L76,输出内容为以 % 开头的文本行:
%begin 1234567890 1 0
%end 1234567890 1 0
%output %0 \033[1mHello\033[0m
%session-changed $1 mysession
%window-add @0
%layout-change @0 abc123 main-horizontal,...
其架构依赖两个核心数据结构:
struct control_block {
size_t size;
char *line;
uint64_t t;
TAILQ_ENTRY(control_block) entry; /* per-pane queue */
TAILQ_ENTRY(control_block) all_entry; /* client-wide queue */
};
struct control_pane {
u_int pane;
struct window_pane_offset offset; /* data written */
struct window_pane_offset queued; /* data queued */
int flags; /* OFF, PAUSED */
TAILQ_HEAD(, control_block) blocks;
};
sequenceDiagram
participant Pane as Pane Process
participant Server as tmux Server
participant Control as control.c
participant Client as GUI Client (iTerm2)
Pane->>Server: Write to PTY
Server->>Server: Parse VT100, update grid
Server->>Control: control_write_output()
Control->>Control: Create control_block
Control->>Control: Queue on pane + client
Note over Control: Flow control check
Control->>Client: %output %0 <data>
Client->>Server: send-keys -t %0 "ls"
Server->>Control: control_notify.c
Control->>Client: %window-renamed @0 newname
控制模式:流量控制与窗格追踪
control.c 开头的注释对流量控制设计做了简洁的说明。每个客户端有一个全局块队列,每个窗格也有自己独立的块队列。%output 块会同时加入这两个队列;而非输出通知(如 %session-changed)则只加入客户端队列。
这种双队列机制保证了消息的有序投递。窗格队列头部的 %output 块会阻塞后续通知,直到数据完整写入客户端为止。这样可以避免 GUI 客户端在收到所有前置输出数据之前,就先收到 %layout-change 通知的情况。
control_pane 结构为每个窗格分别跟踪两个偏移量——已写入量(offset)和已入队量(queued)。当客户端处理速度跟不上时,CONTROL_PANE_PAUSED 标志会被设置,tmux 随即停止为该窗格排入新的输出。暂停/恢复机制(pause-after 客户端标志)允许慢速消费者追上进度,而不会无限期地丢失数据。
control-notify.c 负责将 tmux 的内部事件转换为协议消息。control_notify_session_renamed()、control_notify_window_linked()、control_notify_pane_mode_changed() 等函数由各子系统调用,向所有处于控制模式的客户端写入以 % 开头的通知行。
提示: 运行
tmux -C即可亲身体验控制模式。每一行输入都会作为命令执行,所有通知均以%为前缀实时输出。不妨执行new-session,观察一连串%session-*和%window-*通知的涌现过程,这是理解该协议最直观的方式。
弹出层叠加
弹出层是 tmux 3.2 之后引入的功能,可以悬浮在普通窗口内容之上。它实现于 popup.c,利用第 2 篇中提到的客户端叠加回调系统。
struct popup_data {
struct client *c;
struct cmdq_item *item;
char *title;
struct screen s; /* popup's own screen */
struct grid_cell defaults;
struct colour_palette palette;
struct job *job; /* child process */
struct input_ctx *ictx; /* VT100 parser for popup */
popup_close_cb cb;
/* position and size */
u_int px, py;
u_int sx, sy;
};
弹出层本质上是一个以叠加方式挂载在客户端上的迷你窗格。它拥有独立的屏幕、独立的 VT100 输入解析器,以及独立的子进程(通常是一个 shell)。叠加回调(overlay_draw、overlay_key、overlay_check、overlay_free)挂载在客户端上,screen-redraw.c 中的合成系统会在最终的叠加绘制阶段调用它们。
overlay_check 回调返回可见区域——即屏幕上未被弹出层遮挡的部分——确保底层窗格内容只在可见区域内绘制,从而避免画面闪烁和无谓的重绘开销。
弹出层的生命周期如下:
display-popup命令创建popup_data,启动子进程,设置叠加回调- 按键事件被
overlay_key拦截,转发至弹出层的输入 - 子进程的输出经由弹出层自身的 VT100 解析器写入其屏幕
- 每次重绘时,
overlay_draw将弹出层的屏幕(含边框)绘制在窗口内容之上 - 子进程退出或用户关闭弹出层后,
overlay_free完成所有清理工作
系列总结
经过前六篇文章,我们已完整梳理了 tmux 的整体架构——从单一二进制的客户端-服务器分叉机制,到 VT100 状态机;从红黑树数据结构,到格式字符串迷你语言。这份代码库堪称 C 系统编程的范本:扁平的文件组织与一致的命名约定、数据驱动的状态机、用于扩展性的 vtable 模式,以及面向实际性能需求的精细流量控制。
下一篇文章将聚焦于实践层面:tmux 如何实现跨平台可移植性,以及添加新命令和选项的完整贡献指南。