macOS harvest cursor from any app š
July 27, 2023
July 27, 2023
As a pet project I was building a screenshot app, and I wanted its cursors to match the ones of macOS screenshot utility: and .
This was harder than expected. Iāll tell you the whole story because I find it fun and interesting, but feel free to jump straight to the solution.
NSCursor
In a Mac app, the NSCursor
class exposes a number of default cursors,
like the arrow ,
I-beam ,
pointing hand ,
various resize cursors, and even a cute ādisappearing itemā cursor
(that I kinda want to name āpoofā for some reason).
There is also a crosshair cursor , however itās not the same that the system screenshot utility uses. And the camera cursor is nowhere to be found.
So our last resort is to set a custom cursor from an image, e.g. for a cursor thatās 32x32 pixels where we want the āhot spotā to be in the middle:
let image = NSImage(named: "cursor.png")
let hotSpot = NSPoint(x: 16, y: 16)
let cursor = NSCursor(image: image, hotSpot: hotSpot)
But what image do we use here?
By doing a bit of digging in the /System
directory, we find the
following path:
/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors
This seems to contain all the system cursors, one directory for each,
containing a cursor.pdf
and info.plist
!
Here, we effectively have screenshotselection
that matches the
screen capture utilityās crosshair, and screenshotwindow
that matches
the camera cursor shown during window selection. Neat.
Parsing the info.plist
, we find the hot spot coordinates:
$ plutil -p /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors/screenshotselection/info.plist
{
"hotx" => 15
"hotx-scaled" => 15
"hoty" => 15
"hoty-scaled" => 15
}
We can now load those programmatically:
func loadCursor(_ name: String) -> NSCursor? {
let root =
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors"
guard let data = FileManager.default.contents(atPath: "\(root)/\(name)/info.plist")
else {
return nil
}
guard
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil)
as? [String: Any]
else {
return nil
}
guard let pdfData = try? Data(contentsOf: URL(fileURLWithPath: "\(root)/\(name)/cursor.pdf"))
else {
return nil
}
guard let cursorImage = NSImage(data: pdfData) else {
return nil
}
let hotSpot = NSPoint(
x: plist["hotx"] as! Int? ?? Int(cursorImage.size.width) / 2,
y: plist["hoty"] as! Int? ?? Int(cursorImage.size.height) / 2
)
return NSCursor(image: cursorImage, hotSpot: hotSpot)
}
Letās use this function in a basic example to demonstrate it:
import Cocoa
let app = NSApplication.shared
if let cursor = loadCursor("screenshotselection") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
cursor.set()
}
}
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps: true)
app.run()
Note: here we call cursor.set()
after a delay because it
doesnāt
always work when called right
away for reasons that are not familiar to me.
In a real app, you probably want to subclass NSView
, override
resetCursorRects
, and call addCursorRect
in it.
This actually looks good for the camera! But for the crosshair, it doesnāt seem to match the original one.
The original crosshair size appears to be 50x50 pixels, while this one is 46x46. More importantly, the original one has some kind of light outline that makes it visible on darker backgrounds, that is completely missing from that cursor PDF we just found. You can see the difference easily:
Original | Custom |
---|---|
So the screen capture utility doesnāt seem to be using this cursor from
HIServices.framework
.
I tried exploring the contents of the screen capture app in
/System/Library/CoreServices/screencaptureui.app
, especially the
Contents/Resources/Assets.car
file, exploring it using
Asset Catalog Tinkerer,
but it didnāt contain anything useful.
The next idea I tried was to see if I could somehow access the cursor data from other apps from my Swift app.
It turns out NSCursor
exposes a currentSystem
property, containing current system cursor (as opposed to
NSCursor.current
that contains your own applicationās current cursor).
This way we can easily access the image data of the currentSystem
cursor, as well as its hotSpot
to be used later in our own custom
cursor.
import Cocoa
let cursor = NSCursor.currentSystem!
print(cursor.hotSpot)
let image = cursor.image.cgImage(forProposedRect: nil, context: nil, hints: nil)!
let bitmap = NSBitmapImageRep(cgImage: image)
let data = bitmap.representation(using: .png, properties: [:])!
try! data.write(to: URL(fileURLWithPath: "cursor.png"))
We can put this code in a file test.swift
, and run it with sleep 5 && swift test.swift
.
This gives us 5 seconds to do whatever is needed to show the cursor we
want to harvest, before our script actually runs and saves the current
system cursor to a PNG file.
In the case of the screen capture utility crosshair, Iāve got this (pictured over transparent, grey and dark background to show how well it reacts to those):
Perfect. š
I didnāt want to get into adding support for showing the dynamic coordinates as part of the cursor, so as far as Iām concerned, I got rid of those and used just the crosshair in my app.
I hope you found this post useful! Now if you want to get the cursor data from any app, in its original transparent quality, you can use the simple script above to do so. Enjoy!