Content Table

Hammerspoon 切换程序和窗口大小管理

Hammerspoon is a tool for powerful automation of macOS. 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.

下面的脚本实现了:

  • 切换程序: 按下快捷键 Alt+键 就会切换到键对应的程序 (如果程序没有打开则打开,如果不是当前程序则激活为当前程序):
    • 按下 Alt+F 切换到 Finder
    • 按下 Alt+S 切换到 Safari
  • 窗口管理:
    • 窗口最大化: Alt+Ctrl+Return
    • 窗口左半屏: Alt+Ctrl+Cmd+Left
    • 窗口右半屏: Alt+Ctrl+Cmd+Right
    • 窗口居中: Alt+Ctrl+C
    • 窗口靠左: Alt+Ctrl+Left
    • 窗口靠右: Alt+Ctrl+Right
  • 多屏管理:
    • 在屏幕间移动光标: Ctrl+Z
    • 在屏幕间移动程序: Ctrl+X
  • 蓝牙管理:
    • 打开蓝牙: Alt+T
    • 关闭蓝牙: Alt+T
    • 下午 6 点后系统休眠时自动关闭蓝牙
  • 其他
    • 按下 Ctrl+H 隐藏或者显示桌面文件
    • 按下 Ctrl+D: 切换 Light 和 Dark 模式
    • 按下 Alt+Z: 前一个标签页
    • 按下 Alt+X: 后一个标签页
    • 按下 Alt+H: 方向左
    • 按下 Alt+L: 方向右
    • 按下 Alt+J: 方向下
    • 按下 Alt+K: 方向上
    • 按下 Alt+I: 删除

实现了 Thor 和 Rectangle 的功能,并且解决了 Thor 激活 Finder 的 Bug。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
-- 键和应用对
-- 提示: 数字作为键,需要使用 [Number] 的格式
local KEY_APP_PAIRS = {
A = "IntelliJ IDEA CE.app",
C = "Google Chrome.app",
D = "Dash.app",
E = "Visual Studio Code.app",
-- Q = "QQ.app",
S = "Safari.app",
W = "WeChat.app",
Z = "/System/Applications/Preview.app",
[2] = "Notable.app",
[3] = "Typora.app",
}

-- 显示 Finder: Alt + F
hs.hotkey.bind({"alt"}, "F", function()
hs.application.launchOrFocus('finder')
hs.application.get("com.apple.finder"):setFrontmost(true)
end)

-- 重新加载配置
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
hs.reload()
end)

