-
-
Save nriley/f2dfb2955836462b8f7806ce0da76bfb to your computer and use it in GitHub Desktop.
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 | |
} | |
) |
Sure! At the time there weren't very many good examples of using accessibility in Hammerspoon. I've got my own Monterey version which I'm working on and will post once it's a bit more reliable...
@nriley any updates on your Monterey version?
So, it was working pretty well on 12.2 and then it completely broke on 12.3 because it seems Apple removed/broke the ability to connect from the menubar. I can share what I've got as it might work for you — you'll need to replace "Sneezer II" with the name of your iPad.
function controlCenterApplicationElement()
local ccApp
ccApp = hs.application.find('com.apple.controlcenter')
return hs.axuielement.applicationElement(ccApp)
end
function connectToSidecar(msg, results, count)
local item
sidecarButtonFound = (count > 0)
if not sidecarButtonFound then
return log:d("Can't get Sidecar connection button:", msg)
end
item = results[1]
if item.AXValue == 0 then
log:d("Connecting to Sidecar...")
item:doAXPress()
else
log:d("Closing window - already connected to Sidecar...")
end
doWithDisplayMenuExtra(displayMenuExtraPress)
end
function stopConnectToSidecarButtonSearchTimer()
if connectToSidecarButtonSearchTimer then
connectToSidecarButtonSearchTimer:stop()
connectToSidecarButtonSearchTimer = nil
end
end
function controlCenterWindow(msg, results, count)
local window, connectToSidecarButtonSearch, searchTries
if count == 0 then
return log:d("Can't get Control Center window:", msg)
end
window = results[1]
connectToSidecarButtonSearch = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXCheckBox'},
{attribute = 'AXTitle', value = 'Sneezer II'}})
-- XXX searching by AXIdentifier fails if in "connecting" (spinner) state
stopConnectToSidecarButtonSearchTimer()
sidecarButtonFound = false
searchTries = 0
connectToSidecarButtonSearchTimer = hs.timer.doUntil(
function()
searchTries = searchTries + 1
return (sidecarButtonFound or searchTries == 6)
end,
function()
window:elementSearch(connectToSidecar, connectToSidecarButtonSearch,
{count = 1, depth = 2, noCallback = true})
end,
0.5)
end
function displayMenuExtraPress(msg, results, count)
local menuExtra
if count == 0 then
return log:d("Can't get display menu extra:", msg)
end
menuExtra = results[1]
menuExtra:doAXPress()
end
function displayMenuExtraConnect(msg, results, count)
local ccAX, ccWindowSearch
displayMenuExtraPress(msg, results, count)
ccAX = controlCenterApplicationElement()
ccWindowSearch = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXWindow'},
{attribute = 'AXTitle', value = 'Control Center'}})
-- ccAX:elementSearch(debugUI, ccWindowSearch,
-- {objectOnly = false, asTree = true, depth = 2})
ccAX:elementSearch(controlCenterWindow, ccWindowSearch,
{count = 1, depth = 1})
end
function doWithDisplayMenuExtra(callback)
local ccAX, displayMenuExtraSearch
ccAX = controlCenterApplicationElement()
displayMenuExtraSearch = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXMenuBarItem'},
{attribute = 'AXSubrole', value = 'AXMenuExtra'},
{attribute = 'AXIdentifier', value = 'com.apple.menuextra.display'}
})
-- ccAX:elementSearch(debugUI, displayMenuExtraSearch,
-- {objectOnly = false, asTree = true, depth = 2})
ccAX:elementSearch(callback, displayMenuExtraSearch,
{count = 1, depth = 2})
end
function connectSidecar()
doWithDisplayMenuExtra(displayMenuExtraConnect)
end
-- what we actually get when the iPad connects:
-- connect, disconnect, connect
-- ...but then we can't connect without triggering a timeout
-- so, wait 5 seconds
connectSidecarTimer = hs.timer.delayed.new(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
}
)
Thanks!
Got sick of this not working on macOS 12.3, so here's a version that uses System Preferences to connect. It's a bit of a combination of the 10.15 and 12.2 versions. I tried shortening the 5 second timer but unfortunately there are still issues with trying to connect immediately after plugging in via USB.
function systemPreferencesApplicationElement()
local spApp
spApp = hs.application.find('com.apple.systempreferences')
if spApp then
return hs.axuielement.applicationElement(spApp)
else
return nil
end
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
hs.application.find('com.apple.systempreferences'):kill()
end
function stopConnectToSidecarItemSearchTimer()
if connectToSidecarItemSearchTimer then
connectToSidecarItemSearchTimer:stop()
connectToSidecarItemSearchTimer = nil
end
end
function addDisplayMenu(msg, results, count)
local menu, connectToSneezerItemSearch
if count == 0 then
return log:d("Can't find Add Display menu:", msg)
end
menu = results[1]
menu:doAXPress()
connectToSidecarItemCriteria = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXMenuItem'},
{attribute = 'AXIdentifier', value = 'menuAction:'},
{attribute = 'AXTitle', value = 'Sneezer II'}})
-- iPad may not appear immediately
-- wait up to 3 seconds for it to appear
stopConnectToSidecarItemSearchTimer()
sidecarItemFound = false
connectToSidecarItemSearchTimer = hs.timer.doUntil(
function()
return sidecarItemFound
end,
function()
menu:elementSearch(connectToSidecar, connectToSidecarItemCriteria,
{count = 1, depth = 2, noCallback = true})
end,
0.5)
hs.timer.doAfter(3, stopConnectToSidecarItemSearchTimer)
end
function stopSystemPreferencesSearchTimer()
if systemPreferencesSearchTimer then
systemPreferencesSearchTimer:stop()
systemPreferencesSearchTimer = nil
end
end
function connectSidecar()
hs.urlevent.openURL("file:///System/Library/PreferencePanes/Displays.prefPane")
addDisplayMenuCriteria = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXPopUpButton'},
{attribute = 'AXTitle', value = 'Add Display'}
})
spAX = nil
addDisplayMenuSearch = nil
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 ~= nil then
log:d("Searching for Add Display menu")
addDisplayMenuSearch = spAX:elementSearch(addDisplayMenu, addDisplayMenuCriteria,
{count = 1, depth = 2})
end
end,
0.5)
hs.timer.doAfter(3, stopSystemPreferencesSearchTimer)
end
-- what we actually get when the iPad connects:
-- connect, disconnect, connect
-- ...but then we can't connect without triggering a timeout
-- so, wait 5 seconds
connectSidecarTimer = hs.timer.delayed.new(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
}
)
I have a bug to report but it's okay, we have a work-around.
Problem:
When you connect your iPad, the screen opens and it starts the charges. That's fine. But while the screen is on, the menu field you are triggering the Sidecar is showing two different options under two different categories; Linking the controls with Universal Control and Extending/Mirroring the screen to the device.
Work-around:
After plugging-in the device. Just click the lock button on it. After Screen goes off, you should be fine. 🤷🏻♂️
Good timing! I fixed this last week, should have posted an update…
function systemPreferencesApplicationElement()
local spApp
spApp = hs.application.find('com.apple.systempreferences')
if spApp then
return hs.axuielement.applicationElement(spApp)
else
return nil
end
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
-- first item may be Universal Control
if count == 2 then
item = results[2]
else
item = results[1]
end
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
hs.application.find('com.apple.systempreferences'):kill()
end
function stopConnectToSidecarItemSearchTimer()
if connectToSidecarItemSearchTimer then
connectToSidecarItemSearchTimer:stop()
connectToSidecarItemSearchTimer = nil
end
end
function addDisplayMenu(msg, results, count)
local menu, connectToSneezerItemSearch
if count == 0 then
return log:d("Can't find Add Display menu:", msg)
end
menu = results[1]
menu:doAXPress()
connectToSidecarItemCriteria = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXMenuItem'},
{attribute = 'AXIdentifier', value = 'menuAction:'},
{attribute = 'AXTitle', value = 'Sneezer II'}})
-- iPad may not appear immediately
-- wait up to 3 seconds for it to appear
stopConnectToSidecarItemSearchTimer()
sidecarItemFound = false
connectToSidecarItemSearchTimer = hs.timer.doUntil(
function()
return sidecarItemFound
end,
function()
menu:elementSearch(connectToSidecar, connectToSidecarItemCriteria,
{count = 2, depth = 2, noCallback = true})
end,
0.5)
hs.timer.doAfter(3, stopConnectToSidecarItemSearchTimer)
end
function stopSystemPreferencesSearchTimer()
if systemPreferencesSearchTimer then
systemPreferencesSearchTimer:stop()
systemPreferencesSearchTimer = nil
end
end
function connectSidecar()
hs.urlevent.openURL("file:///System/Library/PreferencePanes/Displays.prefPane")
addDisplayMenuCriteria = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXPopUpButton'},
{attribute = 'AXTitle', value = 'Add Display'}
})
spAX = nil
addDisplayMenuSearch = nil
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 ~= nil then
log:d("Searching for Add Displays menu")
addDisplayMenuSearch = spAX:elementSearch(addDisplayMenu, addDisplayMenuCriteria,
{count = 1, depth = 2})
end
end,
0.5)
hs.timer.doAfter(3, stopSystemPreferencesSearchTimer)
end
-- what we actually get when the iPad connects:
-- connect, disconnect, connect
-- ...but then we can't connect without triggering a timeout
-- so, wait 5 seconds
connectSidecarTimer = hs.timer.delayed.new(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
}
)
I made the same changes you did and tried and didn't work. And I thought my Lua is rusty.
Let me try the updated version of yours. I'll inform you.
Thank you for your effort and time! 😇
Edit: I just forgot to rename the device name literal. Sorry.
It works! 🥳
Great! (Of course, all this is going to break again in a few months, sigh…)
Finally upgraded to macOS 13. Surprisingly, only some very small changes need to be made to the last version I posted. Just matching the popup button:
addDisplayMenuCriteria = hs.axuielement.searchCriteriaFunction({
{attribute = 'AXRole', value = 'AXPopUpButton'},
{attribute = 'AXDescription', value = 'Add'}
})
and looking deeper in the hierarchy for it:
addDisplayMenuSearch = spAX:elementSearch(addDisplayMenu, addDisplayMenuCriteria,
{count = 1, depth = 5})
@nriley Does this work for you with macOS 14?
@kmplngj I haven't upgraded yet on my main Mac due to various persistent issues in macOS 14. Still works on current macOS 13.
@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 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.
Thank you @nriley. SidecarLauncher is working perfectly on Mac OS 14.2.1.
Thank you for this. Semi-repurposed it in my own version:
https://gist.github.com/Technifocal/6284a6d2bfc76fa1fac18c1e9121c3bd