Skip to content

Repository files navigation

Hyper

一个 macOS 常驻小工具,做三件事:

  1. Caps Lock 变成 Hyper 键(⌘⌃⌥⇧ 四修饰键同时按下);
  2. Hyper + 字母 → 打开 / 切换 / 隐藏对应 app
  3. 剪贴板历史 + 批量复制粘贴Hyper + Space 唤出面板)。

Apple Silicon 原生,无第三方依赖,菜单栏常驻。


安装

仓库里带了构建好的 Hyper.appgit clone 下来的文件不带隔离属性,所以拷过去就能直接打开,不会被 Gatekeeper 拦:

git clone https://github.com/indincys/hyperkey.git
cp -R hyperkey/Hyper.app /Applications/ && open /Applications/Hyper.app

git clone 克隆到终端当前所在目录——刚打开终端的话就是主目录,不是「下载」文件夹。)

如果直接双击了 clone 目录里的 Hyper.app,它会主动提示帮你搬进「应用程序」文件夹。建议接受:辅助功能授权绑定应用路径,先搬完再授权才不会白授权一次;而且从 git 工作区里运行的话,自动更新会往仓库里写文件。

从源码构建

./build.sh

需要 Xcode 命令行工具(xcode-select --install)。产出 ./Hyper.app

正式发布请务必用固定证书签名,否则每次更新用户都要重新授权一次 —— 见下面的「版本更新」。

首次运行

授予「辅助功能」权限。 没有权限时启动会直接把引导页摆出来,照着点即可。授权后无需重启——程序监听权限变更通知,拿到就自动接管。

如果装过 Karabiner-Elements,得先让它放开键盘。 它的 DriverKit 驱动会抢占物理键盘,Caps Lock 在它那里就被处理掉了,根本走不到本工具的重映射。两个办法:在它的 Settings → Devices 里取消勾选键盘(可逆),或者彻底卸载:

# 注意顺序:官方的 uninstall.sh 不会注销 DriverKit 驱动,
# 却会删掉唯一能注销它的工具,导致系统扩展变成清不掉的孤儿。
/Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager deactivate
sudo "/Library/Application Support/org.pqrs/Karabiner-Elements/uninstall.sh"
rm -rf ~/.config/karabiner ~/.local/share/karabiner

跑完重启一次,残留的登录项条目才会消失。./doctor.sh 会告诉你清干净了没有。

让授权不要每次重建都失效

辅助功能权限绑定在 app 的代码签名身份上。build.sh 默认用 ad-hoc 签名(codesign -s -),每次重建都会产生新身份,于是每次都要重新授权一次。

跑一次这个脚本,建一个固定的自签名证书:

./make-signing-cert.sh

之后这样构建,身份就不再变化,授权一次长期有效:

SIGN_ID="Hyper Self-Signed" ./build.sh

脚本会弹一次登录密码框(写入当前用户的证书信任设置,不碰系统域,不需要 sudo)。首次用它签名时钥匙串还会问一次权限,选「始终允许」。


分享给别人

先把最重要的一句说清楚:「下载后双击直接打开、零提示」这个体验做不到。 公证(notarization)必须要 Developer ID 证书,而它只能通过付费的 Apple Developer Program 获得。自签名证书在别人的电脑上不受信任,帮不上这个忙。

所以剩下的都是绕开 Gatekeeper 的办法。隔离属性(quarantine)是下载它的那个程序打上去的——浏览器、邮件、AirDrop 都会打,而有些传输方式不会。这一点决定了哪条路最省事。

方案 A:通过 git 仓库分发(推荐,对方不用装开发工具)

git clone 下来的文件不带隔离属性,Gatekeeper 因此完全不介入。把构建好的 Hyper.app 一起提交进仓库,对方:

git clone <你的仓库地址> && cp -R hyper/Hyper.app /Applications/ && open /Applications/Hyper.app

双击即开,没有任何拦截提示。这是几个朋友之间分享最顺的一条路。

方案 B:发压缩包 + 一条命令去掉隔离

打包发给对方(微信、网盘、邮件都行),对方拖进 /Applications 后跑一次:

xattr -dr com.apple.quarantine /Applications/Hyper.app

一条命令,之后正常打开。缺点是要开终端。

