首页
/ Omarchy 统一剪贴板与剪贴板历史:Super 快捷键体系、双向历史记录与底层实现全解析

Omarchy 统一剪贴板与剪贴板历史:Super 快捷键体系、双向历史记录与底层实现全解析

2026-09-08 17:13:55作者:田桥桑Industrious

导读

Omarchy 是一套「美观、现代且有主见」的 Linux 发行版桌面方案,其 shell 内建了全系统统一的剪贴板快捷键与剪贴板历史管理器。本文以官方手册 manual/08-unified-clipboard-history.md 为核心骨架,深入讲解 Super + C/V/X 如何把普通应用与终端的两套复制粘贴习惯统一起来,并剖析由 Omarchy shell 提供的、同时支持文本与图片的剪贴板历史:从调用方式、搜索与键盘导航,到数据抓取管线、去重与持久化、敏感数据保护等底层实现,全部基于本仓库真实源码与测试用例展开。

Linux 剪贴板的割裂现状

在传统 Linux 桌面上,复制粘贴存在两套互相割裂的快捷键:

  • 终端里通常需要用 Ctrl + Shift + C 复制、Ctrl + Shift + V 粘贴(因为 Ctrl + C 被保留为发送中断信号);
  • 其他所有应用里则是 Ctrl + C / Ctrl + V

对于任何不是从 Linux 时代成长起来的用户,这种差异都很难适应。从 Mac 迁移过来的用户还需要额外习惯 CtrlSuper(Windows 键/Command)的角色互换。

Omarchy 用一组"几乎处处生效"的统一剪贴板快捷键同时解决了这两个问题:

快捷键 功能
Super + C 复制
Super + X 剪切
Super + V 粘贴
Super + Ctrl + V 剪贴板历史

注:大多数 Agent 框架(agent harnesses)会使用 Ctrl + V 粘贴图片、使用 Super + V 粘贴文本。

统一快捷键背后的终端感知分发机制

"统一"并不等于"把所有按键原样发给所有应用"。Omarchy 需要根据当前焦点窗口是否为终端来"翻译"按键,再注入到对应的快捷键。这套逻辑完整实现在 default/hypr/bindings/clipboard.lua 中:

o.bind("SUPER + C", "Universal copy", universal_clipboard_shortcut("CTRL", "C", "CTRL", "Insert"))
o.bind("SUPER + V", "Universal paste", universal_clipboard_shortcut("CTRL", "V", "SHIFT", "Insert"))
o.bind("SUPER + X", "Universal cut", send_shortcut_once("CTRL", "X"))
o.bind("SUPER + CTRL + V", "Clipboard manager", "omarchy-shell shell toggle omarchy.clipboard")

实际翻译映射如下:

Super 快捷键 非终端窗口 终端窗口
Super + C 复制 Ctrl + C Ctrl + Insert
Super + V 粘贴 Ctrl + V Shift + Insert
Super + X 剪切 Ctrl + X Ctrl + X

其中关键的 universal_clipboard_shortcut 是一个"终端感知"的分发函数(default/hypr/bindings/clipboard.lua):

  • 通过 active_window_is_terminal() 检查当前活动窗口是否带有 terminal 标签(终端标签的定义统一收敛在 default/hypr/apps/terminals.lua,动态标签会携带尾随的 *,匹配时通过 gsub("%*$", "") 去掉再比较);
  • 是终端就注入 Ctrl + Insert / Shift + Insert 这类传统终端复制粘贴键位,否则注入标准的 Ctrl + C / Ctrl + V

为什么不直接用 wtype 注入

绑定文件的注释揭示了一个容易被忽略的坑:合成键击不能使用 wtype(虚拟键盘),因为物理上仍按住的 Super 会与注入的键位在 seat 上"合并",从而产生错误的组合键。因此这里改用 Hyprland 的 hl.dispatch(hl.dsp.send_key_state(...)) 直接向焦点 surface 发送显式修饰键 + 键位的 down/up 状态。省略窗口目标(window target)意味着这些快捷键既能作用于普通窗口,也能作用于获得焦点的 layer-shell surface(例如 Omarchy 自家的面板)。

