2

I'm making a Swift MacOS app which interacts with an external device via serial port. I can control the device through the app, but I want to be able to control it even within other apps using AppleScript (all I need is one simple method like tell application "App" to send "string"). I've searched numerous sources and couldn't find anything helpful.

I have zero knowledge in Obj-C.

Update:
I've read through some other tutorials and kinda got the idea. Unfortunately, I still don't understand how to make one simple method like tell application "App" to send "string".
E.g. Spotify Mac app has this string in its .sdef file:

    <command name="play track" code="spfyPCtx" description="Start playback of a track.">
        <access-group identifier="com.spotify.playback"/>
        <cocoa class="SPPlayTrackScriptCommand"/>
        <direct-parameter description="the URI of the track to play" type="text"/>
    </command>

Their app receives commands like tell application "Spotify" to play track <track> and executes them. This is exactly what I want.
Unfortunately, I have no idea how they handle this command in the main code. Maybe SPPlayTrackScriptCommand is implemented somehow.
I've read through many tutorials and still can't find anything about this particular thing.

Update #2
That's what I've managed to achieve:
Scriptable.sdef:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">

<dictionary title="App Scripting Terminology">
    
    <suite name="Scriptable App Suite" code="ScNo" description="Scriptable App suite.">
        
        <command name="send" code="SeNdtext">
            <access-group identifier="*"/>
            <direct-parameter description="Command to send" type="text" requires-access="r">
                <cocoa key="commandFlag"/>
            </direct-parameter>
            <result type="boolean" description="Did it do?"/>
        </command>
        
        <class name="application" code="capp" description="The application's basic scripting object.">
            <cocoa class="ScriptableApplication"/>
            <responds-to command="send">
                <cocoa method="send:"/>
            </responds-to>
        </class>
        
    </suite>
    
</dictionary>

ScriptableApp.swift:

@objc(ScriptableApplication) class ScriptableApplication: NSObject {
    func send(_ command: NSScriptCommand) -> Bool {
        //let commandFlag = command.evaluatedArguments?["commandFlag"] as? String
        print("A")
        return true
    }
}

Test AppleScript:

tell application "App"
    send "string"
end tell

Nothing works and all I get is missing value.

Thank you

aydar.media
  • 124
  • 1
  • 13
  • 1
    Making an app scriptable is not trivial. Here is a tutorial: https://www.raywenderlich.com/1033-making-a-mac-app-scriptable-tutorial Actually you don't need knowledge in Objective-C. – vadian Feb 17 '22 at 17:30
  • 2
    Things have really not changed since I explained it here: http://www.apeth.net/matt/scriptability/scriptabilityTutorial.html – matt Feb 17 '22 at 18:24
  • @matt, thanks, I was looking for a Swift tutorial though. I've updated my question, any help would be much appreciated. – aydar.media Feb 17 '22 at 19:14
  • @vadian, thanks, this tutorial does explain how to create commands such as `tell application "App" to mark the first task as done`, which are object-based (object is _the first task_). I managed to get this to work, but I don't want to mess around with objects. Instead I need a simple `tell application "App" to do "this"` command, and the tutorial does not cover such case. – aydar.media Feb 17 '22 at 19:17
  • Understood, but the facts are language agnostic. Just mentally translate to Swift. And of course the key thing is the sdef which is in neither language (it is XML). – matt Feb 17 '22 at 19:18
  • Plus the example I give of how to implement `tell application "MyApp" to pair person 1 to person 2` is exactly what you want to know. Try to escape your prejudices and try reading what I've written. – matt Feb 17 '22 at 19:25
  • @matt, thanks, I've read what you'd written. Maybe I'm stupid as hell but it seems to me that the aforementioned example only works if I implement the custom _person_ class inside of .sdef file and put ` ` inside of it. I don't want to do this. – aydar.media Feb 17 '22 at 19:37
  • For a single, simple command, I wouldn’t even bother using CocoaScripting. Use `-[NSAppleEventManager setEventHandler:andSelector:forEventClass:andEventID:]` to install a handler object for your `SeNd/text` event and remove the Cocoa-specific tags from your SDEF so it just supplies the human-readable terminology. – foo Feb 17 '22 at 20:55

1 Answers1

4

So, I managed to do exactly what I wanted after 4 hours of tedious research.

Here's all the code:
Scriptable.sdef:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">

<dictionary title="App Scripting Terminology">
    
    <suite name="Scriptable App Suite" code="NoSu" description="Scriptable App suite.">
        
        <command name="send" code="NoOnSend">
            <cocoa class="App.ScriptableApplicationCommand"/>
            <access-group identifier="*"/>
            <parameter code="NoCm" name="command" description="Command to send" type="text">
                <cocoa key="commandFlag"/>
            </parameter>
            <result type="text" description="ACK"/>
        </command>
        
    </suite>
    
</dictionary>

ScriptableCommand.swift:

import Foundation
import Cocoa

class ScriptableApplicationCommand: NSScriptCommand {
    override func performDefaultImplementation() -> Any? {
        let text = self.evaluatedArguments!["commandFlag"] as! String
        print(text)
        return text
    }
}

Test AppleScript:

tell application "Noonecares"
    send command "s"
end tell

Turns out App.ScriptableApplicationCommand is essential. ScriptableApplicationCommand alone doesn't do a thing.
At least today I learned something new.

aydar.media
  • 124
  • 1
  • 13
  • This helped me a lot to get a simple but effective scriptablility, great! One thing: "App" is depending on the project, I don't know how it is defined, I had to replace it with my app's project name. – soundflix Apr 30 '23 at 18:46
  • @soundflix -- Good sleuthing and I agree. In the example code in the question, it's clear that "App" is the name of his app. Pedagogically, it would have been clearer as something like "MyApp" and "MyApp.ScriptableApplicationCommand" so people would not read the "App" part as something Apple-specific, as you did. I do think "ScriptableApplicationCommand" is absolutely clear as an example. – August Jun 04 '23 at 06:04
  • You have `suite name="Scriptable App Suite" code="NoSu"` and `command name="send" code="NoOnSend"`. I'm still figuring this out myself and I have seen multiple notes that the first four letters of the command code "should" be the same as the suite code. I don't know if that's convention or if it "must" be the same. Not having any working code yet, I can't test if it makes a difference. – August Jun 04 '23 at 07:04