蘋果環境

Hammerspoon

視窗管理與自動化

以 Lua 腳本自動化 macOS 操作,此處用於視窗管理快捷鍵。取代原本的 BetterTouchTool。

為什麼從 BetterTouchTool 換過來

BetterTouchToolHammerspoon
授權只涵蓋兩年,過期需重購或降版MIT 開源,無授權問題
廣告有無
設定二進位匯出檔,不易版控純文字 init.lua,可讀可比對
開機自啟GUI 勾選寫在設定檔內,換機即生效

安裝步驟

  • 官網 - https://www.hammerspoon.org/
  • 安裝:brew install --cask hammerspoon
  • 開啟 Hammerspoon(首次啟動會要求輔助使用權限,見下節)

授權輔助使用權限

  • 開啟系統設定(option + F10)❯ 隱私權與安全性 ❯ 輔助使用
  • 找到 Hammerspoon - 打開開關
  • 也可用指令直接開啟該頁:open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"

❗️ 沒有此權限,快捷鍵按下去不會有任何反應(不會報錯,只是沒動作)。任何能操作視窗的工具都需要它。

設定檔

設定檔放在 ~/.hammerspoon/init.lua,重灌後直接抓回來:

mkdir -p ~/.hammerspoon
curl -o ~/.hammerspoon/init.lua https://oawu.tw/blog/macos/hammerspoon/files/init.lua
  • 設定檔 - init.lua
  • 修改後自動重新載入 - hs.pathwatcher 監看 ~/.hammerspoon/,存檔即生效
  • 開機自動啟動 - 由設定檔內的 hs.autoLaunch(true) 處理,不必去 GUI 勾選
  • require("hs.ipc") - 讓終端機的 hs 指令能與 Hammerspoon 通訊,用於驗證與除錯

快捷鍵

快捷鍵功能
command + option + ↑將目前視窗移到下一個螢幕並填滿;只有一個螢幕時,原地填滿

實作重點

  • 不需要條件判斷 - hs.screen:next() 是封閉循環(A→B→A),單螢幕時回傳自己,自動退化為原地最大化
  • 不用 maximize() - screen:frame() 已扣除選單列與 Dock,直接 setFrame() 即可
  • 綠燈全螢幕要先退出 - 全螢幕視窗自成一個 Space,無法跨螢幕移動,故先 setFullScreen(false) 再延遲重試
  • 關閉動畫 - hs.window.animationDuration = 0,避免動畫與 setFrame 的時序競爭

移除 Caps Lock 的切換延遲

macOS 的鍵盤輸入濾波器會把每次 Caps Lock 壓住 75ms 才註冊,用意是防止手掌誤觸。但按得比 75ms 快時,該次按鍵會被整個丟棄 - 以 Caps Lock 切換中英文輸入法時就會漏掉。

設定檔用 hidutil 把延遲改成 0:

hs.execute([[hidutil property --set '{"CapsLockDelayOverride":0}']])

這個做法有兩個限制,都在設定檔內處理掉了:

  • 重開機即失效 - hidutil 改的是 runtime 參數,不寫進任何偏好設定檔。靠 Hammerspoon 開機自啟時重套一次
  • 只作用於當下已連接的鍵盤 - 之後插入的鍵盤不吃。以 hs.usb.watcher 偵測裝置插入,延遲 2 秒後重套(給裝置時間完成列舉)

系統設定的 GUI 沒有這個選項,defaults 也讀不到,只能靠 hidutil。

Finder 的快捷鍵衝突

command + option + ↑ 在 Finder 原本是「前往上層資料夾並關閉目前視窗」,會被本設定蓋掉。

但單純的「前往上層資料夾」是 command + ↑,不受影響,代價很小,因此不做讓行 - 讓此快捷鍵在所有 app 中行為一致。

若日後想讓 Finder 保留原行為,可用 hs.application.watcher 在切換到 Finder 時 hotkey:disable(),離開時 enable()。此法已驗證可行(切到 Finder 時 getHotkeys() 會歸零),僅因代價不划算而未採用。

驗證

hs -c "print(hs.accessibilityState())"     # 輔助使用權限,應為 true
hs -c "print(hs.autoLaunch())"             # 開機自啟,應為 true
hs -c "print(#hs.hotkey.getHotkeys())"     # 已註冊的快捷鍵數,應為 1
hidutil property --get "CapsLockDelayOverride"   # Caps Lock 延遲,應為 0

列出所有螢幕與其可用區(可用區已扣掉選單列,故高度會比實際解析度少):

hs -c "for i,s in ipairs(hs.screen.allScreens()) do local f=s:frame(); print(i, s:name(), f.w..'x'..f.h, f.x..','..f.y) end"

踩雷

  • 有些視窗的尺寸是固定的 - macOS 的「系統設定」(Ventura 後的 SwiftUI 版)寬度鎖死不可調整,套用後只有高度會變。這不是設定的問題,驗證時要換一個可自由縮放的視窗
  • hs.eventtap.keyStroke 無法觸發自己註冊的快捷鍵 - 模擬按鍵測不出來,驗證要用實體按鍵
  • hs.reload() 期間 IPC 會短暫斷線 - hs 指令 timeout 屬正常,稍候再試;若持續無回應則重啟 Hammerspoon
  • 首次啟動時 hs 指令不通 - require("hs.ipc") 是後來才加進設定檔的,需重啟 Hammerspoon 一次才會生效
  • 部分 app 跨螢幕移動後視窗會消失 - 實測時 Docker Desktop 的視窗在移到另一螢幕並填滿後不見了(daemon 與容器不受影響,從選單列圖示可重開)。僅觀察到一次,未重現驗證

相關參考