Hammerspoon 深度技术报告:用 Lua 掌控你的 Mac

> 写于 2026-03-17 | 技术向 · 中文 · 适合 macOS 开发者/高级用户

目录

1. Hammerspoon 是什么

2. 核心原理:Lua Bridge to macOS APIs

3. 主要功能模块

4. 典型使用场景与代码示例

5. 与同类工具对比

6. 进阶技巧与最佳实践

7. 推荐社区资源

什么是 Hammerspoon

Hammerspoon 是一款免费开源的 macOS 自动化工具,它的核心思想极为简洁:将 macOS 系统 API 通过 Lua 脚本语言完整暴露出来,让用户用编程的方式控制操作系统的一切。

与其他自动化工具不同,Hammerspoon 不提供图形化配置界面。你的配置文件就是代码——一个或多个 Lua 脚本文件,存放在 ~/.hammerspoon/init.lua(及其引用的模块)。

关键特性:

Hammerspoon 于 2014 年从 Mjolnir(一个更早期的类似工具)fork 而来,由社区维护至今,已有超过 11,000 GitHub Star。

核心原理

Lua Bridge 架构

Hammerspoon 的架构本质上是一个 Objective-C / Swift + Lua 的双向桥接层


┌─────────────────────────────────────────┐
│           Hammerspoon App (ObjC)         │
│                                         │
│  ┌─────────────┐   ┌─────────────────┐  │
│  │  LuaSkin    │   │  CoreFoundation │  │
│  │  (Lua 5.4)  │◄──►  NSWindow, etc. │  │
│  └─────────────┘   └─────────────────┘  │
│         ▲                   ▲           │
│         │                   │           │
│    用户 Lua 脚本        macOS APIs       │
│    init.lua             AX, CGEvent     │
│                         NSWorkspace     │
└─────────────────────────────────────────┘

LuaSkin 是 Hammerspoon 的核心库,它:

1. 内嵌了完整的 Lua 5.4 运行时

2. 将 macOS 的 Objective-C 对象(NSWindow、NSScreen、NSApplication 等)包装成 Lua 表(table)

3. 将 macOS 事件(键盘、鼠标、窗口焦点变化等)转换为 Lua 回调

4. 允许 Lua 代码调用 CoreGraphics、Accessibility API、NSWorkspace 等系统级接口

初始化流程


1. macOS 启动 Hammerspoon.app
2. Hammerspoon 初始化 Lua 虚拟机(LuaSkin)
3. 加载所有内置扩展(hs.window, hs.hotkey, hs.screen 等)
4. 执行 ~/.hammerspoon/init.lua
5. Lua 代码注册事件监听器(EventTap、Watcher、Timer 等)
6. Hammerspoon 进入 RunLoop,等待事件触发回调

当你修改配置后执行 hs.reload(),Hammerspoon 会销毁当前 Lua 状态,重新走步骤 3-6,实现热更新。

关键技术:Accessibility API

Hammerspoon 大量依赖 macOS 的 Accessibility API(AX)——这是苹果为辅助功能设计的一套接口,允许应用程序查询和控制其他应用的 UI 元素(窗口、按钮、文本框等)。

因此,首次使用需要在「系统偏好设置 → 隐私与安全 → 辅助功能」中给 Hammerspoon 授权。

主要功能模块

Hammerspoon 的功能通过 hs.* 命名空间的模块组织。以下是核心模块概览:

hs.hotkey — 快捷键绑定

最常用的模块,支持绑定任意组合键到 Lua 函数:


-- 基础绑定:Cmd+Alt+T 打开 Terminal
hs.hotkey.bind({"cmd", "alt"}, "T", function()
  hs.application.launchOrFocus("Terminal")
end)

-- 带 pressedfn / releasedfn / repeatfn 的完整形式
local pressed = function() print("key down") end
local released = function() print("key up") end
hs.hotkey.bind({"ctrl"}, "F19", pressed, released)

-- 创建可启用/禁用的热键对象
local hk = hs.hotkey.new({"cmd", "shift"}, "R", function()
  hs.reload()
  hs.notify.new({title="Hammerspoon", informativeText="Config reloaded!"}):send()
end)
hk:enable()

hs.window — 窗口管理

提供窗口查询、移动、缩放、聚焦等操作:


