Skip to content

Instantly share code, notes, and snippets.

@dmohs
Created May 10, 2022 17:22
Show Gist options
  • Save dmohs/78f80c16caf13edfa0a5b493525e5ade to your computer and use it in GitHub Desktop.
Save dmohs/78f80c16caf13edfa0a5b493525e5ade to your computer and use it in GitHub Desktop.
Hammerspoon function to set default browser
function setDefaultBrowser(appFileName, notify)
local logger = hs.logger.new('defbr', 5)
local app = hs.application.open('com.apple.systempreferences')
local axapp = hs.axuielement.applicationElement(app)
-- Attempt to open the menu until it successfully opens (returns true)
hs.timer.waitUntil(function()
return app:selectMenuItem({'View', 'General'})
end, function()
print('General group opened.')
findPopup()
end)
function findPopup()
local dwbPopup = nil
-- Attempt to find the popup (combo box)
hs.timer.doUntil(function() return dwbPopup end, function()
axapp:elementSearch(function(msg, results)
if results then dwbPopup = results[1] end
end, function(e)
return e:attributeValue('AXDescription') == 'Default Web Browser popup'
end)
end)
hs.timer.waitUntil(function() return dwbPopup end, function()
print('Popup found.')
chooseAppFileName(dwbPopup)
end)
end
function chooseAppFileName(dwbPopup)
dwbPopup:performAction('AXShowMenu')
dwbPopup:elementSearch(function(msg, results)
local item = results[1]
item:performAction('AXPress')
notify()
end, function(e)
return e:attributeValue('AXTitle') == appFileName
end)
end
end
@asmagill
Copy link

Two thoughts...

  1. hs.timer.waitUntil and doUntil only check once a second. You can add a third argument which indicates how often to run the check, e.g. hs.timer.waitUntil(function() ... end, function() ... end, 0.1) would check every tenth a second (or as close to it as your machine's activity will allow).
  2. hs.axuielement:elementSearch is slow. It's powerful, and needed when you really don't know where something is located, but avoid it when you can, or limit the search space by starting as close to the element as you can reasonably get with reliability because by its nature it has to iterate over all of the elements from the starting point until it finds a result.

The following seemed a little snappier on my machine:

function setDefaultBrowser(appFileName, notify)
  local logger = hs.logger.new('defbr', 5)
  local app = hs.application.open('com.apple.systempreferences')
  local axapp = hs.axuielement.applicationElement(app)

  -- Attempt to open the menu until it successfully opens (returns true)
  hs.timer.waitUntil(function()
    return app:selectMenuItem({'View', 'General'})
  end, function()
    print('General group opened.')
    findPopup()
  end, 0.1)

  function findPopup()
    local dwbPopup = nil
    -- Attempt to find the popup (combo box)
    hs.timer.doUntil(function() return dwbPopup end, function()
--       axapp:elementSearch(function(msg, results)
--         if results then dwbPopup = results[1] end
--       end, function(e)
--         return e:attributeValue('AXDescription') == 'Default Web Browser popup'
--       end)
--     end)
      local children = axapp.AXFocusedWindow:childrenWithRole("AXPopUpButton")
      for i,e in ipairs(children) do
        if e:attributeValue('AXDescription') == 'Default Web Browser popup' then
          dwbPopup = e
          break
        end
      end
    end, 0.1)
    hs.timer.waitUntil(function() return dwbPopup end, function()
      print('Popup found.')
      chooseAppFileName(dwbPopup)
    end, 0.1)
  end

  function chooseAppFileName(dwbPopup)
    dwbPopup:performAction('AXShowMenu')
    dwbPopup:elementSearch(function(msg, results)
      local item = results[1]
      item:performAction('AXPress')
      notify()
    end, function(e)
      return e:attributeValue('AXTitle') == appFileName
    end)
  end
end

Two notes about these changes:

  1. If you do want to use elementSearch in findPopup, don't change the check time of hs.timer.doUntil -- elementSearch already breaks up it's work into tiny chuncks to keep your system responsive and avoid blocking in Hammerspoon and decreasing the timer check interval slows this down even further.
  2. the elementSearch for the menu item in chooseAppFileName is a perfectly fine use of the method because we're already on a fairly limited branch -- a single menu; it won't search outside of the menu, just to its end.

@dbalatero
Copy link

@asmagill It would be quite useful to document the tiny chunking that elementSearch does somewhere in the docs: https://www.hammerspoon.org/docs/hs.axuielement.html#elementSearch

@dmohs
Copy link
Author

dmohs commented May 14, 2022

@asmagill, indeed, both changes improve the performance significantly. I had originally tried a timer interval of 0.1, but I removed it because it wasn't obvious that it helped and I worried that floats were not acceptable arguments. I assumed elementSearch was expensive, but also that the page was relatively small and would be searched quickly. I suspect that both of these incorrect assumptions combined to make it feel like I wasn't getting anywhere. Your changes make this delightfully snappy!

Thank you so much for putting the time and energy into this and helping me understand it better.

@asmagill
Copy link

Re adding details, it's hinted at in the first line of the Notes (• This method utilizes coroutines to keep Hammerspoon responsive), but you're right, it's not clear and just mentions slow when includeParents is set. I'll give it some thought to see how best to re-word it -- I have some time coming up in the next couple of weeks that I plan to devote to Hammerspoon and will clarify it then.

Glad that it helps! I have a couple of tools I use when trying to narrow axuielement searches and I'm hoping to provide some wiki articles showing and discussing them during the same period.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment