-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
207 lines (173 loc) · 5.85 KB
/
init.lua
File metadata and controls
207 lines (173 loc) · 5.85 KB
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
--- === WindowStrider ===
---
--- Keyboard-driven window switcher that cycles through windows of specified applications.
---
local obj = {}
obj.__index = obj
obj.name = "WindowStrider"
obj.version = "1.0"
obj.author = "Dubzer"
obj.license = "MIT"
local log = hs.logger.new("WindowStrider")
local prettyAlert = dofile(hs.spoons.resourcePath("prettyAlert.lua"))
local formatHotkey = dofile(hs.spoons.resourcePath("formatHotkey.lua"))
local tinsert, tsort = table.insert, table.sort
-- keep references to listeners to avoid getting garbage collected
local _keep = {}
---@generic T
---@param it T
---@return T
local function keep(it)
tinsert(_keep, it)
return it
end
local function wf_getWindowList(self, reverse)
local r = {}
for window, _ in pairs(self.windows) do
tinsert(r, window)
end
if reverse then
tsort(r, function(a, b)
return a.timeFocused < b.timeFocused
end)
else
tsort(r, function(a, b)
return a.timeFocused > b.timeFocused
end)
end
return r
end
---@class CycleState
---@field visited table<number, boolean> @Set-like table keyed by window id
---@field count integer @Number of windows currently in `visited`
---@field reversed boolean @Whether the list should be traversed in reverse
local CycleState = {}
CycleState.__index = CycleState
function CycleState.new()
---@type CycleState
local self = setmetatable({}, CycleState)
self:reset()
return self
end
function CycleState:reset()
self.visited = {}
self.count = 0
self.reversed = false
end
---@param windowId number
function CycleState:add(windowId)
if self.visited[windowId] == nil then
self.visited[windowId] = true
self.count = self.count + 1
end
end
--- @param apps table
--- @return function cycleWindows
local function createWindowSwitcher(apps)
local filter = keep(hs.window.filter.new(function(window)
local application = window:application()
if not application then return false end
local bundleID = application:bundleID()
for _, app in ipairs(apps) do
if bundleID == app and window:isStandard() then
return true
end
end
return false
end))
local cycleState = CycleState.new()
-- force wf to populate its internal list of windows
---@diagnostic disable-next-line: undefined-field
filter:keepActive()
return function()
local starttime = hs.timer.secondsSinceEpoch()
local focused = hs.window.focusedWindow()
if focused and focused:isFullScreen() then
return
end
local focusedApp = focused and focused:application()
local cycling = focusedApp and hs.fnutils.contains(apps, focusedApp:bundleID())
local windows = wf_getWindowList(filter, cycleState.reversed)
-- launch the app if no windows are found
if #windows == 0 then
hs.application.open(apps[1])
return
end
if cycling then
local focusedId = focused:id()
assert(focusedId ~= nil)
cycleState:add(focusedId)
-- wrap around if we've visited all windows
if cycleState.count == #windows then
cycleState:reset()
cycleState:add(focusedId)
cycleState.reversed = true
end
else
cycleState:reset()
end
-- find first unvisited window
for _, w in ipairs(windows) do
if cycleState.visited[w.id] == nil then
if not focused or w.id ~= focused:id() then
w.window:focus()
end
cycleState:add(w.id)
break
end
end
local timeTaken = hs.timer.secondsSinceEpoch() - starttime
if timeTaken > 0.03 then
log.d("long time taken: " .. timeTaken)
end
end
end
--- Binds a hotkey to switch between windows of the specified applications.
--- @param mods table The modifiers for the hotkey (e.g., {"option"})
--- @param key string The key for the hotkey (e.g., "2")
--- @param apps table A list of application bundle IDs (e.g., {"com.brave.Browser"})
function obj:bindHotkey(mods, key, apps)
local cycleWindows = createWindowSwitcher(apps)
keep(hs.hotkey.bind(mods, key, cycleWindows))
return self
end
--- Binds a hotkey for dynamic app pinning.
--- When pressed with the record modifier, pins the currently focused app.
--- When pressed without, cycles through windows of the pinned app.
--- @param mods table The base modifiers for the hotkey (e.g., {"option"})
--- @param key string The key for the hotkey (e.g., "1")
--- @param recordMod string Additional modifier for recording (e.g., "shift")
function obj:bindPinHotkey(mods, key, recordMod)
local pinnedBundleID = nil
local cycleWindows = nil
local recordModifiers = {}
for _, mod in ipairs(mods) do
tinsert(recordModifiers, mod)
end
tinsert(recordModifiers, recordMod)
keep(hs.hotkey.bind(recordModifiers, key, function()
local focused = hs.window.focusedWindow()
if not focused then
prettyAlert("⚠️", "No focused window to pin")
return
end
local app = focused:application()
if not app then
log.e("focused:application() returned nil")
return
end
pinnedBundleID = app:bundleID()
cycleWindows = createWindowSwitcher({pinnedBundleID})
prettyAlert("📌", app:name() .. " → " .. formatHotkey(mods, key))
end))
-- Switch hotkey: cycles through pinned app's windows
keep(hs.hotkey.bind(mods, key, function()
if not pinnedBundleID then
prettyAlert("⚠️", "No app pinned")
return
end
cycleWindows()
end))
return self
end
return obj