此外,为避免 Hyprland send_shortcut 偶发地让合成键"卡住/重复",send_shortcut_once 把一次快捷键拆成 down 与 up 两次发送,两者间隔 50ms(default/hypr/bindings/clipboard.lua):

local function send_shortcut_once(mods, key)
  return function()
    hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
    hl.timer(function()
      hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
    end, { timeout = 50, type = "oneshot" })
  end
end

最后一个绑定 Super + Ctrl + V 直接调用 omarchy-shell shell toggle omarchy.clipboard 来开关剪贴板历史浮层,即进入下一节要展开的剪贴板历史。

剪贴板历史:文本与图片的统一档案库

剪贴板历史由 Omarchy shell 提供,同时支持文本图片两类内容,不是只能记文本的简单管理器。

基本用法:弹出、选择、粘贴

Super + Ctrl + V 弹出剪贴板历史浮层,通过方向键在条目间移动,按 回车(Return) 选中条目——随后该条目会被写回系统剪贴板,随时可以用 Super + V 粘贴到任意应用:

Omarchy 剪贴板历史选择界面截图

从浮层的 UI 实现(shell/plugins/clipboard/Clipboard.qml)看,这是一块铺满全屏、带暗色 scrim 遮罩的 layer-shell overlay(WlrLayershell.namespace: "omarchy-clipboard"WlrLayer.Overlay、独占键盘焦点),中央是左右双栏卡片:左侧为条目列表(每行可显示文本预览或缩略图),右侧为选中条目的完整内容或图片预览,支持鼠标悬停选择与点击激活。

从源码层看,回车选中文本条目时(Clipboard.qmlapplySelected),会以 execDetached 方式调用辅助脚本 bin/omarchy-clipboard-paste-text 并携带 --shift-insert--history-index

  1. 脚本先从 $HOME/.local/state/omarchy/clipboard-history.json 中按索引取出该条目的完整文本(bin/omarchy-clipboard-paste-text);
  2. wl-copy 将其写回系统剪贴板——这正是文档所说"放置到剪贴板、可随时用 Super + V 粘贴"的底层保障;
  3. 随后通过 wtype -M shift -k Insert -m shift 注入一次 Shift + Insert 键击,尝试把内容直接送入当前焦点窗口(对终端而言 Shift + Insert 就是粘贴)。

图片条目走另一条路径:调用 bin/omarchy-clipboard-paste-file <mime-type> <path>--copy-only 变体只写回剪贴板、不注入键击),数据文件路径同样由历史索引解析而来。

打字即搜索

历史列表不仅支持浏览,还支持增量即时搜索——呼出浮层后直接开始打字即可过滤,无需先按任何搜索快捷键:

Omarchy 剪贴板历史即时搜索界面截图

搜索命中范围值得说明:文本条目按其文本内容匹配;图片条目则按其元数据(类型 image screenshot、MIME、捕获时间戳)匹配,因此你可以通过输入"截图"或某时间词找回一张图片。这一逻辑集中在 ClipboardHistory.jssearchableText

function searchableText(entry) {
  if (!entry) return ""
  if (entry.type === "image") return "image screenshot " + String(entry.mime || "") + " " + String(entry.capturedAt || "")
  return String(entry.text || "") + " " + fileEntryText(entry)
}

浮层内的完整键盘操作

阅读 Clipboard.qmlKeys.onPressed 的分发逻辑,可以整理出浮层的完整键位表:

按键 行为
Escape 有搜索词时先清空搜索词,再次按下则关闭浮层
任意可打印字符 追加到搜索词并实时过滤(也支持退格等编辑操作)
/ 上/下移动选中行
PageUp / PageDown 一次移动 6 行
Home / End 跳到第一条 / 最后一条
Return 将选中条目放回剪贴板(并尝试立即粘贴)
Shift + Return 仅复制选中条目(--copy-only),不粘贴
Alt + Return 打开(open)选中条目
Delete 从历史中删除当前选中条目
Shift + Delete 请求清空全部历史(弹出确认对话框)

其中打开(open)行为由 bin/omarchy-clipboard-open 实现并交由 Alt + Return 触发(Clipboard.qml 中的 openIndex):

  • URL 或纯域名样式的文本:通过 omarchy-launch-browser 在浏览器打开;
  • 普通文本:写入 $XDG_STATE_HOME/omarchy/clipboard-open/ 下的临时文件,再用默认编辑器打开(bin/omarchy-clipboard-open);
  • 图片:交给 tensaku-edit 打开。

