-
Notifications
You must be signed in to change notification settings - Fork 0
/
FrameMaster.lua
361 lines (325 loc) · 13.5 KB
/
FrameMaster.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
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
-- FrameMaster: https://github.com/sryo/Spoons/blob/main/FrameMaster.lua
-- Take control of your Mac's 'hot corners', menu bar, and dock.
local killMenu = true -- prevent the menu bar from appearing
local killDock = true -- prevent the dock from appearing
local onlyFullscreen = false -- but only on fullscreen spaces
local buffer = 4 -- increase if you still manage to activate them
local showTooltips = true -- set this to false to improve performance if necessary
local tooltipMaxLength = 50 -- maximum length for tooltip messages
local reopenAfterKill = true -- show an autoclosing modal to reopen the last killed app
local function getWindowTitle(cornerAction)
local window = hs.window.focusedWindow()
local title = window and window:title() or "Window"
return title, (cornerAction and cornerAction .. " " .. title or "No action")
end
local function getAppName(cornerAction)
local app = hs.application.frontmostApplication()
local appName = app and app:name() or "App"
return appName, (cornerAction and cornerAction .. " " .. appName or "No action")
end
local lastKilledApp = nil
function showReopenDialog()
if reopenAfterKill then
local message = "You just killed " .. lastKilledAppName .. ". Would you like to reopen it?"
local script = [[
tell application "System Events"
activate
display dialog "]] .. message .. [[" buttons {"Ignore", "Reopen"} default button 2 giving up after 5
if result is not missing value and button returned of result is "Reopen" then
return "Reopen"
else
return "Ignore"
end if
end tell
]]
local appleScriptTask = hs.task.new("/usr/bin/osascript", function(exitCode, stdOut, stdErr)
print("exitCode: " .. exitCode .. " stdOut: " .. stdOut .. " stdErr: " .. stdErr)
if exitCode == 0 and stdOut:find("Reopen") then
hs.application.launchOrFocusByBundleID(lastKilledApp)
end
end, { "-e", script })
appleScriptTask:start()
end
end
local function isDesktop()
local window = hs.window.focusedWindow()
return window and window:role() == "AXScrollArea"
end
local function getDockPosition()
local handle = io.popen("defaults read com.apple.dock orientation")
local result = handle:read("*a")
handle:close()
return result:gsub("^%s*(.-)%s*$", "%1")
end
local dockPos = getDockPosition()
hotCorners = {
topLeft = {
action = function()
local app = hs.application.frontmostApplication()
if not app or isDesktop() then return "No action" end
local window = app:focusedWindow()
local nextWindow = hs.window.orderedWindows()[2]
local executedActionMessage
if hs.eventtap.checkKeyboardModifiers().shift then
app:kill9()
if nextWindow then nextWindow:focus() end
lastKilledAppName = getAppName()
lastKilledApp = app:bundleID()
showReopenDialog()
print(lastKilledApp)
else
hs.eventtap.keyStroke({"cmd"}, "w")
hs.timer.usleep(100000) -- Wait a little for the close action to complete
local allWindows = app:allWindows()
local visibleWindows = {}
for i, win in ipairs(allWindows) do
if win:isVisible() then
table.insert(visibleWindows, win)
end
end
if #visibleWindows == 0 or not window then
app:kill()
if nextWindow then
hs.timer.doAfter(0.5, function() nextWindow:focus() end)
end
executedActionMessage = "Quitted " .. getAppName()
else
executedActionMessage = "Closed " .. getWindowTitle()
end
end
hs.timer.doAfter(.5, function()
local currentMousePosition = hs.mouse.absolutePosition()
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.mouseMoved, currentMousePosition):post()
end)
return executedActionMessage
end,
message = function()
local app = hs.application.frontmostApplication()
if hs.eventtap.checkKeyboardModifiers().shift then
return "Kill " .. getAppName()
elseif not app or not app:focusedWindow() then
return "Quit " .. getAppName()
else
return "Close " .. getWindowTitle()
end
end
},
topRight = {
action = function()
local window = hs.window.focusedWindow()
if not window or isDesktop() then return "No action" end
if hs.eventtap.checkKeyboardModifiers().shift then
window:toggleZoom()
return "Zoomed " .. getWindowTitle()
else
hs.eventtap.keyStroke({"ctrl", "cmd"}, "F")
return "Toggled Fullscreen for " .. getWindowTitle()
end
end,
message = function()
if hs.eventtap.checkKeyboardModifiers().shift then
return "Zoom " .. getWindowTitle()
else
return "Toggle Fullscreen for " .. getWindowTitle()
end
end
},
bottomRight = {
action = function()
local window = hs.window.focusedWindow()
if not window or isDesktop() or window:isFullScreen() then return "No action" end
if hs.eventtap.checkKeyboardModifiers().shift then
window:application():hide()
return "Hid " .. getWindowTitle()
else
window:minimize()
return "Minimized " .. getWindowTitle()
end
end,
message = function()
local window = hs.window.focusedWindow()
if not window or isDesktop() or (window and window:isFullScreen()) then return "" end
if hs.eventtap.checkKeyboardModifiers().shift then
return "Hide " .. getWindowTitle()
else
return "Minimize " .. getWindowTitle()
end
end
},
bottomLeft = {
action = function()
if hs.eventtap.checkKeyboardModifiers().shift then
local app = hs.application.get("System Preferences")
if not app then
hs.application.launchOrFocus("System Preferences")
return "Launched System Preferences"
else
app:activate()
return "Focused System Preferences"
end
else
local app = hs.application.get("Finder")
hs.application.launchOrFocus("Finder")
return "Launched Finder"
end
end,
message = function()
if hs.eventtap.checkKeyboardModifiers().shift then
return "Open System Preferences"
else
return "Open Finder"
end
end
}
}
local lastCorner = nil
local lastTooltipTime = 0
local lastTooltipCorner = nil
local function getCurrentScreenSize()
local currentScreen = hs.mouse.getCurrentScreen()
return currentScreen and currentScreen:currentMode() or nil
end
local screenSize = getCurrentScreenSize()
function updateScreenSize()
screenSize = getCurrentScreenSize()
end
screenWatcher = hs.screen.watcher.newWithActiveScreen(updateScreenSize)
screenWatcher:start()
function checkForHotCorner(x, y)
updateScreenSize()
return ((x <= 1 and y <= buffer) and "topLeft") or
((x >= screenSize.w - 1 and y <= buffer) and "topRight") or
((x >= screenSize.w - 1 and y >= screenSize.h - buffer) and "bottomRight") or
((x <= 1 and y >= screenSize.h - buffer) and "bottomLeft")
end
function showTooltip(corner)
if corner ~= lastTooltipCorner or hs.timer.secondsSinceEpoch() - lastTooltipTime >= 1 then
local message = hotCorners[corner].message()
if message ~= "" then
showMessage(lastCorner, message)
lastTooltipTime = hs.timer.secondsSinceEpoch()
lastTooltipCorner = corner
end
end
end
function truncateString(input, maxLength)
maxLength = maxLength or 50
if #input > maxLength then
local partLen = math.floor(maxLength / 2)
input = input:sub(1, partLen - 2) .. '...' .. input:sub(-partLen)
end
return input
end
tooltipAlert = hs.canvas.new({x = 0, y = 0, w = 1, h = 1})
tooltipAlert:level(hs.canvas.windowLevels._MaximumWindowLevelKey)
tooltipAlert[1] = {
type = "rectangle",
action = "fill",
roundedRectRadii = { xRadius = 4, yRadius = 4 },
fillColor = { white = 0, alpha = 0.75 },
frame = textFrame
}
tooltipAlert[2] = {
type = "text",
text = styledMessage,
textLineBreak = "clip",
textColor = { white = 1, alpha = 1 },
frame = textFrame
}
function showMessage(corner, message)
local fontSize = 20
local styledMessage = hs.styledtext.new(truncateString(message, tooltipMaxLength), {
font = {size = fontSize},
color = {white = 1, alpha = 1},
shadow = {
offset = {h = -1, w = 0},
blurRadius = 2,
color = {alpha = 1}
}
})
local textSize = hs.drawing.getTextDrawingSize(styledMessage)
local tooltipHeight = 24
local tooltipX = corner == "topLeft" or corner == "bottomLeft" or screenSize.w - textSize.w
local tooltipY = corner == "topLeft" or corner == "topRight" or screenSize.h - tooltipHeight
local textFrame = hs.geometry.rect(tooltipX, tooltipY, textSize.w, tooltipHeight)
tooltipAlert:frame(textFrame)
tooltipAlert[1].fillColor.alpha = 0.75
tooltipAlert[2].text = styledMessage
tooltipAlert[2].textColor.alpha = 1
tooltipAlert:alpha(1)
tooltipAlert:behavior("canJoinAllSpaces")
-- Note: for this to show up above full screen windows we need to hide the hammerspoon dock icon in hammerspoon preferences.
tooltipAlert:show()
if hideTooltipTimer then
hideTooltipTimer:stop()
end
hideTooltipTimer = hs.timer.doAfter(0.75, hideTooltip)
end
function hideTooltip()
local fadeOutDuration = 0.125
local fadeOutStep = 0.025
local fadeOutAlphaStep = fadeOutStep / fadeOutDuration
local currentAlpha = 1.0
local function fade()
local point = hs.mouse.absolutePosition()
if lastCorner == checkForHotCorner(point.x, point.y) then
-- cursor still in corner, return without fading tooltip.
return
end
currentAlpha = currentAlpha - fadeOutAlphaStep
tooltipAlert:alpha(currentAlpha)
if currentAlpha > 0 then
hs.timer.doAfter(fadeOutStep, fade)
else
tooltipAlert:hide()
end
end
fade()
end
if showTooltips then
cornerHover = hs.eventtap.new({hs.eventtap.event.types.mouseMoved, hs.eventtap.event.types.flagsChanged}, function(event)
local point = hs.mouse.absolutePosition()
local currentCorner = checkForHotCorner(point.x, point.y)
-- If the mouse is in a corner, update the tooltip message and the last corner
if currentCorner and not isDesktop() then
lastCorner = currentCorner
showMessage(lastCorner, hotCorners[lastCorner].message())
-- If the mouse moved out from the last corner, hide the tooltip and clear the last corner
elseif lastCorner and not currentCorner then
hideTooltip()
lastCorner = nil
end
-- If Shift key status changed while the mouse is in a corner, update the tooltip message
if lastCorner and event:getType() == hs.eventtap.event.types.flagsChanged then
showMessage(lastCorner, hotCorners[lastCorner].message())
end
local win = hs.window.focusedWindow()
local screenFrame = win and win:screen() and win:screen():fullFrame()
if screenFrame then
if (not onlyFullscreen) or (onlyFullscreen and win and win:isFullScreen()) then
if killMenu and not hs.eventtap.checkKeyboardModifiers().shift and event:location().y < buffer and (event:location().x > buffer and event:location().x < screenFrame.w - buffer) then
return true
elseif killDock and not hs.eventtap.checkKeyboardModifiers().shift then
if dockPos == "bottom" and (screenFrame.h - event:location().y) < buffer and (event:location().x > buffer and event:location().x < screenFrame.w - buffer) then
return true
elseif dockPos == "left" and event:location().x < buffer and (event:location().y > buffer and event:location().y < screenFrame.h - buffer) then
return true
elseif dockPos == "right" and (screenFrame.w - event:location().x) < buffer and (event:location().y > buffer and event:location().y < screenFrame.h - buffer) then
return true
end
end
end
end
return false
end):start()
end
cornerClick = hs.eventtap.new({hs.eventtap.event.types.leftMouseDown}, function(event)
local point = hs.mouse.absolutePosition()
lastCorner = checkForHotCorner(point.x, point.y)
if lastCorner and not isDesktop() then
local message = truncateString(hotCorners[lastCorner].action())
showMessage(lastCorner, message)
return true
end
return false
end):start()