首页
首页
文章目录
  1. 🛠️ 工具准备
  2. 💡 实现逻辑
  3. 📜 脚本代码
  4. 更新版本
  5. 最终版?
  6. 最最终版
  7. ✅ 使用说明
  8. 🔧 技术细节优化
  9. 🧾 总结

多显示器环境下,向特定程序发送空格键并恢复焦点(Quark 网盘)

最近在使用夸克看教程,需要频繁地暂停并跟着视频做。当前夸克是不支持通过媒体按键控制播放和暂停的,只能使用空格控制,并只有获取到焦点时才能生效。

现在需要无需切换光标或窗口,就能对另一个屏幕上的程序发送控制命令(如播放/暂停)

以 macOS 为例,使用 Hammerspoon 实现以下自动化流程:

  • 聚焦某个程序(如 Quark 网盘)
  • 向其发送空格键(控制播放/暂停)
  • 然后自动恢复原程序焦点与鼠标位置

🛠️ 工具准备

💡 实现逻辑

  1. 使用快捷键(如 Option + 空格)触发脚本  
  2. 记录当前前台应用与鼠标位置  
  3. 激活目标程序(Quark 网盘)并发送空格键  
  4. 智能判断鼠标是否移动,决定是否恢复位置  
  5. 切回原程序

📜 脚本代码

将以下代码添加到你的 ~/.hammerspoon/init.lua 中:

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

