这是一篇耽搁了很久,一直躺在我的 Obsidian 笔记中的一篇文章,一直就想好好介绍一下 Hammerspoon,但是因为过去虽然也在用 macOS,但是使用最多的还是 Ubuntu,Hammerspoon 只能在 macOS 上使用,就没有那么大的兴致再花时间学习它的使用。但最近更新了一下系统,发现 Hammerspoon 出了一点问题,没有了 Hammerspoon 之后我才发现很多不适应的地方,那就在花一点时间梳理一下我的配置。

Hammerspoon 是什么?

Hammerspoon 是一个 macOS 上开源的自动化工具,什么叫做自动化工具呢?通过 Hammerspoon ,可以使用一些脚本来实现原来只能通过界面操作,或快捷键才能达到的效果,并且实现系统自动化。最简单的例子,比如当我连上家里的 WiFi 的时候,就自动将音量调成 3 档;再比如当我切换窗口的时候,自动切换输入法,比如在 IDEA IntelliJ 中自动使用英文输入法,当我打开 Obsidian 则自动切换成中文输入法。

Hammerspoon 使用 Lua 脚本语言与操作系统通信。通过编写 Lua 脚本实现与 macOS API 的交互,Hammerspoon 提供的 API,包括应用的、窗口的、鼠标指针、文件系统、声音设备、电池、屏幕、键盘/鼠标事件、粘贴板、地理位置服务、WiFi 等等。Hammerspoon 是操作系统和 Lua 执行引擎的桥梁,通过 Hammerspoon 可以让 macOS 实现非常强大的自动化。

官网: https://www.hammerspoon.org/

Hammerspoon 能做什么

Hammerspoon 实际上是将 macOS 的系统接口实现了一层转发,让用户可以通过简单的 Lua 脚本进行配置,从而实现一定的 UI 自动化,一旦能够直接从 API 层面对接操作系统,那么 Hammerspoon 能够做的事情就非常多了:

  • 创建并管理全局快捷键,这样就可以创建一组属于自己的快捷键工作流
  • 管理窗口 move windows,结合自定义快捷键可以非常轻松地对窗口进行管理
  • 发送通知 display notifications
  • 和其他应用交互 talk to other applications
  • 添加自定义内容到状态栏 add things to your menu bar,有了这个功能就可以在状态栏上添加任何自定义的内容,比如有人利用公开的天气 API 在状态栏显示最近和未来的天气,可以显示系统的网速等等
  • 监听文件内容变化,可以利用这个自动加载配置,或者自动提交任务等等
  • 监听 WiFi 变化,可以利用不同的网络环境配置不同的网络配置,自动切换不同的工作环境等等
  • 监听应用的启动和停止
  • 在屏幕上绘制 draw on the screen
  • 监听电池变化 watch for changes to your battery
  • 当电脑接口对接硬件时触发动作 carry out actions when you plug things into your computer

Installation

通过 Homebrew 安装:

brew install --cask hammerspoon

上手配置

Hammerspoon 的默认配置在 ~/.hammerspoon/init.lua,我个人通过将配置文件放在 dotfiles 中软链接到目的位置来同步配置。

做一个最简单的例子,在 init.lua 文件中写入:

hs.alert.show("Config reload!")

然后重新加载 Hammerspoon 配置,就会看到在屏幕中央出现 “Config reload!” 的弹出提示。

使用 Hammerspoon 作为管理窗口工具

虽然 [[Mac 上的窗口管理工具]] 有很多,免费的,收费的,Moom, Rectangle 等等,但是自由度都没有 Hammerspoon 多。

下面是我使用的一些窗口管理快捷键。

按下 Option+r 进入窗口的管理模式,在该模式下按下快捷键可以实现非常多的操作:

  • HL/JK 可以用来将窗口按照左、右、下、上分屏
  • ASDW 可以用来移动窗口的位置,对应上下左右
  • Y/O/U/I 可以将窗口变成四分之一屏幕大小,分别是左上,右上,左下,右下
  • Shift+HL/JK 可以收缩窗口大小,比如 Shift+H 就是让窗口右侧往左侧缩小
  • =/- 用来扩展和收缩窗口
  • Left/Right/Up/down 将窗口移动到左边、右边、上边、下边
  • F 全屏
  • C 中央
  • ESC/Q 退出该模式
  • Tab 显示 Cheatsheet

