hammerspoon: Hammerspoon hangs spradically when entering hyper mode and displaying a modal window
Hi,
I am using Hammerspoon for a few years now and I have a very hard to track down issue. Every once in a while (a week or a few days or so) my script hangs without any error messages in the Hammerspoon Console.
I am using macOS 12.6.8. I am using my script to quick launch applications and as a window manager. It works like that:
| Keyboard shortcut | Description |
|---|---|
| <kbd>option</kbd>+<kbd>W</kbd> | enter hyper mode and display a modal window |
In hyper mode:
| Keyboard shortcut | Description |
|---|---|
| <kbd>W</kbd> | close hypermode |
| Arrows | control window position |
| Other keys | quick launch applications or control windows in other ways |
I use that script extensively every day, but only every few days or weeks when I open hyper mode, the modal shows up, but the script does not react to key strokes in hyper mode. There is no error message or any other message in the Hammerspoon Console when I press a key.
Reload config stops the script and closes the modal window, but the issue persists when entering hyper mode again. By trial and error over the years I realized that the issue gets resolved when I put my MacBook to sleep and log in again. Then again the issue comes back sporadically. I guess it is some timing issue / race condition.
I hope someone has an idea how to track down the issue when it occurs the next time. The good thing is, as soon as it appears I can reproduce it, by reloading the script until I login or reboot the next time.
Since I have no idea which part of the script is relevant for the bug, I post the full script in here:
local browser = "org.mozilla.firefox"
local editor = "com.sublimetext.4"
local fileManager = "com.apple.finder"
local passwordManager = "org.keepassxc.keepassxc"
local terminal = "org.alacritty"
local youtube = "/Applications/Firefox.app/Contents/MacOS/firefox -new-tab https://www.youtube.com &"
hs.window.animationDuration = 0
local windowGap = 4
local hyperStyle = {
fadeInDuration = 0,
fillColor = { white = 1, alpha = 2 / 3 },
radius = 24,
strokeColor = { red = 19 / 255, green = 182 / 255, blue = 133 / 255, alpha = 1},
strokeWidth = 16,
textColor = { white = 0.125 },
textSize = 48,
}
local hyper = hs.hotkey.modal.new("option", "W")
local hyperAlerts
function hyper:entered()
hyperAlerts = {}
for i, screen in pairs(hs.screen.allScreens()) do
alert = hs.alert("Hyper Mode ✈", hyperStyle, screen, "")
hyperAlerts[i] = alert
end
end
function hyper:exited()
for i, alert in pairs(hyperAlerts) do
hs.alert.closeSpecific(alert, 0.25)
end
end
hyper:bind("", "B", function()
hs.application.launchOrFocusByBundleID(browser)
hyper:exit()
end)
hyper:bind("", "E", function()
hs.application.launchOrFocusByBundleID(editor)
hyper:exit()
end)
hyper:bind("", "F", function()
hs.application.launchOrFocusByBundleID(fileManager)
hyper:exit()
end)
hyper:bind("", "K", function()
hs.application.launchOrFocusByBundleID(passwordManager)
hyper:exit()
end)
hyper:bind("", "R", function() hs.reload() end)
hyper:bind("", "T", function()
hs.application.launchOrFocusByBundleID(terminal)
hyper:exit()
end)
hyper:bind("", "W", function() hyper:exit() end)
hyper:bind("", "Y", function()
os.execute(youtube)
hyper:exit()
end)
local windowPositions = {
UP=1,
RIGHT=2,
DOWN=3,
LEFT=4
}
local windowPosition = Nil
-- Move window to bottom of screen if it has a fixed ratio
function correctBottomPosition()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
if windowFrame.h < (screenFrame.h - windowGap) / 2 then
windowFrame.y = screenFrame.y + screenFrame.h - windowFrame.h
window:setFrame(windowFrame)
end
end
-- Move window to right of screen if it has a fixed ratio
function correctRightPosition()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
if windowFrame.w < (screenFrame.w - windowGap) / 2 then
windowFrame.x = screenFrame.x + screenFrame.w - windowFrame.w
window:setFrame(windowFrame)
end
end
hyper:bind("", "Left", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x
windowFrame.w = (screenFrame.w - windowGap) / 2
if windowPosition ~= windowPositions.UP and windowPosition ~= windowPositions.DOWN then
windowFrame.y = screenFrame.y
windowFrame.h = screenFrame.h
end
window:setFrame(windowFrame)
if windowPosition == windowPositions.DOWN then
correctBottomPosition()
end
windowPosition = windowPositions.LEFT
end)
hyper:bind("", "Right", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x + (screenFrame.w + windowGap) / 2
windowFrame.w = (screenFrame.w - windowGap) / 2
if windowPosition ~= windowPositions.UP and windowPosition ~= windowPositions.DOWN then
windowFrame.y = screenFrame.y
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
else
window:setFrame(windowFrame)
correctRightPosition()
end
if windowPosition == windowPositions.DOWN then
correctBottomPosition()
end
windowPosition = windowPositions.RIGHT
end)
hyper:bind("", "Up", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.y = screenFrame.y
windowFrame.h = (screenFrame.h - windowGap) / 2
if windowPosition ~= windowPositions.LEFT and windowPosition ~= windowPositions.RIGHT then
windowFrame.x = screenFrame.x
windowFrame.w = screenFrame.w
end
window:setFrame(windowFrame)
if windowPosition == windowPositions.RIGHT then
correctRightPosition()
end
windowPosition = windowPositions.UP
end)
hyper:bind("", "Down", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.y = screenFrame.y + (screenFrame.h + windowGap) / 2
windowFrame.h = (screenFrame.h - windowGap) / 2
if windowPosition ~= windowPositions.LEFT and windowPosition ~= windowPositions.RIGHT then
windowFrame.x = screenFrame.x
windowFrame.w = screenFrame.w
window:setFrame(windowFrame)
else
window:setFrame(windowFrame)
correctBottomPosition()
end
if windowPosition == windowPositions.RIGHT then
correctRightPosition()
end
windowPosition = windowPositions.DOWN
end)
hyper:bind("", "D", function()
local window = hs.window.focusedWindow()
window:moveToScreen(window:screen():next())
end)
hyper:bind("", "Space", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
hyper:bind("", "C", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x + screenFrame.w / 6
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w * 2 / 3
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
hyper:bind("", "Tab", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x + (screenFrame.w - windowFrame.w) / 2
windowFrame.y = screenFrame.y + (screenFrame.h - windowFrame.h) / 2
window:setFrame(windowFrame)
end)
hyper:bind("", "1", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w * 2 / 3
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
hyper:bind("", "2", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x + screenFrame.w * 2 / 3
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w * 1 / 3
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
hyper:bind("", "3", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w * 1 / 3
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
hyper:bind("", "4", function()
local window = hs.window.focusedWindow()
local windowFrame = window:frame()
local screenFrame = window:screen():frame()
windowFrame.x = screenFrame.x + screenFrame.w * 1 / 3
windowFrame.y = screenFrame.y
windowFrame.w = screenFrame.w * 2 / 3
windowFrame.h = screenFrame.h
window:setFrame(windowFrame)
end)
About this issue
- Original URL
- State: open
- Created 8 months ago
- Comments: 16 (1 by maintainers)
I haven’t fully tested this, because on my system SIM doesn’t interfere with
hs.hotkey, but I’ve tested the basic idea of having Hammerspoon steal focus to make another app turn off SIM, and it seems to work for both Firefox password fields and iTerm. In theory it should be something like this:This version is based on the hyper mode code that @tobx originally posted.
This version is based on @NightMachinery's version of the hyper mode config + the canvas-based indicator from #3586.
If
hyper:entered()detects that SIM is on, it will grab focus using the webview, then wait up to 1⁄4 second for SIM to turn off. (If it’s still on at that point, it gives up and shows a warning, attempting to tell you what app has SIM enabled.)hyper:exited()then puts focus back in the original window.Any hyper mode commands that manipulate the current window will need to be modified to use
realCurrentWindowinstead ofhs.window.focusedWindow(). Any hyper mode commands that change which window is focused should do something like:@Rhys-T Thank you a lot already. I am pretty sure that I did not always use the same app when the issue occurred, but I will double check that and note which apps I used when the issue occurs again. 50% of me using that script is moving the terminal position, but I only use Alacritty.
Input Monitoringonly shows two apps that I hardly ever use. I tried with both apps open and had no issue with my script. I added theisSecureInputEnabledand report back if there is more information.Are there specific apps that you’re usually in when you have this problem? I’m wondering if it could be an app enabling secure input mode (a.k.a. “secure keyboard input”).
Basically, an app can put the system into a mode where no other apps (like, for instance, Hammerspoon) can watch what’s being typed. This normally gets turned on when you’re in a password field. Some terminal apps turn it on by default too (at least iTerm2 and Apple’s Terminal, though possibly not Alacritty?). And sometimes certain apps can forget to turn it off again afterwards.
I think this originally just affected
hs.eventtaps, but at some point started blockinghs.hotkeys as well - see #2880. Some people in that thread can still geths.hotkeys that involve modifier keys to work, but others can’t. I wonder if it’s something like “ignore any hotkeys that were created after secure input was turned on (e.g. by a modal being entered)”.Try changing your
hyper.entered()function to look like this:If the modal only gets stuck after showing that warning message, then it’s definitely secure input mode that’s causing the problem. You might also want to bind <kbd>Escape</kbd> in the modal to an exit command, so you can at least get out of hyper mode when this happens - it sounds like <kbd>Escape</kbd> hotkeys still work while SIM is on.[^canttest] (Or maybe just add
hyper:exit(); returnafter the warning message, to immediately kick you back out of hyper mode before it gets stuck.)[^canttest]: I’m stuck on an older system at the moment, so I’m afraid I can’t test this yet.
If the app that’s responsible still turns SIM off when it’s done, you might be able to work around it by creating and focusing an invisible
hs.webview[^whynotcanvas] inhyper:entered()(thus taking you out of the password field or whatever), then restoring focus to the correct window inhyper:exited(). (Of course, all the other commands that look aths.window.focusedWindow()would need to be changed to work on whatever window was saved byhyper:entered().)[^whynotcanvas]: Edit 11/17/2023: I originally said to use an empty
hs.canvas, because it’s probably more lightweight than a webview, but Hammerspoon doesn’t seem to give you a way to focus anhs.canvas.hs.webviews don’t directly have afocusmethod either, but you can at least dosomeWebview:hswindow():focus().See also: #2897, #2858