MacOS 使用 Hammerspoon 进行窗口管理

背景

最近把公司电脑从 Windows 换到了 MacOS, 在重新配置 OS 阶段,发现在 MacOS 上进行窗口管理比较痛苦,自己之前的个人 MaCOS 一直使用的是Moom 来进行管理,但是使用功能很有限,加上现在 MacOS 自身的窗口提示会与 Moom 冲突,所以决定更换一个。我个人对于窗口管理的需求就是方便的在不同显示器切换,焦点窗口全屏、半屏、1/3 屏转换快速。对于平铺式窗口管理觉得学习成本和适应成本可能有些高,不打算折腾,之前看到 disksing 使用 Hammerspoon 进行窗口管理的文章: 适合程序员的桌面窗口管理方案 ,打算尝试一下。

Hammerspoon

Hammerspoon 是一个开源的自动化工具,可以实现很多功能,包含不限于:窗口管理、桌面管理、Wifi 触发器、时钟管理等等,所有功能都是通过 Lua 语言来实现的。用户可以自己编写 Lua,或者使用 Spoons 中提供的一些 Lua 模块来简化配置。

官网包含了一个简单的 Window resize 示例,还是比较好理解的,先找到当前focuse 窗口,然后找到当前窗口大小,调整大小为目标值,重新设置窗口:

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
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Left", function()
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 / 2
f.h = max.h
win:setFrame(f)
end)

```

---



## 窗口管理

在 Github 上可以看到很多 Hammerspoon 的示例,目前写的比较完善的有以下几个:
* https://github.com/ashfinal/awesome-hammerspoon
* https://github.com/wangshub/hammerspoon-config
* https://github.com/S1ngS1ng/HammerSpoon

虽然 Hammerspoon 提供了很多功能,但是我不想用它来实现很多功能,毕竟 AllinOne 我(没)不(什)是(么)很(好)喜(下)欢(场)。目前还没有使用 MacOS 存在一个常用的布局,所以先实现一些快捷键来满足需求就好。这里使用的 Spoon 包含两个:WinWin 和 ModalMgr。也不打算进行配置拆分,所有配置都在 init.lua 中写明,具体配置如下:

```lua
hs.hotkey.alertDuration = 0
hs.hints.showTitleThresh = 0
hs.window.animationDuration = 0

hsreload_keys = hsreload_keys or {{"cmd", "shift", "ctrl"}, "R"}
if string.len(hsreload_keys[2]) > 0 then
hs.hotkey.bind(hsreload_keys[1], hsreload_keys[2], "重新加载配置!", function() hs.reload() end)
hs.alert.show("配置文件已经重新加载! ")
end

hs.loadSpoon("ModalMgr")
hs.loadSpoon("WinWin")


if spoon.WinWin then
spoon.ModalMgr:new("resizeM")
local cmodal = spoon.ModalMgr.modal_list["resizeM"]
cmodal:bind('', 'escape', '退出 ', function() spoon.ModalMgr:deactivate({"resizeM"}) end)
cmodal:bind('', 'Q', '退出', function() spoon.ModalMgr:deactivate({"resizeM"}) end)
cmodal:bind('', 'tab', '键位提示', function() spoon.ModalMgr:toggleCheatsheet() end)

cmodal:bind('', 'A', '向左移动', function() spoon.WinWin:stepMove("left") end, nil, function() spoon.WinWin:stepMove("left") end)
cmodal:bind('', 'D', '向右移动', function() spoon.WinWin:stepMove("right") end, nil, function() spoon.WinWin:stepMove("right") end)
cmodal:bind('', 'W', '向上移动', function() spoon.WinWin:stepMove("up") end, nil, function() spoon.WinWin:stepMove("up") end)
cmodal:bind('', 'S', '向下移动', function() spoon.WinWin:stepMove("down") end, nil, function() spoon.WinWin:stepMove("down") end)

cmodal:bind('', 'H', '左半屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfleft") end)
cmodal:bind('', 'L', '右半屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfright") end)
cmodal:bind('', 'K', '上半屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfup") end)
cmodal:bind('', 'J', '下半屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfdown") end)

cmodal:bind('', 'Y', '屏幕左上角', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerNW") end)
cmodal:bind('', 'O', '屏幕右上角', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerNE") end)
cmodal:bind('', 'U', '屏幕左下角', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerSW") end)
cmodal:bind('', 'I', '屏幕右下角', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerSE") end)

cmodal:bind('', 'F', '全屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("fullscreen") end)
cmodal:bind('', 'C', '居中', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("center") end)
cmodal:bind('', 'G', '左三分之二屏居中分屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("centermost") end)
cmodal:bind('', 'Z', '展示显示', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("show") end)
cmodal:bind('', 'V', '编辑显示', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("shows") end)

cmodal:bind('', 'X', '二分之一居中分屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("center-2") end)

cmodal:bind('', '=', '窗口放大', function() spoon.WinWin:moveAndResize("expand") end, nil, function() spoon.WinWin:moveAndResize("expand") end)
cmodal:bind('', '-', '窗口缩小', function() spoon.WinWin:moveAndResize("shrink") end, nil, function() spoon.WinWin:moveAndResize("shrink") end)

cmodal:bind('ctrl', 'H', '向左收缩窗口', function() spoon.WinWin:stepResize("left") end, nil, function() spoon.WinWin:stepResize("left") end)
cmodal:bind('ctrl', 'L', '向右扩展窗口', function() spoon.WinWin:stepResize("right") end, nil, function() spoon.WinWin:stepResize("right") end)
cmodal:bind('ctrl', 'K', '向上收缩窗口', function() spoon.WinWin:stepResize("up") end, nil, function() spoon.WinWin:stepResize("up") end)
cmodal:bind('ctrl', 'J', '向下扩镇窗口', function() spoon.WinWin:stepResize("down") end, nil, function() spoon.WinWin:stepResize("down") end)


cmodal:bind('', 'left', '窗口移至左边屏幕', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("left") end)
cmodal:bind('', 'right', '窗口移至右边屏幕', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("right") end)
cmodal:bind('', 'up', '窗口移至上边屏幕', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("up") end)
cmodal:bind('', 'down', '窗口移动下边屏幕', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("down") end)
cmodal:bind('', 'space', '窗口移至下一个屏幕', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("next") end)
cmodal:bind('', 'B', '撤销最后一个窗口操作', function() spoon.WinWin:undo() end)
cmodal:bind('', 'R', '重做最后一个窗口操作', function() spoon.WinWin:redo() end)

cmodal:bind('', '[', '左三分之二屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("mostleft") end)
cmodal:bind('', ']', '右三分之二屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("mostright") end)
cmodal:bind('', ',', '左三分之一屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("lesshalfleft") end)
cmodal:bind('', '.', '中分之一屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("onethird") end)
cmodal:bind('', '/', '右三分之一屏', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("lesshalfright") end)

cmodal:bind('', 't', '将光标移至所在窗口中心位置', function() spoon.WinWin:centerCursor() end)

hsresizeM_keys = hsresizeM_keys or {"alt", "R"}
if string.len(hsresizeM_keys[2]) > 0 then
spoon.ModalMgr.supervisor:bind(hsresizeM_keys[1], hsresizeM_keys[2], "进入窗口管理模式", function()
spoon.ModalMgr:deactivateAll()
spoon.ModalMgr:activate({"resizeM"}, "#B22222")
end)
end
end



spoon.ModalMgr.supervisor:enter()

目前尝试增加应用切换窗口自动处理,但是 M1 上的 Hammerspoon 模块会 hang,还没搞清楚怎么回事,后续有时间再看看。