hammerspoon-AppWindowSwitcher 快速启动及切换 App
# 现状
目前不论是 Manico
还是 Raycast
等都可以用来快速启动和切换 App,但都有一个问题,那就是使用快捷键打开窗口后,无法再使用该快捷键切换到相同程序的其他窗口。
比如我配置的是 alt + k
打开 VSCode
窗口,当我打开 3 个 VSCode
窗口时,按 alt + k
就会切换到第一个 VSCode
窗口,再按 alt + k
就隐藏窗口了,再按 alt + k
就会把 3 个窗口都展示出来,想要切换到别的 VSCode
窗口,只能使用 cmd + `,而理想情况是我可以无脑按 alt + k
来切换到任意一个 VSCode
窗口,但目前这些软件都做不到。
# 需求
我这里列一下我的需求吧:
- 当没有打开 vscode 时,使用 alt + k 打开
- 当已经打开 vscode,但没有聚焦(不在最前面),使用 alt + k 聚焦到 vscode
- 当只有一个 vscode 打开时,使用 alt + k 最小化 vscode
- 当打开多个 vscode 时,使用 alt + k 可以循环切换
# 解决方案
在咨询了 codeium (opens new window) 后,它推荐我使用 hammerspoon
来实现,并且也按照上面的需求给我生成了代码:
-- 存储窗口列表的全局变量
windows = {}
-- 更新窗口列表
function updateWindows(bundleID)
windows = {}
local apps = hs.application.runningApplications()
for i, app in ipairs(apps) do
if app:bundleID() == bundleID then
local wins = app:allWindows()
for j, win in ipairs(wins) do
if win:isStandard() then
table.insert(windows, win)
end
end
end
end
end
-- 切换窗口
function switchApp(bundleID)
updateWindows(bundleID)
local visibleWindows = hs.fnutils.filter(windows, function(win)
return win:isVisible()
end)
if #windows == 0 then
-- 没有打开该应用程序,打开它
local appName = hs.application.infoForBundleID(bundleID).CFBundleName
hs.application.launchOrFocus(appName)
elseif #visibleWindows == 0 then
-- 有应用程序打开,但没有一个在前台,将第一个移至前台
windows[1]:focus()
elseif #visibleWindows == 1 then
-- 当前应用程序的窗口在前台
local win = hs.window.frontmostWindow()
local isCurrentAppFrontmost = false
if win:id() == visibleWindows[1]:id() then
isCurrentAppFrontmost = true
end
if isCurrentAppFrontmost then
-- 当前应用程序在前台,将其隐藏
visibleWindows[1]:application():hide()
else
-- 当前应用程序不在前台,将其窗口移至前台
visibleWindows[1]:focus()
end
else
-- 循环切换窗口
-- 这里逻辑有点问题:比如当前 currentIndex 是 1,那就切换到第二个窗口
-- 然后第二个窗口的 currentIndex 就变成 1 了,再切换的时候又切换回原来那个窗口了
-- 不会在多个窗口中循环切换,只会在两个窗口中循环切换
local currentIndex = 0
for i, win in ipairs(visibleWindows) do
if win:id() == hs.window.frontmostWindow():id() then
currentIndex = i + 1
end
end
if currentIndex > #visibleWindows then
currentIndex = 1
end
visibleWindows[currentIndex]:focus()
end
end
-- 绑定 Alt + K 切换 VS Code
hs.hotkey.bind('alt', 'k', function()
switchApp('com.microsoft.VSCode')
end)
-- 绑定 Alt + J 切换 Edge
hs.hotkey.bind('alt', 'j', function()
switchApp('com.microsoft.edgemac')
end)
-- 绑定 Alt + H 切换 Finder
hs.hotkey.bind({ "alt" }, "h", function()
hs.application.launchOrFocus("Finder")
end)
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
# 解决结果
基本上把我的需求都实现了,但还有一点问题:当打开超过 2 个窗口后,切换的时候,会只在前两个窗口循环切换,而不是在所有窗口中循环切换
注:只能说目前的 AIGC
很强大,我一个从来没有接触过 lua
的人都可以写出这种代码了
那我们还需要解决一下多窗口切换的问题
但这个一直没有找到一个合适的解决方案,而且 codeium
也没有给出好方案,有时候还会导致以前的功能无法使用,所以还是需要再努力一下啦。(笑
# 使用 Hammerspoon AppWindowSwitcher 快速启动及切换 App
直到在 hammerspoon
的插件商城中找到了这个:Hammerspoon docs: AppWindowSwitcher (opens new window)
可以在多窗口之间来回切换,但缺少两个功能:
- 当没有打开 vscode 时,使用 alt + k 打开
- 当只有一个 vscode 打开时,使用 alt + k 最小化 vscode
所以我打算改造一下这个插件,实现这两个功能
# 改造插件
# 没有打开软件时
通过查看源码发现当没有打开软件的时候,只是弹窗提示了一下:
if newW then
newW:raise():focus()
else
hs.alert.show("No window open for " ..
hs.inspect(matchtexts))
end
2
3
4
5
6
那这样就好办了,我们直接使用命令打开该软件就可以了
hs.application.launchOrFocus(matchtexts)
结果发现 matchtexts
是一个表结构:
if type(matchtexts) == "string" then
matchtexts = {matchtexts} -- further code assumes a table
2
询问 codeium
后给出的答案:
这段代码检查变量 matchtexts 是否是一个字符串。如果是的话,它将 matchtexts 转换成一个表(Lua 中的数组),这样接下来的代码就可以统一地假设 matchtexts 是一个表,并据此进行操作。
这是一个常见的编程模式,用于确保数据类型的一致性,特别是当一个函数可以接受多种类型的输入时。在这种情况下,代码首先将所有的输入标准化为表,然后剩余的代码只需要处理这一种类型。
以下是一段完整的示例,以展示如何在函数中应用这种模式:
-- 假设这个函数可以接受一个字符串或者字符串表作为参数
function processTexts(matchtexts)
-- 如果 matchtexts 是一个字符串,将其转换为包含该字符串的表
if type(matchtexts) == "string" then
matchtexts = {matchtexts} -- 现在 matchtexts 总是一个表
end
-- 接下来的代码可以假设 matchtexts 是一个表,并相应地处理它
for _, text in ipairs(matchtexts) do
-- 对每个文本项进行处理
print(text) -- 这里只是打印出来,实际代码可能做更复杂的操作
end
end
-- 调用函数,传入字符串
processTexts("hello")
-- 调用函数,传入表
processTexts({"hello", "world"})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这个函数中,不管是传入一个字符串还是一个表,processTexts 都能正确地处理。通过在开始时对输入进行标准化,函数后面的逻辑就可以简化,因为它只需要处理一种数据类型。
然后我又问了一下如何取表里的值呢?
在 Lua 中,可以通过键(索引)来取表(table)里的值。表可以是数组风格的,使用整数索引,也可以是哈希表风格的,使用字符串或其他类型的键。下面是如何取表里值的示例:
数组风格的表
local fruits = {"apple", "banana", "orange"}
-- 获取第一个元素
local firstFruit = fruits[1] -- "apple"
-- 获取第二个元素
local secondFruit = fruits[2] -- "banana"
print(firstFruit) -- 输出 "apple"
print(secondFruit) -- 输出 "banana"
2
3
4
5
6
7
8
9
10
在数组风格的表中,索引通常从 1 开始。
哈希表风格的表
local person = {
name = "John",
age = 30,
isEmployed = true
}
-- 获取"name"键对应的值
local name = person["name"] -- 或者 person.name
-- 获取"age"键对应的值
local age = person["age"] -- 或者 person.age
print(name) -- 输出 "John"
print(age) -- 输出 30
2
3
4
5
6
7
8
9
10
11
12
13
14
在哈希表风格的表中,可以使用字符串作为键。
请注意,如果你尝试获取一个不存在的键的值,Lua 会返回 nil,表示没有值。
local value = person["salary"] -- person 表中没有 "salary" 这个键
print(value) -- 输出 "nil"
2
在 Lua 中,nil 也用于表示值的缺失或者未定义。
好,那我们就取第一个值就可以了:
hs.application.launchOrFocus(matchtexts[1])
结果发现不行,需要使用 launchOrFocusByBundleID
函数:
hs.application.launchOrFocusByBundleID(matchtexts[1])
至此,当没有打开 vscode 时,使用 alt + k 打开这个功能就做完了
再看另一个功能
# 只打开一个窗口时
主要是这段代码:
if obj.match(hs.window.focusedWindow(), matchtexts) then
-- app has focus, find last matching window
for _, w in pairs(hs.window.orderedWindows()) do
if obj.match(w, matchtexts) then
newW = w -- remember last match
end
end
else
2
3
4
5
6
7
8
focusedWindow
用于获取当前活动窗口,orderedWindows
用于获取所有窗口的列表(以层级堆叠的顺序)。
这段代码主要的逻辑为:
如果当前活动窗口与快捷键对应的窗口匹配,那么就说明当前软件已经打开并激活了(再最前面),那我们就循环所有窗口,找到与快捷键对应的最后一个窗口,并将其激活。
所以如果只有一个 VSCode
窗口的话,那 newW
与 focusedWindow
是同一个,只要比较这两个相不相同,就知道是不是只有一个打开的窗口了
if obj.match(focusedWindow, matchtexts) then
-- app has focus, find last matching window
for _, w in pairs(hs.window.orderedWindows()) do
if obj.match(w, matchtexts) then
newW = w -- remember last match
end
end
-- If the last matched window is the same as the focused window, hide it.
if newW and newW:id() == focusedWindow:id() then
newW:application():hide()
return
end
else
2
3
4
5
6
7
8
9
10
11
12
13
# 总结
ok, 到此我们就完成了我们的需求,切换软件更快了
当然我们也牺牲了原插件的一个功能:窗口名称匹配
比如原插件可以使用 [{"O", "o"}] = {hyper, "o"}
来实现 hyper-o
cycles all windows whose application title starts with "O" or "o".
但我们现在不行了,因为我们加了一个逻辑:当软件没有打开时要打开软件,但我们没法打开窗口标题以 O/o
开头的软件
不过目前我也没这个需求,就不管了
# 更新
最近看到了另一篇文章,也是讲如何切换窗口的:Using Hammerspoon to switch apps – rakhesh.com (opens new window)
这个老哥自己编写了一个切换窗口的功能,非常符合我的需求,故我想采用这个,而不是原插件的(毕竟原插件是遍历所有窗口,效率有点低,切换窗口有时候会卡顿一下)
他后面还扩展了一下一个快捷键切换多个 app 的功能,这个我就不需要了,故我只需要他 Mod 1
的代码即可,改动改动即可
新建一个 switchApp.lua
文件:
-- launch, focus or rotate application
local function switchApp(app)
local focusedWindow = hs.window.focusedWindow()
-- If already focused, try to find the next window
if focusedWindow and focusedWindow:application():bundleID() == app then
local appWindows = hs.application.get(app):allWindows()
if app == 'com.apple.finder' then
appWindows = hs.fnutils.filter(appWindows, function(win)
-- If the app is Finder, remove Desktop
return win:title() and win:title() ~= ''
end)
end
if #appWindows > 0 then
if #appWindows == 1 then
appWindows[1]:application():hide()
else
-- It seems that this list order changes after one window get focused,
-- let's directly bring the last one to focus every time
appWindows[#appWindows]:focus()
end
else -- this should not happen, but just in case
hs.application.launchOrFocusByBundleID(app)
end
else -- if not focused
hs.application.launchOrFocusByBundleID(app)
end
end
local shortcuts = {
{ 'O', 'com.tencent.WeWorkMac' },
{ 'I', 'com.colliderli.iina' },
{ 'L', 'com.googlecode.iterm2' },
{ 'K', 'com.microsoft.VSCode' },
{ 'J', 'com.microsoft.edgemac' },
{ 'H', 'com.apple.finder' },
{ ';', 'com.tencent.xinWeChat' },
}
for i, shortcut in ipairs(shortcuts) do
hs.hotkey.bind('alt', shortcut[1], function()
switchApp(shortcut[2])
end)
end
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
在 init.lua
中引入:
require('./switchApp')
这样就可以快速高效的切换窗口了
当然以上代码我都放在 GitHub (opens new window) 上了,如果你觉得好用,请点个 star 吧
- 01
- 搭配 Jenkins 实现自动化打包微前端多个项目09-15
- 02
- 自动化打包微前端多个项目09-15
- 03
- el-upload 直传阿里 oss 并且显示自带进度条和视频回显封面图06-05