-- 获取当前聚焦窗口
local win = hs.window.focusedWindow()

-- 获取窗口所在屏幕
local screen = win:screen()
local frame = screen:frame()

-- 将窗口移到屏幕左半边
win:setFrame({
  x = frame.x,
  y = frame.y,
  w = frame.w / 2,
  h = frame.h
})

-- 窗口最大化
win:maximize()

-- 按名称查找窗口
local safariWin = hs.window.find("Safari")

hs.screen — 多屏幕支持


-- 获取所有屏幕
local screens = hs.screen.allScreens()
for i, s in ipairs(screens) do
  print(i, s:name(), s:frame())
end

-- 获取主屏幕
local main = hs.screen.mainScreen()
print(main:currentMode())  -- {w=2560, h=1440, scale=2}

hs.eventtap — 事件拦截与模拟

可以监听甚至修改键盘鼠标事件,是实现键位重映射的核心:


-- 将 Caps Lock 重映射为 Escape(需配合 Karabiner 或系统设置关闭 Caps Lock)
local capsEsc = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event)
  if event:getKeyCode() == hs.keycodes.map["f18"] then  -- F18 代表 Caps Lock
    event:setKeyCode(hs.keycodes.map["escape"])
  end
  return false  -- false = 继续传递事件
end)
capsEsc:start()

-- 模拟按键
hs.eventtap.keyStroke({"cmd"}, "c")  -- 模拟 Cmd+C
hs.eventtap.keyStroke({}, "return")  -- 模拟回车

hs.application — 应用程序控制


-- 启动或聚焦应用
hs.application.launchOrFocus("Finder")

-- 获取所有运行中的应用
local apps = hs.application.runningApplications()
for _, app in ipairs(apps) do
  print(app:name(), app:pid())
end

-- 向应用发送菜单命令
local app = hs.application.get("Finder")
app:selectMenuItem({"File", "New Finder Window"})

hs.timer — 定时器


-- 5秒后执行一次
hs.timer.doAfter(5, function()
  hs.alert.show("5秒到了!")
end)

-- 每60秒重复执行
local t = hs.timer.new(60, function()
  -- 检查某些状态...
end)
t:start()

-- 在特定时间执行
hs.timer.doAt("09:00", function()
  hs.notify.new({title="早安", informativeText="今天也要加油!"}):send()
end)

hs.notify — 系统通知


-- 发送简单通知
hs.notify.new({
  title = "Hammerspoon",
  informativeText = "任务完成",
  soundName = NSUserNotificationDefaultSoundName
}):send()

-- 带点击回调的通知
local n = hs.notify.new(function(notif)
  hs.application.launchOrFocus("Safari")
end, {
  title = "打开浏览器",
  informativeText = "点击打开 Safari"
})
n:send()

hs.wifi — WiFi 事件监听


-- 监听 WiFi 网络变化
local wifiWatcher = hs.wifi.watcher.new(function()
  local network = hs.wifi.currentNetwork()
  if network == "HomeNetwork" then
    -- 连接到家庭网络,关闭 VPN
    hs.execute("launchctl unload ~/Library/LaunchAgents/com.vpn.plist")
    hs.alert.show("🏠 到家了,VPN 已关闭")
  elseif network then
    -- 连接到其他网络,开启 VPN
    hs.execute("launchctl load ~/Library/LaunchAgents/com.vpn.plist")
    hs.alert.show("🌐 公共网络,VPN 已开启")
  end
end)
wifiWatcher:start()

hs.battery — 电池监控


local battWatcher = hs.battery.watcher.new(function()
  local pct = hs.battery.percentage()
  local charging = hs.battery.isCharging()
  
  if pct <= 20 and not charging then
    hs.notify.new({
      title = "⚡ 电量不足",
      informativeText = string.format("剩余 %.0f%%,请及时充电", pct)
    }):send()
  end
end)
battWatcher:start()

hs.drawing / hs.canvas — 自定义 UI 绘制