方案 C:不用终端,走系统设置放行

对方双击 → 被拦 → 打开「系统设置 → 隐私与安全性」→ 往下滚,会看到「已阻止使用 Hyper」→ 点「仍要打开」。

注意 macOS 15 之后右键→打开这个老办法已经失效了,只剩系统设置这一条 GUI 路径。

方案 D:让对方自己构建

需要先装 Xcode 命令行工具(xcode-select --install,约 700MB):

git clone <仓库> && cd hyper && ./build.sh && cp -R Hyper.app /Applications/

本地构建出来的东西同样不带隔离属性。适合技术型朋友。

无论哪条路都躲不掉的一步

每台电脑都要各自授予「辅助功能」权限。 这是 macOS 的强制要求,没有任何办法代劳或预置。好在应用启动时如果没权限,会直接把引导页摆在对方面前,照着点就行。


版本更新

更新后会不会又要重新授权?

只要每次发版都用同一个证书签名,就不会。

macOS 把辅助功能授权绑定在 app 的「指定要求」(Designated Requirement)上。用固定证书签名后,这个要求长这样:

identifier "com.indincys.hyper" and certificate leaf = H"aa2aee…"

它认的是证书,不是二进制哈希。所以新版本只要还是这张证书签的,就满足同一个要求,授权原样保留

对比一下 ad-hoc 签名(codesign -s -)的指定要求:

cdhash H"…"

认的是二进制哈希,改一行代码就变,于是每次更新都要重新授权

所以发布正式版必须用 SIGN_ID 构建,不能图省事用默认的 ad-hoc。

⚠️ 一定要备份证书私钥

这张证书的私钥只在你这台机器的钥匙串里。弄丢了就再也签不出「同一个身份」,此后每个用户的每次更新都要重新授权,且无法挽回。

导出备份:

security export -k ~/Library/Keychains/login.keychain-db \
    -t identities -f pkcs12 -o hyper-signing-backup.p12

会让你设一个保护口令。把这个 .p12 和口令存到安全的地方(密码管理器里),不要提交进仓库

发新版本

release.sh,不要手工发:

./release.sh 1.0.3 [发布说明文件]

它会改版本号、用固定证书构建、校验签出来的确实是证书身份(是 ad-hoc 就中止并回滚改动)、提交打 tag、推送、创建 GitHub Release 并上传压缩包。

那个校验不是多余的。发版时如果哪一次忘了用固定证书,退回 ad-hoc 签名,所有用户的辅助功能授权都会失效——而且发出去就收不回来了。

应用内自动更新

已实现。每天自动检查一次,也可以从菜单栏「检查更新…」手动触发。发现新版本会提示,确认后自动下载、替换、重启。

几个设计要点:

  • 下载来的东西必须先过安全关卡。 那是从网络拿到的可执行代码,安装前必须满足当前运行版本的指定要求(同 bundle ID + 同签名证书)。被篡改的下载、被劫持的发布资源、中间人替换,都会在这一步被拒掉并丢弃。
  • 不会触发 Gatekeeper。 隔离属性是下载方主动打的(浏览器,或声明了 LSFileQuarantineEnabled 的程序)。用 URLSession 自己下载、而我们没声明那个键,所以文件不带隔离属性。
  • 替换可回滚。 进程不能替换自己正在运行的 bundle,所以替换交给一个分离出去的脚本,等本进程退出后执行。它先把旧版挪到一边而不是删掉,新版就位后才清理;任何一步失败都回滚到旧版并照常启动——绝不会让用户落到「没有应用」的状态
  • 更新不需要重新授权,前提还是那条:同一张证书。

配置

菜单栏图标 → 设置…(或 ⌘,)。没有辅助功能权限时,应用启动会直接把引导页摆出来——它没权限就什么都做不了,静静待在菜单栏只会让人一头雾水。

  • 快捷键:「添加应用」弹出可搜索的应用列表(自动扫描 /Applications/System/Applications~/Applications),点一下就加进来。按键那一栏直接按你想要的键即可录入,不是从下拉框里挑。按 Esc 取消录入。重复的按键会标橙,找不到的应用会标红。
  • 通用:开机自启、重复按键是否隐藏、单击 Caps Lock 的行为、调试日志。

