AppleScript Commands

I have a small utility named Status Quo and I would like to command it via
AppleScript. Lot of tutorials around, but they’re for much more complicated
cases. Here’s the simplified one.

Goal is to implement following commands:

tell application "Status Quo" to setAvailable
tell application "Status Quo" to setAvailable
message "Hallo"
tell application "Status Quo" to setDND
message "DND Test" expiresAt "2015-12-31T12:00:00+01:00"
tell application "Status Quo" to setAway
message "Away Test" expiresAt "2015-12-31T12:00:00+01:00"

First thing to do is to prepare scripting definition (.sdef) file. It’s quite
easy if you’re aiming for commands only.

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

<dictionary title="Status Quo Terminology">
<suite name="Status Quo Suite" code="SQSU"
description="Commands just for the Status Quo application.">

<command name="setAvailable" code="SQSUSAVA"
description="Sets Status Quo to Available with optional message.">

<cocoa class="ScriptSetAvailable"/>
<parameter name="message" code="SMSG" type="text"
description="Message." optional="yes"/>
<result type="boolean" description="Did set to Available?"/>
</command>

<command name="setDND" code="SQSUSDND"
description="Sets Status Quo to DND.">

<cocoa class="ScriptSetDND"/>
<parameter name="message" code="SMSG" type="text"
description="Message."/>
<parameter name="expiresAt" code="SEAT" type="text"
description="ISO8601 date."/>
<result type="boolean" description="Did set to DND?"/>
</command>

<command name="setAway" code="SQSUSAWA"
description="Sets Status Quo to Away.">

<cocoa class="ScriptSetAway"/>
<parameter name="message" code="SMSG" type="text"
description="Message."/>
<parameter name="expiresAt" code="SEAT" type="text"
description="ISO8601 date."/>
<result type="boolean" description="Did set to Away?"/>
</command>

</suite>
</dictionary>

Following rules must be met to avoid The application has a corrupted dictionary error message:

  • suit code = 4 upper cased characters,
  • command code = 8 upper cased characters,
  • parameter code = 4 upper cased characters.

Lower case is reserved for Apple. Rest is pretty descriptive itself.

XML file saved as StatusQuo.sdef and added to the Status Quo Xcode project. Now we have to
add two properties into Info.plist file:

  • Scripting definition file nameStatusQuo.sdef,
  • ScriptableYES.

Application is scriptable and the last thing to do is to implement
ScriptSetAvailable, ScriptSetAway and ScriptSetDND classes by subclassing
NSScriptCommand and overriding the performDefaultImplementation function.

private class ScriptPostStatusQuoCommand: NSScriptCommand {
var messageArgument: String {
return arguments?["message"] as? String ?? ""
}
var expiresAtArgument: String? {
return arguments?["expiresAt"] as? String
}

var expiresAtDate: NSDate? {
guard let expiresAt = expiresAtArgument else {
return nil
}

return ISO8601DateFormatter.dateFromString(expiresAt) ??
NSDate.dateWithNaturalLanguageString(expiresAt,
locale: NSLocale.currentLocale()) as? NSDate ??
NSDate.dateWithNaturalLanguageString(expiresAt,
locale: NSLocale.systemLocale()) as? NSDate
}

func postStatusQuo(withStatus status: Status) -> AnyObject? {
let message = messageArgument
let expiresAt = expiresAtDate

if status != .Available && expiresAt == nil {
return false
}

let statusQuo = CustomStatusQuo(status: status, message: message,
expiresAt: expiresAt)

suspendExecution()
DDLogVerbose("Scripting Status Quo (statusQuo)")
Session.sharedInstance.post(statusQuo) { [weak self] success in
self?.resumeExecutionWithResult(success)
}
return nil
}
}

@objc(ScriptSetAvailable)
private class ScriptSetAvailable: ScriptPostStatusQuoCommand {
override func performDefaultImplementation() -> AnyObject? {
return postStatusQuo(withStatus: .Available)
}
}

@objc(ScriptSetDND)
private class ScriptSetDND: ScriptPostStatusQuoCommand {
override func performDefaultImplementation() -> AnyObject? {
return postStatusQuo(withStatus: .DND)
}
}

@objc(ScriptSetAway)
private class ScriptSetAway: ScriptPostStatusQuoCommand {
override func performDefaultImplementation() -> AnyObject? {
return postStatusQuo(withStatus: .Away)
}
}

NSScriptCommand supports both synchronous and asynchronous commands. Just
return value if your command is synchronous. Call suspendExecution(),
return nil and return your real value via resumeExecutionWithResult()
function if your command is asynchronous.

And … that’s it. We’re done. Lot of ways how to do it, but I personally
do like subclasses of NSScriptCommand. Here’re some links if you want to learn more:

Enjoy holidays!