Read OSS

交互模式:复制模式的文本选择与控制模式的协议

高级

前置知识

  • 本系列第 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_modewindow_copy_mode_view 两个入口接入窗口模式的虚函数表(vtable,详见第 2 篇)。init 回调负责创建后备屏幕,key 回调处理所有用户交互,free 回调则负责清理模式专属的状态。

复制模式:搜索、选择与粘贴

搜索是复制模式中最复杂的功能。前向和后向搜索均支持字面字符串和正则表达式两种方式。搜索的执行流程如下:

  1. 编译搜索字符串(如启用则按正则处理)
  2. window_copy_search_marks() 扫描后备网格并标记所有匹配位置
  3. 标记数据按行存储为数组,每个单元格带有一个位标志,表示是否属于某个匹配
  4. 当前匹配项与其他匹配项使用不同的高亮样式加以区分
  5. 增量搜索会随用户输入实时更新标记
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-vicopy-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_drawoverlay_keyoverlay_checkoverlay_free)挂载在客户端上,screen-redraw.c 中的合成系统会在最终的叠加绘制阶段调用它们。

overlay_check 回调返回可见区域——即屏幕上未被弹出层遮挡的部分——确保底层窗格内容只在可见区域内绘制,从而避免画面闪烁和无谓的重绘开销。

弹出层的生命周期如下:

  1. display-popup 命令创建 popup_data,启动子进程,设置叠加回调
  2. 按键事件被 overlay_key 拦截,转发至弹出层的输入
  3. 子进程的输出经由弹出层自身的 VT100 解析器写入其屏幕
  4. 每次重绘时,overlay_draw 将弹出层的屏幕(含边框)绘制在窗口内容之上
  5. 子进程退出或用户关闭弹出层后,overlay_free 完成所有清理工作

系列总结

经过前六篇文章,我们已完整梳理了 tmux 的整体架构——从单一二进制的客户端-服务器分叉机制,到 VT100 状态机;从红黑树数据结构,到格式字符串迷你语言。这份代码库堪称 C 系统编程的范本:扁平的文件组织与一致的命名约定、数据驱动的状态机、用于扩展性的 vtable 模式,以及面向实际性能需求的精细流量控制。

下一篇文章将聚焦于实践层面:tmux 如何实现跨平台可移植性,以及添加新命令和选项的完整贡献指南。