Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Last active October 31, 2025 17:25
Show Gist options
  • Select an option

  • Save stephancasas/a36c81fbc4189f46bc803f388a1985be to your computer and use it in GitHub Desktop.

Select an option

Save stephancasas/a36c81fbc4189f46bc803f388a1985be to your computer and use it in GitHub Desktop.
Toggle sidecar or screen mirroring from Control Center in macOS Ventura
#!/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*']]);
})();
@knotts2010
Copy link

knotts2010 commented Sep 18, 2025

Upgrade to Tahoe 26.0 broke the disconnect; it brings up the device listing for screen mirroring but doesn't select it to disconnect. Any chance this can be updated? @di11ard @nedx86 @stephancasas
Grateful for your help.

@di11ard
Copy link

di11ard commented Sep 18, 2025

@knotts2010 Shit. I was already dreading the upgrade and based on your message here, I may hold off a bit longer. I assure you, if my computer updates to Tahoe, and it breaks my script, I will go balls to the wall to find a solution because I use this script multiple times a day.

@knotts2010
Copy link

knotts2010 commented Sep 18, 2025

So I used what @thedavidthomas captured above and modified it to reflect the changes in Tahoe 26.0 UI and it works. Credit to him for the actual script. I increased the delay to match what worked in my environment. Hope this helps. I still like the control center approach and will wait to see if any of the experts can get that fixed.

tell application "System Settings"
    activate
    delay 0.5
    reveal pane id "com.apple.Displays-Settings.extension"
    repeat until window 1 exists
        delay 0.1
    end repeat
    delay 0.5 -- (sometimes needs a bit extra time to load)
    tell application "System Events"
        tell first window of application process "System Settings"
            -- wait for the page to be loaded, so that the pop up button can be clicked
            repeat until exists (menu button 1 of group 1 of group 3 of splitter group 1 of group 1)
                delay 0.1
            end repeat
            tell menu button 1 of group 1 of group 3 of splitter group 1 of group 1
                click
                tell menu 1
                    set found to false
                    repeat with aMenuItem in menu items
                        if name of aMenuItem contains "Mirror or Extend to" then
                            set found to true
                        end if
                        if name of aMenuItem contains "iPad" and found then
                            click aMenuItem
                            delay 4 -- wait for the click to complete
                            exit repeat
                        end if
                    end repeat
                end tell
            end tell
        end tell
    end tell
    quit
end tell

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