所有改动即时写入配置文件,没有「未保存」状态。手改文件同样即时生效,两边不会打架。

配置文件在 ~/.config/hyper/config.json,首次运行自动生成。

{
  "enabled": true,
  "tapAction": "none",
  "tapThresholdMs": 200,
  "toggleHideIfFrontmost": true,
  "bindings": {
    "c": "com.google.Chrome",
    "t": "com.mitchellh.ghostty",
    "w": "com.tencent.xinWeChat",
    "e": "/Applications/Microsoft Excel.app"
  }
}
字段 说明
enabled 总开关。菜单栏也能临时暂停。
debug 打开后把每个按键的键码记到 debug 日志,用于排查「按了没反应」。平时请保持关闭。
bindings 键 → app。值可以是 bundle ID,也可以是 .app 路径。
toggleHideIfFrontmost true:目标 app 已在最前时再按一次会隐藏它。false:永远只切到前台。按住 Hyper 不松手连按同一个键即可来回切换
tapAction 单击 Hyper(按下又快速松开、中间没按别的键)触发什么。默认 none
tapThresholdMs 判定为「单击」的时间上限,毫秒。

键名az09, . / ; ' [ ] - = ` \space tab return delete escapef1f12f13f20up down left right。表里没有的键可以写原始键码,例如 "kc:42"

键名对应的是 ANSI(美式 QWERTY)物理键位,因为虚拟键码描述的就是位置。这正好符合肌肉记忆——你按的是那个位置的键。

查 bundle ID

/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "/Applications/某个应用.app/Contents/Info.plist"

tapAction 的写法"none""escape""f18""cmd+space""kc:53" 这类。修饰键可用 cmd / ctrl / opt / shift

只写修饰键、不带任何普通键也是合法的,比如 "ctrl+opt+cmd"——单击会合成一次纯修饰键的按下松开,中间不带键码。给「只收修饰键的录制框」用,见下面。

让单击去触发别的 app(例如输入法的语音输入)

{ "tapAction": "f18", "tapThresholdMs": 200 }

然后到那个 app 里把它的快捷键录成 F18。单击 Caps Lock 会合成一次 F18,长按不会——因为「单击」的判定是在松开的那一刻才做的:中间按过任何别的键、或者按住超过 tapThresholdMs,这一下就不算单击。

怎么录一个键盘上没有的键

这里有个先有鸡还是先有蛋的问题:F18 之所以好用,正是因为没有哪块键盘上有它——于是你也按不出来。在对方的录制框里按 Caps Lock,录进去的是 F19,而 F19 在按住 Hyper 的整个过程里都处于按下状态,于是切 app 也会连带触发对方的功能(这正是「为什么映射到 F19」要解决的问题,录错键就等于把它又请了回来)。

设置 → 通用 → 单击 Caps Lock 选「发送 F18」之后,下面会出现一个 「在别的 app 里录 F18 · 借我 20 秒」。点它,这 20 秒里 Caps Lock 就是一个货真价实的 F18(Hyper 本身暂时不工作),去对方的录制框里按一下 Caps Lock 即可,到时自动变回来。

命令行等价物(应用没跑的时候用):

hidutil property --set '{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":0x700000039,"HIDKeyboardModifierMappingDst":0x70000006D}]}'
# 录完,重开 Hyper 就会自动改回 F19

另外别用普通键或常规组合键(比如 ⌥Z)当这个触发键。合成出来的那一下是真事件,前台 app 也收得到,可能撞上它自己的快捷键。F18 不会跟任何人抢。

对方只收修饰键的时候

{ "tapAction": "ctrl+cmd" }

有些录制框一个键码都不肯存。豆包输入法的免按模式就是——它只收修饰键,F18 在那里根本录不进去,借 20 秒也没用:卡住它的不是「这个键按不出来」,是「这类键我一概不收」。

这种情况把单击设成 "ctrl+cmd":合成的是两个修饰键依次按下、停 70ms、再依次松开,全程不带任何键码。到对方的录制框里把这两个键真按一遍录进去就行,不需要借 20 秒。

为什么是 ⌃⌘,而不是别的组合。 把按住 Caps Lock 的完整状态序列摊开看:

