Hammerspoon / hammerspoon

Staggeringly powerful macOS desktop automation with Lua
http://www.hammerspoon.org
MIT License
11.99k stars 582 forks source link

Hammerspoon hangs spradically when entering hyper mode and displaying a modal window #3555

Open tobx opened 11 months ago

tobx commented 11 months ago

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
option+W enter hyper mode and display a modal window

In hyper mode:

Keyboard shortcut Description
W  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)
tobx commented 11 months ago

Today Hammerspoon did not react again and I noticed something more:

  1. Even if I completely quit the Hammerspoon process and start it again, the problem does not get resolved. I have to take the whole MacBook to sleep and wake it up again to resolve the problem.

  2. Hammerspoon does not really hang when I start hyper mode. I can still use the arrow keys to move my windows around, but I cannot use any of the "letter"-keys to quick-launch applications. I can also not use option+W to exit hyper mode.

  3. That might be important. When I am in hyper mode and press the arrow keys, they are captured by Hammerspoon and used to move windows, but the rest of the keys ("letter"-keys) are not captured and are entered in the currently active application as if they were not bound to Hammerspoon. Again, I use this extensively for days without an issue and suddenly this happens. I have not yet found any special behavior that I do different when this happens.

Rhys-T commented 11 months ago

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 blocking hs.hotkeys as well - see #2880. Some people in that thread can still get hs.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:

function hyper:entered()
  if hs.eventtap.isSecureInputEnabled() then
    hs.alert("⚠️ Secure Input is on. Hyper Mode commands might not work.")
  end

  -- The rest of this is the code you already had here:
  hyperAlerts = {}
  for i, screen in pairs(hs.screen.allScreens()) do
    alert = hs.alert("Hyper Mode ✈", hyperStyle, screen, "")
    hyperAlerts[i] = alert
  end
