Hammerspoon优化MacOS多屏幕体验
2025-5-30更新
这里提到的第二个bug可以在聚焦前调用 win:raise()
再进行 focus()
。这样可以解决窗口没法聚焦,且不会影响主屏幕的窗口。
Mac上多屏幕最常见的使用痛点就是:
- 焦点丢失(
cmd+w
和cmd+m
); cmd+tab
切出来finder没有窗口且没有焦点,切出关闭了窗口的app丢失焦点;cmd+tab
没法切出最小化的窗口;- 跨多屏幕移动光标,触摸板要冒火星子;
- 同一个app的多个窗口跨多屏幕,
cmd+tab
或者cmd+`
切换时候不知道切哪里去了;
上面部分问题其实是cmd+w
的使用问题,换成cmd+h
就好了,但是cmd+w
他距离近,单手就能操作啊 😂 。
当熟悉了快捷键之后,Mac上三指上滑选择app、单击触摸板和滑动触摸板其实是效率很低的操作,操作频繁了小臂肌肉酸胀是真的遭不住(特别是点击和滑动触摸板)。
Hammerspoon文档 非常详细,可用的API都列了出来。
本文主要使用屏幕和窗口相关的东西,还有很多强大的功能,可以参考官方文档自己试试。
Tips:
- 如果发现
hs.application.watcher
事件 触发异常 ,注意触发器是不是使用了local
修饰。 - 各种事件触发不了,特别是微信,重启Hammerspoon有奇效。
- debug过程会导致Hammerspoon内存占用提升,弄好了记得重启app。
- Hammerspoon坐标都是从屏幕左上角算(0, 0),可以参考 hs.geometry
使用中发现的bug
- canvas 相关操作当重载配置成功后不要立刻有canvas.show操作,不然canvas无法从屏幕上消失。
- 当有app的窗口跨多个显示器时,外接屏幕上的窗口调用
focus()
之后可能无法获取焦点,可以查看这个issue (看起来像是个陈年老bug)。
该bug最大的问题就是导致当A 2个窗口最大化分布在主屏幕和屏幕2时,在主屏幕最大化窗口上开另一个窗口化的B,此时如果让屏幕2上的A窗口获取焦点会失败,且焦点是到了A在主屏幕上的窗口,导致B被遮住。正常移动鼠标到屏幕2上获取焦点是不会出现这个问题的。
基础函数和基本变量
基础变量:
1 |
|
移动光标:
1 |
|
提示信息canvas(复用整个用于提示的canvas):
1 |
|
将目标屏幕第一个顺序窗口设置为焦点:
1 |
|
这里遇到个问题,在app多个窗口分布在不同屏幕时候,在调用 focus()
的时候窗口会出现无法获得焦点的情况。
这里可以用个临时解决方法,获取焦点的同时将鼠标移动到窗口上,发送一个鼠标左键点击事件,但是可能会出现误点击的情况。
获取目标屏幕的下一个屏幕:
1 |
|
多屏幕间移动鼠标
这里绑定的是 alt+tab
,聚焦下一个屏幕的最前窗口,然后将光标移动过去,并且显示提示信息,这里是屏幕编号。
1 |
|
cmd+tab
切出关闭或者最小化窗口的app
cmd+tab
切换app是激活了app,所以需要订阅app的激活事件。同时只需要快捷键触发的窗口激活事件,使用 activedByShortcut
来判断是否来自于快捷键激活。
1 |
|
appWatcher
这里不能使用 local
修饰,否则监听器只能处理一次就会被垃圾回收器回收了。
重新打开app:
1 |
|
这里需要注意finder是没法 cmd+q
退出,且所有窗口关闭也会有一个窗口在,需要单独处理。
监听快捷键:
1 |
|
event事件一定要返回false,否则其他app就无法监听到该事件了。
注意这里的 shortcutWatcher
同样是不能用 local
修饰,不然会出现事件只能触发一次的问题。应该是和lua内部垃圾收集器有关系,局部变量直接被回收了。
关闭或最小化窗口后自动获取下一个窗口焦点
这里需要绑定窗口的一些事件过滤器,可以参考 hs.window.filter 。
注意下面两个事件,微信触发不稳定,不清楚什么情况,如果触发不了就重启Hammerspoon试试。
1 |
|
很多app都会触发 windowDestroyed
事件,所以这里定义一个 destroyedByShortcut
用于判断是否是快捷键触发的关闭窗口(鼠标单击关闭窗口也可以处理,这里没处理)。
Hammerspoon是没有api能判断窗口事件触发时候是快捷键还是鼠标的,还需要监听一个键盘按下的事件:
1 |
|
shortcutWatcher
和上面的一样,可以放一起。
窗口获取焦点屏幕提醒
这个操作和上面类似,订阅窗口焦点事件:
1 |
|
同样也只处理快捷键触发的事件:
1 |
|
窗口管理
将窗口移动到指定屏幕
将当前焦点的窗口移动到下一个屏幕:
1 |
|
多桌面管理(hs.spaces)
hs.spaces
属于 实验功能 。
hs.spaces.allSpaces()
获取到的id和系统中桌面id没太大关联:1
2
3
4
5> hs.spaces.allSpaces()
{
["xxx-2D66-02CA-B9F7-xxx"] = { 4, 5, 1 },
["xxx-B95C-45F1-A8D1-xxx"] = { 6022 }
}在mac内屏幕创建一个桌面之后
1
2
3
4
5> hs.spaces.allSpaces()
{
["xxx-2D66-02CA-B9F7-xxx"] = { 4, 5, 1, 6058 },
["xxx-B95C-45F1-A8D1-xxx"] = { 6022 }
}从上面看出返回的桌面顺序应该是对的,但是id却没什么踪迹可循了。显示器的id也是用的UUID。
文档建议系统设置中开启显示器拥有额外的空间,那么定位桌面位置只能通过返回的桌面数组下标来确定了。hs.spaces.spacesForScreen([screen])
通过屏幕ID获取桌面(有序)。hs.spaces.windowsForSpace(spaceID)
获取桌面所有窗口。hs.spaces.windowSpaces(window)
获取窗口所在桌面。hs.spaces.spacesForScreen([screen])
获取屏幕所有桌面。hs.spaces.moveWindowToSpace
API在macOS 15上不可用,参考 #3698 。hs.spaces.gotoSpace
是通过模拟操作实现的,动画较长,体验一般,且在macOS 15上不可用。
后续更新
采用上面的方案,使用标志位来判断是否是快捷键触发的操作,在使用rime输入法时候会出现:
多tab窗口(比如自带终端)在关闭一个tab之后(还有其他tab),焦点切换到下一个app。
调试发现在终端切换中英文输入时候会触发 windowDestroyed
事件,且在 cmd+w
关闭时候输入法的事件是先于tab的, 导致标志位被异常触发。
解决方式就是排除输入法的事件不进行处理,且不处理自带终端的 cmd+w
事件。
同时还出现了奇怪的跨窗口canvas提醒,明明都在一个屏幕上进行的窗口切换,这个后面有稳定复现再排查了。
windowDestroyed
该事件在不同app下的触发机制是不一样的,常见的Chrome、Sublime Text等如果是一个窗口多个tab,关闭一个tab不会触发事件。
但是在macOS的自带终端中又是相反,关闭任意一个tab都会触发该事件,且 window:tabCount()
返回0。
又仔细翻了下 hs.window.filter
文档,发现是实验性质的。
上面提到的几个窗口事件目前就 windowFocused
触发是没什么问题。
本站所有文章除特别声明外,均采用 BY-NC-SA 4.0 许可协议。转载请注明出处!