阶段 状态序列
按下 ⌥ → ⌃⌥ → ⌃⌥⇧ → ⌃⌥⇧⌘
松开 ⌃⌥⇧ → ⌃⌥ → ⌥ → —

⌃⌘ 在这条序列里一次都没出现过,所以按住 Hyper 切 app 不可能误触发它——靠的是组合本身不相交,不是「多一个 Shift 所以集合对不上」这种要赌对方做精确匹配的差异。同样安全的还有 ⌥⌘、⇧⌘。

反过来,⌃⌥ 和 ⌃⌥⇧ 绝对不能用:它们就在序列里,你每次切 app 都会顺手触发对方的功能。单独的 ⌥ 也不行——它是最外层,在每次按住的首尾各有一瞬间是独自按下的。

顺带一提,豆包连这个组合都未必给你留:它对 ⌃⌥ 会直接报「暂不支持」,录 ⌃⌥⌘ 会悄悄吞掉 ⌥ 只留下 ⌃⌘。这类录制框各有各的脾气,先在它那儿手动按一遍看能录成什么,再回来配 tapAction

"shift" 单独一个是合法写法,但别用:单独的 Shift 按下松开正是中文输入法用来切中英的信号(见下面「怎么变成「真 Hyper」」里的按下顺序)。

已经按着的修饰键不会被重复发送。比如你手上正按着 ⌘、这时单击了一下 Caps Lock,那就只补一个 ⌃——对方看到的 flags 一样是 ⌃⌘,但不会收到一个「已经按下的键又按了一次」。


工作原理

为什么不能只用 event tap 拦 Caps Lock

macOS 在 IOHIDSystem 内部就把 Caps Lock 的锁定状态和 LED 处理完了,然后事件才到达 CGEventTap。所以 tap 里 return nil 只能吞掉事件,拦不住状态翻转,而且照样吃到系统给 Caps Lock 的那段内置按下延迟。

所以第一步走 IOKit HID 层:把 Caps Lock 的 HID usage(0x700000039)重映射成 F19(0x70000006E),也就是 hidutil property --set 背后那套接口。这一层在 caps 逻辑之前,按键根本走不到锁定判定,直接以一个普通 F19 的身份出现。不需要 root,不需要内核扩展或系统扩展。

这个映射是每次启动、每次设备接入都要重下的,所以程序在启动时、系统唤醒后、以及监听到任意 HID 设备接入时都会重新应用,并回读校验。

为什么映射到 F19,而不是惯例的 F18

因为这个键会被别人看见,而且是在我们之前看见。

事件监听(CGEventTap)不是链条的最前端:输入法之类的东西也会挂自己的监听,还可能排在我们前面。程序在自己的监听里把这个键完全吞掉,只能保证它不再往下游走,拦不住排在前面的人。于是每一次按住 Hyper——哪怕只是准备接着按个字母——在它们眼里都是一次实打实的按键。

这不是推测。实测中微信输入法对 Caps Lock 按下的反应,比这个事件到达我们的监听早 100~150ms(见下面「Caps Lock 的按下事件是迟到的」)。

F18 恰恰是最容易被人绑走的那个键:它是各路改键工具的惯例目标,于是也就成了大家在输入法、启动器里录快捷键时会录到的键。比如把微信输入法的「语音输入」设成 F18,那么每次长按 Hyper 都会开始录音。

所以内部改用 F19,把 F18 空出来:它只在真正的单击时由 tapAction 合成一次,长按永远不发。要把单击接到外部工具上,就用它——见「配置」里的 tapAction

怎么变成「真 Hyper」

收到 F19 按下时,修饰键不会立刻注入。按下的那一刻还分不清这是单击还是长按,而两者要的恰好相反:长按需要 ⌘⌃⌥⇧ 落下,单击需要机器完全不受打扰——它接下来只会变成一次 tapAction,别的什么都不该发生。急着按下再 100ms 后收回不是免费的:下游眼里一次修饰键按下就是一个有意义的真事件(微信输入法的语音面板会把它当成「用户开始打字了」而自己关掉)。

所以决定推迟到能推的最后一刻:另一个键加入这次按住,或者按住时间超过 tapThresholdMs,哪个先到算哪个。没超时的单击自始至终一个修饰键都不发——没有要收回的东西,也就没有被误读成输入的机会。