-- 在屏幕上绘制自定义文字提示(类似 OSD)
local function showOSD(text)
  local screen = hs.screen.mainScreen()
  local frame = screen:frame()
  
  local canvas = hs.canvas.new({
    x = frame.w / 2 - 150,
    y = frame.h - 120,
    w = 300,
    h = 60
  })
  
  canvas:appendElements({
    type = "rectangle",
    action = "fill",
    fillColor = {red=0, green=0, blue=0, alpha=0.75},
    roundedRectRadii = {xRadius=10, yRadius=10}
  }, {
    type = "text",
    text = text,
    textFont = "SF Pro Display",
    textSize = 20,
    textColor = {white=1, alpha=1},
    textAlignment = "center",
    frame = {x=0, y=10, w=300, h=40}
  })
  
  canvas:show()
  hs.timer.doAfter(2, function() canvas:delete() end)
end

showOSD("🎉 配置已重载")

典型使用场景

场景一:完整的窗口管理系统

这是 Hammerspoon 最广泛的使用场景。以下是一套完整的窗口布局方案,支持左半、右半、左三分之一等常用布局:


-- window-management.lua
-- 引入到 init.lua: require("window-management")

local wm = {}

-- 屏幕边距(像素)
local MARGIN = 8

-- 获取可用屏幕区域(减去边距)
local function getFrame(screen)
  local f = screen:frame()
  return {
    x = f.x + MARGIN,
    y = f.y + MARGIN,
    w = f.w - MARGIN * 2,
    h = f.h - MARGIN * 2
  }
end

-- 布局定义(比例)
local layouts = {
  left_half      = {x=0,    y=0, w=0.5,  h=1},
  right_half     = {x=0.5,  y=0, w=0.5,  h=1},
  top_half       = {x=0,    y=0, w=1,    h=0.5},
  bottom_half    = {x=0,    y=0.5, w=1,  h=0.5},
  left_third     = {x=0,    y=0, w=0.333,h=1},
  center_third   = {x=0.333,y=0, w=0.333,h=1},
  right_third    = {x=0.667,y=0, w=0.333,h=1},
  left_two_third = {x=0,    y=0, w=0.667,h=1},
  right_two_third= {x=0.333,y=0, w=0.667,h=1},
  top_left       = {x=0,    y=0, w=0.5,  h=0.5},
  top_right      = {x=0.5,  y=0, w=0.5,  h=0.5},
  bottom_left    = {x=0,    y=0.5,w=0.5, h=0.5},
  bottom_right   = {x=0.5,  y=0.5,w=0.5, h=0.5},
  maximize       = {x=0,    y=0, w=1,    h=1},
}

-- 应用布局到当前窗口
function wm.applyLayout(name)
  local win = hs.window.focusedWindow()
  if not win then return end
  
  local screen = win:screen()
  local sf = getFrame(screen)
  local l = layouts[name]
  
  if not l then
    hs.alert.show("未知布局: " .. name)
    return
  end
  
  win:setFrame({
    x = sf.x + sf.w * l.x,
    y = sf.y + sf.h * l.y,
    w = sf.w * l.w - MARGIN,
    h = sf.h * l.h - MARGIN
  })
end

-- 将窗口移到下一个屏幕
function wm.moveToNextScreen()
  local win = hs.window.focusedWindow()
  if not win then return end
  win:moveToScreen(win:screen():next(), true, true)
end

-- 快捷键绑定
local M = {"ctrl", "alt"}   -- modifier keys

hs.hotkey.bind(M, "left",  function() wm.applyLayout("left_half") end)
hs.hotkey.bind(M, "right", function() wm.applyLayout("right_half") end)
hs.hotkey.bind(M, "up",    function() wm.applyLayout("maximize") end)
hs.hotkey.bind(M, "down",  function() wm.applyLayout("bottom_half") end)
hs.hotkey.bind(M, "1",     function() wm.applyLayout("left_third") end)
hs.hotkey.bind(M, "2",     function() wm.applyLayout("center_third") end)
hs.hotkey.bind(M, "3",     function() wm.applyLayout("right_third") end)
hs.hotkey.bind(M, "n",     function() wm.moveToNextScreen() end)

return wm

场景二:应用切换器(Hyper Key 模式)

将 Caps Lock 改造为 Hyper 键(Cmd+Ctrl+Alt+Shift),实现一键直达应用:


-- hyper.lua
-- 前提:用 Karabiner-Elements 将 Caps Lock 映射为 F18

local hyper = {"cmd", "ctrl", "alt", "shift"}