end

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 Escape in the modal to an exit command, so you can at least get out of hyper mode when this happens - it sounds like Escape hotkeys still work while SIM is on.[^canttest] (Or maybe just add hyper:exit(); return after 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] in hyper:entered() (thus taking you out of the password field or whatever), then restoring focus to the correct window in hyper:exited(). (Of course, all the other commands that look at hs.window.focusedWindow() would need to be changed to work on whatever window was saved by hyper: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 an hs.canvas. hs.webviews don't directly have a focus method either, but you can at least do someWebview:hswindow():focus().

See also: #2897, #2858

tobx commented 11 months ago

@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 Monitoring only shows two apps that I hardly ever use. I tried with both apps open and had no issue with my script. I added the isSecureInputEnabled and report back if there is more information.

tobx commented 9 months ago

@Rhys-T

It took a while, but it happened again.

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

I have not yet found anything that I did different or any new apps that I used. I do my day to day work and suddenly it happens.

function hyper:entered()
  if hs.eventtap.isSecureInputEnabled() then
    hs.alert("⚠️ Secure Input is on. Hyper Mode commands might not work.")
  end

I did this and yes is is secure input mode that's causing the problem. Closing apps or changing focus manually did not help. Do you know by chance any way to figure out which app is currently using secure input mode?

asmagill commented 9 months ago

I came across https://alexwlchan.net/2021/secure-input/ which provides a python script you can run to determine which application(s) have secure input enabled and have tested it with Terminal.

I've worked with the IOKit registry before, and this should be addable as a function to Hammerspoon, but not certain when I will get a chance; in the mean time, this should help.

tobx commented 9 months ago

I came across https://alexwlchan.net/2021/secure-input/

This is great! With python3 -c 'import getpass; getpass.getpass()' I can now enable secure input mode and test how to handle it.

Anyway, when I enable secure input mode I cannot use my Hammerspoon script, but as soon as I switch the window focus to another window, I can use it again. This does not work in case of my described error. The Hammerspoon script does not work in this case no matter which window has focus, even not after restarting Hammerspoon.

So, now I have to wait for the next error occurrence and then try to use the mentioned python script to figure out which application is responsible for that.

Rhys-T commented 9 months ago

@tobx Maybe try changing the code to this, so that you don't have to worry about accidentally changing the secure input state while trying to run the command? (Make sure to change the path to the find_processes_using_secure_input script.)

function hyper:entered()
  hyperAlerts = {} -- moved this up here so that I can make these alerts match yours

  if hs.eventtap.isSecureInputEnabled() then
    local secureInputInfo = hs.execute("/path/to/find_processes_using_secure_input") -- change this path
    local msg = "⚠️ Secure Input is on. Hyper Mode commands might not work.\n"..secureInputInfo
    print(msg) -- leave a copy of the message in the console, so you can still see it after the alert goes away
    for i, screen in pairs(hs.screen.allScreens()) do
      hyperAlerts['Secure Input '..i] = hs.alert(msg, hyperStyle, screen, "")
    end
  end

  -- The rest of this is the code you already had here:
  for i, screen in pairs(hs.screen.allScreens()) do
    alert = hs.alert("Hyper Mode ✈", hyperStyle, screen, "")
    hyperAlerts[i] = alert
  end
end
tobx commented 9 months ago

It just happened again and the process blocking secure input is called loginwindow. The Keyboard Maestro Wiki has some information about it here. It is basically saying that there is nothing they can do. I have not found any options to figure out which daemon is blocking secure input.

I think I will give up here. The best workaround I found is to lock the screen with control + command + Q and then login again with the fingerprint. In any case this seems not to be an Hammerspoon issue.

Rhys-T commented 9 months ago

Yeah, I've seen lots of reports of loginwindow supposedly having SIM turned on. Some of them suggest that loginwindow is actually forgetting to turn it off. In others, it's actually being caused by some normal (non-daemon) app - often a password manager or web browser that the user had recently entered a password into - but the system is confused and lists it as loginwindow instead. Quitting that other app ends up fixing the problem. So it may still be worth trying to narrow the problem down to a specific app… although the fact that locking/unlocking the screen clears it up would seem to suggest that it really is loginwindow.

The only other thing I can think of to try is setting up a timer to watch for SIM to turn on/off and notify you, so that you can hopefully tell what's causing it that way:

local simCheckInterval <const> = 5 -- seconds
local simWasOn = false
local lastSIMApp = nil
simCheckTimer = hs.timer.doEvery(simCheckInterval, function()
    local simIsOn = hs.eventtap.isSecureInputEnabled()
    local simApp = simIsOn and hs.execute[[ps -c -o pid=,command= -p $(ioreg -l -w 0 | grep -Eo '"kCGSSessionSecureInputPID"=[0-9]+' | cut -d= -f2 | sort | uniq)]] or nil
    if simIsOn ~= simWasOn or simApp ~= lastSIMApp then
        local msg = 'Secure Input Mode is ' .. (simIsOn and 'ON' or 'OFF')
        if simIsOn then
            msg = msg .. '\nEnabled by:\n' .. simApp
        else
            msg = msg .. '\nWas enabled by:\n' .. lastSIMApp
        end
        msg = msg:gsub('loginwindow', 'unknown (supposedly loginwindow)')
        msg = msg:gsub('^%s*(.-)%s*$', '%1')
        print(msg)
        for i, screen in pairs(hs.screen.allScreens()) do
            hs.alert(msg, { fillColor = { hex = simIsOn and '300000' or '003000' } }, screen)
        end
        simWasOn = simIsOn
        lastSIMApp = simApp
    end
end)

You can adjust how often it checks on the first line. Smaller values will give a better idea of when it's getting turned on, but might put a bit more load on your system. It should display any changes using hs.alert so you see it immediately, as well as printing them to the console so you can go back and reread it after the alert goes away.

NightMachinery commented 8 months ago

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.webview2 in hyper:entered() (thus taking you out of the password field or whatever), then restoring focus to the correct window in hyper:exited(). (Of course, all the other commands that look at hs.window.focusedWindow() would need to be changed to work on whatever window was saved by hyper:entered().)

@Rhys-T Do you have the code for this workaround? My browser turns secure input mode on for password fields, and this greatly hinders me, as I have my clipboard manager open using a hyper keybinding.

Rhys-T commented 8 months ago

@Rhys-T Do you have the code for this workaround? My browser turns secure input mode on for password fields, and this greatly hinders me, as I have my clipboard manager open using a hyper keybinding.

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. ```lua local realCurrentWindow local maxSIMWaitTime = 0.25 -- seconds local focusStealingWebview = hs.webview.new{x=0, y=0, w=0, h=0} local isSecureInputEnabled = hs.eventtap.isSecureInputEnabled function hyper:entered() hyperAlerts = {} realCurrentWindow = hs.window.focusedWindow() if isSecureInputEnabled() then focusStealingWebview:show():hswindow():focus() -- Whichever app is enabling SIM might not disable it immediately. -- Watch for SIM to shut off, giving up after `maxSIMWaitTime` seconds. local endTime = hs.timer.absoluteTime() + maxSIMWaitTime*1000000000 -- convert to nanoseconds while isSecureInputEnabled() and hs.timer.absoluteTime() < endTime do -- Normally I try to avoid hs.timer.usleep, because it basically hangs Hammerspoon. -- But for really short periods like this, it's probably cleaner than rewriting with timers or coroutines. hs.timer.usleep(1000) end if isSecureInputEnabled() then -- Still in Secure Input Mode - give up and show alerts about it. local secureInputInfo = hs.execute[[ps -c -o pid=,command= -p $(ioreg -l -w 0 | grep -Eo '"kCGSSessionSecureInputPID"=[0-9]+' | cut -d= -f2 | sort | uniq]] local msg = "⚠️ Secure Input is on. Hyper Mode commands might not work.\nEnabled by:\n"..secureInputInfo msg = msg:gsub('loginwindow', 'unknown (supposedly loginwindow)') msg = msg:gsub('^%s*(.-)%s*$', '%1') print(msg) -- leave a copy of the message in the console, so you can still see it after the alert goes away for i, screen in pairs(hs.screen.allScreens()) do hyperAlerts['Secure Input '..i] = hs.alert(msg, hyperStyle, screen, "") end end end -- The rest of this is the alert code from the original post: 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 if realCurrentWindow then realCurrentWindow:focus() realCurrentWindow = nil end focusStealingWebview:hide() end -- Then in any functions that manipulate the current window, use `realCurrentWindow` instead of `hs.window.focusedWindow()`. ```
This version is based on @NightMachinery's version of the hyper mode config + the canvas-based indicator from #3586. ```lua local realCurrentWindow local maxSIMWaitTime = 0.25 -- seconds local focusStealingWebview = hs.webview.new{x=0, y=0, w=0, h=0} local hyperSIMAlerts local isSecureInputEnabled = hs.eventtap.isSecureInputEnabled function hyper:entered() hyper_modality.entered_p = true -- I have not yet added the redis updaters for purple_modality. redisActivateMode("hyper_modality") realCurrentWindow = hs.window.focusedWindow() if isSecureInputEnabled() then focusStealingWebview:show():hswindow():focus() -- Whichever app is enabling SIM might not disable it immediately. -- Watch for SIM to shut off, giving up after `maxSIMWaitTime` seconds. local endTime = hs.timer.absoluteTime() + maxSIMWaitTime*1000000000 -- convert to nanoseconds while isSecureInputEnabled() and hs.timer.absoluteTime() < endTime do -- Normally I try to avoid hs.timer.usleep, because it basically hangs Hammerspoon. -- But for really short periods like this, it's probably cleaner than rewriting with timers or coroutines. hs.timer.usleep(1000) end if isSecureInputEnabled() then -- Still in Secure Input Mode - give up and show alerts about it. local secureInputInfo = hs.execute[[ps -c -o pid=,command= -p $(ioreg -l -w 0 | grep -Eo '"kCGSSessionSecureInputPID"=[0-9]+' | cut -d= -f2 | sort | uniq]] local msg = "⚠️ Secure Input is on. Hyper Mode commands might not work.\nEnabled by:\n"..secureInputInfo msg = msg:gsub('loginwindow', 'unknown (supposedly loginwindow)') msg = msg:gsub('^%s*(.-)%s*$', '%1') print(msg) -- leave a copy of the message in the console, so you can still see it after the alert goes away hyperSIMAlerts = {} for i, screen in pairs(hs.screen.allScreens()) do hyperSIMAlerts[i] = hs.alert(msg, screen, "") end end end hyperModeIndicator:show() end function hyper:exited() hyper_modality.entered_p = false hyper_modality.exit_on_release_p = false hyperModeIndicator:hide() if hyperSIMAlerts then for i, alert in pairs(hyperSIMAlerts) do hs.alert.closeSpecific(alert, 0.25) end end if realCurrentWindow then realCurrentWindow:focus() realCurrentWindow = nil end focusStealingWebview:hide() redisDeactivateMode("hyper_modality") end -- Then in any functions that manipulate the current window, use `realCurrentWindow` instead of `hs.window.focusedWindow()`. ```

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 realCurrentWindow instead of hs.window.focusedWindow(). Any hyper mode commands that change which window is focused should do something like:

someWindow:focus()
if focusStealingWebview:isVisible() then
  focusStealingWebview:hswindow():focus() -- to make sure that the hotkeys don't stop working
end
realCurrentWindow = someWindow -- so you don't get sent back to the original window after releasing hyper
NightMachinery commented 8 months ago

@Rhys-T I tried this. I even changed the webview to make it visible. The webview shows up correctly, but it doesn't get focused; the focus is still on the password input bar.

In the code below, I have removed the timer check; I just manually wait for almost a second myself. The focus never changes.

local focusStealingWebview = hs.webview.new{x=0, y=0, w=500, h=500}

function hyper_modality:entered()
    hyper_modality.entered_p = true

    -- I have not yet added the redis updaters for purple_modality.
    redisActivateMode("hyper_modality")

    if isSecureInputEnabled() then
        realCurrentWindow = hs.window.focusedWindow()

        focusStealingWebview:show():hswindow():focus()
    else
        realCurrentWindow = nil
    end

    if hyper_alert_canvas_p then
        hyperModeIndicator:show()
    else
        -- ...
    end
end
NightMachinery commented 8 months ago

I am currently doing

local function doEscape()
    hs.eventtap.keyStroke({}, "escape")
end

instead to make the password input element de-focused, and it works. It doesn't return the focus to the password element afterwards though.

Rhys-T commented 8 months ago

@Rhys-T I tried this. I even changed the webview to make it visible. The webview shows up correctly, but it doesn't get focused; the focus is still on the password input bar.

Odd… it works on my machine. In hindsight, however, I'm not sure why it's working - if I'm understanding the docs correctly, I didn't set the webview to be able to receive keyboard focus!

Can you try adding one or both of these after the line that first creates the webview?

focusStealingWebview:allowTextEntry(true)
-- and/or
focusStealingWebview:windowStyle(hs.webview.windowMasks.titled)

Other info that might be helpful:


I am currently [simulating an Escape press] instead to make the password input element de-focused, and it works. It doesn't return the focus to the password element afterwards though.

Interesting. Escape doesn't normally de-focus inputs in any of the major browsers as far as I know. Are you running any sort of extension like Vimium or Tridactyl that could be adding that? In any case, if we can't get the webview approach working, de-focusing the input could be a viable alternative to de-focusing the entire window - for some apps. Doesn't seem to work in terminals, though - at least for Apple Terminal and iTerm.

local prevFocusedElement

-- in hyper_modality:entered(), if SIM is on
local axApp = hs.axuielement.applicationElement(hs.application.frontmostApplication())
if axApp then
    prevFocusedElement = axApp.AXFocusedUIElement
    prevFocusedElement.AXFocused = false
end

-- in hyper_modality:exited()
if prevFocusedElement and prevFocusedElement:isValid() then
    prevFocusedElement.AXFocused = true
end
NightMachinery commented 8 months ago

@Rhys-T

env:


I tried commenting

hs.dockicon.hide()

but it didn't change anything.

I tried both

focusStealingWebview:allowTextEntry(true)
-- and/or
focusStealingWebview:windowStyle(hs.webview.windowMasks.titled)

but they didn't work. The web view does get the focus IF a password input bar is not in focus.

When the web view does get the focus, it does not return it when it is hidden. The web view also has a problem of being only in the space it was created on.

I also tried on a non-browser app with a password input bar. The results were the same. Unfortunately, the escape trick does not work with non-browser apps.

The AXFocused trick works great on the browser, but it doesn't work on that other app; when I do prevFocusedElement.AXFocused = false, it has no effects and prevFocusedElement.AXFocused stays true. Is there a way I can defocus the element more forcefully?

JonathanDoughty commented 7 months ago

Not sure if it would help but I take a different approach when Hammerspoon tells me that Secure Input Mode is enabled for a field: I temporarily keep a reference to the table with password info that I would normally eventtap.keyStrokes() in, and set a timer to wipe that reference from memory.

Then when the called function sees that reference it extracts the pass phrase from the password info, sets the pasteboard contents to that, and sets another timer to clear that from the pasteboard.

Finally the characters from the pasteboard get individually eventtap.keyStroke()-ed in, along with any modifiers, with a slight delay so that too-smart algorithms don't object to non-human input speeds.

Full disclosure-wise, there is - of course - a brief period when the password is accessible to other apps on the paste/clipboard. I chose this approach because occasionally I would run into obstinate apps that rolled their own "secure input". For them I would copy their password to the clip board and then just invoke the pasteboard character stroking via a separate key binding.

See also the example configuration for this, which is all geared toward using passwords maintained in KeyChain.

This has been working for me for years, through Ventura; I have not taken the Sonoma plunge yet. The references to modal in the discussion above, however, remind me that using ModalMgr for these key bindings broke with Big Sur and I never put in the effort to address that breakage. I switched back to not using modal key bindings for password entry. This may all be unhelpful as a result.