工具链迁移 Wayland:剪贴板踩坑与适配方案

八月 09, 2026 更新: 九月 13, 2026 [桌面与效率] #Wayland #clipboard #tmux #waypipe

背景

X11 时代复制粘贴很简单:xclip / xsel 全家桶,tmux、fzf、vim 里全是它们的影子。切到 Wayland 后,剪贴板不再由"全局 X server"统一管理,而是每个合成器(compositor)自己实现,工具链也得跟着换。

本文记录迁移到 Wayland 剪贴板工具链的完整踩坑过程与最终方案,包含一个容易误判的 waypipe 报错,以及虚拟机显卡升级(QXL → virtio-gpu 3D)的实测落地过程(2026-09-13,含 SPICE GL/VNC 互斥等坑)。

Wayland 与 X11 剪贴板的差异

项目X11 (xclip/xsel)Wayland (wl-clipboard)
环境变量DISPLAYWAYLAND_DISPLAY + XDG_RUNTIME_DIR
剪贴板一个全局剪贴板clipboard 与 primary selection 分开
复制命令xclip -selection clipboardwl-copy(clipboard)/ wl-copy --primary
粘贴命令xclip -owl-paste
常驻方式-r 保持存活默认 fork 守护进程,持有数据直到被替换
回显管道-f 支持 stdin 原样回显无回显选项

Wayland 的双剪贴板:clipboard 与 primary selection

X11 有三个 selection(primary / secondary / clipboard),Wayland 只有两个:clipboard 和 primary selection,它们不是同一个协议的两种用法,而是两套机制几乎一样、语义完全不同的协议。

协议层面

clipboardprimary selection
协议wl_data_device(Wayland 核心协议自带)wp_primary_selection(独立扩展协议,2024 年才进 stable)
触发方式显式复制:Ctrl+C / 右键复制 / 应用内"复制"按钮选中即复制:鼠标选中文本的瞬间,应用立刻设置 selection,没有"复制"这个动作
常用快捷键复制 Ctrl+C(终端 Ctrl+Shift+C)、剪切 Ctrl+X、粘贴 Ctrl+V(终端 Ctrl+Shift+V);Ctrl+Insert / Shift+Insert 在多数场景也映射到 clipboard没有复制快捷键(选中即复制);粘贴是鼠标中键单击;Shift+Insert 在部分终端(xterm 系、foot)粘贴 primary
粘贴方式Ctrl+V / 右键粘贴 / wl-paste中键单击(终端/编辑器里) / wl-paste --primary
生命周期持续到下一次复制或被显式清空下一次选中立刻被替换;多数合成器在焦点切换后清空
对应工具wl-copy / wl-pastewl-copy --primary / wl-paste --primary

易混点:Shift+Insert 的映射因终端而异——X11 传统(xterm)它粘贴 primary,GNOME Terminal 等则当作 clipboard 粘贴;Wayland 终端(如 foot)保留 X11 语义:Shift+Insert = primary,Ctrl+Shift+V = clipboard。

联系

两套协议的实现结构完全同构:data_source(数据提供方)+ data_offer(数据接受方)+ data_device(会话对象),走同一套 MIME 类型协商机制。wl-copy 加不加 --primary 只是往哪套协议上挂 source。

部分合成器提供"联动"选项(比如选中文本时同步写入 clipboard),但那是合成器的额外实现,不是协议保证的行为——迁移时不要假设 Ctrl+C 和选中是同一个剪贴板。

wl-copy 工作原理:为什么复制工具必须常驻

理解了这个机制,前面表格里"daemon 常驻"和"管道挂死"两个坑就都说得通了。

惰性传输(lazy transfer)

Wayland 剪贴板不是"复制时把数据交给合成器",而是"注册一个数据源,谁要粘贴谁自己来取":

复制方(wl-copy)                    合成器                      粘贴方(wl-paste)
     │ 创建 data_source               │                           │
     │ 声明 MIME(text/plain 等)        │                           │
     │ set_selection(source) ───────►│                           │
     │                                │── 收到 data_offer ───────►│
     │                                │◄───── receive(mime, fd) ──│
     │◄── send(mime, fd) 回调 ────────│                           │
     │ 把数据写进 fd ──────────────────────────────►               │
     │                                │                           │

关键点:数据在"复制"时刻并没有传输。合成器只记下"当前剪贴板内容是哪个进程提供的"。直到有人真的执行粘贴,合成器才会回调数据源进程的 send() 回调,把数据中转到粘贴方。