local appHotkeys = {
  ["T"] = "iTerm",
  ["B"] = "Safari",
  ["C"] = "Visual Studio Code",
  ["S"] = "Slack",
  ["F"] = "Finder",
  ["M"] = "Mail",
  ["N"] = "Notion",
  ["P"] = "Spotify",
  ["X"] = "Xcode",
  ["D"] = "Discord",
}

for key, app in pairs(appHotkeys) do
  hs.hotkey.bind(hyper, key, function()
    hs.application.launchOrFocus(app)
  end)
end

-- Hyper+R 重载配置
hs.hotkey.bind(hyper, "R", function()
  hs.reload()
end)

场景三:基于上下文的自动化(Context Switching)

不同工作场景自动切换配置,比如开会时静音、在家时关 VPN、连显示器时切换布局:


-- context.lua
local contexts = {}

-- 应用焦点变化监听
local appWatcher = hs.application.watcher.new(function(name, event, app)
  if event == hs.application.watcher.activated then
    if name == "zoom.us" or name == "Microsoft Teams" then
      -- 进入会议模式:关闭通知,静音提醒
      hs.alert.show("📹 会议模式:通知已暂停")
    elseif name == "Final Cut Pro" or name == "DaVinci Resolve" then
      -- 视频编辑模式:禁用可能干扰快捷键的热键
      hs.alert.show("🎬 视频编辑模式")
    end
  end
end)
appWatcher:start()

-- 屏幕数量变化(连接/断开外接显示器)
local screenWatcher = hs.screen.watcher.new(function()
  local screens = hs.screen.allScreens()
  local count = #screens
  
  hs.timer.doAfter(2, function()  -- 等待屏幕稳定
    if count >= 2 then
      hs.alert.show(string.format("🖥️ 检测到 %d 块屏幕,应用多屏布局", count))
      -- 在这里重新排列应用窗口
      applyMultiScreenLayout()
    else
      hs.alert.show("💻 单屏模式")
    end
  end)
end)
screenWatcher:start()

-- 多屏布局:把常用应用分配到不同屏幕
function applyMultiScreenLayout()
  local screens = hs.screen.allScreens()
  if #screens < 2 then return end
  
  local mainScreen = hs.screen.mainScreen()
  local secondScreen = mainScreen:next()
  
  local layout = {
    {"Safari",             nil, mainScreen,   hs.layout.left50,  nil, nil},
    {"Visual Studio Code", nil, mainScreen,   hs.layout.right50, nil, nil},
    {"iTerm2",             nil, secondScreen, hs.layout.maximized, nil, nil},
    {"Slack",              nil, secondScreen, hs.layout.left50,  nil, nil},
  }
  
  hs.layout.apply(layout)
end

场景四:Spoon —— 模块化插件系统

Spoon 是 Hammerspoon 的官方插件标准,一个 Spoon 就是一个 .spoon 目录,包含 init.lua 和元数据:


-- 使用 SpoonInstall 管理 Spoons
hs.loadSpoon("SpoonInstall")

-- 安装并配置 ReloadConfiguration Spoon(文件变化自动重载)
spoon.SpoonInstall:andUse("ReloadConfiguration", {
  watch_paths = {os.getenv("HOME") .. "/.hammerspoon"}
})

-- 安装并配置 WindowScreenLeftAndRight(窗口在屏幕间移动)
spoon.SpoonInstall:andUse("WindowScreenLeftAndRight", {
  hotkeys = {
    screen_left  = {{"ctrl", "alt", "cmd"}, "Left"},
    screen_right = {{"ctrl", "alt", "cmd"}, "Right"}
  }
})

-- 安装 ClipboardTool(剪贴板历史)
spoon.SpoonInstall:andUse("ClipboardTool", {
  hotkeys = {show_clipboard = {{"ctrl", "alt", "cmd"}, "v"}},
  config = {
    max_size = 100,
    show_in_menubar = false,
  },
  start = true
})

场景五:URL Scheme 处理与 Alfred/Raycast 联动


-- url-handler.lua
-- 注册自定义 URL Scheme,用于从 Alfred/Raycast/脚本触发 Hammerspoon 操作
-- 在 macOS 上注册需要额外步骤,这里用 hs.urlevent 模块

hs.urlevent.bind("windowLeft", function(eventName, params)
  local win = hs.window.focusedWindow()
  if win then
    local f = win:screen():frame()
    win:setFrame({x=f.x, y=f.y, w=f.w/2, h=f.h})
  end
end)