真要注入时,程序按硬件的顺序逐个合成 flagsChanged 事件:Option → Control → ShiftCommand,松开时反序卸载。

这个顺序不是随便排的,两个键不能被下游单独看见:

  • Shift 不能单独按下再单独松开。中文输入法(微信输入法、搜狗、系统拼音……)正是用「单独敲一下 Shift」来切换中/英的。按住 Hyper 期间如果没有任何按键透传出去——轻点一下,或者按的是被完全吞掉的绑定键——注入序列就恰好是一次干净的 Shift 单击,用户的输入法会被切掉。所以 Shift 排在别人之后按下、在别人之前松开:它的按下和松开事件里都带着别的修饰键,而且两者之间还夹着 Command 的一按一松。
  • Command 不能单独按下。只有 Command 按下的那一瞬间会让某些 app 闪一下菜单栏提示,所以它最后按下、最先松开。

于是最外层落到 Option 头上,它在首尾各有一瞬间是独自按下的——macOS 上没有任何东西会去读一次单独的 Option。

于是有两条路径同时成立:

  • 配置里绑定过的键 —— 事件被完全吞掉,转成启动动作,下游 app 什么都收不到;
  • 其他所有键 —— 原样透传,只是 flags 里合并了 ⌘⌃⌥⇧。这就是为什么 Hyper+K 在别的 app 里也能当普通快捷键注册。

防「修饰键卡死」

这是这类工具最危险的失效模式:如果 F19 的抬起事件丢了,四个修饰键会永久锁住,机器直接没法用。所有出口都汇到同一处清理逻辑:

  • 事件监听被系统停用(tapDisabledByTimeout / tapDisabledByUserInput)→ 清理状态并立刻重新启用;
  • 系统睡眠 / 唤醒、锁屏 / 屏保 → 清理;
  • 收到 SIGTERM / SIGINT / SIGHUP → 走正常退出流程,顺带还原 Caps Lock。

其中「事件监听被系统停用后不重新启用」是这类工具「用着用着突然失灵」的头号原因。

曾经这里还有一条「切换前台 app → 清理」。它是个错误:启动应用本身就会触发前台切换,于是每次成功启动后程序都把 hyper 状态清成「已松开」,用户手还按着却认为松开了——按住 Hyper 连按多个键因此完全失效。防卡死的措施把它要保护的功能给废了。现在前台切换只用来记录「谁在最前」,不碰 hyper 状态。

还有一种卡死不在事件层,在 Caps Lock 自己的锁定状态上:程序退出时会把 Caps Lock 还原成真正的 Caps Lock,下次启动才重新映射。这中间的几秒里它就是一个普通的 Caps Lock——自动更新恰好就是这样安装的(退出、替换、重启),只要有一次按键落在这个窗口里,caps 就锁上了。而等映射装回去,这个锁再也解不开:唯一能切换 caps 的那个键已经是 F19 了。用户拿到的是一块一直在大写、按什么都救不回来的键盘,而且完全没有理由把它跟这个 app 联系起来。

所以每次应用映射之后都顺手读一遍 IOHIDGetModifierLockState,锁着就关掉。

有一种失效是事件监听自己看不见的:Secure Input 在按住期间开启,会把 F19 的抬起事件吞掉。这种情况用一个一次性看门狗兜底——按下时挂上、正常抬起时取消,所以平时根本不会触发。它触发时也不会盲目松开,而是先问 HID 层「F19 到底还按着没有」,还按着就重新挂上。因此故意长按永远不会被打断。

为什么几乎不做定时轮询

早期版本有两个定时器(2 秒查一次权限、10 秒查一次健康状态),已经全部拆掉,换成事件驱动:

要感知的事 触发源
辅助功能权限被授予 / 撤销 com.apple.accessibility.api 分布式通知 + 前台 app 切换(从系统设置切回来时正好命中)
事件监听被停用 系统会把 tapDisabled 事件送进回调本身
HID 映射失效 IOKit 设备接入通知 + 系统唤醒通知
配置文件被改 DispatchSource 文件监听
菜单栏状态过期 菜单打开的那一刻顺手校验
剪贴板变化 没有事件源——只能轮询,见下