这一套窗口管理方法来自 ashfinal/awesome-hammerspoon

不过我个人最常使用的快捷键还是 Hyper + h/l/j/k 可以将当前的窗口以左/右/下/上方式进行分屏。这里需要结合 [[Mac 应用 Karabiner Elements 键盘自定义工具]]

[[Mac 下的自定义快捷键]]

使用 Hammerspoon 实现 Hyper + h/l/j/k 管理窗口的相关配置:

hyper = {"ctrl", "alt", "cmd", "shift"}
function move_window(direction)
    return function()
        local win      = hs.window.focusedWindow()
        local app      = win:application()
        local app_name = app:name()
        local f        = win:frame()
        local screen   = win:screen()
        local max      = screen:frame()
        if direction == "left" then
            f.x = max.x + 6
            f.w = (max.w / 2) - 9
        elseif direction == "right" then
            f.x = (max.x + (max.w / 2)) + 3
            f.w = (max.w / 2) - 9
        elseif direction == "up" then
            f.x = max.x + 6
            f.w = max.w - 12
        elseif direction == "down" then
            f.x = (max.x + (max.w / 8)) + 6
            f.w = (max.w * 3 / 4) - 12
        end
        f.y = max.y + 6
        f.h = max.h - 12
        win:setFrame(f, 0.0)
    end
end
hs.hotkey.bind(hyper, "Left", move_window("left"))
hs.hotkey.bind(hyper, "Right", move_window("right"))
hs.hotkey.bind(hyper, "Up", move_window("up"))
hs.hotkey.bind(hyper, "Down", move_window("down"))
hs.hotkey.bind(hyper, "H", move_window("left"))
hs.hotkey.bind(hyper, "L", move_window("right"))
hs.hotkey.bind(hyper, "K", move_window("up"))
hs.hotkey.bind(hyper, "J", move_window("down"))

使用 Hammerspoon 在切换 WiFi 时自动切换对应设置

当连接的 WiFi 发生变化的时候触发一个监听事件,更加详细的配置可以看我的 dotfiles

function ssidChangedCallback()
    newSSID = hs.wifi.currentNetwork()

    local devices = hs.usb.attachedDevices()

    if newSSID == homeSSID and lastSSID ~= homeSSID then
        -- We just joined our home WiFi network
        hs.alert.show("Welcome home!")
        hs.audiodevice.defaultOutputDevice():setVolume(25)
        -- result = hs.network.configuration:setLocation("Home")
        -- hs.alert.show(result)
    elseif newSSID ~= homeSSID and lastSSID == homeSSID then
        -- We just departed our home WiFi network
        hs.alert.show("left home!")
        hs.audiodevice.defaultOutputDevice():setVolume(0)
        -- result = hs.network.configuration:setLocation("Automatic")
        -- hs.alert.show(result)
    end

    if newSSID == workSSID then
        hs.alert.show("work karabiner setup")
        selectKarabinerProfile("goku")
    else
        hs.alert.show("built-in karabiner setup")
        selectKarabinerProfile("goku")
    end

    lastSSID = newSSID
end

wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()

使用 Hammerspoon 一键布局桌面窗口

我抢了同事一个显示器使用,所以外接了三个显示器,在每一个显示器中都有默认的布局。我一般左边竖置的显示器常驻一个 Terminal,中间横置的一个显示器为主要工作的区域,一般放 IntelliJ IDEA,DataGrip,SmartGit 等等其他工具,右侧竖置的显示器上面为即时通信窗口,下面是浏览器。

使用 Hammerspoon 可以很快速的恢复所有窗口的布局,不过我自己用的并不多。

输入法自动切换

比如在特定应用中自动切换成 Rime 输入法 或者切换成 ABC 英文。比如在 IntelliJ IDEA 中不会输入中文的,直接切换成 ABC 输入英文即可,而当切换到浏览器的时候切换到 Rime。

