Hammerspoon 是一个 macOS 程序,它将 macOS 上的一些功能封装为 lua 接口,使用 lua 进行配置,以常驻后台的形式运行,可以监听特定事件,典型的比如实现一些快捷键功能。本文介绍两个我正在使用的 Hammerspoon 应用场景。

场景一:锁定指定程序的输入法#
在我对英语阅读和书写越发熟悉之后,我越来越乐意用纯英文环境来摆脱输入法切换的麻烦,至少在 terminal 和 vim 的环境中是这样的。在 linux 上我使用的工具是 lilydjwg/fcitx.vim,但是在 macOS 下需要另找一个方法。
实际上存在不少类似于 fcitx.vim 的插件,它们的思路大致相同。这些插件虽然可以帮助我自动切换输入法,但是一旦我意外的触发输入法切换,它们都无法自动将输入法拉回我期望的状态。也许只是我对这些插件缺乏研究,但仍然有几项理由让我转到使用 hammerspoon 来解决:
- 一些插件需要安装额外的命令行工具来调用系统的输入法切换,这些命令行工具通常很细小,且看起来缺乏维护;
- 这些插件往往只是给 vim 编写的,我希望在 terminal 中也有同样的效果;
- 存在专门的 macOS 应用来锁定输入法,它们或专有,或收费,或缺乏维护,或功能无法精细定制;
- 关键的,它们通常监听程序窗口的聚焦来切换输入法,但是往往不监听“输入法发生了(意外的)切换”本身,使我无法定制自己想要的策略;
最重要的,hammerspoon 使用 lua 进行配置,可以实现灵活且精细的逻辑。如果你正在使用 neovim 和 wezterm,你一定知道我在说什么。下面是功能的代码实现。
在窗口聚焦时切换到指定输入法#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| local function setupAppWatch()
local appIme = {
["WezTerm"] = "com.apple.keylayout.ABC",
["Neovide"] = "com.apple.keylayout.ABC",
["neovide"] = "com.apple.keylayout.ABC",
}
hs.application.watcher
.new(function(appName, eventType, appObject)
if eventType == hs.application.watcher.activated then
local expectedImeId = appIme[appName]
if expectedImeId and hs.keycodes.currentSourceID() ~= expectedImeId then
hs.keycodes.currentSourceID(expectedImeId)
end
end
end)
:start()
end
|
意外切换输入法时将其自动纠正回来#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| local function setupImeWatch()
local appIme = {
["WezTerm"] = "com.apple.keylayout.ABC",
["Neovide"] = "com.apple.keylayout.ABC",
["neovide"] = "com.apple.keylayout.ABC",
}
hs.keycodes.inputSourceChanged(function()
local ok, appName = pcall(function()
return hs.window.focusedWindow():application():name()
end)
if ok and appName then
local expectedImeId = appIme[appName]
if expectedImeId and hs.keycodes.currentSourceID() ~= expectedImeId then
hs.keycodes.currentSourceID(expectedImeId)
end
end
end)
end
|
辅助功能:获取当前窗口的名称和当前输入法的名称#
当按下 ctrl + cmd + .
时,通过系统通知打印当前窗口的应用名称和输入法名称:
1
2
3
4
5
6
7
8
9
| local function setupKeymap()
hs.hotkey.bind({ "ctrl", "cmd" }, ".", function()
local focusedApp = hs.window.focusedWindow():application()
local appName = focusedApp:name()
local appPath = focusedApp:path()
local imeId = hs.keycodes.currentSourceID()
hs.notify.show(appName, appPath, "IME: " .. imeId)
end)
end
|
场景二:使用快捷键将当前窗口在多个显示器之间循环移动#
使用外接显示器,或使用 iPad “随航”时,用鼠标或触摸板拖动窗口到指定显示器,拖拽的路线显得格外漫长。且光标和窗口在显示器之间的穿行带有几分惊险,因为屏幕之间的位移多少带有一些错位。
既然 hammerspoon 已经是我常驻的百宝箱之一,那么用它来解决这个问题就变得非常合适。下面是代码实现,这段代码是豆包 AI 给出的,目前为止我很满意它的效果:
使用 ctrl + cmd + n
快捷键将当前窗口在多个显示器之间循环移动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| local function moveWindowToNextScreen()
local win = hs.window.focusedWindow()
if not win then
return
end
local screens = hs.screen.allScreens()
if #screens < 2 then
return
end
local currentScreen = win:screen()
local currentIndex = 0
for i, screen in ipairs(screens) do
if screen == currentScreen then
currentIndex = i
break
end
end
local nextIndex = currentIndex % #screens + 1
local nextScreen = screens[nextIndex]
win:moveToScreen(nextScreen)
end
hs.hotkey.bind({ "ctrl", "cmd" }, "n", moveWindowToNextScreen)
|
完整的配置在我的 dotfile 仓库中:chaneyzorn/dotfiles - hammerspoon
更多用法,可以查看 hammperspoon 官网提供的接口文档。另外推荐一个可以参考更多用法的仓库:wangshub/hammerspoon-config
参考文档#