剪贴板历史是持续累积的档案:它既可能包含几 KB 的代码块,也可能包含大段粘贴内容,还可能是你在文件管理器中复制的图片或文件。为了在搜索/渲染数据完整性之间取得平衡,ClipboardHistory.js 对每个条目在列表中渲染的文本做了 8192 字符的显示上限(displayTextLimitClipboardHistory.js),并在换行边界截断以免截断 file:// URI 造成假路径;但粘贴时按索引从历史文件中读取完整内容,因此没有任何数据丢失。这一设计在源码注释中写得很清楚:一次超大粘贴若不加限制,会在每次按键时造成数百 MB 的字符串处理并拖垮整个 shell。

底层实现:从剪贴板事件到历史条目

剪贴板历史并不是"定时轮询剪贴板",而是一套事件驱动的抓取管线。它由 shell 侧一个独立的 clipboard 插件承载,插件声明位于 shell/plugins/clipboard/manifest.json

{
  "schemaVersion": 1,
  "id": "omarchy.clipboard",
  "name": "Clipboard",
  "version": "1.0.0",
  "author": "Omarchy",
  "description": "A clipboard manager to view and paste history",
  "kinds": ["overlay"],
  "keepLoaded": true,
  "entryPoints": { "overlay": "Clipboard.qml" }
}

keepLoaded: true 意味着该插件随 shell 常驻,从而能持续监听剪贴板事件;入口是 overlay 类型的 Clipboard.qml

事件监听与三路抓取

剪贴板事件由 wl-paste --watch 监听,每次剪贴板内容变化时调用抓取脚本 shell/plugins/clipboard/capture.sh。Clipboard.qml 中并行了三路进程(Clipboard.qml):

  1. 当前快照currentProc):shell 启动时立即执行一次 capture.sh,把启动瞬间已存在的剪贴板内容收录进历史;
  2. 文本监听textWatchProc):wl-paste --type text --watch capture.sh text,监听文本变化;
  3. 图片监听imageWatchProc):wl-paste --type image/png --watch capture.sh image/png,监听 PNG 图片变化(在 Wayland 下选择要抓取的代表性 MIME)。

两个 --watch 进程都用 setpriv --pdeathsig TERM 启动,使监听进程在 shell 退出时由内核自动回收;Clipboard.qml 的 initProc 在每次 shell 加载时先用 pkill -f 清掉上一个 shell 实例遗留的监听进程,再启动自己的监听。而 watchRestartTimer(间隔 1 秒)会在监听进程异常退出时将其重新拉起——因为一旦监听死亡,剪贴板历史就会在无人察觉的情况下停止记录(此时复制仍正常、浮层仍可打开、旧条目仍在,只是新内容不再被记录),这是源码注释里明确指出的动机,测试也专门覆盖了这条链路。

capture.sh:编码探测与敏感数据保护

每次剪贴板变化,capture.sh 都会输出一条 JSON 记录到 stdout,由浮层解析后并入历史。抓取脚本本身要处理两类难题:

(1)敏感的剪贴板内容直接跳过。 脚本开头就检查两件事(capture.sh):

  • 环境变量 CLIPBOARD_STATE=sensitive 被设置;
  • 当前剪贴板的类型列表里含有 x-kde-passwordManagerHint(密码管理器写入剪贴板时常用该 MIME 提示)。

只要命中其中一条,脚本立即 exit 0 且不产生任何历史记录,从源头避免密码、一次性验证码等敏感信息落入剪贴板历史文件。

(2)文本编码探测。 剪贴板里的文本不总是 UTF-8,尤其从 Windows 应用或旧软件复制的内容可能是 UTF-16。emit_textcapture.sh)用 Perl 实现了一套编码决策:

  • 带 BOM 的 FF FE / FE FF 直接判定为 UTF-16LE / UTF-16BE;
  • 无 BOM 时通过"NUL 字节分布"启发式判断:统计每个字节通道的 NUL 数量,若三个四分之三以上的码元填充一致且对侧字节 NUL 少于四分之一,才判定为 UTF-16LE/BE,否则回退到 UTF-8;
  • 对启发式判定出的 UTF-16 结果还要做控制字符扫描校验,避免把 NUL 分隔的 UTF-8 数据误判成 UTF-16;
  • 解码失败时一律回退 UTF-8 原样保留。