结合 Rime 输入法的自动设置输入法的自动切换 就非常舒服了。

完美的代替了 kawa 这款切换输入法的工具。

定时自动执行脚本

比如我使用 Obsidian 来作笔记,同时使用 git 来做版本管理,写一个脚本,每 30 分钟提交一次。

log = hs.logger.new('autoscript', 'debug')
local cmdArr = {
    "cd /Users/einverne/Sync/wiki/ && /bin/bash auto-push.sh",
}

function shell(cmd)
    hs.alert.show("execute")
    log.i('execute')
    result = hs.osascript.applescript(string.format('do shell script "%s"', cmd))
    hs.execute(cmd)
end

function runAutoScripts()
    for key, cmd in ipairs(cmdArr) do
        shell(cmd)
    end
end

myTimer = hs.timer.doEvery(10, runAutoScripts)
myTimer:start()

比如定时提交 git commit,定时 git push 等等。当然直接使用 Crontab 来实现也是可以的。

Mute on sleep

在笔记本合上时静音

function muteOnWake(eventType)
  if (eventType == hs.caffeinate.watcher.systemDidWake) then
    local output = hs.audiodevice.defaultOutputDevice()
    output:setMuted(true)
  end
end
caffeinateWatcher = hs.caffeinate.watcher.new(muteOnWake)
caffeinateWatcher:start()

Locking the screen

定义锁屏的快捷键。

-- lock screen shortcut
hs.hotkey.bind({'ctrl', 'alt', 'cmd'}, 'L', function() hs.caffeinate.startScreensaver() end)

监听 USB 事件并做相应的设置

对我而言最常见的就是当我接入外接键盘的时候,自动切换 karabiner-Elements 的键盘 profile。

这样当我使用 macOS 自带的键盘和外置键盘的时候就可以保持一致的使用习惯。

绑定快捷键快速打开应用

快速打开终端:

hs.hotkey.bind({'ctrl', 'alt', 'cmd'}, 'K', function () hs.application.launchOrFocus("iTerm") end)

更多的例子可以参考我的 dotfiles

防止长时间不用进入休眠

参考 https://github.com/einverne/dotfiles/hammerspoon/ 关键字 Caffeine。

将当前窗口移动到其他屏幕中

定义了快捷键 Hyper + 1/2/3 将当前窗口快速移动到其他显示器:

function moveWindowToDisplay(d)
  return function()
    local displays = hs.screen.allScreens()
    local win = hs.window.focusedWindow()
    win:moveToScreen(displays[d], false, true)
  end
end

hs.hotkey.bind({"ctrl", "alt", "cmd"}, "1", moveWindowToDisplay(1))
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "2", moveWindowToDisplay(2))
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "3", moveWindowToDisplay(3))

对虚拟桌面的支持

非官方支持

扩展

Hammerspoon 的配置文件是使用 Lua 书写,如果熟悉 Lua,可以更进一步使用 Lua 的 moonscript 来简化配置。

brew install lua@5.3
luarocks-5.3 install moonscript
luarocks-5.3 install lodash

参考 这里

这里

Hammerspoon 官网文档

Spoon 是什么?

Spoon 是预置在 Hammerspoon 内的插件系统,Spoon 是使用纯 Lua 实现的插件,可以方便用户集成集成到 Hammerspoon 的配置中。

可以从官方的页面获取 Spoon ,源码可以参考对应的 GitHub 页面,下载后解压得到 .spoon 文件,双击导入即可。文件会自动将自己拷贝到 ~/.hammerspoon/Spoons/NAME.spoon,然后在 init.luahs.loadSpoon("NAME") 即可。

更具体的 Spoon 的使用可以参考官网。

编写 Spoon

Spoon 文件有一定的格式,方便集成调用。

Spoon 文件中的常用方法:

  • NAME: init(),这个方法会被 hs.loadSpoon() 调用,会进行一些初始设置,这里面不应该执行任何动作
  • NAME: start(),如果有需要在后台进行的任务,可以由这个方法启动
  • NAME: stop(),关闭后台任务
  • NAME: bindHotkeys(mapping),定义功能快捷键,通常是 table 的形式:

reference