hs.hotkey.bind({"alt"}, "space", function()

    local quarkAppName = "Quark"

    local frontApp = hs.application.frontmostApplication()

    local originalMousePos = hs.mouse.getAbsolutePosition()



    local quarkApp = hs.application.find(quarkAppName)

    if quarkApp then

        local win = quarkApp:mainWindow()

        if win then

            quarkApp:activate(true)

            win:focus()



            hs.timer.doAfter(0.2, function()

                hs.eventtap.event.newKeyEvent({}, "space", true):post()

                hs.eventtap.event.newKeyEvent({}, "space", false):post()



                hs.timer.doAfter(0.3, function()

                    if frontApp then frontApp:activate(true) end



                    -- 检测鼠标是否移动,如果没有再恢复位置

                    local currentMousePos = hs.mouse.getAbsolutePosition()

                    local dx = math.abs(currentMousePos.x - originalMousePos.x)

                    local dy = math.abs(currentMousePos.y - originalMousePos.y)

                    if dx < 5 and dy < 5 then

                        hs.mouse.setAbsolutePosition(originalMousePos)

                    end

                end)

            end)

        else

            hs.alert("未找到 Quark 窗口")

        end

    else

        hs.alert("未找到 Quark 应用")

    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
44
45
46
-- 监听系统的播放键(Play/Pause)
local quarkPlayKeyTap = hs.eventtap.new({hs.eventtap.event.types.systemDefined}, function(event)
local eventData = event:systemKey()

-- 只处理按下的 Play 键
if not eventData or eventData.key ~= "PLAY" or not eventData.down then
return false
end

local quarkAppName = "夸克网盘"
local quarkApp = hs.application.find(quarkAppName)

-- 如果夸克没运行,让系统继续处理播放键
if not quarkApp then
return false
end

local frontApp = hs.application.frontmostApplication()
local originalMousePos = hs.mouse.getAbsolutePosition()
local win = quarkApp:mainWindow()

-- 如果没有窗口,也放行
if not win then
return false
end

-- 激活夸克并发空格
quarkApp:activate(true)
win:focus()

hs.timer.doAfter(0.2, function()
hs.eventtap.keyStroke({}, "space")

hs.timer.doAfter(0.3, function()
if frontApp then frontApp:activate(true) end
local currentMousePos = hs.mouse.getAbsolutePosition()
if math.abs(currentMousePos.x - originalMousePos.x) < 5 and math.abs(currentMousePos.y - originalMousePos.y) < 5 then
hs.mouse.setAbsolutePosition(originalMousePos)
end
end)
end)

return true -- 阻止默认行为(避免 Music.app 被激活)
end)

quarkPlayKeyTap:start()

最终版?

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
local log = hs.logger.new("🔊 播放键", "info")
local quarkAppName = "夸克网盘"

-- 全局变量,防止 GC
tap = nil
local lastTriggered = 0

local function isFrontmostAppQuark()
local frontApp = hs.application.frontmostApplication()
if not frontApp then return false end
local name = frontApp:name()
if not name then return false end
return name == quarkAppName
end

local lastFocusedWindow = nil
local lastMousePos = nil

local function sendSpaceToQuark()
local now = hs.timer.secondsSinceEpoch()
if now - lastTriggered < 1.5 then
log.i("⚠️ 操作过于频繁,忽略本次触发")
return
end
lastTriggered = now

local frontApp = hs.application.frontmostApplication()
log.i("当前前台应用: " .. (frontApp and frontApp:name() or "未知"))

lastMousePos = hs.mouse.absolutePosition()
lastFocusedWindow = hs.window.frontmostWindow()

local quarkApp = hs.application.find(quarkAppName)
if not quarkApp then
log.e("❌ 未找到 '" .. quarkAppName .. "' 应用")
return
end
log.i("'" .. quarkAppName .. "' 窗口数量: " .. tostring(#quarkApp:allWindows()))

local tryCount = 0
local maxTries = 10
local tryInterval = 0.3

local function tryActivate()
tryCount = tryCount + 1
log.i("尝试切换焦点到 '" .. quarkAppName .. "',次数:" .. tryCount)
quarkApp:activate(true)

hs.timer.doAfter(tryInterval, function()
local quarkWindows = quarkApp:visibleWindows()
if #quarkWindows > 0 then
log.i("聚焦 '" .. quarkAppName .. "' 可见窗口")
quarkWindows[1]:focus()
else
log.w("⚠️ '" .. quarkAppName .. "' 无可见窗口")
end

if isFrontmostAppQuark() then
log.i("✅ 成功切换焦点到 '" .. quarkAppName .. "',发送空格键")
hs.eventtap.keyStroke({}, "space")
hs.timer.doAfter(0.05, function()
hs.eventtap.keyStroke({}, "space")
log.i("⬇️ 空格键发送 x2")
hs.timer.doAfter(0.2, function()
if lastFocusedWindow and lastFocusedWindow:application() then
log.i("🔄 尝试恢复焦点到原应用")
local newMousePos = hs.mouse.absolutePosition()
if newMousePos.x == lastMousePos.x and newMousePos.y == lastMousePos.y then
local app = lastFocusedWindow:application()
app:activate(true)
hs.timer.doAfter(0.1, function()
if lastFocusedWindow:isVisible() then
lastFocusedWindow:focus()
log.i("✅ 焦点已恢复")
else
log.w("⚠️ 原窗口不可见")
end
end)
else
log.i("🖱️ 鼠标移动过,未恢复焦点")
end
else
log.w("⚠️ 无原焦点窗口")
end
end)
end)
else
if tryCount < maxTries then
log.w("⚠️ 焦点切换失败,第 " .. tryCount .. " 次," .. tryInterval .. " 秒后重试")
hs.timer.doAfter(tryInterval, tryActivate)
else
log.e("❌ 多次尝试切换焦点失败,放弃")
end
end
end)
end

tryActivate()
end

local function buildTap()
return hs.eventtap.new({ hs.eventtap.event.types.systemDefined }, function(event)
local data = event:systemKey()
if data then
log.i("检测系统键: key=" .. tostring(data.key) .. ", down=" .. tostring(data.down))
if data.key == "PLAY" then
log.i("🎬 捕获播放键")
sendSpaceToQuark()
return true
end
end
return false
end, { allowBubbles = true })
end

tap = buildTap()
tap:start()

-- 每分钟检查 tap 是否正常运行,否则重建
hs.timer.doEvery(60, function()
if not tap:isRunning() then
log.w("⚠️ tap 未运行,尝试重建")
if tap then tap:stop() end
tap = buildTap()
tap:start()
log.i("🔄 已重建 tap 监听器")
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
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
local log = hs.logger.new("🔊 播放键", "info")
local quarkAppName = "夸克网盘"

-- 全局变量,防止 GC
tap = nil
local lastTriggered = 0

local function isFrontmostAppQuark()
local frontApp = hs.application.frontmostApplication()
if not frontApp then return false end
local name = frontApp:name()
if not name then return false end
return name == quarkAppName
end

local lastFocusedWindow = nil
local lastMousePos = nil

local function sendSpaceToQuark()
local now = hs.timer.secondsSinceEpoch()
if now - lastTriggered < 1.5 then
log.i("⚠️ 操作过于频繁,忽略本次触发")
return
end
lastTriggered = now

local frontApp = hs.application.frontmostApplication()
log.i("当前前台应用: " .. (frontApp and frontApp:name() or "未知"))

lastMousePos = hs.mouse.absolutePosition()
lastFocusedWindow = hs.window.frontmostWindow()

local quarkApp = hs.application.find(quarkAppName)
if not quarkApp then
log.e("❌ 未找到 '" .. quarkAppName .. "' 应用")
return
end
log.i("'" .. quarkAppName .. "' 窗口数量: " .. tostring(#quarkApp:allWindows()))

local tryCount = 0
local maxTries = 10
local tryInterval = 0.3

local function tryActivate()
tryCount = tryCount + 1
log.i("尝试切换焦点到 '" .. quarkAppName .. "',次数:" .. tryCount)
quarkApp:activate(true)

hs.timer.doAfter(tryInterval, function()
local quarkWindows = quarkApp:visibleWindows()
if #quarkWindows > 0 then
-- 优先寻找标题包含“视频”字样的窗口
local targetWindow = nil
for _, win in ipairs(quarkWindows) do
local title = win:title() or ""
log.i("检查窗口标题: " .. title)
if title:match("视频") or title:match("播放") or title:match("%.mp4") then
targetWindow = win
break
end
end
if targetWindow then
log.i("🎯 选择播放窗口: " .. (targetWindow:title() or "无标题"))
targetWindow:focus()
else
log.w("⚠️ 未找到播放窗口,聚焦第一个窗口")
quarkWindows[1]:focus()
end
else
log.w("⚠️ '" .. quarkAppName .. "' 无可见窗口")
end

if isFrontmostAppQuark() then
log.i("✅ 成功切换焦点到 '" .. quarkAppName .. "',发送空格键")
hs.eventtap.keyStroke({}, "space")
hs.timer.doAfter(0.05, function()
hs.eventtap.keyStroke({}, "space")
log.i("⬇️ 空格键发送 x2")
hs.timer.doAfter(0.2, function()
if lastFocusedWindow and lastFocusedWindow:application() then
log.i("🔄 尝试恢复焦点到原应用")
local newMousePos = hs.mouse.absolutePosition()
if newMousePos.x == lastMousePos.x and newMousePos.y == lastMousePos.y then
local app = lastFocusedWindow:application()
app:activate(true)
hs.timer.doAfter(0.1, function()
if lastFocusedWindow:isVisible() then
lastFocusedWindow:focus()
log.i("✅ 焦点已恢复")
else
log.w("⚠️ 原窗口不可见")
end
end)
else
log.i("🖱️ 鼠标移动过,未恢复焦点")
end
else
log.w("⚠️ 无原焦点窗口")
end
end)
end)
else
if tryCount < maxTries then
log.w("⚠️ 焦点切换失败,第 " .. tryCount .. " 次," .. tryInterval .. " 秒后重试")
hs.timer.doAfter(tryInterval, tryActivate)
else
log.e("❌ 多次尝试切换焦点失败,放弃")
end
end
end)
end

tryActivate()
end

local function buildTap()
return hs.eventtap.new({ hs.eventtap.event.types.systemDefined }, function(event)
local data = event:systemKey()
if data then
log.i("检测系统键: key=" .. tostring(data.key) .. ", down=" .. tostring(data.down))
if data.key == "PLAY" then
log.i("🎬 捕获播放键")
sendSpaceToQuark()
return true
end
end
return false
end, { allowBubbles = true })
end

tap = buildTap()
tap:start()

-- 每分钟检查 tap 是否正常运行,否则重建
hs.timer.doEvery(60, function()
if not tap:isRunning() then
log.w("⚠️ tap 未运行,尝试重建")
if tap then tap:stop() end
tap = buildTap()
tap:start()
log.i("🔄 已重建 tap 监听器")
end
end)

✅ 使用说明

  1. 启动 Quark 网盘并加载播放界面
  2. 回到其他应用继续工作(如浏览器、VS Code)
  3. 按下 Option + 空格
       - 自动切换至 Quark
       - 空格键触发播放/暂停
       - 稍后自动回到原应用
       - 如果鼠标没动,还原到原本的鼠标位置

    🔧 技术细节优化

  • 使用 hs.eventtap.event.newKeyEvent(),更底层模拟空格键,更兼容 Electron 应用(如 Quark 网盘)
  • app:activate(true) + win:focus() 保证窗口成为第一响应者
  • 鼠标恢复前做位置差判断,避免“跳回”造成干扰

🧾 总结

通过 Hammerspoon 脚本,可以在 macOS 上无感控制任意 App。即使是在多屏使用场景,也能做到。

不打断当前任务,一键遥控另一个程序行为。
自动化提升的不只是效率,更是专注力。