test/shell.d/clipboard-test.sh 用大量用例验证了这套边界行为,包括:BOM 标记的大小端 UTF-16、无 BOM 的纯 ASCII UTF-16LE、带中文/emoji 的 UTF-16、恰好卡在阈值边界的样本、NUL 填充的 UTF-8 不应被误判、畸形 UTF-16 应回退、x-kde-passwordManagerHintCLIPBOARD_STATE=sensitive 下不记录等等。

图片条目的落盘与去重

图片数据不会塞进 JSON 历史文件本身,而是落到独立的图片目录。capture.shemit_imagecapture.sh)流程如下:

  • 图片目录为 $XDG_STATE_HOME/omarchy/clipboard-images/(默认即 ~/.local/state/omarchy/clipboard-images/);
  • 支持的 MIME 优先顺序为 image/pngjpegwebpgifbmptiffjpeg 统一转存为 .jpg 扩展名);
  • sha256sum 对图片内容取哈希作为文件名:完全相同的图片只存一份,避免重复截图造成磁盘浪费;
  • 输出形如 {"type":"image","mime":"image/png","path":"<hash>.png","capturedAt":"Friday 14:42"} 的 JSON,其中 capturedAt 是抓取时刻的"星期 + 时间"。

浮层在展示图片条目时会使用这些元数据:时间戳为 image/png 的条目预览文案是"Screenshot from …",其它 MIME 则显示"Image from …"(见 ClipboardHistory.js 的 imagePreviewText 以及对应测试断言)。

历史去重、容量与排序策略

ClipboardHistory.js 是整个历史数据层的核心纯逻辑模块(可被 Node 直接单测),关键策略包括:

  • 去重置顶(dedupe & move-to-front)addEntry 在把新条目加入历史的同时,如果旧历史里已存在相同内容的条目(文本按内容比较、图片按路径比较,见 entryKey),会把重复项移除,再把新条目放到最前。因此"重新复制同一段内容"不会在历史里产生两条,而只是把它提升到顶部(ClipboardHistory.js)。
  • 容量上限 500:QML 侧 historyLimit: 500addEntry 在追加后只保留前 500 条,超出部分自动淘汰;列表同一屏最多渲染 50 行(displayRows 的默认 limit)。
  • 数据清洗parseHistory/normalizeEntry 会丢弃空串、纯空白、格式非法、缺 path 的图片等无效条目;文本条目只保留去空白后仍有内容的片段。
  • 文件条目:如果复制的文本是 file:// URI(典型如从文件管理器复制路径),filePaths/fileEntryText 会识别出单文件(显示文件名)或多文件(显示"N files"),单张图片路径还会在列表中直接生成缩略图;测试覆盖了 .gif 内联预览、.mp4 不内联预览、5000 个文件的长列表不被截断等场景。

历史文件与主题联动

历史数据持久化在两个位置,均以明文 JSON / 文件形式存放于用户状态目录(XDG_STATE_HOME,默认 ~/.local/state/omarchy/):

  • clipboard-history.json:全部条目的 JSON 数组(缩进美化后的文本形式),Clipboard.qml 用 FileView 监听该文件,watchChanges: true 并采用原子写入,外部改动会触发 reload() 同步;
  • clipboard-images/:图片内容的哈希文件,以及打开文本条目时 omarchy-clipboard-open 写入的 clipboard-open/ 临时文件目录。

视觉层面,剪贴板浮层复用了菜单(menu) 的表面设计令牌——背景、前景、边框、scrim、选中行配色、圆角、字体与间距均取自 Color.menu.*Style 主题变量(Clipboard.qml 头部注释明确写道"themes that style the menu also style the clipboard")。因此任何为 Omarchy 菜单定制的主题都会自动覆盖剪贴板历史浮层,无需单独适配。

隐私清理:单条删除与全量清空

