Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Last active December 14, 2024 05:43
Show Gist options
  • Save stephancasas/a36c81fbc4189f46bc803f388a1985be to your computer and use it in GitHub Desktop.
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*']]);
})();
@tonydehnke
Copy link

Seems to be broken in MacOS Sequiia 15.1 Beta - has anyone else tested it there?

@RainerUderia
Copy link

Doesn't work for me either - MacOS Sequiia 15.1

@dpyy
Copy link

dpyy commented Dec 4, 2024

Any fix for 15.1?

@phewstaff
Copy link

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*']]);
})();

@Cyan-ZSD
Copy link

@phewstaff Thanks a lot for your efforts! I got it fixed as well by your code.

@dpyy
Copy link

dpyy commented Dec 14, 2024

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