最后一行是唯一的例外,而且是 macOS 逼出来的:NSPasteboard 没有任何变化通知。没有 KVO,没有 NSNotification,什么都没有(讽刺的是 iOS 反而有 UIPasteboardChangedNotification)。唯一的公开信号是 changeCount 这个整数,所以这个平台上每一个剪贴板工具都在轮询——Maccy、Flycut、ClipMenu、Jumpcut 全都是 TimerchangeCount,间隔 0.5~1 秒。

但本工具有一个它们没有的东西:event tap 已经看得见每一次按键。于是这里做成混合的:

  • 主路径是事件驱动的。 tap 里看到 ⌘C / ⌘X,立刻查一次 changeCount(30ms 和 250ms 各一次,后者接住那些异步往剪贴板写的 app)。延迟接近 0,比任何轮询间隔都快。
  • 轮询只当兜底。 1.5 秒一次,接住不走键盘的复制:右键菜单、Edit 菜单、app 自己往剪贴板写。锁屏和睡眠期间彻底停掉。

changeCount 是一次返回整数的 Mach 调用,不拷贝任何数据。所以兜底频率比业界普遍的 0.5 秒低三倍,常见路径反而更快。

侵入性管理

UserKeyMapping 是一份全系统共享的列表。程序启动时先读一遍现有内容,只往里加自己那一条(caps lock → F19),其余条目原样带过;退出时也只摘掉自己那条,别人的原样放回。所以哪怕你另外用 hidutil 设过键位映射,装不装这个工具都不受影响。

退出时还原覆盖了正常退出、菜单退出、以及 SIGTERM/SIGINT/SIGHUP。只有 kill -9 兜不住。

Caps Lock 的按下事件是迟到的

macOS 给 Caps Lock 的那段内置按下延迟,在重映射之后依然存在。 重映射躲开的是「大写锁定状态」那套逻辑,没躲开消抖:实测这个按键的 key-down 要 100~150ms 才到达事件监听。把两个进程的日志并排看就很清楚——输入法在 18.366 就有反应,我们在 18.496 才收到那个按键。

这一条不理清楚,Hyper+字母 的行为会莫名其妙:

  • 在这段盲区里按下的字母,比 Caps Lock 的按下事件先到。那一刻我们还以为 Hyper 没按下,于是字母被当成普通按键放行——被输入法打了出来,app 也没切。
  • 如果这个字母在盲区里按下又松开,那么等 Caps Lock 的按下事件到达时,这次按住从头到尾没见过任何别的键,会被判成一次干净的单击——于是 app 切了,tapAction 也发了。

解决办法是不看事件顺序,直接问硬件:每次有普通键按下、而我们以为 Hyper 没按住时,用 CGEventSource.keyState 问一句触发键此刻是不是按着的。按着就立刻开始这次按住。没有时间窗口,没有阈值——键按着就是按着,队列排到哪儿了不重要。

修饰键状态跟踪

合成出来的每一个事件,flags 都是在「用户真正按住了什么」之上叠出来的,所以这个底数必须是对的。程序直接抄事件自带的 flags,不去推。

推的写法很诱人:修饰键只在真实状态变化时才发 flagsChanged,那么它此前在不在按下集合里,似乎就唯一确定了这次是按下还是松开。问题是这个推理没有纠错能力——漏一次就永久反了。而事件确实会漏:监听被系统停用后重新启用、resetState 在键还按着的时候清空集合、Secure Input 吞掉整段序列。一个被卡在「按下」的修饰键会被悄悄并进之后每一个合成事件里,序列发出去时声称某个键按着而其实没有,并且一直错到用户下次碰那个键为止。

这个 bug 真实存在过,而且藏了很久。它在按住路径上被四个修饰键淹没了看不出来,直到单击路径只发两个键才暴露:日志里左 Command 的实际转换是 5 次按下、7 次松开,多出来的两次「松开」在集合里找不到,就被当成「按下」插了进去。后果是每次切完 app 松手后,全局修饰键状态里会残留一个 ⌘ 约 180ms——偶发地把紧接着敲的那个字母变成 ⌘+字母。