剪贴板历史默认把最近 500 条内容以明文留在 ~/.local/state/omarchy/clipboard-history.json,这既是便利也是隐私风险。Omarchy 提供了两条清理路径:

  • 单条删除:在浮层中选中某一条后按 Delete,调用 removeEntryAt 把该索引从历史数组移除并立刻回写文件;
  • 全量清空:按 Shift + Delete 会先弹出 ConfirmDialog(文案为 "Delete entire clipboard history?",确认按钮为 "Delete"),确认后才调用 clearHistory() 清空并持久化——这是针对误操作的双重确认设计。
# 默认数据位置(可随时删除以清理历史)
~/.local/state/omarchy/clipboard-history.json   # 历史条目 JSON
~/.local/state/omarchy/clipboard-images/        # 图片哈希文件与临时打开文件

需要提醒的是,敏感内容的"自动豁免"只发生在抓取阶段(见上文 CLIPBOARD_STATE=sensitive 与密码管理器提示检查);对已经进入历史的内容,及时使用单条删除或全量清空是更稳妥的隐私做法。

质量保障:测试如何验证整套剪贴板机制

剪贴板功能在仓库中有专门的测试文件 test/shell.d/clipboard-test.sh,覆盖了三层:

  1. 纯逻辑层(Node 断言):把 ClipboardHistory.js 作为 CommonJS 模块直接加载,逐一断言 normalizeEntryparseHistoryaddEntry 去重置顶、removeEntryAt 越界保护、clearHistorydisplayRows 的搜索/预览/索引保持/8192 字符截断上限、文件 URI 条目识别与 N 文件汇总等行为;
  2. 抓取脚本层(bash + 桩工具):用临时目录里伪造的 wl-paste/wl-copy/wtype 桩替换真实命令,验证 capture.sh 对普通文本、各类 UTF-16 样本、PNG/JPEG 图片落盘、敏感标记跳过、以及 wl-paste --watch 监听进程的启动/回收/pdeathsig 随主进程消亡等场景;
  3. 辅助脚本层:验证 omarchy-clipboard-paste-text--shift-insert 下既写回剪贴板又注入 wtype -M shift -k Insert -m shift--copy-only 下只复制不注入键击、omarchy-clipboard-paste-file 的图片复制行为,以及 omarchy-clipboard-open 对 URL/普通文本/图片分别路由到浏览器/编辑器/tensaku-edit

这些测试不仅验证了功能正确性,也把"监听进程必须随 shell 同生共死""图片按内容哈希去重""敏感剪贴板必须静默跳过"等设计约束固化成了可回归的验收标准。

小结:一套按键,一个档案库

从用户视角看,Omarchy 的剪贴板方案可以浓缩为三个层次:

  • 统一快捷键层Super + C/V/X 通过终端感知分发,在普通应用与终端之间自动翻译键位,让"一个肌肉记忆走天下"成为可能(default/hypr/bindings/clipboard.lua);
  • 历史档案层Super + Ctrl + V 呼出由 shell 常驻插件维护的文本/图片双类型历史,回车即取用、打字即过滤,配合方向键、DeleteShift + Delete 完成完整的管理闭环(shell/plugins/clipboard/Clipboard.qml);
  • 基础设施层:事件驱动的 wl-paste --watch 抓取、UTF-16 编码启发式解码、密码管理器提示与敏感标记豁免、图片哈希去重落盘、500 条容量与去重置顶,以及 50 行 / 8192 字符的渲染上限设计,共同保证了它既有终端用户的效率,也有 Wayland 桌面应有的稳定与克制(shell/plugins/clipboard/capture.shClipboardHistory.js)。

如果你希望进一步改造或理解这套机制,可以从以下文件继续深入:剪贴板快捷键的注入与终端识别见 default/hypr/bindings/clipboard.lua;插件声明、历史抓取、纯逻辑与 UI 分别在 manifest.jsoncapture.shClipboardHistory.jsClipboard.qml;三个底层辅助命令(文本/文件粘贴与打开历史条目)位于 bin/omarchy-clipboard-paste-textbin/omarchy-clipboard-paste-filebin/omarchy-clipboard-open。相关终端与桌面导航语境可配合阅读 manual/04-navigation.mdmanual/07-hotkeys.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391