As far back as 2005, I was looking at adding support for NSServices to Permanent Eraser. However, due to poor design decisions, it was not possible to implement them due to timing issues. With Snow Leopard's improved support for NSServices, I created a plug-in service to allow a user to right-click on a file and be able to erase it with Permanent Eraser.
With the intended successor to Permanent Eraser 2, I have been experimenting with adding support for NSServices. NSServices have been around for a long time, so the available information is somewhat jumbled, especially with some additions which were added with Mac OS X 10.5.
Info.plist
The first step is to add NSServices
key-value pair the Info.plist. Below is a simple example to configure a single service.
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Service Menu Title</string>
</dict>
<key>NSMessage</key>
<string>doSomeStuffMethodName</string>
<key>NSPortName</key>
<string>SomeApp</string>
<key>NSRequiredContext</key>
<dict/>
<key>NSSendFileTypes</key>
<array>
<string>public.item</string>
</array>
</dict>
</array>
Here is another example with some excellent comments on what the various keys represent.
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Folder Handling Demo</string>
</dict>
<key>NSMessage</key>
<string>handleServices</string> <!-- This specifies the selector -->
<key>NSPortName</key>
<string>Tmp</string> <!-- This is the name of the app -->
<!-- Here we're limiting where the service will appear. -->
<key>NSRequiredContext</key>
<dict>
<key>NSTextContent</key>
<string>FilePath</string>
</dict>
<!-- This service is only really useful from the Finder. So
we want the finder only to send us the URI "public.directory"
which *will* include packages (on the off-chance you want to
see the full package directory name) -->
<key>NSSendFileTypes</key>
<array>
<!-- Check out "System-Declared Uniform Type Identifiers"
in the Apple documentation for the various UTI types.
In this example, all we want is a directory, which is
a super-type to other types (e.g. public.folder) -->
<string>public.folder</string>
</array>
</dict>
</array>
For Permanent Eraser, I have a more complex version set up.
<key>NSServices</key>
<array>
<dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>E</string>
</dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Erase File</string>
</dict>
<key>NSMessage</key>
<string>eraseService</string>
<key>NSPortName</key>
<string>Permanent Eraser</string>
<key>NSRequiredContext</key>
<dict>
<key>NSTextContent</key>
<array>
<string>FilePath</string>
</array>
</dict>
<!--
<key>NSSendFileTypes</key>
<array>
<string>public.file-url</string>
<string>public.url</string>
<string>public.item</string>
<string>public.folder</string>
</array>
-->
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
<string>NSURLPboardType</string>
<string>public.utf8-plain-text</string>
<string>public.url</string>
<string>public.file-url</string>
</array>
</dict>
</array>
Commented out is the NSSendFileTypes
key-value pair, which is similar in its functionality to NSSendTypes
. According to the Services Implementation Guide the primary difference is that NSSendFileTypes
only accepts Uniform Type Identifiers (UTIs), whereas NSSendTypes
can accept the older style pasteboard types (e.g. NSStringPboardType, NSURLPboardType) and UTIs.
To localize strings for keys like NSMenuItem
and NSServiceDescription
, create a ServicesMenu.strings
file with the translated strings. If a string (such as for NSServiceDescription
) is particularly long, use a shorter token like SERVICE_DESCRIPTION
for the key in the strings file.
The Code
From the AppDelegate's applicationDidFinishLaunching
method, I call the following setupServiceProvider()
method, which creates an instance of the ContextualMenuServiceProvider class and then calls NSUpdateDynamicServices()
to dynamically refresh any new services.
func setupServiceProvider() {
NSApplication.shared.servicesProvider = ContextualMenuServiceProvider()
// Call this for sanity's sake to refresh the known services
NSUpdateDynamicServices()
}
The ContextualMenuServiceProvider.swift
file:
import Foundation
import Cocoa
class ContextualMenuServiceProvider: NSObject {
@objc func eraseService(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer <NSString>) {
// Just for reference, looking at the number of available pasteboard types
if let pBoardTypes = pasteboard.types {
NSLog("Number of pasteboard types: \(pBoardTypes.count)")
NSLog("Pasteboard Types: \(pBoardTypes)")
}
// NSFilenamesPboardType is unavailable in Swift, use NSPasteboard.PasteboardType.fileURL
guard let pboardInfo = pasteboard.string(forType: NSPasteboard.PasteboardType.fileURL) else {
NSLog("Could not find an appropriate pasteboard type")
return
}
let urlPath = URL(fileURLWithPath: pboardInfo)
let standardizedURL = URL(fileURLWithPath: pboardInfo).standardized
let messageText = "Hola info \(pboardInfo) of type \(pboardInfoType) at \(urlPath.absoluteURL) with standardized URL \(standardizedURL)"
NSLog(messageText)
}
}
File System Path Types
For Permanent Eraser, I need to get the path for the selected file. When parsing out the returned file path from the pasteboard info, it returned an unintelligible path like:
file:///.file/id=6571367.8622082855
. This is certainly not what I was expecting and resulted in some confusion until I learned more about the various methods that macOS can represent a file's path. What I was receiving here was a file reference URL. The advantage of this type is that it can point to the same file, even if the original file is moved (somewhat similar to how a Mac alias can still point to the correct file even if it is relocated). However, what I wanted was a path-based URL, which is easier for me to read and work with.
For most URLs, you build the URL by concatenating directory and file names together using the appropriate NSURL methods until you have the path to the item. A URL built in that way is referred to as a path-based URL because it stores the names needed to traverse the directory hierarchy to locate the item. (You also build string-based paths by concatenating directory and file-names together, with the results stored in a slightly different format than that used by the NSURL class.) In addition to path-based URLs, you can also create a file reference URL, which identifies the location of the file or directory using a unique ID.
All of the following entries are valid references to a file called MyFile.txt in a user’s Documents directory:
Path-based URL:
file://localhost/Users/steve/Documents/MyFile.txt
File reference URL:
file:///.file/id=6571367.2773272/
String-based path:
/Users/steve/Documents/MyFile.txt
There are several ways to convert the file reference URL to a more readable format. Using a quick AppleScript from the Terminal, one can get the string-based path this way:
osascript -e 'get posix path of posix file "file:///.file/id=6571367.4833330"'
In Swift the conversion to a path-based URL is like:
let standardizedURL = URL(fileURLWithPath: pboardInfoFileURL).standardized
Testing
To test if your new service works, copy the application into the Applications
folder and then launch your app to ensure that the system recognizes the new service. The NSUpdateDynamicServices()
call is used to help refresh the system. However if it appears that macOS is not updating properly, then try running the following commands from the Terminal:
/System/Library/CoreServices/pbs -flush
To list the registered services, use pbs
with the -dump_pboard
option.
/System/Library/CoreServices/pbs -dump_pboard
References
- File System Programming Guide: Specifying the Path to a File or Directory
- Services Implementation Guide
- Services Properties
- Introduction to Uniform Type Identifiers Overview
- System-Declared Uniform Type Identifiers
- System Declared Uniform Type Identifiers
- Get path from OS X file reference URL alias (file:///.file/id=...)
- Writing a Snow Leopard Service for Finder.app
- Finder NSService Context Menu errors with: Cannot find service provider for selector
- Updating OSX right click context menu with new service item
- Cocoa finder menu item for folders only
- NSURL returns file's id instead of file's path
- Customizing the Cocoa Text System
- Convert weird Mac OS file reference URL to a POSIX path