使用 vim 跨 ssh 复制文本
涛叔最近有朋友问如何在不用鼠标的前提下把服务器上 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] |
其中复制命令长这样:
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 平台:
💡Tip在 windows 中 安装 git 附赠的那个 Git Bash 是 mintty,它也支持 OSC 52,只需在它的配置文件 ~/.minttyrc 中加入 AllowSetSelection=true 就行了。