Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MacOS app - why does CommandLine.arguments of an already open app not contain the passed --args arguments?

I have a macOS app which most users use by drag and dropping images on the app icon or my menu bar icon. Some users also use my app through Terminal by running the following commands:

open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg

My app handles the link/links being passed in the func application(_ sender: NSApplication, openFiles filenames: [String]) method of AppDelegate.

This works fine so far. If my app is already open, the openFiles still gets called by MacOS with the new image path and my app opens a new window to display it. This all works well.

Now I want users to be able to pass certain arguments to my app. For example this:

open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg --args full

Here I want to receive the full argument. I read on few other posts to use the CommandLine.arguments API. But this doesn't seem to contain the arguments. Each time the value of CommandLine.arguments equals to this:

["/Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app/Contents/MacOS/AppName", "-NSDocumentRevisionsDebugMode", "YES"]

I think this is because the CommandLine.arguments only works when the app is initially launched and arguments are passed into the main function. But for an already opened app, these don't get passed as main isn't called again for an already running app.

How do I achieve this?

like image 336
sudoExclaimationExclaimation Avatar asked Oct 12 '25 15:10

sudoExclaimationExclaimation


1 Answers

With the -n option of the open command, you would always get a new instance of the app that could receive the arguments this way. However, you probably don`t want to have multiple instances of your app.

This means that you need a small command line program that performs a request with the arguments if the app is already running. In case the app is not running yet, it should probably start the app with the arguments.

There are several ways to communicate between a command line program and a GUI app. Depending on the exact requirements, they have corresponding advantages and disadvantages.

Command Line Tool

However, the actual process is always the same. Here exemplary with the help of the DistributedNotificationCenter, there the command line tool could look like this:

import AppKit


func stdError(_ msg: String) {
    guard let msgData = msg.data(using: .utf8) else { return }
    FileHandle.standardError.write(msgData)
}


private func sendRequest(with args: [String]) {
    if let json = try? JSONEncoder().encode(args) {
        
        DistributedNotificationCenter.default().postNotificationName(Notification.Name(rawValue: "\(bundleIdent).openRequest"),
                                                                     object: String(data: json, encoding: .utf8),
                                                                     userInfo: nil,
                                                                     deliverImmediately: true)
    }
}

let bundleIdent = "com.software7.test.NotificationReceiver"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter { $0.bundleIdentifier == bundleIdent }.isEmpty
let args = Array(CommandLine.arguments.dropFirst())

if(!isRunning) {
    if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdent) {
        let configuration = NSWorkspace.OpenConfiguration()
        configuration.arguments = args
        NSWorkspace.shared.openApplication(at: url,
                                           configuration: configuration,
                                           completionHandler: { (app, error) in
                                            if app == nil {
                                                stdError("starting \(bundleIdent) failed with error: \(String(describing: error))")
                                            }
                                            exit(0)
                                           })
    } else {
        stdError("app with bundle id \(bundleIdent) not found")
    }
} else {
    sendRequest(with: args)
    exit(0)
}


dispatchMain()

Note: since DistributedNotificationCenter can no longer send the userInfo with current macOS versions, the arguments are simply converted to JSON and passed with the object parameter.

App

The actual application can then use applicationDidFinishLaunching to determine if it was started freshly with arguments. If so, the arguments are evaluated. It also registers an observer for the notification. When a notification is received, the JSON is converted to the arguments. Here they are simply displayed in an alert in both cases. Could look like this:

import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let bundleIdent = "com.software7.test.NotificationReceiver"
        DistributedNotificationCenter.default().addObserver(self,
                                                            selector: #selector(requestReceived(_:)),
                                                            name: Notification.Name(rawValue: "\(bundleIdent).openRequest"),
                                                            object: nil)
        let args = Array(CommandLine.arguments.dropFirst())
        if !args.isEmpty {
            processArgs(args)
        }
    }
    
    private func processArgs(_ args: [String]) {
        let arguments = args.joined(separator: "\n")
        InfoDialog.show("request with arguments:", arguments)
    }
    
    @objc private func requestReceived(_ request: Notification) {
        if let jsonStr = request.object as? String {
            if let json = jsonStr.data(using: .utf8) {
                if let args = try? JSONDecoder().decode([String].self, from: json) {
                    processArgs(args)
                }
            }
        }
    }
}


struct InfoDialog {
    
    static func show(_ title: String, _ info: String) {
        let alert = NSAlert()
        alert.messageText = title
        alert.informativeText = info
        alert.alertStyle = .informational
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }
        
}

As mentioned earlier, the appropriate method of inter-process communication should be chosen depending on the exact requirements, but the flow would always be roughly the same.

Test

Test Call

like image 98
Stephan Schlecht Avatar answered Oct 14 '25 20:10

Stephan Schlecht