所以数据源进程(wl-copy 的守护进程)必须活着——它死了,合成器就只能给粘贴方一份空数据。xclip 常驻是同一个道理。

wl-copy 的执行流程

  1. 读入全部 stdin(或参数/文件内容),按内容推断 MIME:UTF-8 纯文本 → text/plain;charset=utf-8,含 NUL 字节/非 UTF-8 → application/octet-stream
  2. 内容先落到临时缓冲(/tmp/wl-copy-buffer-*/ 目录 + memfd),大剪贴板(截图、视频帧)不会撑爆内存——实测守护进程的 fd 列表里能看到这个文件
  3. 连接合成器,创建 wl_data_source,声明 MIME
  4. set_selection() 提交 → fork 守护进程:父进程退出,子进程保持 wayland 连接,等待 send() 回调
  5. 每次有人粘贴,把缓冲里的数据写进合成器给的 fd;被新复制替换或收到 release 后退出

这解释了两个坑

关键差异一:wl-copy 没有 -f 回显

X11 时代的经典管道:

xclip -i -f -selection primary | xclip -i -f -selection clipboard

xclip -f 会把 stdin 原样回显到 stdout,所以可以串联多个 selection。wl-copy 没有这个选项,wl-copy | wl-copy --primary 的第二段会读到空。

解法:先用临时文件接住数据,再分别喂给两个 wl-copy:

f=$(mktemp /tmp/wlyank.XXXXXX)
cat >"$f"          # 接住 stdin
wl-copy <"$f"      # clipboard
wl-copy --primary <"$f"   # primary

关键差异二:daemon 常驻 + 管道挂死

echo foo | wl-copy 在脚本里没问题,但放到管道链尾部会挂住:wl-copy fork 出的守护进程继承了管道读端 fd,shell 等管道关闭时永远等不到。

实测:printf x | wl-copy 在 bash 里能卡 120 秒以上。所以 tmux 绑定里不要写成管道,用文件重定向最稳。

关键差异三:没有全局剪贴板,只有 socket

Wayland 客户端通过 $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY 这个 unix socket 连合成器。坑点:

解法(wlyank.sh 运行时自愈):

# socket 失效时自动回退到最新可用 socket
if [ -n "${WAYLAND_DISPLAY:-}" ] && [ ! -S "/run/user/$(id -u)/$WAYLAND_DISPLAY" ]; then
  export WAYLAND_DISPLAY=$(ls -t /run/user/$(id -u)/wayland-* 2>/dev/null | head -1 | xargs -r basename)
fi

疑难报错一:dmabuf 错误其实是 waypipe 的

症状:tmux 消息行每隔一阵刷一条绿色报错:

$591700: 15.880863 [src/dmabuf.c:125] Failed to open drm fd for /dev/dri/renderD128: No such file or directory

第一反应是 wl-copy 的错——查了 wl-clipboard 2.2.1 源码,根本没有 dmabuf.c。报错格式 $pid: elapsed [src/xxx.c:line] 是 waypipe 的日志风格。

排查过程

  1. 看进程:机器上跑着 waypipe ... --display wayland-n49YaBhs server,wayland-n49YaBhs 正是 tmux 客户端在用的 socket——这个会话是经 waypipe 隧道转发的。
  2. 时间线吻合:waypipe 进程启动 60 秒后出现 60.485827 的报错,与每次复制/粘贴的传输时机对得上。
  3. 根因:waypipe 在隧道间传缓冲区时优先走 GPU dmabuf 直传,但这台虚拟机是 QXL 显卡,/dev/dri 下没有渲染节点 renderD128 → 每次传剪贴板都异步失败一次。
  4. 复制本身是成功的(waypipe/wl-copy 会回退到共享内存拷贝),纯粹是噪音——但天天刷也烦。

解决

waypipe 客户端加 --no-gpu:

waypipe --no-gpu ssh -t <server> 'env SUDO_ASKPASS=/usr/local/libexec/sudo-askpass sudo -A <command>'

--no-gpu 禁用 GPU 相关协议、改走共享内存拷贝。注意是客户端侧参数,而且当前已建立的隧道要重连才生效。

教训:报错文本里的源码路径 + pid 前缀是定位哪个程序在叫的关键证据,别被"看起来像剪贴板错误"带偏。先 grep 一下报错字符串属于哪个项目,再动手改。

