asmagill / hs._asm.axuielement

Accessing Accessibility Objects with Hammerspoon
34 stars 2 forks source link

doMenuItem example broken in bigsur? #23

Closed psifertex closed 3 years ago

psifertex commented 3 years ago

I'm trying to adapt https://github.com/asmagill/hs._asm.axuielement/blob/master/examples/dockStuff.lua to be able to "love" music in apple music with a global hotkey. I'm dropping the following into my init.lua because I'm a slob. 😉

local axuielement = require("hs.axuielement")
local inspect     = require("hs.inspect")

function getDock()
    local dockElement = axuielement.applicationElement("Dock")
    assert(dockElement, "Unable to aquire Dock accessibility element")
    return dockElement
end

function getItemListFromDock()
    local dockElement = getDock()
    local axlist
    for i,v in ipairs(dockElement) do
        if v.AXRole == "AXList" and v.AXRoleDescription == "list" then
            axlist = v.AXChildren
            break
        end
    end
    assert(axlist, "Unable to get child list from Dock")
    return axlist
end

function doMenuItem(app, item)
    local axlist = getItemListFromDock()
    for i,v in ipairs(axlist) do
        if v.AXSubrole == "AXApplicationDockItem" and v.AXTitle == app then
            v:doAXShowMenu()
            for i2,v2 in ipairs(v[1]) do -- first child of application will be "AXMenu" and its children, the items in it.
                if v2.AXTitle == item then
                    v2:doAXPress()
                    return
                end
            end
            v:doAXShowMenu() -- close menu so we can error
            error(tostring(item) .. " not found in " .. tostring(app) .. " menu list")
        end
    end
    error(tostring(app) .. " not found in Dock application list")
end

--Love Music
hs.hotkey.bind({"cmd", "shift"}, "L", function() doMenuItem("Music", "Love") end)

The Music dock is successfully opened, but the "Love" item isn't clicked. Error is:

2021-01-04 14:38:38: 14:38:38 ERROR:   LuaSkin: hs.hotkey callback: attempt to index a nil value
stack traceback:
    [C]: in for iterator 'for iterator'
    /Users/jwiens/.hammerspoon/init.lua:139: in function 'doMenuItem'
    /Users/jwiens/.hammerspoon/init.lua:153: in function </Users/jwiens/.hammerspoon/init.lua:153>

Line 139 corresponds with the for i2, v2 in line.

I managed to get the code to work while doing some debugging by adding some logging lines just after doAXShowMenu() so I'm betting this is just a race-condition, but I don't know what the right fix would be?

asmagill commented 3 years ago

Most likely a race condition as you suspect... the solution is put in a delay or loop to detect when the menu is actually populated, but to make things responsive, it's probably best to put the loop into a coroutine so it can yield every few milliseconds and keep Hammerspoon or the OS from seeming unresponsive... I should probably update the example at some point with something like this:

-- How long to wait for menu to appear before assuming a problem
local menuTimeout = 5

function doMenuItem(app, item)
    local axlist = getItemListFromDock()
    for i,v in ipairs(axlist) do
        if v.AXSubrole == "AXApplicationDockItem" and v.AXTitle == app then
            local waitForMenu -- predeclare so it can be referred to within assignment
            waitForMenu = coroutine.wrap(function()
                v:doAXShowMenu()
                local startTime = os.time()
                local doLoop = true
                while doLoop do
                    -- this allows HS to do other things while waiting for menu to appear
                    coroutine.applicationYield()
                    doLoop = (#v == 0) and ((os.time() - startTime) < menuTimeout)
                end
                -- this makes coroutine an upvalue so it won't be collected
                waitForMenu = nil
                if #v == 0 then
                    error("doMenuItem timeout -- Dock may need restarting")
                else
                    for i2,v2 in ipairs(v[1]) do
                        if v2.AXTitle == item then
                            v2:doAXPress()
                            return
                        end
                    end
                    v:doAXShowMenu() -- close menu so we can error
                    error("doMenuItem " .. tostring(item) .. " not found in " .. tostring(app) .. " menu list")
                end
            end)()
            return
        end
    end
    error(tostring(app) .. " not found in Dock application list")
end

Here we put the code to find the item in the menu into a coroutine so it can loop while waiting for the accessibility items for the menu to be created but yield each iteration so everything else remains unblocked. (If you're already familiar with Lua coroutines, coroutine.applicationYield is our own addition that takes care of adding a timer to resume automatically so you don't have to do so yourself).

psifertex commented 3 years ago

Thanks! That's very helpful, works great.