I vibe-coded a small Swift command-line utility that sets VoiceOver’s Mouse Pointer setting to “Ignores VoiceOver Cursor,” regardless of its current state.
Here's exactly what it does: It uses the private ScreenReaderCore framework and SCRCUserDefaults to set SCRConfigurationCursorTrackingMToVO and SCRConfigurationCursorTrackingVOToM to false, synchronizes the settings, then makes an announcement using "say."
Save the source as vo-ignore.swift and compile it with:
swiftc -O -o vo-ignore vo-ignore.swift
Then run ./vo-ignore whenever you need to force the Mouse Pointer setting back to Ignore. I wrapped it in an AppleScript and associated it with the keyboard commander VO+I shortcut.
This code works for me on Golden Gate. But it relies on a private Apple framework, so there’s no guarantee Apple won’t change or break it in a future macOS release.
I might put this in a github repo if there's demand for it. All the cool kids use github, right? In the meantime, here's the source:
import Foundation
import ObjectiveC.runtime
// Use VoiceOver's private preferences API so changes reach the running process.
let framework = "/System/Library/PrivateFrameworks/ScreenReaderCore.framework"
guard let bundle = Bundle(path: framework) else {
fatalError("Cannot locate ScreenReaderCore")
}
try bundle.loadAndReturnError()
guard let cls: AnyClass = NSClassFromString("SCRCUserDefaults") else {
fatalError("Cannot find SCRCUserDefaults")
}
// Get the shared VoiceOver preferences object.
let sharedSelector = NSSelectorFromString("sharedUserDefaults")
guard let sharedMethod = class_getClassMethod(cls, sharedSelector) else {
fatalError("Cannot find sharedUserDefaults")
}
typealias SharedFunction =
@convention(c) (AnyClass, Selector) -> AnyObject
let sharedFunction = unsafeBitCast(
method_getImplementation(sharedMethod),
to: SharedFunction.self
)
let defaults = sharedFunction(cls, sharedSelector)
// Set both cursor-tracking directions off.
let setSelector = NSSelectorFromString("setValue:forKey:")
guard let setMethod = class_getInstanceMethod(cls, setSelector) else {
fatalError("Cannot find setValue:forKey:")
}
typealias SetFunction =
@convention(c) (AnyObject, Selector, AnyObject, AnyObject) -> Void
let setFunction = unsafeBitCast(
method_getImplementation(setMethod),
to: SetFunction.self
)
let off = NSNumber(value: false)
setFunction(
defaults,
setSelector,
off,
"SCRConfigurationCursorTrackingMToVO" as NSString
)
setFunction(
defaults,
setSelector,
off,
"SCRConfigurationCursorTrackingVOToM" as NSString
)
// Flush preferences and propagate the change.
let syncSelector = NSSelectorFromString("synchronize")
guard let syncMethod = class_getInstanceMethod(cls, syncSelector) else {
fatalError("Cannot find synchronize")
}
typealias SyncFunction =
@convention(c) (AnyObject, Selector) -> Void
let syncFunction = unsafeBitCast(
method_getImplementation(syncMethod),
to: SyncFunction.self
)
syncFunction(defaults, syncSelector)
// Speak a short confirmation.
let say = Process()
say.executableURL = URL(fileURLWithPath: "/usr/bin/say")
say.arguments = ["Mouse set to ignores."]
try? say.run()
Comments
Thanks
It’s so interesting to see developers accomplishing what Apple itself should have done. Clearly, we need to take matters into our own hands to address long-standing issues with VoiceOver.
Paul, I suggest sharing this solution with Apple’s Accessibility team—it might help show them how this issue can finally be resolved.
Thanks again!