hs.urlevent.bind("openApp", function(eventName, params)
  if params["name"] then
    hs.application.launchOrFocus(params["name"])
  end
end)

-- 触发方式(在终端或脚本中):
-- open "hammerspoon://windowLeft"
-- open "hammerspoon://openApp?name=Safari"

场景六:菜单栏小工具


-- menubar.lua
-- 在菜单栏显示当前网络 IP 或其他状态

local menubar = hs.menubar.new()

local function updateMenubar()
  local ip = hs.network.primaryIPAddress()
  if ip then
    menubar:setTitle("🌐 " .. ip)
  else
    menubar:setTitle("❌ 无网络")
  end
end

-- 创建下拉菜单
menubar:setMenu(function()
  return {
    {title = "复制 IP", fn = function()
      local ip = hs.network.primaryIPAddress()
      if ip then
        hs.pasteboard.setContents(ip)
        hs.alert.show("IP 已复制: " .. ip)
      end
    end},
    {title = "-"},  -- 分隔线
    {title = "重载 Hammerspoon", fn = function() hs.reload() end},
    {title = "退出 Hammerspoon", fn = function() hs.quit() end},
  }
end)

updateMenubar()

-- 每30秒刷新一次
hs.timer.new(30, updateMenubar):start()

工具对比

特性HammerspoonKeyboard MaestroBetterTouchToolRaycast
**价格**免费开源$36 一次买断$22/年 或 $45 买断免费+$96/年 Pro
**配置方式**Lua 代码图形化流程图形化图形化+插件
**上手难度**高(需懂 Lua)极低
**窗口管理**⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
**快捷键**⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
**触控板手势**⭐⭐⭐⭐⭐
**自动化深度**⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
**启动器**⭐⭐(需手写)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
**可扩展性**⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐(插件市场)
**系统资源**极低
**脚本语言**LuaAppleScript/ShellAppleScript/ShellSwift/TypeScript

各工具定位

Hammerspoon 是「程序员的工具」——如果你愿意写代码,它能做任何事,但开箱即用的体验几乎为零。

Keyboard Maestro 是「超级用户的工具」——强大的图形化宏编辑器,几乎不需要写代码就能实现复杂自动化,剪贴板、表单填写、应用控制一应俱全,是 Hammerspoon 的图形化替代品。

BetterTouchTool 的核心优势是触控板手势——自定义 Magic Trackpad 手势、Force Touch 操作、Touch Bar(老 MacBook)按钮是其他工具无法替代的领域。窗口管理也做得不错。

Raycast 是「现代启动器」——专注于 Spotlight 增强、快速启动、AI 集成,有活跃的插件市场,是 Alfred 的强力竞争者,但窗口管理相对有限。

最佳实践:很多高级用户会同时使用多个工具——用 Hammerspoon 做窗口管理和系统自动化,用 Raycast 做应用启动和快速查询,用 BetterTouchTool 做手势。工具不冲突。

进阶技巧

1. 模块化组织配置

随着配置增长,建议按功能拆分文件,用 require 组织:


-- ~/.hammerspoon/init.lua

-- 工具函数
local utils = require("utils")

-- 功能模块
require("window-management")   -- 窗口管理
require("hyper")               -- Hyper 键绑定
require("context")             -- 上下文感知
require("menubar")             -- 菜单栏小工具
require("wifi-watcher")        -- WiFi 自动化

-- Spoons
require("spoons")              -- Spoon 管理

-- 重载通知
hs.notify.new({
  title = "Hammerspoon",
  informativeText = "✅ 配置已重载 " .. os.date("%H:%M:%S")
}):send()

2. 使用 hs.inspect 调试


-- 在控制台(Console)中调试 Lua 值
local win = hs.window.focusedWindow()
print(hs.inspect(win:frame()))
-- 输出: {h = 900.0, w = 1440.0, x = 0.0, y = 0.0}

-- 打印所有屏幕信息
for i, s in ipairs(hs.screen.allScreens()) do
  print(i, hs.inspect(s:frame()))
end

3. 防止重复绑定(reload 安全)


-- 使用 hs.hotkey.deleteAll() 或追踪热键对象
local hotkeyList = {}

