Skip to content

Instantly share code, notes, and snippets.

@nriley
Last active March 6, 2024 17:02
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nriley/f2dfb2955836462b8f7806ce0da76bfb to your computer and use it in GitHub Desktop.
Save nriley/f2dfb2955836462b8f7806ce0da76bfb to your computer and use it in GitHub Desktop.
Hammerspoon script for ensuring Sidecar is active when iPad is plugged in (macOS 10.15; see comments for versions up to macOS 13)
hs.loadSpoon('SpoonInstall')
spoon.SpoonInstall.use_syncinstall = true
Install = spoon.SpoonInstall
log = hs.logger.new('init', 5)
-- function debugUI(msg, table)
-- log:d(msg)
-- log:d(hs.inspect(table))
-- end
function connectToSidecar(msg, results, count)
local item
sidecarItemFound = (count > 0)
if not sidecarItemFound then
return log:d("Can't get Sidecar connection menu item:", msg)
end
item = results[1]
if item.AXMenuItemMarkChar == nil then
-- log:d("Connecting to Sidecar...")
item:doAXPress()
else
-- log:d("Closing menu - already connected to Sidecar...")
item.AXParent:doAXCancel()
end
end
function stopConnectToSidecarItemSearchTimer()
if connectToSidecarItemSearchTimer then
connectToSidecarItemSearchTimer:stop()
connectToSidecarItemSearchTimer = nil
end
end
function displaysMenu(msg, results, count)
local menu, connectToSneezerItemSearch
if count == 0 then
return log:d("Can't get displays menu:", msg)
end
menu = results[1]
menu:doAXPress()
connectToSneezerItemSearch = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXMenuItem'},
{attribute = 'AXIdentifier', value = '_deviceSelected:'},
{attribute = 'AXTitle', value = 'Sneezer'}})
-- menu:elementSearch(debugUI, connectToSneezer,
-- {objectOnly = false, asTree = true, depth = 2})
-- iPad may not appear immediately in the Displays menu
-- wait up to 3 seconds for it to appear
stopConnectToSidecarItemSearchTimer()
sidecarItemFound = false
connectToSidecarItemSearchTimer = hs.timer.doUntil(
function()
return sidecarItemFound
end,
function()
menu:elementSearch(connectToSidecar, connectToSneezerItemSearch,
{count = 1, depth = 2, noCallback = true})
end,
0.5)
hs.timer.doAfter(3, stopConnectToSidecarItemSearchTimer)
end
function connectSidecar()
local suisApp, suisAX, displayMenuSearch
suisApp = hs.application.find('com.apple.systemuiserver')
suisAX = hs.axuielement.applicationElement(suisApp)
displayMenuSearch = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXMenuBarItem'},
{attribute = 'AXSubrole', value = 'AXMenuExtra'},
{attribute = 'AXDescription', value = '^Displays', pattern = true}
})
-- suisAX:elementSearch(debugUI, displayMenuSearch,
-- {objectOnly = false, asTree = true, depth = 2})
suisAX:elementSearch(displaysMenu, displayMenuSearch,
{count = 1, depth = 2})
end
-- what we actually get when the iPad connects:
-- connect, disconnect, connect
-- so, don't trigger twice within half a second
connectSidecarTimer = hs.timer.delayed.new(0.5, function()
connectSidecar()
end)
function iPadConnected(connected)
-- log:d("iPad connected?", connected)
if connected then
connectSidecarTimer:start()
else
connectSidecarTimer:stop()
end
end
Install:andUse(
"USBDeviceActions",
{
config = {
devices = {
iPad = { fn = iPadConnected }
}
},
start = true
}
)
@nriley
Copy link
Author

nriley commented Jan 29, 2024

@kmplngj I haven't upgraded yet on my main Mac due to various persistent issues in macOS 14. Still works on current macOS 13.

@peterhartree
Copy link

@nriley Thank you for sharing this script. I've been looking for something like this for ages.

On macOS 14 I had to tweak the connectSidecar() function to avoid a "spaX is nil" error:

function connectSidecar()
  hs.urlevent.openURL("file:///System/Library/PreferencePanes/Displays.prefPane")
  addDisplayMenuCriteria = hs.axuielement.searchCriteriaFunction({
    {attribute = 'AXRole', value = 'AXPopUpButton'},
    {attribute = 'AXDescription', value = 'Add'}
  })

  systemPreferencesSearchTimer = hs.timer.doUntil(
    function()
      return spAX ~= nil and addDisplayMenuSearch and addDisplayMenuSearch:matched() > 0
    end,
    function()
      if addDisplayMenuSearch and addDisplayMenuSearch:isRunning() then
        return
      end
      spAX = systemPreferencesApplicationElement()
      if spAX then
        log:d("Searching for Add Displays menu")
        addDisplayMenuSearch = spAX:elementSearch(addDisplayMenu, addDisplayMenuCriteria,
                                                  {count = 1, depth = 2})
      else
        log:d("spAX is nil, waiting for System Preferences")
      end
    end,
    0.5)
  hs.timer.doAfter(3, stopSystemPreferencesSearchTimer)
end

With that edit your script opens System Preferences as expected. Unfortunately it then gets stuck on "Searching for Add Displays" menu:

2024-03-04 11:40:29:                          table: 0x60000316bcc0 Searching for Add Displays menu
2024-03-04 11:40:29:                          table: 0x60000316bcc0 Can't find Add Display menu: completed

I don't know how to figure out what the menu is called on macOS 14. Do you have a quick tip on how to do this? Thanks in advance!

@peterhartree
Copy link

Here's a screenshot of the "Displays" dialog on macOS 14:
image

I tried setting {attribute = 'AXDescription', value = '+'}, but no dice.

@nriley
Copy link
Author

nriley commented Mar 5, 2024

@peterhartree Sorry, I don't have macOS 14 easily accessible to test. You can try using Accessibility Inspector if you want to figure out what changed since the prior version, but instead of scripting System Settings you could also try https://github.com/Ocasio-J/SidecarLauncher — just read about it today. It uses a private API to enable/disable Sidecar and is tested on macOS 14.

@peterhartree
Copy link

Thank you @nriley. SidecarLauncher is working perfectly on Mac OS 14.2.1.

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