-- 隐藏/显示桌面文件
hs.hotkey.bind({"ctrl"}, "H", function()
-- 找到可见文件个数,如果可见文件数为 0 则说明文件都隐藏了或者没有文件执行显示操作,否则隐藏
script = [[
n=$(ls -lO ~/Desktop | grep -v hidden | grep -v total | wc -l | xargs);
[ $n -eq '0' ] && chflags nohidden ~/Desktop/* || chflags hidden ~/Desktop/*
]]
hs.execute(script)
end)

-- 切换 Light 和 Dark 模式
hs.hotkey.bind({"ctrl"}, "D", function()
script = [[
tell application "System Events"
tell appearance preferences
set dark mode to not dark mode
end tell
end tell
]]
hs.osascript.applescript(script)
end)

--------------------------------------------------------------------------------------
-- 按下 "Alt+键" 会打开或激活对应的应用,如果应用不是绝对路径,则指的是 /Applications 中的应用 --
--------------------------------------------------------------------------------------
function openAppWithHotkey(keyAppPairs)
for key, app in pairs(keyAppPairs) do
-- local app = entry[2]
-- local key = entry[1]

-- 路径不以 / 开头,则指的是 /Applications 中的应用,把路径补充完整
if string.sub(app, 0, 1) ~= "/" then
app = "/Applications/" .. app
end

-- hs.alert.show(app)

hs.hotkey.bind({"alt"}, key .. "", function()
hs.application.open(app)

-- 解决某些应用 x 全屏的时候 (如 Safari),切换到桌面 n,当前应用仍然为应用 x,按下激活应用 x 的快捷键不生效的问题
hs.application.frontmostApplication():setFrontmost(true)
end)
end
end

openAppWithHotkey(KEY_APP_PAIRS)

--------------------------------------------------------------------------------------
-- 窗口管理 --
--------------------------------------------------------------------------------------
-- 窗口最大化
hs.hotkey.bind({"alt", "ctrl"}, "Return", function()
sizeFocusedWindow("Max")
end)

-- 窗口左半屏
hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Left", function()
sizeFocusedWindow("Half Left")
end)

-- 窗口右半屏
hs.hotkey.bind({"alt", "ctrl", "cmd"}, "Right", function()
sizeFocusedWindow("Half Right")
end)

-- 窗口靠左
hs.hotkey.bind({"alt", "ctrl"}, "Left", function()
moveFocusedWindow("Edge Left")
end)

-- 窗口靠右
hs.hotkey.bind({"alt", "ctrl"}, "Right", function()
moveFocusedWindow("Edge Right")
end)

-- 窗口居中
hs.hotkey.bind({"alt", "ctrl"}, "C", function()
moveFocusedWindow("Center")
end)

-- 设置当前窗口的大小
function sizeFocusedWindow(mode)
local win = hs.window.focusedWindow()
local f = win:frame()
local screen = win:screen()
local max = screen:frame()

f.x = max.x
f.y = max.y
f.w = max.w
f.h = max.h

if mode == "Max" then

end
if mode == "Half Left" then
f.w = max.w / 2
end
if mode == "Half Right" then
f.x = max.x + max.w / 2
f.w = max.w / 2
end

win:setFrame(f, 0) -- 0 取消动画
end

-- 移动当前窗口
function moveFocusedWindow(mode)
local win = hs.window.focusedWindow()
local f = win:frame()
local screen = win:screen()
local max = screen:frame()

if mode == "Edge Left" then
f.x = max.x
f.y = 0
end
if mode == "Edge Right" then
f.x = max.x + (max.w - f.w)
f.y = 0
end
if mode == "Center" then
f.x = max.x + (max.w - f.w) / 2
f.y = max.y + (max.h - f.h) / 5 * 2
f.x = f.x > 0 and f.x or 0
f.y = f.y > 0 and f.y or 0
end

win:setFrame(f, 0) -- 0 取消动画
end


--------------------------------------------------------------------------------------
-- 多屏管理 --
--------------------------------------------------------------------------------------

-- 在屏幕间移动光标
hs.hotkey.bind({'ctrl'}, 'z', function()
local screen = hs.mouse.getCurrentScreen()
local nextScreen = screen:next()
local rect = nextScreen:fullFrame()
local center = hs.geometry.rectMidPoint(rect)

hs.mouse.setAbsolutePosition(center)
hs.alert.show('🐻‍❄️🦮🐶🦅🐘🦁', nextScreen)

-- hs.timer.doAfter(0.6, function()
-- end)
end)

-- 在屏幕间移动程序
hs.hotkey.bind({'ctrl'}, 'x', function()
local win = hs.window.focusedWindow()
local screen = win:screen()
win:moveToScreen(screen:next(), 0)

-- 移动到其他屏幕后应用的标题包含 "RDP-Windows" 则全屏
if string.find(win:title(), "RDP-Windows", 1, true) then
win:setFullScreen(true)
end
end)

--------------------------------------------------------------------------------------
-- 蓝牙管理 --
--------------------------------------------------------------------------------------

-- 去掉字符串后面的空白字符
function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end

-- 打开与关闭蓝牙,需要安装蓝牙工具 brew install blueutil
hs.hotkey.bind({'alt'}, 't', function()
-- 打开或关闭蓝牙
hs.execute("/opt/homebrew/bin/blueutil -p toggle")

-- 检查蓝牙状态
local state = trim(hs.execute("/opt/homebrew/bin/blueutil -p"))

if state == "1" then
hs.alert.show("蓝牙已打开")
else
hs.alert.show("蓝牙已关闭")
end
end)

-- 系统事件监听回调函数,事件类型可参考 https://www.hammerspoon.org/docs/hs.caffeinate.watcher.html
-- 系统休眠时关闭蓝牙: https://gist.github.com/ysimonson/fea48ee8a68ed2cbac12473e87134f58
function watchCallback(event)
-- 18 点后休眠时才自动关闭蓝牙
local hour = os.date("*t").hour
if event == hs.caffeinate.watcher.systemWillSleep and hour >= 18 then
hs.execute("/opt/homebrew/bin/blueutil -p 0")
end
end

watcher = hs.caffeinate.watcher.new(watchCallback)
watcher:start()

--------------------------------------------------------------------------------------
-- Send key --
--------------------------------------------------------------------------------------
-- VIM 模式的方向键:H J K L
hs.hotkey.bind({"alt"}, "H", function()
hs.eventtap.keyStroke({}, 'left')
end)
hs.hotkey.bind({"alt"}, "L", function()
hs.eventtap.keyStroke({}, 'right')
end)
hs.hotkey.bind({"alt"}, "J", function()
hs.eventtap.keyStroke({}, 'down')
end)
hs.hotkey.bind({"alt"}, "K", function()
hs.eventtap.keyStroke({}, 'up')
end)
hs.hotkey.bind({"alt"}, "I", function()
hs.eventtap.keyStroke({}, 'delete')
end)
hs.hotkey.bind({"alt"}, "Z", function()
hs.eventtap.keyStroke({"cmd", "shift"}, '[')
end)
hs.hotkey.bind({"alt"}, "X", function()
hs.eventtap.keyStroke({"cmd", "shift"}, ']')
end)

Hammerspoon 还能实现非常多的其他功能,可以阅读帮助文档查看更多 API 的使用。