手上的 MacBook Pro 是 2016 年年底买的,至今一年都没到。前一段时间就出现了 b 键有时候按一下出来两个字符的问题。最近几天 n 键也出现了相同的问题。这个事件还是有概率会发生,打字的时候让人非常不爽。
我在网上查了一下,出现类似问题的不止我一个人。看起来是新出的蝴蝶键盘品控的问题。有人说拿去送修了,修完了还是有这个问题。所以我就一直忍着,忍着忍着忍着忍不了了😂哪有打字,打到一半,发现多了个 n,再回来删除的道理!
所以,今天要解决这个问题!
思索一下软件方案。首先分析双击的事件的原因,猜测是按下 n 键后,触发 KeyDown
,释放 n 键后,触发 KeyUp
;而后手抬起的过程中,意外地再一次触发了 KeyDown
和 KeyUp
。这中间的时间很短。
如果要解决这个问题,需要实现的功能就是:分析当前的 KeyDown
事件,与上一次该键的 KeyUp
时间间隔。如果异常的短,说明是键盘导致的双击事件,则忽略该 KeyDown
信号。
有了思路之后,再考虑如何实现该功能。macOS 上有一大批键盘相关的软件,包括收费的 Keyboard Maestro,开源的 Karabiner-Elements,还有针对自定义快捷键的 Hammerspoon。笔者都尝试了一遍之后,发现并不容易在这些软件上实现需求。
只能靠自己写了。幸运的是,我是个软件工程师 :D
首先,搜索相关的方案。需求为拦截全局的键盘信号,搜索词为 "macOS global keyboard event"。
看了一些搜索结果之后,发现了一段不错的代码:Global keyboard hook for OSX,作者为 quietcricket,四年前上传的。代码实现的功能为调换 a 键和 z 键。按照代码中提示的编译命令编译,成功。
执行,提示 Accessibility 问题,一般涉及到全局键盘的软件都会需要 Accessibility 授权。在设置中找到 Security & Privacy
→ Accessibility
。笔者是在 iTerm2 中执行编译后的 binary 的,所以打开 iTerm2 的授权。再次执行,OK。测试,全局的 a 键和 z 键确实被调换了,包括快捷键。
阅读代码。代码中:
// Swap 'a' (keycode=0) and 'z' (keycode=6).
if (keycode == (CGKeyCode)0)
keycode = (CGKeyCode)6;
else if (keycode == (CGKeyCode)6)
keycode = (CGKeyCode)0;
实现了调换的功能。程序使用了 C 编写,可以直接将代码后缀名改成 C++,使用 G++ 编译。加上打印 keycode 的功能,就可以看到所有按键的 keycode 值了。经过测试,n 键和 b 键的 keycode 值为 45 和 11。
使用 chrono 获取按键 KeyUp
时的时间点,并记录;在 KeyDown
时判断,如果与上次 KeyUp
的时间间隔过短,则忽略该事件。最终代码如下:
// alterkeys.c
// http://osxbook.com
//
// You need superuser privileges to create the event tap, unless accessibility
// is enabled. To do so, select the "Enable access for assistive devices"
// checkbox in the Universal Access system preference pane.
// modified by SF-Zhou
// To: Kill Double Typing on MacBook
// Complile: g++ -O2 -Wall -o kill_double_typing kill_double_typing.cpp -framework ApplicationServices
// Run: ./kill_double_typing
#include <ApplicationServices/ApplicationServices.h>
#include <iostream>
#include <chrono>
#include <unordered_map>
using namespace std;
typedef chrono::time_point<std::chrono::high_resolution_clock> Time;
typedef long long ll;
unordered_map<CGKeyCode, Time> last_time;
Time time_now() {
return chrono::high_resolution_clock::now();
}
// This callback will be invoked every time there is a keystroke.
CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
// Paranoid sanity check.
if ((type != kCGEventKeyDown) && (type != kCGEventKeyUp)) {
return event;
}
// The incoming keycode.
CGKeyCode keycode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
// printf("%d\n", keycode); // print keycode
if (keycode == 11 /* b */ || keycode == 45 /* n */) {
if (type == kCGEventKeyUp) {
last_time[keycode] = time_now();
} else {
if (last_time.count(keycode)) {
ll microseconds = chrono::duration_cast<chrono::microseconds>(
time_now() - last_time[keycode]
).count();
// ignore if time less than 30ms
if (microseconds < 30000) {
return NULL;
}
}
}
}
// Set the modified keycode field in the event.
CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, (int64_t)keycode);
// We must return the event for it to be useful.
return event;
}
int main(void) {
CFMachPortRef eventTap;
CGEventMask eventMask;
CFRunLoopSourceRef runLoopSource;
// Create an event tap. We are interested in key presses.
eventMask = ((1 << kCGEventKeyDown) | (1 << kCGEventKeyUp));
eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, 0, eventMask, myCGEventCallback, NULL);
if (!eventTap) {
fprintf(stderr, "failed to create event tap\n");
exit(1);
}
// Create a run loop source.
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
// Add to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
// Enable the event tap.
CGEventTapEnable(eventTap, true);
// Set it all running.
CFRunLoopRun();
// In a real program, one would have arranged for cleaning up.
exit(0);
}
将上述代码命名为 kill_double_typing.cpp
,编译:
g++ -O2 -Wall -o kill_double_typing kill_double_typing.cpp -framework ApplicationServices
编译后得到可执行文件,可以使用 nohup 命令,将其设为后台执行。做得更复杂一点的,可以设置为自动启动,就自己搜索啦。
目前只是一个非常初级的版本,还有很多可以调优的地方,笔者会慢慢改进这个小程序的。
软件的方式是很有局限性的,最好的方案,当然是提高苹果的品控啦,不过我说是没用的😂。