有一天,我用Homebrew
安装了一些软件——因为已经是一个月前的事情了,所以已经记不清是安装了什么。安装后并没有立即出现什么问题,只是又过了两天我重新启动电脑后,发现同样是由Homebrew
安装的Emacs
不由分说地无法启动了。这下可麻烦了,毕竟我是org-mode
的重度使用者,还需要偶尔用SLIME
写点Common Lisp的代码,而它们都运行在Emacs
中。
直觉告诉我,也许重新安装一下Emacs
,一切就可以恢复正常。重装了Emacs
后,又遇到了别的问题——用BetterTouchTools
在Touch Bar中添加的按钮,无法在Emacs
已经启动的情况下,切换到它的窗口上。
非要说,问题其实也不大,毕竟很多时候是将MacBook Pro合上盖子当主机用的,Touch Bar在工作时的使用频率并不高。此外,糊Node.js
等语言的代码时也用不到Emacs
——还是VSCode
更合适。
但这就是令人不爽,因此我决定要解决它——用Hammerspoon
。
Hammerspoon是什么?
Hammerspoon
的官网很好地说明了这款工具的定位和原理
This is a tool for powerful automation of OS X. At its core, Hammerspoon is just a bridge between the operating system and a Lua scripting engine. What gives Hammerspoon its power is a set of extensions that expose specific pieces of system functionality, to the user.
- 它运行在OS X上——现在应该叫macOS;
- 它是用来自动化操作的——就像系统内置的
Automator
或第三方的Alfred Workflow
那样; - 它的原理是将操作系统的功能封装成了可以用
Lua
代码调用的模块;
例如下面的代码
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "E", function() |
就可以让使用者在按下组合键⌘⌥⌃e
的时候,在屏幕正中间显示Hello World!
这段文本
为什么用Hammerspoon?
Hammerspoon
正好可以解决我的问题,它的hs.window
模块既可以让使用者遍历所有打开的窗口(用hs.window.allWindows
函数),也可以聚焦到指定的窗口上(用focus
方法)。有了它们,将Emacs
调到最前面(front-most)来也就是水到渠成的事情了:
- 调用函数
hs.window.allWindows
函数,获得所有窗口的列表; - 逐个检查列表中的窗口对象,如果属于
Emacs
的,就调用窗口的方法focus
,并跳出循环。
剩下的两个问题便是:
Emacs
的bundle ID
是什么;- 如何知道一个窗口对象的
bundle ID
。
Emacs的bundle ID
Bundle ID
可以在macOS中独一无二地标识一个应用。要想知道Emacs
的bundle ID
是什么,只需要打开文件/Applications/Emacs.app/Contents/Info.plist
,看看其中键为CFBundleIdentifier
的值即可。
1 | ➜ Contents grep -A 1 'CFBundleIdentifier' Info.plist |
可以看到,Emacs
的bundle ID
是org.gnu.Emacs
。
来点Lua代码吧
有了Emacs
的bundle ID
,接下来就可以在Hammerspoon
中定义快捷键了。由于最后会通过Touch Bar上的按钮来触发这组快捷键,复杂点也不要紧,因此我直接沿用了Hammerspoon
的入门指引中作为例子的⌘⌥⌃w
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
为了在一个循环中逐个遍历窗口对象,将hs.window.allWindows
的返回值保存到一个局部变量中
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
照着简书上的这篇文章,依葫芦画瓢地用for
和pairs
来遍历变量windows
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
窗口自身没有bundle ID
,为此需要先获取窗口所属的应用。查看文档可以知道,有一个application
方法正是用来获取应用对象的
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
调用allWindows
时使用的是英文句号(.
),调用application
则是用冒号(:
),这正是Lua
中调用函数与方法时语法上的差异。
再用应用的bundleID
方法获得它的bundle ID
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
现在,只要变量bundleID
等于Emacs
的bundle ID
就可以聚焦到当前遍历的窗口上了
1 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() |
让Touch Bar按钮触发这一切
只需要在BetterTouchTools
中配置一下即可
这个方法比此前唤起/Applications/Emacs.app
的方式更好,因为它只依赖于Emacs
逻辑上亘古不变的东西——bundle ID
,而不依赖于其物理上的安装位置。