Omarchy 统一剪贴板与剪贴板历史:Super 快捷键体系、双向历史记录与底层实现全解析
导读
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 迁移过来的用户还需要额外习惯 Ctrl 与 Super(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 粘贴到任意应用:
从浮层的 UI 实现(shell/plugins/clipboard/Clipboard.qml)看,这是一块铺满全屏、带暗色 scrim 遮罩的 layer-shell overlay(WlrLayershell.namespace: "omarchy-clipboard"、WlrLayer.Overlay、独占键盘焦点),中央是左右双栏卡片:左侧为条目列表(每行可显示文本预览或缩略图),右侧为选中条目的完整内容或图片预览,支持鼠标悬停选择与点击激活。
从源码层看,回车选中文本条目时(Clipboard.qml 的 applySelected),会以 execDetached 方式调用辅助脚本 bin/omarchy-clipboard-paste-text 并携带 --shift-insert 与 --history-index:
- 脚本先从
$HOME/.local/state/omarchy/clipboard-history.json中按索引取出该条目的完整文本(bin/omarchy-clipboard-paste-text); - 用
wl-copy将其写回系统剪贴板——这正是文档所说"放置到剪贴板、可随时用Super + V粘贴"的底层保障; - 随后通过
wtype -M shift -k Insert -m shift注入一次Shift + Insert键击,尝试把内容直接送入当前焦点窗口(对终端而言Shift + Insert就是粘贴)。
图片条目走另一条路径:调用 bin/omarchy-clipboard-paste-file <mime-type> <path>(--copy-only 变体只写回剪贴板、不注入键击),数据文件路径同样由历史索引解析而来。
打字即搜索
历史列表不仅支持浏览,还支持增量即时搜索——呼出浮层后直接开始打字即可过滤,无需先按任何搜索快捷键:
搜索命中范围值得说明:文本条目按其文本内容匹配;图片条目则按其元数据(类型 image screenshot、MIME、捕获时间戳)匹配,因此你可以通过输入"截图"或某时间词找回一张图片。这一逻辑集中在 ClipboardHistory.js 的 searchableText:
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.qml 中 Keys.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 字符的显示上限(displayTextLimit,ClipboardHistory.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):
- 当前快照(
currentProc):shell 启动时立即执行一次capture.sh,把启动瞬间已存在的剪贴板内容收录进历史; - 文本监听(
textWatchProc):wl-paste --type text --watch capture.sh text,监听文本变化; - 图片监听(
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_text(capture.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-passwordManagerHint 与 CLIPBOARD_STATE=sensitive 下不记录等等。
图片条目的落盘与去重
图片数据不会塞进 JSON 历史文件本身,而是落到独立的图片目录。capture.sh 的 emit_image(capture.sh)流程如下:
- 图片目录为
$XDG_STATE_HOME/omarchy/clipboard-images/(默认即~/.local/state/omarchy/clipboard-images/); - 支持的 MIME 优先顺序为
image/png、jpeg、webp、gif、bmp、tiff(jpeg统一转存为.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: 500,addEntry在追加后只保留前 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,覆盖了三层:
- 纯逻辑层(Node 断言):把 ClipboardHistory.js 作为 CommonJS 模块直接加载,逐一断言
normalizeEntry、parseHistory、addEntry去重置顶、removeEntryAt越界保护、clearHistory、displayRows的搜索/预览/索引保持/8192 字符截断上限、文件 URI 条目识别与 N 文件汇总等行为; - 抓取脚本层(bash + 桩工具):用临时目录里伪造的
wl-paste/wl-copy/wtype桩替换真实命令,验证capture.sh对普通文本、各类 UTF-16 样本、PNG/JPEG 图片落盘、敏感标记跳过、以及wl-paste --watch监听进程的启动/回收/pdeathsig随主进程消亡等场景; - 辅助脚本层:验证
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 常驻插件维护的文本/图片双类型历史,回车即取用、打字即过滤,配合方向键、Delete、Shift + Delete完成完整的管理闭环(shell/plugins/clipboard/Clipboard.qml); - 基础设施层:事件驱动的
wl-paste --watch抓取、UTF-16 编码启发式解码、密码管理器提示与敏感标记豁免、图片哈希去重落盘、500 条容量与去重置顶,以及 50 行 / 8192 字符的渲染上限设计,共同保证了它既有终端用户的效率,也有 Wayland 桌面应有的稳定与克制(shell/plugins/clipboard/capture.sh、ClipboardHistory.js)。
如果你希望进一步改造或理解这套机制,可以从以下文件继续深入:剪贴板快捷键的注入与终端识别见 default/hypr/bindings/clipboard.lua;插件声明、历史抓取、纯逻辑与 UI 分别在 manifest.json、capture.sh、ClipboardHistory.js 与 Clipboard.qml;三个底层辅助命令(文本/文件粘贴与打开历史条目)位于 bin/omarchy-clipboard-paste-text、bin/omarchy-clipboard-paste-file 与 bin/omarchy-clipboard-open。相关终端与桌面导航语境可配合阅读 manual/04-navigation.md 与 manual/07-hotkeys.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

