使用 vim 跨 ssh 复制文本

2021-10-05 ⏳3.9分钟(1.6千字)

最近有朋友问如何在不用鼠标的前提下把服务器上 vim 的内容复制到本机剪贴板。说实话,我都是直接用鼠标选中再复制的。但因为朋友问起,只好研究一下。还真就找到了不用鼠标的方案。这种方法不但可以把 vim 的内容复制到本机剪切板,还可以复制到本机 tmux 的剪切板,非常方便。今天整理成文,分享给大家。

我在多彩的终端介绍了命令行转义字符(ANSI Escape Sequences)。转义字符以 0x1b 开头,紧接着是一个字节,取值范围是 @A–Z[\]^_,不同的字母表示不同的含义。我在前文中介绍的是 0x1b[,叫作 Control Sequence Introducer,简写为 CSI。通过CSI可以在命令行下显示粗体、斜体、下划线字符,也可以显示不同的颜色,甚至还能显示简单的动画。今天要实现复制内容,需要用到0xeb],叫作Operating System Command,简写OSC。完整的 OSC指令如下:

OSC Description Status Format
0 Set window title Only window title ESC ] 0 ; [title]
2 Set window title Converted to 0
4 Set/read color palette Supported ESC ] 4 ; index1;rgb1;…;indexN;rgbN
9 iTerm2 notifications Supported ESC ] 9 ; [message]
10 Set foreground color Supported ESC ] 10 ; [X11 color spec]
11 Set background color Supported ESC ] 11 ; [X11 color spec]
50 Set the cursor shape Supported ESC ] 50 ; CursorShape=[0|1|2]
52 Clipboard operations Only “c” ESC ] 52 ; c ; [base64 data]
777 urxvt modules Only “notify” ESC ] 777 ; notify ; [title] ; [body]

来源:chromium.googlesource.com

其中复制命令长这样:

OSC52;c;base64-encoded-text\a

开头是OSC,也就是0x1b[,后面接一个分号;然后是数字52,表示操作剪贴板;再用分号连接一个字母c表示复制(目前只有复制这一种),再接一个分号;然后是被复制的内容,需要转化为 base64 编码;最后面是响铃符号(bel)。复制这功能需要终端软件支持。也就是说,如果终端软件支持,它在收到0x1b[52;c;aGVsbG8=\a这一段字符后,不会直接显示到终端上,而是把hello复制到系统剪贴板。

并非所有终端模拟器都支持复制指令(OSC52),我从网上找到一份支持的列表:

Terminal OSC52 support
Alacritty yes
GNOME Terminal (and other VTE-based terminals) not yet
hterm (Chromebook) yes
iTerm2 yes
kitty yes
screen yes
tmux yes
Windows Terminal yes
rxvt yes (to be confirmed)
urxvt yes (with a script, see here)
foot yes
wezterm yes

我用的是 iTerm2,也不是默认就支持的。需要到 Preferences -> General -> Selection 下勾选 Applications in terminal may access clipboard。其他终端请自行查找设置方法。开启后执行如下命令:

echo -ne "\033]52;c;$(echo -n hello | base64)\a"

然后按系统的粘贴快捷键⌘ + v就可以粘贴hello

那如何将OSC52命令跟 vim 结合起来呢?我从这篇文章中找了vim-oscyank。因为需要支持 windows 平台和 neovim,oscyank 比较复杂的。但 oscyank 的核心也是 OSC52 命令。如果你不愿折腾,可以直接使用这个插件,但我们完全可以自己实现一个更简单的版本。

首先我们需要获取 vim 的复制的内容,这可以通过TextYankPost这一事件实现。我们可以在 vimrc 中加入这么一行:

autocmd TextYankPost * echo v:event

当我们在 vim 复制或者删除内容的时候就会看到如下输出:

{'regcontents': ['hello'], 'visual': v:false, 'inclusive': v:false, 'regname': '', 'operator': 'y', 'regtype': 'V'}

复制的内容可以通过regcontents这个 key 提取。这是一个列表,每个元素对应一行内容。接下来就需要将复制的内容转化成OSC52指令,发送给终端软件,代码也很简单:

function Copy()
  let c = join(v:event.regcontents,"\n")
  let c64 = system("base64", c)
  let s = "\e]52;c;" . trim(c64) . "\x07"
  call chansend(v:stderr, s)
endfunction
autocmd TextYankPost * call Copy()

这里先把autocmd改成调用我们写的Copy函数。注意,v:event是一个全局变量,可以直接在Copy中访问。然后使用\n把要复制的内容连到一起,再使用system调用系统的base64命令完成编码(也可以使用纯 viml 实现 base64 功能,太复杂)。最后拼接 OSC52 指令。这里的\e就是0xeb,而\x07就是\a。最后通过charsend将指令发送给终端。终端收到后会将被复制的内容写入系统剪贴板。

charsend 好像是 neovim 特有的命令,如果要支持 vim,则需要如下代码(来自 oscyank 😇):

function! s:raw_echo(str)
  if has('win32') && has('nvim')
    call chansend(v:stderr, a:str)
  else
    if filewritable('/dev/fd/2')
      call writefile([a:str], '/dev/fd/2', 'b')
    else
      exec("silent! !echo " . shellescape(a:str))
      redraw!
    endif
  endif
endfunction

等等,说了这么多,这种复制跟 ssh 有关系吗?说好的通过 ssh 从远程 vim 复制内容到本地呢?

其实已经说完了。因为 ssh 会把远程 vim 输出的所有内容(包括 OSC52 指令)发送给本机终端处理。只要本机终端支持 OSC52 指令,就会把对应的内容复制的本机系统剪贴板💯

最后说一下使用 tmux 的问题。无论是在本机还是远程机器,只要开了 tmux,前面说的复制功能就会失效。我在网上查了好多资料,都说除了设置set -g set-clipboard on外,还要通过terminal-overrides更新Ms配置。可能是 tmux 版本的问题,我用最新的 tmux 实验发现,只需要添加set -g set-clipboard on配置就行了。无论是本地起 tmux 还是远程起,或者是两边都起,都是没有问题的。开启了set-clipboard之后还有一个福利,可以打通 vim 跟 tmux 的剪切板。也就是说,在 vim 复制的内容可以使用 tmux 的快捷键进行粘贴,非常可口!

以上就是本文的全部内容了。总结一下就是通过给终端模拟软件发送 OSC52 指令来实现复制功能。这种方式可以跨越 ssh 会话,实现远程复制的效果。这种方式还可以实现 vim 与 tmux 的剪贴板互通,非常方便。但是,方便归方便,这种方案本质上还是使用了命令行的转义序列,需要依赖终端模拟软件。如果大家有兴趣,也建议玩一下其他 OSC 指令(比如 OSC9 可以控制 iTerm2 发送系统通知等)。欢迎交流。

Pany357 在 2022-05-20 留言,提到 windows 平台:

在 windows 中 安装 git 附赠的那个 Git Bash 是 mintty,它也支持 OSC 52,只需在它的配置文件 ~/.minttyrc 中加入 AllowSetSelection=true 就行了。