Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • Mac

    • macOS 上有哪些值得推荐的常用软件
    • Karabiner-Elements
    • Alfred
    • Mac 自定义应用程序快捷键
    • Mac 下搭建 Java 开发环境
    • Mac 常用快捷键
    • Mac 使用 技巧
    • Mac 终端软件安装利器 - Homebrew
    • MacTalk
    • iTerm2 用法与技巧
    • Mac 使用 Homebrew 安装 node
    • Mac 使用 VS Code 配合 Remote Development 插件连接 Windows 远程服务器
    • 借助 Homebrew Cask, 教你快速下载安装 Mac App 新姿势
    • hammerspoon-AppWindowSwitcher 快速启动及切换 App
      • 现状
      • 需求
      • 解决方案
      • 解决结果
      • 使用 Hammerspoon AppWindowSwitcher 快速启动及切换 App
      • 改造插件
        • 没有打开软件时
        • 只打开一个窗口时
      • 总结
      • 更新
    • 使用魔法上网后无法访问部分国内网站
  • Windows

  • Linux

  • ShowyEdge
  • 开发效率提升之工具篇
  • 操作系统
  • Mac
Henry
2023-12-04
目录
现状
需求
解决方案
解决结果
使用 Hammerspoon AppWindowSwitcher 快速启动及切换 App
改造插件
没有打开软件时
只打开一个窗口时
总结
更新

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)
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

# 解决结果

基本上把我的需求都实现了,但还有一点问题:当打开超过 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
1
2
3
4
5
6

那这样就好办了,我们直接使用命令打开该软件就可以了

hs.application.launchOrFocus(matchtexts)
1

结果发现 matchtexts 是一个表结构:

if type(matchtexts) == "string" then
  matchtexts = {matchtexts} -- further code assumes a table
1
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"})
1
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"
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在哈希表风格的表中,可以使用字符串作为键。

请注意,如果你尝试获取一个不存在的键的值,Lua 会返回 nil,表示没有值。

local value = person["salary"] -- person 表中没有 "salary" 这个键
print(value) -- 输出 "nil"
1
2

在 Lua 中,nil 也用于表示值的缺失或者未定义。

好,那我们就取第一个值就可以了:

hs.application.launchOrFocus(matchtexts[1])
1

结果发现不行,需要使用 launchOrFocusByBundleID 函数:

hs.application.launchOrFocusByBundleID(matchtexts[1])
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
1
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
1
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
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

在 init.lua 中引入:

require('./switchApp')
1

这样就可以快速高效的切换窗口了

当然以上代码我都放在 GitHub (opens new window) 上了,如果你觉得好用,请点个 star 吧

编辑 (opens new window)
#hammerspoon#AppWindowSwitcher#Mac
上次更新: 12/10/2023, 10:19:16 AM
借助 Homebrew Cask, 教你快速下载安装 Mac App 新姿势
使用魔法上网后无法访问部分国内网站

← 借助 Homebrew Cask, 教你快速下载安装 Mac App 新姿势 使用魔法上网后无法访问部分国内网站→

最近更新
01
搭配 Jenkins 实现自动化打包微前端多个项目
09-15
02
自动化打包微前端多个项目
09-15
03
el-upload 直传阿里 oss 并且显示自带进度条和视频回显封面图
06-05
更多文章>
0 comments
Anonymous
Markdown is supported

Be the first person to leave a comment!

Theme by Vdoing | Copyright © 2017-2025 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式