I've been trying to create a way to tell my (running) macOS app to open some files and supply some additional arguments to the command.
For cold-start apps, using the
$ open MyApp.app fileA.txt --args --foo-arg
would launch the app and I would be able to inspect the --foo-arg
via UserDefaults
/CommandLine
/ProcessInfo
. However, if the app is already running, the --foo-arg
is missing from UserDefaults
/ProcessInfo
/CommandLine
.
I've been struggling to wrap my head around a solution here because I have a few requirements which make things a tad more difficult.
XPC
Some people have suggested I use XPC but after reading about it, I'm not sure how that solution might look?
Apple Script
URL Scheme
I can register my app to have its own URL scheme but the way NSApplicationDelegate handles the incoming URLs comes in two batches. First, the URLs it can open, followed by the URL schemes or the file paths it can't open. ie:
open -a MyApp.app myapp:foo; open -a MyApp.app file.txt
I can probably make this work but it's a tad tacky and I really want to do this the right way.
What is Terminal? Terminal is an app on your Mac that allows you to gain root-level access to your system. Think of it as the 'employee entrance' to your Mac; it lets you get backstage to change things as you see fit in what's known as the command line.
A command-line tool which ingests its arguments and turns them in to Apple Events is the way to go. You can see how this works from the user's point of view by installing the BBEdit command-line tools and then running man bbedit
or man bbdiff
in a Terminal window.
From your command-line tool's point of view, the "interesting" parts are:
Figure out whether the application is running: +[NSRunningApplication runningApplicationsWithBundleIdentifier:]
will help with that.
If the application is not running, then use -[NSWorkspaceURLForApplicationWithBundleIdentifier:]
to first locate the application by bundle ID, then -[NSWorkspace launchApplicationAtURL:options:configuration:error:]
to launch the application. This will return an NSRunningApplication
instance, or NIL and an error. (Make sure to handle the error case.)
Using the NSRunningApplication
instance obtained from either step 1 or step 2, you can now use either the NSAppleEventDescriptor
APIs or the low-level AppleEvent C APIs to construct an event. (The higher-level API is probably easier to use.)
That would go something like this:
Construct a target descriptor using the processIdentifier
from your running application:
targetDesc = [NSAppleEventDescriptor descriptorWithProcessIdentifier: myRunningApplication.processIdentifier;
Construct an "open documents" event, addressed to your target application:
event = [NSAppleEventDescriptor appleEventWithEventClass: kCoreEventClass eventID: kAEOpenDocuments targetDescriptor: targetDesc returnID: kAutoGenerateReturnID transactionID: kAnyTransactionID];
Note: I use kCoreEventClass
/kAEOpenDocuments
as an example - if you're trying to open one or more files with additional information, that's fine. If you're doing some other work, then you should invent a four-character code for an event class which is specific to your application, and a four-character event ID which is unique to the operation you're requesting.
Add the command arguments to the event. For each argument, this consists of creating an appropriate descriptor based on the argument's intrinsic type (boolean, int, string, file URL), and then adding it to the event using a keyword parameter.
(An Apple Event "keyword" is a four-character code. You can invent your own, with constraints (don't use all-lowercase, and you can use ones defined in AEDataModel.h
or AERegistry.h
where they fit with your needs).
For each descriptor you create, add it to the event using -[setParamDescriptor: forKeyword:]
:
myURLParamDesc = [NSAppleEventDescriptor descriptorWithFileURL: myFileURL];
[event setParamDescriptor: myURLParamDesc forKey: kMyFileParamKeyword];
When you've added all of the parameters to the event, send it:
[event sendWithOptions: kAENoReply timeout: FLOAT_MAX error: &error];
On the application side, you'll need to use -[NSAppleEventManager setEventHandler: andSelector: forEventClass: andID:]
. This will get called for your custom event class and ID that you invented above, at which point you can use the descriptor APIs to pull the event apart and run your operation.
Sandboxing takes care of itself: your application automatically gets a sandboxing extension for files that it's been passed via Apple Events.
Your command-line tool is not sandboxed -- it can't be, because it's run from Terminal and (potentially) other nonsandboxed apps.
However, the tool must be signed with the hardened runtime, and with com.apple.security.automation.apple-events = YES
and a com.apple.security.temporary-exception.apple-events
naming your application's bundle identifier, so that the tool can send Apple Events to your application.
(And the tool will need an Info.plist with an NSAppleEventsUsageDescription
string.)
I've left a fair amount as an exercise for the reader; but hopefully this will get you started.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With