local function bindKey(mods, key, fn)
  -- 先删除已有的同组合键绑定
  if hotkeyList[mods .. key] then
    hotkeyList[mods .. key]:delete()
  end
  local hk = hs.hotkey.bind(mods, key, fn)
  hotkeyList[mods .. key] = hk
  return hk
end

4. 异步执行(避免阻塞 UI)


-- 错误做法:直接 execute 会阻塞
-- local output = hs.execute("sleep 5 && echo done")  -- ❌ 冻结 5 秒

-- 正确做法:使用 hs.task 异步执行
local task = hs.task.new("/usr/bin/env", function(exitCode, stdout, stderr)
  -- 完成回调
  hs.alert.show("任务完成: " .. stdout)
end, {"bash", "-c", "sleep 2 && echo '异步完成'"})

task:start()

5. 持久化配置状态


-- 用 hs.settings 持久化跨重载的配置
local SETTINGS_KEY = "myConfig"

-- 读取
local savedConfig = hs.settings.get(SETTINGS_KEY) or {}

-- 写入(自动持久化到 ~/Library/Preferences)
savedConfig.lastReload = os.time()
hs.settings.set(SETTINGS_KEY, savedConfig)

-- 删除
-- hs.settings.clear(SETTINGS_KEY)

6. 性能优化:避免频繁窗口查询


-- 缓存窗口引用,避免每次触发都查询
local windowCache = {}

local appWatcher = hs.application.watcher.new(function(name, event, app)
  if event == hs.application.watcher.launched then
    -- 应用启动时缓存
    hs.timer.doAfter(0.5, function()
      windowCache[name] = app:mainWindow()
    end)
  elseif event == hs.application.watcher.terminated then
    windowCache[name] = nil
  end
end)
appWatcher:start()

7. 构建自定义选择器(Chooser)


-- 快速切换窗口(类似 Cmd+Tab 但更强大)
local windowChooser = hs.chooser.new(function(choice)
  if choice then
    -- 通过 windowID 找到窗口并聚焦
    local win = hs.window.get(choice.windowID)
    if win then
      win:focus()
    end
  end
end)

local function buildWindowList()
  local choices = {}
  for _, win in ipairs(hs.window.allWindows()) do
    if win:isStandard() then
      local app = win:application()
      table.insert(choices, {
        text = win:title(),
        subText = app and app:name() or "Unknown",
        image = app and hs.image.imageFromAppBundle(app:bundleID()) or nil,
        windowID = win:id()
      })
    end
  end
  return choices
end

hs.hotkey.bind({"cmd", "alt"}, "W", function()
  windowChooser:choices(buildWindowList())
  windowChooser:show()
end)

8. 与 Shell 脚本深度集成


-- 执行 shell 命令并处理输出
local function getClipboardHTML()
  -- 获取剪贴板中的 HTML(如果有的话)
  local types = hs.pasteboard.typesAvailable()
  for _, t in ipairs(types) do
    if t == "public.html" then
      return hs.pasteboard.readDataForUTI("public.html")
    end
  end
  return nil
end

-- 调用外部脚本
hs.hotkey.bind({"cmd", "alt"}, "G", function()
  hs.task.new("/usr/local/bin/python3", function(code, out, err)
    if code == 0 then
      hs.pasteboard.setContents(out:gsub("\n$", ""))
      hs.alert.show("✅ 已处理并复制到剪贴板")
    else
      hs.alert.show("❌ 脚本出错: " .. err)
    end
  end, {os.getenv("HOME") .. "/scripts/process.py", hs.pasteboard.getContents()}):start()
end)

社区资源

官方资源

优质配置参考

学习资源

相关工具(与 Hammerspoon 配合使用)

总结

Hammerspoon 的本质是一个以代码为配置的 macOS 超级自动化平台。它的学习曲线陡峭,但一旦上手,几乎没有它做不到的事情——从简单的窗口布局快捷键,到复杂的上下文感知自动化,再到自定义 UI 组件。

适合你的情况:

也许不适合的情况:

最后,Hammerspoon 和其他工具并不互斥。很多高效的 macOS 用户会把 Hammerspoon 作为自动化基础设施,同时搭配 Raycast 做启动器、BetterTouchTool 做手势——让每个工具做它最擅长的事。

本报告基于 Hammerspoon 0.9.100+,macOS 14 Sonoma / 15 Sequoia 环境。API 细节以 官方文档 为准。