tmux 剪贴板绑定最终形态

# copy-mode 按 y:复制到 wayland clipboard
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'wl-copy'

# 鼠标拖拽:clipboard + primary + OSC52 兜底(ssh 远程无 wayland 时降级)
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel wlyank.sh

# prefix + M:切换成纯 OSC52 模式(copy-pipe-no-clear,适合连续选区)

wlyank.sh 完整逻辑:接 stdin → 校验/回退 WAYLAND_DISPLAY → wl-copy + wl-copy --primary(2>/dev/null 压掉连接噪音)→ myyank.sh 发 OSC52 兜底。

依赖关系也要理清:脚本放 ~/.my_local/bin(整目录软链,自动部署),绑定放 ~/.tmux.conf,apt 依赖 wl-clipboard 在根 run.sh 的 install_pre 里——新环境 bash run.sh default 一键落地。

根治方案:QXL → virtio-gpu 3D(2026-09-13 实测落地)

以上都是"兼容性修补"。想让 dmabuf 真正可用,得让虚拟机有渲染节点。2026-09-13 在 his-host 上实际完成,过程与三个坑记录如下。

原理

操作(宿主机上)

virsh shutdown <vm>     # 显卡不能热改,必须关机(reboot 不够)
virsh edit <vm>         # 或 dumpxml 改完 virsh define

video 改成 virtio + 3D 加速:

<video>
  <model type='virtio' heads='1' primary='yes'>
    <acceleration accel3d='yes'/>
  </model>
</video>

spice 侧打开 GL(必须,否则 accel3d 不生效):

<graphics type='spice'>
  <listen type='socket'/>
  <image compression='off'/>
  <gl enable='yes'/>
</graphics>

(宿主必须有 render 节点;rendernode 由 libvirt 自动指向它并写进 QEMU 参数,不用手写。guest 内核需要 virtio_gpu 模块,主线内核默认有。)

启动后宿主机确认 QEMU 真的带上了 GL:

-spice unix=on,addr=/var/lib/libvirt/qemu/domain-4-ubuntu.code/spice.sock,disable-ticketing=on,image-compression=off,gl=on,rendernode=/dev/dri/renderD128,...
{"driver":"virtio-vga-gl","id":"video0",...}

guest 内验证:

ls /dev/dri/          # renderD128 出现
dmesg | grep -i virgl
# [drm] features: +virgl +edid -resource_blob -host_visible

实测踩到的三个坑

  1. GL 与 SPICE TCP 端口互斥。保留 port='5901' listen='127.0.0.1' 时 QEMU 拒绝启动:

    qemu-system-x86_64: SPICE GL support is local-only for now and incompatible with -spice port/tls-port
    

    改成 <listen type='socket'/>(libvirt 自动生成 unix socket 路径)。virt-manager 本机连 socket 不受影响。

  2. GL 与 VNC 互斥。留着 <graphics type='vnc'> 时:

    qemu-system-x86_64: -vnc [::1]:0,...: The console requires a GL context.
    

    必须删掉 VNC graphics(本例它本来就只监听 ::1)。想保留 VNC 就别开 GL,二选一。

  3. virsh screenshot 失效。GL 模式下 QEMU 没有可读的 surface:

    error: 内部错误:无法执行 QEMU 命令 'screendump':no surface
    

    验证画面只能进 virt-manager 看,或在 guest 内截图。

收尾

总结:Wayland 剪贴板适配检查清单

  1. 所有 xclip/xsel 换成 wl-copy/wl-paste,注意 --primary 与默认 clipboard 的区别
  2. 不要写 wl-copy | wl-copy 管道(无回显 + daemon 挂管道),用临时文件
  3. 长驻进程(tmux server)的 WAYLAND_DISPLAY 可能失效,脚本里做 socket 存在性检查 + 回退
  4. 遇到 [src/xxx.c] 格式的报错先确认属于哪个程序(本案例是 waypipe 不是 wl-copy)
  5. waypipe 隧道在无渲染节点机器上必加 --no-gpu
  6. 想根治 dmabuf:虚拟机升 virtio-gpu 3D(virgl),宿主需有 render 节点——2026-09-13 已在 his-host 实测落地(见上,注意 SPICE GL/VNC 互斥)
  7. ssh 远程/无 wayland 场景,OSC52(myyank.sh)兜底不能丢