事件自带的 flags 是系统对同一个问题的答案,不会漂移,所以每一个事件都是一次重新对齐,错误不可能累积。只有我们自己的修饰键正按着的那段时间读不了(它们也在 flags 里),那期间退回按位翻转,出了这段立刻被下一个真实事件纠正回来。

存的是 flags 而不是键码集合,这一点也是必要的:左右修饰键共用同一个 flag 位,「右 Shift 松开但左 Shift 还按着」这个状态,键码集合给不出诚实的答案。

松开 Hyper 时按这个真实底数还原,用户手里真按着的 Shift 不会被误清掉。


剪贴板

三个快捷键,默认落在 space / q / v(都可以在设置里改):

快捷键 做什么
Hyper + Space 打开面板:搜索、预览、单击即粘贴
Hyper + Q 复制并加入批量队列
Hyper + V 吐出队列里的下一条;队列空时粘最近一条

⌘C⌘V 完全没有被碰过。 不拦截、不接管、不改行为。批量功能全部走 Hyper 键,所以不存在「我现在处于什么模式」这种问题——这是刻意的设计,不是没做。

批量复制粘贴

要往表单里填五个字段,正常做法是复制→切窗口→粘贴→切回来→复制下一个,来回五趟。这里是:

  1. 在源头连按五次 Hyper + Q,把五段内容依次收进队列(每次有 HUD 提示「已加入队列 · 第 N 条」,菜单栏图标也会显示队列深度);
  2. 切到目标处,连按五次 Hyper + V,按收集顺序依次吐出。

另一种用法是合并粘贴:面板里 点选多条,按 就把它们用分隔符(默认换行,可改)连成一段一次性粘出去。

面板操作

动作
移动;⇧↑ ⇧↓ 扩展多选
粘贴(多选时为合并粘贴)
⌘↩ 以纯文本粘贴(丢掉字体和颜色)
⌥↩ 加入批量队列
⌘C 只复制,不粘贴
⌘1⌘9 直接粘贴第 N 条
⌘P 收藏 / 取消收藏
⌘⌫ 删除
切换筛选(全部 / 收藏 / 文本 / 链接 / 图片 / 文件)
清空搜索 → 取消多选 → 关闭

存了什么,存在哪

~/.local/share/hyper/clipboard/,纯本地文件,不上传任何地方:

index.json          每条的元信息,一次性读进内存
data/<uuid>.plist   一条的完整载荷(每种剪贴板格式都留着)
thumbs/<uuid>.png   图片的缩略图

每条都保留全部格式,不只是纯文本。所以同一条内容粘进 Pages 是带格式的、粘进终端是纯文本的——由接收方挑它想要的那种。载荷单独存文件而不是塞进索引,因为一张截图就能比整个历史的其余部分还大;这样历史再长,面板打开也是瞬间的。

几个不太显然但重要的处理:

  • 密码不记。 1Password、钥匙串这类工具复制时会打 org.nspasteboard.ConcealedType 标记,看到就跳过。默认开着——这是「剪贴板历史可以放心一直开着」的前提。
  • 截图先转 PNG 再算大小。 剪贴板里的截图同时带 TIFF 和 PNG 两份,是同一张图,而 TIFF 常常大十倍。既然所有 app 都收 PNG,TIFF 就直接丢掉——实测一张 1800×1000 的截图,丢掉 TIFF 后从 1.9 MB 降到 170 KB。这是「截图能不能塞进单条上限」的分水岭。
  • 复制文件存的是引用,不是文件本身。 在访达复制一个 200 MB 的视频,剪贴板里只有一条 file URL。源文件被删或移走,这条就失效了——这是剪贴板本身的性质,所有同类工具都一样。
  • 超过单条上限(默认 20 MB)只留记录,不留正文。 面板里会明确标出来并说明粘不回去,而不是假装能粘然后什么都不发生。
  • 重复复制不会产生第二行,而是把原来那条顶到最前面,收藏状态和来源都保留。

清理策略

滚动淘汰,不是定时清零:30 天1000 条两个条件谁先到按谁执行。收藏的内容两个条件都豁免,永远不会被自动清理。

定时全清会把你昨天特意收藏的东西一起带走,所以没这么做。


