Last active
December 14, 2024 05:43
-
-
Save stephancasas/a36c81fbc4189f46bc803f388a1985be to your computer and use it in GitHub Desktop.
Toggle sidecar or screen mirroring from Control Center in macOS Ventura
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env osascript -l JavaScript | |
/** | |
* ----------------------------------------------------------------------------- | |
* Activate Sidecar/Screen Mirroring from Control Center | |
* ----------------------------------------------------------------------------- | |
* | |
* Created on February 17, 2023 by Stephan Casas | |
* Updated on May 18, 2023 by Stephan Casas | |
* | |
* Options: | |
* - TARGET_DEVICE_NAME | |
* - The name of the sidecar/screen mirroring device to toggle. | |
* - This should be exactly as it's written in screen mirroring menu. | |
* - Include any whitespace characters as given in the menu entry. | |
* | |
* | |
* Notes: | |
* This script was tested on macOS 13 Ventura and may break with future OS | |
* updates. | |
*/ | |
const TARGET_DEVICE_NAME = 'REPLACE_WITH_YOUR_DEVICE_NAME'; | |
const $attr = Ref(); | |
const $windows = Ref(); | |
const $children = Ref(); | |
function run(_) { | |
// Get the current Control Center PID. | |
const pid = $.NSRunningApplication.runningApplicationsWithBundleIdentifier( | |
'com.apple.controlcenter', | |
).firstObject.processIdentifier; | |
// Get the Control Center application. | |
const app = $.AXUIElementCreateApplication(pid); | |
// Get the Control Center menubar extra children. | |
$.AXUIElementCopyAttributeValue(app, 'AXChildren', $children); | |
$.AXUIElementCopyAttributeValue($children[0].js[0], 'AXChildren', $children); | |
// Locate the Control Center menubar extra (also has Clock, Users, etc.). | |
const ccExtra = $children[0].js.find((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXIdentifier', $attr); | |
return $attr[0].js == 'com.apple.menuextra.controlcenter'; | |
}); | |
// Open Control Center window and await draw. | |
$.AXUIElementPerformAction(ccExtra, 'AXPress'); | |
if ( | |
!(() => { | |
const timeout = new Date().getTime() + 2000; | |
while (true) { | |
$.AXUIElementCopyAttributeValue(app, 'AXWindows', $windows); | |
if ( | |
typeof $windows[0] == 'function' && | |
($windows[0].js.length ?? 0) > 0 | |
) { | |
return true; | |
} | |
if (new Date().getTime() > timeout) { | |
return false; | |
} | |
delay(0.1); | |
} | |
})() | |
) { | |
return; | |
} | |
// Get Control Center window children. | |
$.AXUIElementCopyAttributeValue($windows[0].js[0], 'AXChildren', $children); | |
// Locate the Control Center modules group. | |
const modulesGroup = $children[0].js.find((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr); | |
return $attr[0].js == 'AXGroup'; | |
}); | |
// Get the individual modules within the modules group. | |
$.AXUIElementCopyAttributeValue(modulesGroup, 'AXChildren', $children); | |
// Locate the screen-mirroring module. | |
const screenMirroring = $children[0].js.find((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXIdentifier', $attr); | |
return $attr[0].js == 'controlcenter-screen-mirroring'; | |
}); | |
// Activate the screen mirroring module and await draw. | |
$.AXUIElementPerformAction( | |
screenMirroring, | |
// Wtf is this action name, Apple?? | |
'Name:show details\nTarget:0x0\nSelector:(null)', | |
); | |
if ( | |
!(() => { | |
const timeout = new Date().getTime() + 2000; | |
while (true) { | |
$.AXUIElementCopyAttributeValue(modulesGroup, 'AXChildren', $children); | |
if ( | |
typeof $children[0] == 'function' && | |
($children[0].js.length ?? 0) > 0 | |
) { | |
return true; | |
} | |
if (new Date().getTime() > timeout) { | |
return false; | |
} | |
delay(0.1); | |
} | |
})() | |
) { | |
return; | |
} | |
// Get the scroll area containing the device mirroring options. | |
const mirroringOptions = $children[0].js.find((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr); | |
return $attr[0].js == 'AXScrollArea'; | |
}); | |
// Get all mirroring options. | |
$.AXUIElementCopyAttributeValue(mirroringOptions, 'AXChildren', $children); | |
// Locate the toggle element for the target mirroring device. | |
const toggle = $children[0].js | |
.filter((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr); | |
return $attr[0].js == `AXCheckBox`; | |
}) | |
.find((child) => { | |
$.AXUIElementCopyAttributeValue(child, 'AXIdentifier', $attr); | |
return $attr[0].js == `screen-mirroring-device-${TARGET_DEVICE_NAME}`; | |
}); | |
if (!toggle) { | |
console.log( | |
'Error: Could not get toggle for target screen-mirroring device.', | |
); | |
return 1; | |
} | |
// Press the toggle for the target device. | |
$.AXUIElementPerformAction(toggle, 'AXPress'); | |
// Send ⎋ to dismiss Control Center. | |
$.CGEventPost($.kCGHIDEventTap, $.CGEventCreateKeyboardEvent(null, 53, true)); | |
$.CGEventPost($.kCGHIDEventTap, $.CGEventCreateKeyboardEvent(null, 53, true)); | |
return 0; | |
} | |
// prettier-ignore | |
(() => { | |
ObjC.import('Cocoa'); // yes, it's necessary -- stop telling me it isn't | |
ObjC.bindFunction('AXUIElementPerformAction', ['int', ['id', 'id']]); | |
ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['unsigned int']]); | |
ObjC.bindFunction('AXUIElementCopyAttributeValue',['int', ['id', 'id', 'id*']]); | |
})(); |
Doesn't work for me either - MacOS Sequiia 15.1
Any fix for 15.1?
Any fix for 15.1?
I tried to fix for 1-2 hours, though i'm not familiar with any of the technologies except javaScript with some help of gpt.
Works for 15.2. Not perfectly though.
function run(_) {
const TARGET_DEVICE_NAME = 'YOUR_DEVICE_NAME'
const $attr = Ref()
const $windows = Ref()
const $children = Ref()
// Get the current Control Center PID.
const pid =
$.NSRunningApplication.runningApplicationsWithBundleIdentifier('com.apple.controlcenter').firstObject
.processIdentifier
// Get the Control Center application.
const app = $.AXUIElementCreateApplication(pid)
// Get the Control Center menubar extra children.
$.AXUIElementCopyAttributeValue(app, 'AXChildren', $children)
$.AXUIElementCopyAttributeValue($children[0].js[0], 'AXChildren', $children)
// Locate the Control Center menubar extra (also has Clock, Users, etc.).
const ccExtra = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXIdentifier', $attr)
return $attr[0].js == 'com.apple.menuextra.controlcenter'
})
// Open Control Center window and await draw.
$.AXUIElementPerformAction(ccExtra, 'AXPress')
if (
!(() => {
const timeout = new Date().getTime() + 2000
while (true) {
$.AXUIElementCopyAttributeValue(app, 'AXWindows', $windows)
if (typeof $windows[0] == 'function' && ($windows[0].js.length ?? 0) > 0) {
return true
}
if (new Date().getTime() > timeout) {
return false
}
delay(0.1)
}
})()
) {
return
}
// Get Control Center window children.
$.AXUIElementCopyAttributeValue($windows[0].js[0], 'AXChildren', $children)
// Locate the Control Center modules group.
const modulesGroup = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr)
return $attr[0].js == 'AXGroup'
})
// Get the individual modules within the modules group.
$.AXUIElementCopyAttributeValue(modulesGroup, 'AXChildren', $children)
// Locate the screen-mirroring module.
const screenMirroring = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXIdentifier', $attr)
return $attr[0].js == 'controlcenter-screen-mirroring'
})
// Activate the screen mirroring module and await draw.
$.AXUIElementPerformAction(
screenMirroring,
// Wtf is this action name, Apple??
'Name:show details\nTarget:0x0\nSelector:(null)'
)
if (
!(() => {
const timeout = new Date().getTime() + 2000
while (true) {
$.AXUIElementCopyAttributeValue(modulesGroup, 'AXChildren', $children)
if (typeof $children[0] == 'function' && ($children[0].js.length ?? 0) > 0) {
return true
}
if (new Date().getTime() > timeout) {
return false
}
delay(0.1)
}
})()
) {
return
}
// Get the scroll area containing the device mirroring options.
const mirroringOptions = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr)
return $attr[0].js == 'AXScrollArea'
})
// Get all mirroring options.
$.AXUIElementCopyAttributeValue(mirroringOptions, 'AXChildren', $children)
// First, check if we're in the connected state by looking for "Use As Extended Display"
const isConnected = $children[0].js.some((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr)
if ($attr[0].js !== 'AXCheckBox') return false
$.AXUIElementCopyAttributeValue(child, 'AXDescription', $attr)
return $attr[0].js === 'Use As Extended Display'
})
if (isConnected) {
// Look for the disclosure triangle to go back
const backButton = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr)
return $attr[0].js === 'AXDisclosureTriangle'
})
if (!backButton) {
console.log('Error: Could not find back button')
return 1
}
// Click the back button
$.AXUIElementPerformAction(backButton, 'AXPress')
// Wait a moment for the UI to update
delay(0.5)
// Get the updated children
$.AXUIElementCopyAttributeValue(mirroringOptions, 'AXChildren', $children)
}
// Now look for our device toggle
const toggle = $children[0].js.find((child) => {
$.AXUIElementCopyAttributeValue(child, 'AXRole', $attr)
if ($attr[0].js !== 'AXCheckBox') return false
$.AXUIElementCopyAttributeValue(child, 'AXDescription', $attr)
return $attr[0].js === TARGET_DEVICE_NAME
})
if (!toggle) {
console.log('Error: Could not get toggle for target screen-mirroring device.')
console.log('Target device name:', TARGET_DEVICE_NAME)
return 1
}
// Toggle the device
$.AXUIElementPerformAction(toggle, 'AXPress')
return 0
}
// prettier-ignore
(() => {
ObjC.import('Cocoa'); // yes, it's necessary -- stop telling me it isn't
ObjC.bindFunction('AXUIElementPerformAction', ['int', ['id', 'id']]);
ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['unsigned int']]);
ObjC.bindFunction('AXUIElementCopyAttributeValue',['int', ['id', 'id', 'id*']]);
ObjC.bindFunction('AXUIElementCopyAttributeNames', ['int', ['id', 'id*']]);
})();
@phewstaff Thanks a lot for your efforts! I got it fixed as well by your code.
This is amazing! Does anyone have a scrip that turns off mirroring?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Seems to be broken in MacOS Sequiia 15.1 Beta - has anyone else tested it there?