已知限制

  • Secure Input 场景下不工作:系统密码框、以及某些终端开启安全输入时,event tap 收不到键盘事件,快捷键不响应。Karabiner 的虚拟 HID 驱动在这一层之下所以不受影响 —— 这是本方案相比 Karabiner 唯一实质性的能力损失。

  • 上不了 App Store:辅助功能权限与沙盒不兼容。

  • 进程被 SIGKILLkill -9)时无法清理:Caps Lock 会停留在 F19 状态。手动恢复:

    hidutil property --set '{"UserKeyMapping":[]}'

排查

先跑体检脚本,它会一次性检查进程、HID 映射、抢占键盘的其他软件、配置合法性,并附上最近日志:

./doctor.sh

最常见的一种失败:Karabiner 没有退干净。退出 Karabiner-Elements 的界面程序不会停掉它的后台服务和 DriverKit 驱动,而驱动会抢占物理键盘——caps lock 在它那里就被规则转成了 ⌘⌃⌥⇧,根本走不到本工具的 F19 重映射。./doctor.sh 会直接报出来。

彻底停掉 Karabiner 有两条路:

# 可逆:打开 Karabiner-Elements → Settings → Devices → 取消勾选键盘
# 它就不再抢占该设备,随时可以勾回来

# 彻底:官方卸载脚本(需要输入密码)
sudo "/Library/Application Support/org.pqrs/Karabiner-Elements/uninstall.sh"

看日志:

log show --last 5m --info --debug --predicate 'subsystem == "com.indincys.hyper"' --style compact

实时跟:

log stream --level debug --predicate 'subsystem == "com.indincys.hyper"'

查当前 HID 映射(应该能看到 3006477112930064771182,即 caps lock → F19):

hidutil property --get "UserKeyMapping"

源码结构

文件 职责
Sources/Hyper/HIDRemapper.swift Caps Lock → F19 的 HID 层重映射、设备接入监听、回读校验
Sources/Hyper/HyperTap.swift 事件监听、Hyper 修饰键合成、按键拦截、状态清理
Sources/Hyper/AppLauncher.swift 启动 / 切换 / 隐藏,含路径解析缓存
Sources/Hyper/Config.swift 配置读写、校验、文件监听热重载
Sources/Hyper/AppDelegate.swift 菜单栏、权限、事件驱动的状态维护、信号处理
Sources/Hyper/SettingsModel.swift 设置界面的数据层,每次改动直接落盘
Sources/Hyper/SettingsView.swift 设置界面与权限引导页(SwiftUI)
Sources/Hyper/SettingsWindowController.swift 设置窗口的宿主
Sources/Hyper/KeyRecorderField.swift 「按下你想要的键」录入控件(AppKit 绘制)
Sources/Hyper/AppCatalog.swift 扫描已安装应用,供选择器使用
Sources/Hyper/Updater.swift 应用内更新:检查、下载、签名校验、可回滚替换
Sources/Hyper/InstallLocation.swift 检测并引导搬进「应用程序」文件夹
Sources/Hyper/Keys.swift 键码表与修饰键映射表
Sources/Hyper/Clipboard/ClipItem.swift 剪贴板条目模型、多格式载荷编解码、内容分类与预览
Sources/Hyper/Clipboard/ClipStore.swift 历史存储:索引 + 独立载荷文件、去重、滚动淘汰
Sources/Hyper/Clipboard/ClipboardMonitor.swift 剪贴板变化监听(⌘C 事件驱动 + 慢轮询兜底)
Sources/Hyper/Clipboard/ClipboardManager.swift 剪贴板功能的总装:捕获、动作分发、粘贴
Sources/Hyper/Clipboard/Paster.swift 写回剪贴板、还原焦点、合成 ⌘V / ⌘C
Sources/Hyper/Clipboard/PasteQueue.swift 批量粘贴队列
Sources/Hyper/Clipboard/ClipboardPanel.swift 面板窗口、键盘操作、动作路由
Sources/Hyper/Clipboard/ClipboardPanelView.swift 面板界面(SwiftUI)
Sources/Hyper/Clipboard/ClipboardHUD.swift 入队 / 队列状态的短暂提示

About

把 Caps Lock 变成 Hyper 键,并用它一键唤起应用。macOS / Apple Silicon 原生,轻量常驻,零第三方依赖。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages