├── MacOSGlues ├── AppleScriptUtilityGlue.swift ├── AppleScriptUtilityGlue.swift.sdef ├── AutomatorGlue.swift ├── AutomatorGlue.swift.sdef ├── BluetoothFileExchangeGlue.swift ├── BluetoothFileExchangeGlue.swift.sdef ├── CalendarGlue.swift ├── CalendarGlue.swift.sdef ├── ContactsGlue.swift ├── ContactsGlue.swift.sdef ├── DatabaseEventsGlue.swift ├── DatabaseEventsGlue.swift.sdef ├── FinderGlue.swift ├── FinderGlue.swift.sdef ├── FontBookGlue.swift ├── FontBookGlue.swift.sdef ├── GarageBandGlue.swift ├── GarageBandGlue.swift.sdef ├── ITunesGlue.swift ├── ITunesGlue.swift.sdef ├── ImageEventsGlue.swift ├── ImageEventsGlue.swift.sdef ├── Info.plist ├── KeynoteGlue.swift ├── KeynoteGlue.swift.sdef ├── MacOSGlues.h ├── MailGlue.swift ├── MailGlue.swift.sdef ├── MessagesGlue.swift ├── MessagesGlue.swift.sdef ├── NotesGlue.swift ├── NotesGlue.swift.sdef ├── NumbersGlue.swift ├── NumbersGlue.swift.sdef ├── PagesGlue.swift ├── PagesGlue.swift.sdef ├── PhotosGlue.swift ├── PhotosGlue.swift.sdef ├── QuickTimePlayerGlue.swift ├── QuickTimePlayerGlue.swift.sdef ├── RemindersGlue.swift ├── RemindersGlue.swift.sdef ├── SafariGlue.swift ├── SafariGlue.swift.sdef ├── ScriptEditorGlue.swift ├── ScriptEditorGlue.swift.sdef ├── SystemEventsGlue.swift ├── SystemEventsGlue.swift.sdef ├── SystemInformationGlue.swift ├── SystemInformationGlue.swift.sdef ├── SystemPreferencesGlue.swift ├── SystemPreferencesGlue.swift.sdef ├── TerminalGlue.swift ├── TerminalGlue.swift.sdef ├── TextEditGlue.swift ├── TextEditGlue.swift.sdef ├── VoiceOverGlue.swift ├── VoiceOverGlue.swift.sdef ├── XcodeGlue.swift └── XcodeGlue.swift.sdef ├── README ├── SwiftAE.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── Frameworks Release.xcscheme │ │ ├── MacOSGlues.xcscheme │ │ ├── SwiftAutomation.xcscheme │ │ ├── aeglue.xcscheme │ │ └── test.xcscheme └── xcuserdata │ └── has.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── Release.xcscheme │ └── xcschememanagement.plist ├── SwiftAutomation ├── AEApplicationGlue.swift ├── AEConstants.swift ├── AETEParser.swift ├── AppData.swift ├── AppleEventFormatter.swift ├── DefaultTerminology.swift ├── Errors.swift ├── GlueTable.swift ├── Info.plist ├── KeywordConverter.swift ├── SDEFParser.swift ├── Specifier.swift ├── SpecifierExtensions.swift ├── SpecifierFormatter.swift ├── StaticGlueBuilder.swift ├── Support.swift ├── SwiftAutomation.h ├── SwiftGlueTemplate.swift ├── Symbol.swift ├── TermTypes.swift ├── TypeExtensions.swift └── main.swift ├── TODO.txt ├── aeglue └── main.swift ├── doc-generated ├── advanced-type-support.html ├── application-objects.html ├── application_architecture.gif ├── application_architecture2.gif ├── client_app_to_itunes_event.gif ├── commands.html ├── creating-and-using-static-glues.html ├── examples.html ├── finder_to_textedit_event.gif ├── full.css ├── index.html ├── installing-swiftautomation.html ├── low-level-apis.html ├── notes.html ├── object-specifiers.html ├── optimizing-performance.html ├── relationships_example.gif ├── tutorial-introduction.html ├── type-mappings.html ├── understanding-apple-events.html └── welcome.html ├── doc-source ├── 1 welcome.md ├── 10 examples.md ├── 11 advanced-type-support.md ├── 12 optimizing-performance.md ├── 13 low-level-apis.md ├── 14 notes.md ├── 2 installing-swiftautomation.md ├── 3 tutorial-introduction.md ├── 4 understanding-apple-events.md ├── 5 creating-and-using-static-glues.md ├── 6 type-mappings.md ├── 7 application-objects.md ├── 8 object-specifiers.md ├── 9 commands.md ├── application_architecture.gif ├── application_architecture2.gif ├── client_app_to_itunes_event.gif ├── finder_to_textedit_event.gif ├── full.css └── relationships_example.gif ├── mkdoc └── test ├── FinderGlue.swift ├── ITunesGlue.swift ├── TextEditGlue.swift └── main.swift /MacOSGlues/AppleScriptUtilityGlue.swift.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /MacOSGlues/BluetoothFileExchangeGlue.swift.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /MacOSGlues/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MacOSGlues/MacOSGlues.h: -------------------------------------------------------------------------------- 1 | // 2 | // MacOSGlues.h 3 | // MacOSGlues 4 | // 5 | // 6 | // SwiftAutomation glues for macOS applications. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for MacOSGlues. 12 | FOUNDATION_EXPORT double MacOSGluesVersionNumber; 13 | 14 | //! Project version string for MacOSGlues. 15 | FOUNDATION_EXPORT const unsigned char MacOSGluesVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /MacOSGlues/VoiceOverGlue.swift.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | # SwiftAutomation README 2 | 3 | ## About 4 | 5 | SwiftAutomation is an Apple event bridge that allows Apple's Swift language 6 | to control "AppleScriptable" macOS applications directly. For example: 7 | 8 | // tell application "iTunes" to play 9 | try ITunes().play() 10 | 11 | // tell application "Finder" to set fileNames to name of every file of home 12 | let fileNames = try Finder().home.files.name.get() as [String] 13 | 14 | 15 | // tell application "TextEdit" to make new document ¬ 16 | // with properties {text:"Hello World!"} 17 | try TextEdit().make(new: TED.document, 18 | withProperties: [TED.text: "Hello World!"]) 19 | 20 | 21 | The SwiftAutomation framework defines the basic functionality for constructing 22 | object specifiers, converting data between Swift and AE types, and sending 23 | Apple events to running applications. Generated Swift files supply the glue 24 | code for controlling individual applications using human-readable terminology. 25 | 26 | 27 | ## Get it 28 | 29 | To clone the Xcode project to your own machine: 30 | 31 | git clone https://bitbucket.org/hhas/swiftae.git 32 | 33 | A basic Swift "script editor" (currently under development) is also available: 34 | 35 | https://bitbucket.org/hhas/swiftautoedit 36 | 37 | 38 | ## Install it 39 | 40 | To embed SwiftAutomation for use in Swift-based GUI apps see Xcode's Workspace 41 | documentation. 42 | 43 | To use SwiftAutomation in Swift "scripting", see the Installing SwiftAutomation 44 | chapter of the SwiftAutomation documentation. Until Swift provides a stable ABI 45 | some manual set-up is required. 46 | 47 | 48 | ## Try it 49 | 50 | To run simple examples (see test/main.swift), build and run the `test` target. 51 | 52 | Additional glues can be generated by building the `aeglue` target and running 53 | the resulting `aeglue` command line tool in Terminal. For example, to generate 54 | a Swift glue and accompanying documentation for macOS's Photos application: 55 | 56 | /path/to/aeglue -o ~/Desktop Photos 57 | 58 | Note that `aeglue` normally retrieves application terminology using `ascrgdte` 59 | ('get dynamic terminology') Apple events. Some applications (e.g. Finder) have 60 | faulty `ascrgdte` handlers that fail to return correct terminology, in which 61 | case use the `-s` or 'Use SDEF terminology' options instead. (Be aware that 62 | SDEF-based terminology may also contain bugs and omissions, in which case use 63 | raw four-char codes or correct generated glue code by hand.) 64 | 65 | SwiftAutomation requires macOS 10.11 and Swift 3.0 and Xcode 8.0 or later. 66 | 67 | 68 | ## Status 69 | 70 | The code is complete except for testing and bug fixes. The documentation lacks 71 | a usable tutorial chapter. Given current uncertainty regarding the future of 72 | Apple event-based automation the project is on hiatus until WWDC17, after which 73 | a final decision on its future can be made. 74 | 75 | 76 | ## Known issues 77 | 78 | When using SwiftAutomation within an interactive playground, be aware that Xcode 79 | automatically re-runs ALL code within a playground whenever a line of code is 80 | modified, causing ALL application commands to be re-sent. This is not a problem 81 | when using non-mutating application commands such as `get` and `count`; however, 82 | take care when using commands that modify the application's state - `make`, 83 | `move`, `delete`, etc. - within a playground as sending these more than once may 84 | have unintended/undesirable results. This is a playground issue that affects ALL 85 | non-idempotent and/or unsafe function calls, not just application commands. 86 | 87 | 88 | ## Etc. 89 | 90 | SwiftAutomation is released into the public domain. 91 | 92 | No warranty given, E&OE, use at own risk, etc. 93 | 94 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcshareddata/xcschemes/Frameworks Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcshareddata/xcschemes/MacOSGlues.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcshareddata/xcschemes/SwiftAutomation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcshareddata/xcschemes/aeglue.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcshareddata/xcschemes/test.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcuserdata/has.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /SwiftAE.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Frameworks Release.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 4 11 | 12 | MacOSGlues.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 3 16 | 17 | SwiftAutomation.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 0 21 | 22 | aeglue.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 1 26 | 27 | test.xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 2 31 | 32 | 33 | SuppressBuildableAutocreation 34 | 35 | 4F0E39571BAA97D400FF2A96 36 | 37 | primary 38 | 39 | 40 | 4F0E39641BAA98A900FF2A96 41 | 42 | primary 43 | 44 | 45 | 4F0E399B1BAF7A7400FF2A96 46 | 47 | primary 48 | 49 | 50 | 4F1A57F91D9E81C300ACFA51 51 | 52 | primary 53 | 54 | 55 | 4F63C4231B7CF06E000D74EE 56 | 57 | primary 58 | 59 | 60 | 4F9AFB351D5B58EB00A71890 61 | 62 | primary 63 | 64 | 65 | 4FFF5D231D899F0D008ED4F5 66 | 67 | primary 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /SwiftAutomation/DefaultTerminology.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultTerminology.swift 3 | // SwiftAutomation 4 | // 5 | // Standard terminology selectively taken from AppleScript's AEUT resource 6 | // 7 | // Notes: 8 | // 9 | // - this list should be updated if/when new terms are added to AS's dictionary 10 | // 11 | // - old/obsolete terms are retained for sake of compatibility with older Carbon apps' AETEs, which may still use them 12 | // 13 | 14 | import Foundation 15 | 16 | 17 | // TO DO: check for any missing terms (e.g. ctxt) 18 | 19 | 20 | public class DefaultTerminology: ApplicationTerminology { 21 | 22 | // note: each client language should create its own DefaultTerminology instance, passing its own keyword converter to init, then pass the resulting DefaultTerminology instance to GlueData constructor // TO DO: would probably be cleaner if KeywordConverter base class automatically created and cached DefaultTerminology instances 23 | 24 | public let types: [KeywordTerm] 25 | public let enumerators: [KeywordTerm] 26 | public let properties: [KeywordTerm] 27 | public let elements: [ClassTerm] 28 | public let commands: [CommandTerm] 29 | 30 | public init(keywordConverter: KeywordConverterProtocol) { 31 | self.types = self._types.map({KeywordTerm(name: keywordConverter.convertSpecifierName($0), kind: .type, code: $1)}) 32 | self.enumerators = self._enumerators.map({KeywordTerm(name: keywordConverter.convertSpecifierName($0), kind: .enumerator, code: $1)}) 33 | self.properties = self._properties.map({KeywordTerm(name: keywordConverter.convertSpecifierName($0), kind: .property, code: $1)}) 34 | self.elements = self._elements.map({ClassTerm(singular: keywordConverter.convertSpecifierName($0), 35 | plural: keywordConverter.convertSpecifierName($1), code: $2)}) 36 | self.commands = self._commands.map({ 37 | let term = CommandTerm(name: keywordConverter.convertSpecifierName($0), eventClass: $1, eventID: $2) 38 | for (name, code) in $3 { term.addParameter(keywordConverter.convertParameterName(name), code: code) } 39 | return term 40 | }) 41 | } 42 | 43 | private typealias Keywords = [(String, OSType)] 44 | private typealias Elements = [(String, String, OSType)] 45 | private typealias Commands = [(String, OSType, OSType, [(String, OSType)])] 46 | 47 | // note: AppleScript-style keyword names are automatically converted to the required format using the given keyword converter 48 | 49 | // TO DO: review list against current AppleScript AEUT 50 | 51 | private let _types: Keywords = [("anything", _typeWildCard), 52 | ("boolean", _typeBoolean), 53 | ("short integer", _typeSInt16), 54 | ("integer", _typeSInt32), 55 | ("double integer", _typeSInt64), 56 | ("unsigned short integer", _typeUInt16), // no AS keyword 57 | ("unsigned integer", _typeUInt32), 58 | ("unsigned double integer", _typeUInt64), // no AS keyword 59 | ("fixed", _typeFixed), 60 | ("long fixed", _typeLongFixed), 61 | ("decimal struct", _typeDecimalStruct), // no AS keyword 62 | ("small real", _typeIEEE32BitFloatingPoint), 63 | ("real", _typeIEEE64BitFloatingPoint), 64 | ("extended real", _typeExtended), 65 | ("large real", _type128BitFloatingPoint), // no AS keyword 66 | ("string", _typeText), 67 | ("styled text", _typeStyledText), 68 | ("text style info", _typeTextStyles), 69 | ("styled clipboard text", _typeScrapStyles), 70 | ("encoded string", _typeEncodedString), 71 | ("writing code", _pScriptTag), 72 | ("international writing code", _typeIntlWritingCode), 73 | ("international text", _typeIntlText), 74 | ("Unicode text", _typeUnicodeText), 75 | ("UTF8 text", _typeUTF8Text), // no AS keyword 76 | ("UTF16 text", _typeUTF16ExternalRepresentation), // no AS keyword 77 | ("version", _typeVersion), 78 | ("date", _typeLongDateTime), 79 | ("list", _typeAEList), 80 | ("record", _typeAERecord), 81 | ("data", _typeData), 82 | ("script", _typeScript), 83 | ("location reference", _typeInsertionLoc), 84 | ("reference", _typeObjectSpecifier), 85 | ("alias", _typeAlias), 86 | ("file ref", _typeFSRef), // no AS keyword 87 | ("file specification", _typeFSS), 88 | ("bookmark data", _typeBookmarkData), // no AS keyword 89 | ("file URL", _typeFileURL), // no AS keyword 90 | ("point", _typeQDPoint), 91 | ("bounding rectangle", _typeQDRectangle), 92 | ("fixed point", _typeFixedPoint), 93 | ("fixed rectangle", _typeFixedRectangle), 94 | ("long point", _typeLongPoint), 95 | ("long rectangle", _typeLongRectangle), 96 | ("long fixed point", _typeLongFixedPoint), 97 | ("long fixed rectangle", _typeLongFixedRectangle), 98 | ("EPS picture", _typeEPS), 99 | ("GIF picture", _typeGIF), 100 | ("JPEG picture", _typeJPEG), 101 | ("PICT picture", _typePict), 102 | ("TIFF picture", _typeTIFF), 103 | ("RGB color", _typeRGBColor), 104 | ("RGB16 color", _typeRGB16), 105 | ("RGB96 color", _typeRGB96), 106 | ("graphic text", _typeGraphicText), 107 | ("color table", _typeColorTable), 108 | ("pixel map record", _typePixMapMinus), 109 | ("best", _typeBest), 110 | ("type class", _typeType), 111 | ("constant", _typeEnumeration), 112 | ("property", _typeProperty), 113 | ("mach port", _typeMachPort), // no AS keyword 114 | ("kernel process ID", _typeKernelProcessID), // no AS keyword 115 | ("application bundle ID", _typeApplicationBundleID), // no AS keyword 116 | ("process serial number", _typeProcessSerialNumber), // no AS keyword 117 | ("application signature", _typeApplSignature), // no AS keyword 118 | ("application URL", _typeApplicationURL), // no AS keyword 119 | // ("missing value", _cMissingValue), // represented as MissingValue constant, not Symbol instance 120 | ("null", _typeNull), 121 | ("machine location", _typeMachineLoc), 122 | ("machine", _cMachine), 123 | ("dash style", _typeDashStyle), 124 | ("rotation", _typeRotation), 125 | ("item", _cObject), 126 | ("January", _cJanuary), 127 | ("February", _cFebruary), 128 | ("March", _cMarch), 129 | ("April", _cApril), 130 | ("May", _cMay), 131 | ("June", _cJune), 132 | ("July", _cJuly), 133 | ("August", _cAugust), 134 | ("September", _cSeptember), 135 | ("October", _cOctober), 136 | ("November", _cNovember), 137 | ("December", _cDecember), 138 | ("Sunday", _cSunday), 139 | ("Monday", _cMonday), 140 | ("Tuesday", _cTuesday), 141 | ("Wednesday", _cWednesday), 142 | ("Thursday", _cThursday), 143 | ("Friday", _cFriday), 144 | ("Saturday", _cSaturday), 145 | ] 146 | private let _enumerators: Keywords = [("yes", _kAEYes), 147 | ("no", _kAENo), 148 | ("ask", _kAEAsk), 149 | ("case", _kAECase), 150 | ("diacriticals", _kAEDiacritic), 151 | ("expansion", _kAEExpansion), 152 | ("hyphens", _kAEHyphens), 153 | ("punctuation", _kAEPunctuation), 154 | ("whitespace", _kAEWhiteSpace), 155 | ("numeric strings", _kASNumericStrings), 156 | ] 157 | private let _properties: Keywords = [("class", _pClass), 158 | ("id", _pID), 159 | ("properties", _pALL), 160 | ] 161 | private let _elements: Elements = [("item", "items", _cObject), 162 | // what about ("text",cText)? 163 | ] 164 | private let _commands: Commands = [("run", kCoreEventClass, _kAEOpenApplication, []), 165 | ("open", kCoreEventClass, _kAEOpenDocuments, []), 166 | ("print", kCoreEventClass, _kAEPrintDocuments, []), 167 | ("quit", kCoreEventClass, _kAEQuitApplication, [("saving", _keyAESaveOptions)]), 168 | ("reopen", kCoreEventClass, _kAEReopenApplication, []), 169 | //("launch", _kASAppleScriptSuite, _kASLaunchEvent, []), // this is a hardcoded method 170 | ("activate", _kAEMiscStandards, _kAEActivate, []), 171 | ("open location", _GURL, _GURL, [("window", _WIND)]), 172 | ("get", _kAECoreSuite, _kAEGetData, []), 173 | ("set", _kAECoreSuite, _kAESetData, [("to", _keyAEData)]), 174 | ] 175 | 176 | private static let _GURL = try! fourCharCode("GURL") 177 | private static let _WIND = try! fourCharCode("WIND") 178 | private static let _pALL = try! fourCharCode("pALL") 179 | } 180 | 181 | -------------------------------------------------------------------------------- /SwiftAutomation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftAutomation/SDEFParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDEFParser.swift 3 | // SwiftAutomation 4 | // 5 | // 6 | 7 | // TO DO: what about synonym, xref? 8 | 9 | // note: GlueTable will resolve any conflicts between built-in and app-defined name+code definitions 10 | 11 | 12 | // TO DO: see if rewriting to use NSXMLDocument will take care of XInclude (there doesn't seem to be a convenience API for handling includes in SAX parser, and while adding support for simple includes shouldn't be hard some SDEF includes like to use xpointers as well, just to make the things even more insanely complex than they already are) 13 | 14 | 15 | import Foundation 16 | import Carbon 17 | 18 | 19 | 20 | public class SDEFParser: ApplicationTerminology { 21 | 22 | // SDEF names and codes are parsed into the following tables 23 | public private(set) var types: [KeywordTerm] = [] 24 | public private(set) var enumerators: [KeywordTerm] = [] 25 | public private(set) var properties: [KeywordTerm] = [] 26 | public private(set) var elements: [ClassTerm] = [] 27 | public var commands: [CommandTerm] { return Array(self.commandsDict.values) } 28 | 29 | private var commandsDict = [String:CommandTerm]() 30 | private let keywordConverter: KeywordConverterProtocol 31 | private let errorHandler: (Error)->() 32 | 33 | public init(keywordConverter: KeywordConverterProtocol = defaultSwiftKeywordConverter, 34 | errorHandler: @escaping (Error)->()) { 35 | self.keywordConverter = keywordConverter 36 | self.errorHandler = errorHandler // TO DO: currently unused (currently parse methods always throw); also, errorHandler should probably be throwable 37 | } 38 | 39 | // parse an OSType given as 4/8-character "MacRoman" string, or 10/18-character hex string 40 | 41 | func parse(fourCharCode string: NSString) throws -> OSType { // class, property, enum, param, etc. code 42 | if string.length == 10 && (string.hasPrefix("0x") || string.hasPrefix("0X")) { // e.g. "0x00000001" 43 | guard let result = UInt32(string.substring(with: NSRange(location: 2, length: 8)), radix: 16) else { 44 | throw AutomationError(code: 1, message: "Invalid four-char code (bad representation): \(string.debugDescription)") 45 | } 46 | return result 47 | } else { 48 | return try fourCharCode(string as String) 49 | } 50 | } 51 | 52 | func parse(eightCharCode string: NSString) throws -> (OSType, OSType) { // eventClass and eventID code 53 | if string.length == 8 { 54 | return (try fourCharCode(string.substring(to: 4)), try fourCharCode(string.substring(from: 4))) 55 | } else if string.length == 18 && (string.hasPrefix("0x") || string.hasPrefix("0X")) { // e.g. "0x0123456701234567" 56 | guard let eventClass = UInt32(string.substring(with: NSRange(location: 2, length: 8)), radix: 16), 57 | let eventID = UInt32(string.substring(with: NSRange(location: 10, length: 8)), radix: 16) else { 58 | throw AutomationError(code: 1, message: "Invalid eight-char code (bad representation): \(string.debugDescription)") 59 | } 60 | return (eventClass, eventID) 61 | } else { 62 | throw AutomationError(code: 1, message: "Invalid eight-char code (wrong length): \((string as String).debugDescription)") 63 | } 64 | } 65 | 66 | // extract name and code attributes from a class/enumerator/command/etc XML element 67 | 68 | private func attribute(_ name: String, of element: XMLElement) -> String? { 69 | return element.attribute(forName: name)?.stringValue 70 | } 71 | 72 | private func parse(keywordElement element: XMLElement) throws -> (String, OSType) { 73 | guard let name = self.attribute("name", of: element), let codeString = self.attribute("code", of: element), name != "" else { 74 | throw TerminologyError("Missing 'name'/'code' attribute.") 75 | } 76 | return (name, try self.parse(fourCharCode: codeString as NSString)) 77 | } 78 | 79 | private func parse(commandElement element: XMLElement) throws -> (String, OSType, OSType) { 80 | guard let name = self.attribute("name", of: element), let codeString = self.attribute("code", of: element), name != "" else { 81 | throw TerminologyError("Missing 'name'/'code' attribute.") 82 | } 83 | let (eventClass, eventID) = try self.parse(eightCharCode: codeString as NSString) 84 | return (name, eventClass, eventID) 85 | } 86 | 87 | // 88 | 89 | private func parse(typeOfElement element: XMLElement) throws -> (String, OSType) { // class, record-type, value-type 90 | let (name, code) = try self.parse(keywordElement: element) 91 | self.types.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), kind: .type, code: code)) 92 | return (name, code) 93 | } 94 | 95 | private func parse(propertiesOfElement element: XMLElement) throws { // class, class-extension, record-value 96 | for element in element.elements(forName: "property") { 97 | let (name, code) = try self.parse(keywordElement: element) 98 | self.properties.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), kind: .property, code: code)) 99 | } 100 | } 101 | 102 | // parse a class/enumerator/command/etc element of a dictionary suite 103 | 104 | func parse(definition node: XMLNode) throws { 105 | if let element = node as? XMLElement, let tagName = element.name { 106 | switch tagName { 107 | case "class": 108 | let (name, code) = try self.parse(typeOfElement: element) 109 | try self.parse(propertiesOfElement: element) 110 | // use plural class name as elements name (if not given, append "s" to singular name) 111 | // (note: record and value types also define plurals, but we only use plurals for element names and elements should always be classes, so we ignore those) 112 | let plural = element.attribute(forName: "plural")?.stringValue ?? ( 113 | (name == "text" || name.hasSuffix("s")) ? name : "\(name)s") // SDEF spec says to append 's' to name when plural attribute isn't given; in practice, appending 's' doesn't work so well for names already ending in 's' (e.g. 'print settings'), nor for 'text' (which is AppleScript-defined), so special-case those here (note that macOS's SDEF->AETE converter will append "s" to singular names that already end in "s"; nothing we can do about that) 114 | self.elements.append(ClassTerm(singular: name, plural: self.keywordConverter.convertSpecifierName(plural), code: code)) 115 | case "class-extension": 116 | try self.parse(propertiesOfElement: element) 117 | case "record-type": 118 | let _ = try self.parse(typeOfElement: element) 119 | try self.parse(propertiesOfElement: element) 120 | case "value-type": 121 | let _ = try self.parse(typeOfElement: element) 122 | case "enumeration": 123 | for element in element.elements(forName: "enumerator") { 124 | let (name, code) = try self.parse(keywordElement: element) 125 | self.enumerators.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), kind: .enumerator, code: code)) 126 | } 127 | case "command", "event": 128 | let (name, eventClass, eventID) = try self.parse(commandElement: element) 129 | // Note: overlapping command definitions (e.g. 'path to') should be processed as follows: 130 | // - If their names and codes are the same, only the last definition is used; other definitions are ignored 131 | // and will not compile. 132 | // - If their names are the same but their codes are different, only the first definition is used; other 133 | // definitions are ignored and will not compile. 134 | let previousDef = self.commandsDict[name] 135 | if previousDef == nil || (previousDef!.eventClass == eventClass && previousDef!.eventID == eventID) { 136 | let command = CommandTerm(name: self.keywordConverter.convertSpecifierName(name), eventClass: eventClass, eventID: eventID) 137 | self.commandsDict[name] = command 138 | for element in element.elements(forName: "parameter") { 139 | let (name, code) = try self.parse(keywordElement: element) 140 | command.addParameter(self.keywordConverter.convertParameterName(name), code: code) 141 | } 142 | } // else ignore duplicate declaration 143 | default: () 144 | } 145 | } 146 | } 147 | 148 | // parse the given SDEF XML data 149 | 150 | public func parse(_ sdef: Data) throws { 151 | do { 152 | let parser = try XMLDocument(data: sdef, options: XMLNode.Options.documentXInclude) 153 | guard let dictionary = parser.rootElement() else { throw TerminologyError("Missing `dictionary` element.") } 154 | for suite in dictionary.elements(forName: "suite") { 155 | if let nodes = suite.children { 156 | for node in nodes { try self.parse(definition: node) } 157 | } 158 | } 159 | } catch { 160 | throw TerminologyError("An error occurred while parsing SDEF. \(error)") 161 | } 162 | } 163 | } 164 | 165 | 166 | // convenience function 167 | 168 | public func GetScriptingDefinition(_ url: URL) throws -> Data { 169 | var sdef: Unmanaged? 170 | let err = OSACopyScriptingDefinitionFromURL(url as NSURL, 0, &sdef) 171 | if err != 0 { 172 | throw AutomationError(code: Int(err), message: "Can't retrieve SDEF.") 173 | } 174 | return sdef!.takeRetainedValue() as Data 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /SwiftAutomation/SwiftAutomation.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAutomation.h 3 | // SwiftAutomation 4 | // 5 | // 6 | 7 | #import 8 | 9 | //! Project version number for SwiftAutomation. 10 | FOUNDATION_EXPORT double SwiftAutomationVersionNumber; 11 | 12 | //! Project version string for SwiftAutomation. 13 | FOUNDATION_EXPORT const unsigned char SwiftAutomationVersionString[]; 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SwiftAutomation/Symbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Symbol.swift 3 | // SwiftAutomation 4 | // 5 | // 6 | // Represents typeType/typeEnumerated/typeProperty/typeKeyword descriptors. Static glues subclass this to add static vars representing each type/enum/property keyword defined by the application dictionary. 7 | // 8 | // Also used to represent string-based record keys (where type=0 and name!=nil) when unpacking an AERecord's keyASUserRecordFields property, allowing the resulting dictionary to hold any mixture of terminology- (keyword) and user-defined (string) keys while typed as [Symbol:Any]. 9 | // 10 | 11 | 12 | import Foundation 13 | 14 | 15 | 16 | open class Symbol: Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable, SelfPacking { 17 | 18 | private var _descriptor: NSAppleEventDescriptor? 19 | public let name: String?, code: OSType, type: OSType 20 | 21 | open var typeAliasName: String {return "Symbol"} // provides prefix used in description var; glue subclasses override this with their own strings (e.g. "FIN" for Finder) 22 | 23 | public required init(name: String?, code: OSType, type: OSType = typeType, descriptor: NSAppleEventDescriptor? = nil) { // (When unpacking a symbol descriptor returned by an application command, if the returned AEDesc is passed here it'll be cached here for reuse, avoiding the need to fully-repack the new Symbol instance if/when it's subsequently used in another application command. In practice, this is mostly irrelevant to static glues, as Symbol subclasses in static glues are normally constructed via Symbol.symbol(), which lazily instantiates existing glue-defined static vars instead. Whether it makes any noticeable difference to dynamic bridges remains to be seen [it's not likely to be a major bottleneck], but it costs nothing for AppData to include it if it has it.) 24 | self.name = name 25 | self.code = code 26 | self.type = type 27 | self._descriptor = descriptor // The SwiftAutomation_packSelf() method below will cache the resulting descriptor the first time it is called, avoiding the need to fully-repack the same Symbol on subsequent calls. 28 | } 29 | 30 | // special constructor for string-based record keys (avoids the need to wrap dictionary keys in a `StringOrSymbol` enum when unpacking) 31 | // e.g. the AppleScript record `{name:"Bob", isMyUser:true}` maps to the Swift Dictionary `[Symbol.name:"Bob", Symbol("isMyUser"):true]` 32 | 33 | public convenience init(_ name: String, descriptor: NSAppleEventDescriptor? = nil) { 34 | self.init(name: name, code: noOSType, type: noOSType, descriptor: descriptor) 35 | } 36 | 37 | // convenience constructors for creating Symbols using raw four-char codes 38 | 39 | public convenience init(code: String, type: String = "type") { 40 | self.init(name: nil, code: UTGetOSTypeFromString(code as CFString), type: UTGetOSTypeFromString(type as CFString)) 41 | } 42 | 43 | public convenience init(code: OSType, type: OSType = typeType) { 44 | self.init(name: nil, code: code, type: type) 45 | } 46 | 47 | // this is called by AppData when unpacking typeType, typeEnumerated, etc; glue-defined symbol subclasses should override to return glue-defined symbols where available 48 | open class func symbol(code: OSType, type: OSType = typeType, descriptor: NSAppleEventDescriptor? = nil) -> Symbol { 49 | return self.init(name: nil, code: code, type: type, descriptor: descriptor) 50 | } 51 | 52 | // this is called by AppData when unpacking string-based record keys 53 | public class func symbol(string: String, descriptor: NSAppleEventDescriptor? = nil) -> Symbol { 54 | return self.init(name: string, code: noOSType, type: noOSType, descriptor: descriptor) 55 | } 56 | 57 | // display 58 | 59 | public var description: String { 60 | if let name = self.name { 61 | return self.nameOnly ? "\(self.typeAliasName)(\(name.debugDescription))" : "\(self.typeAliasName).\(name)" 62 | } else { 63 | return "\(self.typeAliasName)(code:\(formatFourCharCodeString(self.code)),type:\(formatFourCharCodeString(self.type)))" 64 | } 65 | } 66 | 67 | public var debugDescription: String { return self.description } 68 | 69 | public var customMirror: Mirror { 70 | let children: [Mirror.Child] = [(label: "description", value: self.description), (label: "name", value: self.name ?? ""), 71 | (label: "code", value: fourCharCode(self.code)), (label: "type", value: fourCharCode(self.type))] 72 | return Mirror(self, children: children, displayStyle: .`class`, ancestorRepresentation: .suppressed) 73 | } 74 | 75 | // packing 76 | 77 | public var descriptor: NSAppleEventDescriptor { // used by SwiftAutomation_packSelf and previous()/next() selectors 78 | if self._descriptor == nil { 79 | if self.nameOnly { 80 | self._descriptor = NSAppleEventDescriptor(string: self.name!) 81 | } else { 82 | self._descriptor = NSAppleEventDescriptor(type: self.type, code: self.code) 83 | } 84 | } 85 | return self._descriptor! 86 | } 87 | 88 | // returns true if Symbol contains name but not code (i.e. it represents a string-based record property key) 89 | public var nameOnly: Bool { return self.type == noOSType && self.name != nil } 90 | 91 | public func SwiftAutomation_packSelf(_ appData: AppData) throws -> NSAppleEventDescriptor { 92 | return self.descriptor 93 | } 94 | 95 | // equatable, hashable 96 | 97 | public var hashValue: Int { return self.nameOnly ? self.name!.hashValue : Int(self.code) } // see also comments in `==()` below 98 | 99 | public static func ==(lhs: Symbol, rhs: Symbol) -> Bool { 100 | // note: operands are not required to be the same subclass as this compares for AE equality only, e.g.: 101 | // 102 | // TED.document == AESymbol(code: "docu") -> true 103 | // 104 | // note: AE types are also ignored on the [reasonable] assumption that any differences in descriptor type (e.g. typeType vs typeProperty) are irrelevant as apps will only care about the code itself 105 | return lhs.nameOnly && rhs.nameOnly ? lhs.name == rhs.name : lhs.code == rhs.code 106 | } 107 | } 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /SwiftAutomation/TermTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TermTypes.swift 3 | // SwiftAutomation 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | 11 | public class TerminologyError: AutomationError { 12 | public init(_ message: String, cause: Error? = nil) { 13 | super.init(code: errOSACorruptTerminology, message: message, cause: cause) 14 | } 15 | } 16 | 17 | 18 | public protocol ApplicationTerminology { // GlueTable.add() accepts any object that adopts this protocol (normally AETEParser/SDEFParser, but a dynamic bridge could also use this to reimport previously exported tables to which manual corrections have been made) 19 | var types: [KeywordTerm] {get} 20 | var enumerators: [KeywordTerm] {get} 21 | var properties: [KeywordTerm] {get} 22 | var elements: [ClassTerm] {get} 23 | var commands: [CommandTerm] {get} 24 | } 25 | 26 | 27 | // TO DO: get rid of Term classes; rename TermType enum to Term and attach names and codes to that 28 | 29 | public enum TermType { 30 | case type 31 | case enumerator 32 | case property 33 | case command 34 | case parameter 35 | } 36 | 37 | 38 | public class Term { // base class for keyword and command definitions 39 | 40 | public var name: String // editable as GlueTable may need to escape names to disambiguate conflicting terms 41 | public let kind: TermType 42 | 43 | init(name: String, kind: TermType) { 44 | self.name = name 45 | self.kind = kind 46 | } 47 | } 48 | 49 | 50 | public class KeywordTerm: Term, Hashable, CustomStringConvertible { // type/enumerator/property/element/parameter name 51 | 52 | public let code: OSType 53 | 54 | public init(name: String, kind: TermType, code: OSType) { 55 | self.code = code 56 | super.init(name: name, kind: kind) 57 | } 58 | 59 | public var hashValue: Int { return Int(self.code) } 60 | 61 | public var description: String { return "<\(type(of:self))=\(self.kind):\(self.name)=\(fourCharCode(self.code))>" } 62 | 63 | public static func ==(lhs: KeywordTerm, rhs: KeywordTerm) -> Bool { 64 | return lhs.kind == rhs.kind && lhs.code == rhs.code && lhs.name == rhs.name 65 | } 66 | } 67 | 68 | 69 | public class ClassTerm: KeywordTerm { 70 | 71 | public var singular: String 72 | public var plural: String 73 | 74 | public init(singular: String, plural: String, code: OSType) { 75 | self.singular = singular 76 | self.plural = plural 77 | super.init(name: self.singular, kind: .type, code: code) 78 | } 79 | } 80 | 81 | 82 | public class CommandTerm: Term, Hashable, CustomStringConvertible { 83 | 84 | public let eventClass: OSType 85 | public let eventID: OSType 86 | 87 | private(set) var parametersByName: [String: KeywordTerm] 88 | private(set) var parametersByCode: [OSType: KeywordTerm] 89 | private(set) var orderedParameters: [KeywordTerm] 90 | 91 | public init(name: String, eventClass: OSType, eventID: OSType) { 92 | self.eventClass = eventClass 93 | self.eventID = eventID 94 | self.parametersByName = [String: KeywordTerm]() 95 | self.parametersByCode = [OSType: KeywordTerm]() 96 | self.orderedParameters = [KeywordTerm]() 97 | super.init(name: name, kind: .command) 98 | } 99 | 100 | public var hashValue: Int { return Int(self.eventClass) - Int(self.eventID)} 101 | 102 | public var description: String { 103 | let params = self.orderedParameters.map({"\($0.name)=\(fourCharCode($0.code))"}).joined(separator: ",") 104 | return "" 105 | } 106 | 107 | func addParameter(_ name: String, code: OSType) { 108 | let paramDef = KeywordTerm(name: name, kind: .parameter, code: code) 109 | self.parametersByName[name] = paramDef 110 | self.parametersByCode[code] = paramDef 111 | self.orderedParameters.append(paramDef) 112 | } 113 | 114 | public static func ==(lhs: CommandTerm, rhs: CommandTerm) -> Bool { 115 | return lhs.eventClass == rhs.eventClass && lhs.eventID == rhs.eventID 116 | && lhs.name == rhs.name && lhs.parametersByCode == rhs.parametersByCode 117 | } 118 | } 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /SwiftAutomation/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // SwiftAutomation 4 | // 5 | // SwiftAutomation is released into the public domain. 6 | // 7 | 8 | -------------------------------------------------------------------------------- /doc-generated/application_architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-generated/application_architecture.gif -------------------------------------------------------------------------------- /doc-generated/application_architecture2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-generated/application_architecture2.gif -------------------------------------------------------------------------------- /doc-generated/client_app_to_itunes_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-generated/client_app_to_itunes_event.gif -------------------------------------------------------------------------------- /doc-generated/creating-and-using-static-glues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Creating and using static glues 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Creating and using static glues

12 | 13 |

The SwiftAutomation framework includes a command line aeglue tool for generating static glue files. Glues enable you to control "AppleScriptable" applications using human-readable property and method names derived from their built-in terminology resources.

14 | 15 |

Generating a glue

16 | 17 |

For convenience, add the following shortcut to your ~/.bash_profile:

18 | 19 |
alias aeglue=/Library/Frameworks/SwiftAutomation.framework/Resources/bin/aeglue
 20 | 
21 | 22 |

To view the aeglue tool's full documentation:

23 | 24 |
aeglue -h
 25 | 
26 | 27 |

Glue files follow a standard NAMEGlue.swift naming convention, where NAME is the name of the glue's Application class. The following command generates a TextEditGlue.swift glue file in your current working directory:

28 | 29 |
aeglue TextEdit
 30 | 
31 | 32 |

If an identically named file already exists at the same location, aeglue will normally fail with a "path already exists" error. To overwrite the existing file with no warning, add an -r option:

33 | 34 |
aeglue -r TextEdit
 35 | 
36 | 37 |

To write the file to a different directory, use the -o option. For example, to create a new iTunesGlue.swift file on your desktop:

38 | 39 |
aeglue -o ~/Desktop TextEdit
 40 | 
41 | 42 |

Getting application documentation

43 | 44 |

In addition to generating the glue file, the aeglue tool also creates a NAMEGlue.swift.sdef file containing the application dictionary (interface documentation), reformatted for use with SwiftAutomation. For example, to view the TextEditGlue.swift terminology in Script Editor:

45 | 46 |
open -a 'Script Editor' TextEditGlue.swift.sdef
 47 | 
48 | 49 |

Refer to this documentation when using SwiftAutomation glues in your own code, as it shows element, property, command, etc. names as they appear in the generated glue classes. (Make sure Script Editor's dictionary viewer is set to the "AppleScript" language option for it to display correctly.)

50 | 51 |

Be aware that only 'keyword' definitions are displayed in Swift syntax; 'type' names are unchanged from their AppleScript representation, as are AppleScript terms and sample code that appear in descriptions. SDEF-based documentation is always written for AppleScript users, so unless the application developer provides external documentation for other programming languages some manual translation is required. Furthermore, most applications' SDEF documentation is far from exhaustive, and frequently lacks both detail and accuracy; for instance, the SDEF format doesn't descript precisely what types and combinations of parameters are/aren't accepted by each command, while the documented 'types' of properties, parameters, and return values may be incomplete or wrong. Supplementary documentation, example code, AppleScript user forums, educated guesswork, and trial-and-error experimentation may also be required.

52 | 53 |

The SwiftAutoEdit application includes a File ➝ New ➝ Command Translator menu option that can also help when the correct AppleScript syntax for a command is already known, and all that is needed is some assistance in constructing its Swift equivalent.

54 | 55 |

How glues are structured

56 | 57 |

Each glue file contains the following classes:

58 | 59 |
    60 |
  • Application -- represents the root application object used to send commands, e.g. TextEdit

  • 61 |
  • PREFIXItem, PREFIXItems, PREFIXInsertion, PREFIXRoot -- represents the various forms of Apple Event Object Model queries, a.k.a. object specifiers, e.g. TEDItem

  • 62 |
  • PREFIXSymbol -- represents Apple event type, enumerator, and property names, e.g. TEDSymbol

  • 63 |
64 | 65 |

aeglue automatically disambiguates each glue's class names by adding a three-letter PREFIX derived from the application's name (e.g. TextEditTED). Thus the standard TextEditGlue.swift glue defines TextEdit, TEDItem, TEDItems, TEDInsertion, TEDRoot, and TEDSymbol classes, while FinderGlue.swift defines Finder, FINItem, FINItems, and so on. (Different prefixes allow multiple glues to be imported into a program without the need to fully qualify all references to those classes with the full glue name, i.e. TEDItem is easier to write than TextEditGlue.Item.)

66 | 67 |

Each glue also defines:

68 | 69 |
    70 |
  • PREFIXApp, PREFIXCon and PREFIXIts constants for constructing certain kinds of object specifiers

  • 71 |
  • a PREFIXRecord typealias as a convenient shorthand for Dictionary<PREFIXSymbol:Any>, which is the default type to which Apple event records are mapped.

  • 72 |
  • a PREFIX typealias as a convenient shorthand for PREFIXSymbol.

  • 73 |
74 | 75 |

Glue files may also include custom typealias, enum, and struct definitions that improve integration between Swift and Apple event type systems. Chapter 10 explains how to add and use these features.

76 | 77 |

Customizing glues

78 | 79 |

If the default three-letter prefix is unsuitable for use, use the -p option to specify a custom prefix. The following command creates a new TextEditGlue.swift file that uses the class name prefix TE:

80 | 81 |
aeglue -p TE TextEdit
 82 | 
83 | 84 |

For compatibility, aeglue normally sends the application an ascr/gdte event to retrieve its terminology in AETE format. However, some Carbon-based applications (e.g. Finder) may have buggy ascr/gdte event handlers that return Cocoa Scripting's default terminology instead of the application's own. To work around this, add an -S option to retrieve the terminology in SDEF format instead:

85 | 86 |
aeglue -S Finder
 87 | 
88 | 89 |

The -S option may be quicker when generating glues for CocoaScripting-based apps which already contain SDEF resources. When using the -S option to work around buggy ascr/gdte event handlers in AETE-based Carbon apps, be aware that macOS's AETE-to-SDEF converter is not 100% reliable. For example, four-char code strings containing non-printing characters fail to appear in the generated SDEF XML, in which case aeglue will warn of their omission and you'll have to correct the glue files manually or use SwiftAutomation's lower-level OSType-based APIs in order to access the affected objects/commands.

90 | 91 |
92 |

Tip: When getting started, a quick way to generate standard glues for all scriptable applications in /Applications, including those in subfolders, is to run the following commands:

93 | 94 |
mkdir AllGlues && cd AllGlues && aeglue -S /Applications/*.app /Applications/*/*.app
95 | 96 |

aeglue will log error messages for problematic applications (e.g. those without dictionaries or whose dictionaries contain significant flaws). Any glues that are unsatisfactory or require extra customization can then be manually regenerated one at a time with the appropriate options.

97 |
98 | 99 |

Using a glue

100 | 101 |

To include the generated glue file in your project:

102 | 103 |
    104 |
  1. Right-click in the Project Navigator pane of the Xcode project window, and select Add Files to PROJECT... from the contextual menu.

  2. 105 |
  3. Select the generated glue file (e.g. TextEditGlue.swift) and click Add.

  4. 106 |
  5. In the following sheet, check the "Copy items into destination group's folder", and click Add.

  6. 107 |
108 | 109 |

Subsequent code examples in this manual assume a standard glue file has already been generated and imported; e.g. TextEdit-based examples use a TextEdit glue with the prefix TED, Finder-based examples use a Finder glue with the prefix FIN, etc.

110 | 111 |

How keywords are converted

112 | 113 |

Because scriptable applications' terminology resources supply class, property, command, etc. names in AppleScript keyword format, aeglue must convert these terms to valid Swift identifiers when generating the glue file and accompanying .sdef documentation. For reference, here are the main conversion rules used:

114 | 115 |
    116 |
  • Characters a-z, A-Z, 0-9, and underscores (_) are preserved.

  • 117 |
  • Spaces, hyphens (-), and forward slashes (/) are removed, and the first character of all but the first word are capitalized, e.g. document filedocumentFile, Finder windowFinderWindow.

  • 118 |
  • Any names that match Swift keywords or properties/methods already defined by SwiftAutomation classes have an underscore (_) appended to avoid conflict, e.g. classclass_.

  • 119 |
120 | 121 |

Some rarely encountered corner cases are dealt with by the following conversion rules:

122 | 123 |
    124 |
  • Ampersands (&) are replaced by the word 'And'.

  • 125 |
  • Any other characters are converted to _0x00_-style hexadecimal representations.

  • 126 |
  • Names that begin with an underscore (_) have an underscore appended too.

  • 127 |
  • SwiftAutomation provides default terminology for standard type classes such as integer and unicodeText, and standard commands such as open and quit. If an application-defined name matches a built-in name but has a different Apple event code, SwiftAutomation will append an underscore to the application-defined name to avoid conflict.

  • 128 |
129 | 130 |
131 |
132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /doc-generated/examples.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Examples 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Examples

12 | 13 |

// TO DO: include corresponding aeglue commands as comments

14 | 15 |

Application objects

16 | 17 |
// application id "com.apple.Finder"
 18 | let finder = Finder() // (use the glue's default bundle ID)
 19 | 
 20 | // application "Adobe InDesign CS6"
 21 | let indesign = AdobeInDesign(name: "Adobe InDesign CS6")
 22 | 
 23 | // application "Macintosh HD:Applications:TextEdit.app:"
 24 | let textedit = TextEdit(name: "/Applications/TextEdit.app")
 25 | 
 26 | // application "iTunes" of machine "eppc://jsmith@media-mac.local"
 27 | let itunes = ITunes(url: URL(string: "eppc://jsmith@media-mac.local/iTunes"))
 28 | 
 29 | // application id "com.apple.Stickies" // a non-scriptable application
 30 | let stickies = AEApplication(bundleIdentifier: "com.apple.Stickies")
 31 | 
32 | 33 |

Property references

34 | 35 |
// a reference to startup disk of application "Finder"
 36 | finder.startupDisk
 37 | 
 38 | // a reference to name of folder 1 of home of application "Finder"
 39 | finder.home.folders[1].name
 40 | 
 41 | // a reference to name of every item of home of application "Finder"
 42 | finder.home.items.name
 43 | 
 44 | // a reference to text of every document of application "TextEdit"
 45 | textedit.documents.text
 46 | 
 47 | // a reference to color of character 1 of every paragraph of text ¬
 48 | //     of document 1 of application "TextEdit"
 49 | textedit.documents[1].text.paragraphs.characters[1].color
 50 | 
51 | 52 |

All elements references

53 | 54 |
// a reference to disks of application "Finder"
 55 | finder.disks
 56 | 
 57 | // a reference to every word of every paragraph ¬
 58 | //     of text of every document of application "TextEdit"
 59 | textedit.documents.text.paragraphs.words
 60 | 
61 | 62 |

Single element references

63 | 64 |
// a reference to disk 1 of application "Finder"
 65 | finder.disks[1]
 66 | 
 67 | // a reference to file "ReadMe.txt" of folder "Documents" of home of application "Finder"
 68 | finder.home.folders["Documents"].files["ReadMe.txt"]
 69 | 
 70 | // a reference to paragraph -1 of text of document 1 of application "TextEdit"
 71 | textedit.documents[1].text.paragraphs[-1]
 72 | 
 73 | // a reference to middle paragraph of text of last document of application "TextEdit"
 74 | textedit.documents.last.text.paragraphs.middle
 75 | 
 76 | // a reference to any file of home of application "Finder"
 77 | finder.home.files.any
 78 | 
79 | 80 |

Relative references

81 | 82 |
// a reference to paragraph before paragraph 6 of text of document 1 of application "TextEdit"
 83 | textedit.documents[1].text.paragraphs[6].previous(TED.paragraph)
 84 | 
 85 | // a reference to paragraph after character 30 of document 1 of application "Tex-Edit Plus"
 86 | texeditplus.documents[1].characters[30].next(TEP.paragraph)
 87 | 
88 | 89 |

Element range references

90 | 91 |
// a reference to words 1 thru 4 of text of document 1 of application "TextEdit"
 92 | textedit.documents[1].text.words[1, 4]
 93 | 
 94 | 
 95 | // a reference to paragraphs 2 thru -1 of text of document 1 of application "TextEdit"
 96 | textedit.documents[1].text.paragraphs[2, -1]
 97 | 
 98 | // a reference to folders "Documents" thru "Music" of home of application "Finder"
 99 | finder.home.folders["Documents", "Music"]
100 | 
101 | // a reference to text (word 3) thru (paragraph 7) of document 1 of application "Tex-Edit Plus"
102 | texeditplus.documents[1].text[TEPCon.words[3], TEPCon.paragraphs[7]]
103 | 
104 | 105 |

Test references

106 | 107 |
// a reference to every document of application "TextEdit" whose text is "\n"
108 | textedit.documents[TEDIts.text == "\n"]
109 | 
110 | // a reference to every paragraph of document 1 of application "Tex-Edit Plus" ¬
111 | //      whose first character is last character
112 | texeditplus.documents[1].paragraphs[TEPIts.characters.first == TEPIts.characters.last]
113 | 
114 | // a reference to every file of folder "Documents" of home of application "Finder" ¬
115 | //      whose name extension is "txt" and size < 10240
116 | finder.home.folders["Documents"].files[FINIts.nameExtension == "txt" && FINIts.size < 10240]
117 | 
118 | 119 |

Insertion location references

120 | 121 |
// a reference to end of documents of application "TextEdit"
122 | textedit.documents.end
123 | 
124 | // a reference to before paragraph 1 of text of document 1 of application "TextEdit"
125 | textedit.documents[1].text.paragraphs[1].before
126 | 
127 | 128 |

open command

129 | 130 |

Open a document in TextEdit:

131 | 132 |
// tell application "TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt")
133 | try textedit.open(URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt"))
134 | // TextEdit().documents["ReadMe.txt"]
135 | 
136 | 137 |

get command

138 | 139 |

Get the name of every folder in the user's home folder:

140 | 141 |
// tell application "Finder" to get name of every folder of home
142 | try finder.get(FINApp.home.folders.name)
143 | 
144 | 145 |

Or, more concisely:

146 | 147 |
try finder.home.folders.name.get()
148 | 
149 | 150 |

Remember to declare the command's return type if you intend to use the returned value:

151 | 152 |
let folderNames = try finder.home.folders.name.get() as [String]
153 | print(folderNames.joined(separator: ", "))
154 | // "Desktop, Documents, Downloads, Movies, ..."
155 | 
156 | 157 |

set command

158 | 159 |

Set the content of a TextEdit document:

160 | 161 |
// tell application "TextEdit" to set text of document 1 to "Hello World"
162 | try textedit.documents[1].text.set(to: "Hello World")
163 | 
164 | 165 |

count command

166 | 167 |

Count the words in a TextEdit document:

168 | 169 |
// tell application "TextEdit" to count words of document 1
170 | try textedit.documents[1].words.count() as Int
171 | // 42
172 | 
173 | 174 |

Count the items in the current user's home folder:

175 | 176 |
// tell application "Finder" to count items of home
177 | try finder.home.count(each: FIN.item) as Int
178 | // 11
179 | 
180 | 181 |

(Note that Finder and many other Carbon applications require the count command's each parameter to be given. Cocoa-based apps should accept either form.)

182 | 183 |

make command

184 | 185 |

Create a new TextEdit document:

186 | 187 |
// tell application "TextEdit" to make new document ¬
188 | //     with properties {text:"Hello World\n"}
189 | try textedit.make(new: TED.document, 
190 |        withProperties: [TED.text: "Hello World\n"]) as TEDItem
191 | // TextEdit().documents["Untitled"]
192 | 
193 | 194 |

Append text to a TextEdit document:

195 | 196 |
// tell application "TextEdit" to make new paragraph ¬
197 | //     at end of text of document 1 ¬
198 | //     with properties {text:"Yesterday\nToday\nTomorrow\n"}
199 | try textedit.make(new: TED.paragraph, 
200 |                    at: TEDApp.documents[1].text.end,
201 |              withData: "Yesterday\nToday\nTomorrow\n")
202 | 
203 | 204 |

duplicate command

205 | 206 |

Duplicate a folder to a disk, replacing an existing item if one exists:

207 | 208 |
// tell application "Finder"
209 | //   duplicate folder "Projects" of home to disk "Work" with replacing
210 | // end tell
211 | try finder.home.folders["Projects"].duplicate(to: FINApp.disks["Backup"], replacing: true)
212 | // Finder().disks["Backup"].folders["Projects"]
213 | 
214 | 215 |

add command

216 | 217 |

Add every person with a known birthday to a group named "Birthdays":

218 | 219 |
// tell application "Contacts"
220 | //   add every person whose birth date is not missing value to group "Birthdays"
221 | // end tell
222 | try contacts.people[CONIts.birthDate != MissingValue].add(to: CONApp.groups["Birthdays"])
223 | 
224 | 225 |

quit command

226 | 227 |

Close every TextEdit document without saving:

228 | 229 |
// tell application "TextEdit" to quit saving no
230 | try textedit.quit(saving: TED.no)
231 | 
232 | 233 |

Quit the Stickies app if it's currently running:

234 | 235 |
let stickies = AEApplication(bundleIdentifier: "com.apple.Stickies") // default glue
236 | if stickies.isRunning { try? stickies.quit() }
237 | 
238 | 239 |
240 |
241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /doc-generated/finder_to_textedit_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-generated/finder_to_textedit_event.gif -------------------------------------------------------------------------------- /doc-generated/full.css: -------------------------------------------------------------------------------- 1 | /* sticky footer code from www.cssstickyfooter.com */ 2 | 3 | html, body {height: 100%;} 4 | 5 | #wrap {min-height: 100%;} 6 | 7 | #main {overflow:auto; 8 | padding-bottom: 100px; /* must be same height as the footer */ 9 | } 10 | 11 | #footer { 12 | color:#00406e; background-color: #cedbef; 13 | position: relative; 14 | margin-top: -80px; /* negative value of footer height */ 15 | height: 80px; 16 | clear:both; 17 | } 18 | 19 | /*Opera Fix*/ 20 | body:before { 21 | content:""; 22 | height:100%; 23 | float:left; 24 | width:0; 25 | margin-top:-32767px;/ 26 | } 27 | 28 | 29 | /* general */ 30 | 31 | body { 32 | font-family:Arial,sans-serif; line-height:140%; 33 | color:#000; background-color:#fff; 34 | margin: 0 12% 0 8%; font-size: 0.9em; 35 | } 36 | 37 | /*headings*/ 38 | 39 | h1 { 40 | font-family:Arial,sans-serif; font-weight:bold; line-height:110%; 41 | color:#00457e; background-color:#cedbef; 42 | border-bottom:solid #92b4db 6px; 43 | padding:0.4em 0.6em 0.5em; margin:0; 44 | } 45 | 46 | 47 | h2, h3, h4 {font-family: Palatino, Georgia, Serif; color:#00457e; background-color:transparent; font-weight:bold;} 48 | 49 | h2 {font-size:1.6em; padding:0 0 1px; border-bottom:solid #f77700 2px; margin:1.5em 0 0.5em;} 50 | h3 {font-size:1.1em; padding:0 0 0px; border-bottom:dotted #f77700 1px; margin:0.5em 0 0.5em;} 51 | h4 {font-size:1.0em; margin:1.8em 0 0.6em 0;} 52 | 53 | 54 | /*body text*/ 55 | 56 | p {margin: 1em 0;} 57 | 58 | ul {padding-left: 0.5em; margin-left:0.5em;} 59 | 60 | var {color:#d55511;} 61 | 62 | dl, pre { 63 | line-height:130%; margin:2em 0em; 64 | } 65 | 66 | dt, code {color:#00457e; background-color:transparent;} 67 | dt {font-weight:bold;} 68 | dd {margin-left:0} 69 | 70 | .hilitebox {padding:0.5em 0 0.55em; margin:1.3em 2em 1.4em; border:solid #f77700; border-width: 2px 0 2px; } 71 | 72 | code {font-family:Courier,Monospace;} 73 | 74 | pre {color:#00406e; background-color:transparent;} 75 | 76 | 77 | dd pre, .hilitebox pre { 78 | color:#000; background-color:#cedbef; 79 | padding:1.8em 2em 2em; margin:1.2em 2em 1.2em 0; 80 | } 81 | 82 | 83 | dd+dt {margin-top:1em;} 84 | h3+p {margin-top:-0.3em;} 85 | h2+h3 {margin-top: 1.2em;} 86 | h4+p {margin-top:-0.35em;} 87 | 88 | 89 | .params {list-style-type: none;} 90 | 91 | .comment {color:#666; background-color:transparent;} 92 | 93 | hr {height: 1px; background-color: #00457e; border: 0px solid #00457e; margin-top:3em;} 94 | 95 | dl hr { 96 | color:#e8e8ff; background-color:transparent; height:1px; 97 | border:dashed #00457e; border-width: 1px 0 0; margin:0 -2em; 98 | } 99 | 100 | table { 101 | line-height:130%; width:100%; color:#00457e; background-color:#cedbef; 102 | border-bottom:solid #cedbef 10px; margin:1.2em 0em 2.4em; 103 | border-collapse:collapse; padding: 0 0 2em; 104 | } 105 | 106 | tr, th, td {padding: 0.4em 1.6em; margin: 0; border-width: 0;} 107 | 108 | th {text-align:left; font-size:0.95em; color:#00457e; background-color:#92b4db;} 109 | 110 | thead {background-color:#ccd;} 111 | 112 | .altrow {background-color:#e0e9f8;} 113 | 114 | .ruleindex tbody {font-size:90%;} 115 | 116 | 117 | /*links*/ 118 | 119 | /*a {font-style:italic;}*/ 120 | 121 | 122 | a:link {color:#00457e; background-color:transparent;} 123 | a:visited {color:#445555; background-color:transparent;} 124 | 125 | dt a:link, dt a:visited {text-decoration: none;} 126 | 127 | a img {border-width:0;} 128 | 129 | 130 | /* navigation*/ 131 | 132 | .navbar { 133 | font-size:0.9em; font-weight:bold; 134 | color:#e06000; background-color: white; 135 | padding: 0.3em 0 0.4em; margin: 0; 136 | } 137 | 138 | .navbar+h2 {margin-top: 1em} 139 | 140 | .navbar a:link, .navbar a:visited, .footer a:link, .footer a:visited {font-style:normal; text-decoration:none;} 141 | .navbar a:hover, .navbar a:active, .footer a:hover, .footer a:active {text-decoration:underline;} 142 | 143 | /*footer*/ 144 | 145 | .footer { 146 | font-size:0.9em; font-weight:bold; 147 | color:#00406e; background-color: #cedbef; 148 | border-top:solid #92b4db 6px; 149 | padding: 0.3em 13px 0.5em; margin: 0; 150 | } 151 | 152 | .ruledoc {font-size:95%; margin:2em; padding: 0 2em 1em; border:solid black 1px;} 153 | 154 | .ruledoc-excerpt {font-size:95%; margin:2em; padding: 1em 2em; border:solid black 1px;} 155 | 156 | .syntax-error {color:#fff;background-color:#c00} 157 | -------------------------------------------------------------------------------- /doc-generated/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation 5 | 6 | 7 | 8 | 9 |
10 |
11 |

SwiftAutomation

12 | 13 |

Table of contents

14 |
    15 |
  1. Welcome
  2. 16 | 17 |
  3. Installing SwiftAutomation
  4. 18 | 19 |
  5. A tutorial introduction
  6. 20 | 21 |
  7. Understanding Apple events
  8. 22 | 23 |
  9. Creating and using static glues
  10. 24 | 25 |
  11. Apple event type mappings
  12. 26 | 27 |
  13. Application objects
  14. 28 | 29 |
  15. Object specifiers
  16. 30 | 31 |
  17. Commands
  18. 32 | 33 |
  19. Examples
  20. 34 | 35 |
  21. Improving type system integration
  22. 36 | 37 |
  23. Optimizing performance
  24. 38 | 39 |
  25. Using the low-level `AEApplication` glue
  26. 40 | 41 |
  27. Notes
  28. 42 |
43 |
44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /doc-generated/installing-swiftautomation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Installing SwiftAutomation 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Installing SwiftAutomation

12 | 13 |

[[ TO DO: include screenshots? ]]

14 | 15 |

Get SwiftAutomation

16 | 17 |

Run the following command in Terminal to clone the SwiftAutomation repository to your Mac:

18 | 19 |
git clone https://bitbucket.org/hhas/swiftae.git
20 | 
21 | 22 |

Minimum requirements: macOS 10.11 and Xcode 8.1/Swift 3.0.1.

23 | 24 |

Installing for Swift "scripting"

25 | 26 |

Open the SwiftAE project in Xcode. Select the Product ➞ Scheme ➞ Release menu option followed by Product ➞ Build to build the following three products: SwiftAutomation.framework, aeglue (this is automatically embedded in the framework), and MacOSGlues.framework.

27 | 28 |

To reveal the folder containing the built products, scroll down to "Products" in the Xcode project window's Project Navigator list, right-click the "SwiftAutomation.framework" entry (its name should now be black, not red), and select Show in Finder from the contextual menu.

29 | 30 |

Launch Terminal and type the following into a new window:

31 | 32 |
cd /Library/Frameworks
33 | 
34 | 35 |

Next type the following, including a space at the end:

36 | 37 |
sudo ln -s
38 | 
39 | 40 |

then drag SwiftAutomation.framework from the Finder window onto Terminal to insert the full path to the framework at the end of the command. On pressing Return, enter an an administrator login to allow the ln command to create a symlink in /Library/Frameworks. Repeat the process for the MacOSGlues.framework.

41 | 42 |

To confirm this setup works, enter the following Swift "script" into a plain text or code editor of your choice:

43 | 44 |
#!/usr/bin/swift -target x86_64-apple-macosx10.12 -F /Library/Frameworks
45 | 
46 | import SwiftAutomation
47 | import MacOSGlues
48 | 
49 | print(try Finder().home.name.get()) // output your home folder's name
50 | 
51 | 52 |

Save it as automate.swift in your home folder, type cd ~/ && chmod +x test.swift in Terminal to make it executable, then run it as follows:

53 | 54 |
./automate.swift
55 | 
56 | 57 |

All going well, the script should compile and run in well under a second, and print the name of your home folder to stdout. Success!

58 | 59 |

(All not going well, Swift will log an error message describing why it failed. If you cannot troubleshoot it yourself, please get in touch.)

60 | 61 |

Remember to rebuild SwiftAE's Release products whenever a new version of Xcode/Swift is installed: until Swift provides a stable ABI, the compiled SwiftAutomation and MacOSGlues frameworks can only be used by scripts compiled with the exact same version of Swift as they were.

62 | 63 |

Embedding in GUI apps

64 | 65 |

Because Swift does not yet provide a stable ABI, it is not possible for a GUI application built by one version of the Swift compiler to link to a framework built by another. Instead, a Swift-based application project must compile all of its Swift framework dependencies and embed those frameworks within its .app bundle as part of its build process. Technical Note TN2435 Embedding Frameworks In An App explains how to embed a third-party Xcode framework project such as SwiftAE inside your own Xcode project.

66 | 67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /doc-generated/low-level-apis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Using the low-level `AEApplication` glue 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Using the low-level `AEApplication` glue

12 | 13 |

While glue files' terminology-based properties and methods are recommended for controlling individual "AppleScriptable" applications, SwiftAutomation also includes lower-level APIs for interacting with "non-scriptable" "applications that do not include an AETE/SDEF terminology resource, or whose terminology contains defects that render some or all of the generated glue unusable, or when sending standard commands that do not require an application-specific glue. These low-level APIs are present on all generated glues' Application and ObjectSpecifier classes if needed, and also on the default AEApplicationGlue that is included in SwiftAutomation as standard.

14 | 15 |

Sending standard Apple events

16 | 17 |

The following commands are defined on all Application and Specifier classes, including the default AEApplication, and should be recognized by all macOS applications:

18 | 19 |
run()
 20 | reopen()
 21 | launch()
 22 | activate()
 23 | open(Array<URL>) // list of file URLs
 24 | openLocation(String) // a URL string (http:/mailto:/etc.)
 25 | print(Array<URL>) // list of file URLs
 26 | quit( [ saving: AE.yes|AE.no|AE.ask ] )
 27 | 
28 | 29 |

(Standard get and set commands are also defined, but will only work in apps that implement an AEOM.)

30 | 31 |

As with application-specific commands, standard commands will throw a CommandError on failure, so remember to prefix with try.

32 | 33 |

For example, to open a file:

34 | 35 |
// tell application id "com.apple.TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt")
 36 | let textedit = AEApplication(bundleIdentifier: "com.apple.TextEdit")
 37 | try textedit.open(URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt"))
 38 | 
39 | 40 |

Or to quit multiple applications without saving changes to any open documents:

41 | 42 |
for appName in ["TextEdit", "Preview", "Script Editor"] {
 43 |   let app = AEApplication(name: appName)
 44 |   if app.isRunning { try? app.quit(saving: AE.no) }
 45 | }
 46 | 
47 | 48 |

Sending Apple events using four-char codes

49 | 50 |

All specifiers implement a low-level sendAppleEvent(...) method, allowing Apple events to be built and sent using four-char codes (a.k.a. OSTypes):

51 | 52 |
sendAppleEvent(_ eventClass: OSType/String, _ eventID: OSType/String, _ parameters: [OSType/String:Any] = [:],
 53 |                requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil,
 54 |                withTimeout: TimeInterval? = nil, considering: ConsideringOptions? = nil) throws -> T/Any
 55 | 
56 | 57 |

Four-char codes may be given as OSType (UInt32) values or as OSType-encodable String values containing exactly four MacRoman characters. Invalid strings will cause sendAppleEvent() to throw a CommandError.

58 | 59 |

For example:

60 | 61 |
// tell application id "com.apple.TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt")
 62 | // tell application id "com.apple.TextEdit" to «event aevtodoc» (POSIX file "/Users/jsmith/ReadMe.txt")
 63 | let textedit = AEApplication(bundleIdentifier: "com.apple.TextEdit")
 64 | try textedit.sendAppleEvent("aevt", "odoc", ["----": URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt")])
 65 | 
 66 | // tell application id "com.apple.TextEdit" to quit saving no
 67 | // tell application id "com.apple.TextEdit" to «event aevtquit» given «class savo»: «constant ****ask »
 68 | try textedit.sendAppleEvent("aevt", "quit", ["savo": AE.ask])
 69 | 
70 | 71 |

While the Carbon AE headers define constants for common four-char codes, e.g. cDocument = 'docu' = 0x646f6375, as of Swift3/Xcode8/macOS10.12 some constants are incorrectly mapped to Int (SInt64) instead of OSType (UInt32), so their use is best avoided.

72 | 73 |

Constructing object specifiers using four-char codes

74 | 75 |

All object specifiers implement low-level methods for constructing property and all-elements specifiers

76 | 77 |
* userProperty(_ name: String) -- user-defined identifier, e.g. `someProperty` (note: case-[in]sensitivity rules are target-specific)
 78 | 
 79 | * property(_ code: OSType/String) -- four-char code, either as OSType (UInt32) or four-char string, e.g. `cDocument`/`"docu"`
 80 | 
 81 | * elements(_ code: OSType/String) -- ditto
 82 | 
83 | 84 |

The default AEApplicationGlue defines AEApp, AECon, and AEIts roots for constructing untargeted specifiers using four-char codes only.

85 | 86 |

Insertion and element selectors are the same as in application-specific glues; see Chapter 7 for details.

87 | 88 |

For example:

89 | 90 |
// every paragraph of text of document 1 [of it]
 91 | // every «class cpar» of «property ctxt» of «class docu» [of it]
 92 | AEApp.elements("docu")[1].property("ctxt").elements("cpar")
 93 | AEApp.elements(0x646f6375)[1].property(0x63747874).elements(0x63706172)
 94 | 
95 | 96 |

Constructing symbols using four-char codes

97 | 98 |

The default AEApplicationGlue defines an AESymbol class, type aliased as AE, for constructing Symbol instances using four-char codes:

99 | 100 |
AESymbol(code: OSType/String, type: OSType/String = typeType/"type")
101 | 
102 | 103 |

For example:

104 | 105 |
// document
106 | // «class docu»
107 | AE(code: "docu")
108 | AE(code: 0x646f6375)
109 | 
110 | // name
111 | // «property pnam»
112 | AE(code: "pnam", type: "prop") // (note: "type" is more commonly used than "prop")
113 | AE(code: 0x706e616d, type: 0x70726f70)
114 | 
115 | // ask
116 | // «constant ****ask »
117 | AE(code: "ask ", type: "enum")
118 | AE(code: 0x61736b20, type: 0x656e756d)
119 | 
120 | 121 |

AESymbol instances can be used interchangeably with glue-defined PREFIXSymbol classes. SwiftAutomation only compares Symbol instances' code and type properties when comparing for equality; thus the following equality test returns true:

122 | 123 |
AE(code: "docu") == TED.document
124 | 
125 | 126 |

Using symbols as AERecord keys

127 | 128 |

AppleScript records can contain any combination of keyword- and/or identifier-based keys, so the Symbol class also defines an init(_ name: String) initializer, allowing identifier-based record keys to be constructed as well:

129 | 130 |
// {name: "Sam", age: 32, isSingle: true}
131 | [AE(code:"pnam"): "Sam", AE("age"): 32, AE("issingle"): true]
132 | 
133 | 134 |

Be aware that case-[in]sensitivity rules for identifier strings can vary depending on how and where the record is used; for case-insensitivity, use all-lowercase.

135 | 136 |

To determine if a Symbol instance represents a keyword or an identifier:

137 | 138 |
AE(code:"pnam").nameOnly // false 
139 | AE("issingle").nameOnly  // true
140 | 
141 | 142 |

Scriptable applications do not normally use identifier-based keys in records; however, they may be used by AppleScript-based applets and in NSAppleScript/NSUserAppleScriptTask calls.

143 | 144 |
145 |
146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /doc-generated/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Notes 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Notes

12 | 13 |

// TO DO: update

14 | 15 |

Security issues

16 | 17 |

If including user names and/or passwords in remote application URLs, please note that XXApplication and XXSpecifier objects will retain those URL strings over their entire lifetime. Security here is the developer's responsibility, as it's their code that creates and retains these objects.

18 | 19 |

GUI Scripting

20 | 21 |

Non-scriptable applications may in some cases be controlled from SwiftAutomation by using System Events to manipulate their graphical user interface. Note that the "Enable access for assistive devices" checkbox must be selected in the Universal Access system preferences pane for GUI Scripting to work.

22 | 23 |

Type bridging limitations

24 | 25 |

Some applications (e.g. QuarkXpress) may return values which SwiftAutomation cannot convert to equivalent Cocoa types. These values are usually of types which are defined, used and understood only by that particular application, and will be represented in Objective-C as NSAppleEventDescriptor instances which should generally be treated as opaque values.

26 | 27 |

A few standard but rarely used AE types are also currently unbridged with Cocoa counterparts, such as image types (typePict, typeTIFF, etc.). Client code should perform any necessary conversions itself.

28 | 29 |

SwiftAutomation and threads

30 | 31 |

SwiftAutomation is thread-safe and events can be sent (and their replies received) from any thread.

32 | 33 |

Credits

34 | 35 |

Many thanks to all those who have contributed comments, suggestions and bug reports.

36 | 37 |
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /doc-generated/optimizing-performance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Optimizing performance 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Optimizing performance

12 | 13 |

// TO DO: check code examples work

14 | 15 |

About performance

16 | 17 |

Apple event IPC is subject to a number of potential performance bottlenecks:

18 | 19 |
    20 |
  • Sending Apple events is more expensive than calling local functions.

  • 21 |
  • There may be significant overheads in how applications resolve individual object references.

  • 22 |
  • Packing and unpacking large and/or complex values (e.g. a long list of object specifiers) can take an appreciable amount of time.

  • 23 |
24 | 25 |

[TO DO: include note that AEs were originally introduced in System 7, which had very high process switching costs, so were designed to compensate for that]

26 | 27 |

Fortunately, it's often possible to minimise performance overheads by using fewer commands to do more work. Let's consider a typical example: obtaining the name of every person in Contacts.app who has a particular email address. There are several possible solutions to this, each with very different performance characteristics:

28 | 29 |

The iterative OO-style approach

30 | 31 |

While iterating over application objects and manipulating each in turn is a common technique, it's also the slowest by far:

32 | 33 |
  // `aeglue Contacts.app`
 34 |   let contacts = Contacts()
 35 | 
 36 |   let desiredEmail = "sam.brown@example.com"
 37 | 
 38 |   var foundNames = [String]()
 39 |   for person in contacts.people.get() as [CONItem] {
 40 |     for email in people.emails.get() as [CONItem] {
 41 |         if email.value.get() == desiredEmail {
 42 |             foundNames += person.name.get()
 43 |         }
 44 |     }
 45 |   }
 46 |   print(foundNames)
 47 | 
48 | 49 |

The above code sends one Apple event to get a list of references to all people, then one Apple event for each person to get a list of references to their emails, then one Apple event for each of those emails. Thus the time taken increases directly in proportion to the number of people in Contacts. If there's hundreds of people to search, that's hundreds of Apple events to be built, sent and individually resolved, and performance suffers as a result.

50 | 51 |

The solution, where possible, is to use fewer, more sophisticated commands to do the same job.

52 | 53 |

The smart query-oriented approach

54 | 55 |

While there are some situations where iterating over and manipulating each application object individually is the only option (for example, when setting a property in each object to a different value), in this case there is plenty of room for improvement. Depending on how well an application implements its AEOM support, it's possible to construct queries that identify more than one application object at a time, allowing a single command to manipulate multiple objects in a single operation.

56 | 57 |

In this case, the entire search can be performed using a single complex query sent to Contacts via a single Apple event:

58 | 59 |
  let contacts = Contacts()
 60 | 
 61 |   let desiredEmail = "sam.brown@example.com"
 62 | 
 63 |   let foundNames = contacts.people[CONIts.emails.value.contains(desiredEmail)].name.get() as [String]
 64 | 
 65 |   print(foundNames)
 66 | 
67 | 68 |

To explain:

69 | 70 |
    71 |
  • The query states: "Find the name of every person object that passes a specific test."

  • 72 |
  • The test is: "Does a given value, "sam.brown@example.com", appear in a list that consists of the value of each email object contained by an individual person?"

  • 73 |
  • The command is: "Evaluate that query against the AEOM and get (return) the result, which is a list of zero or more strings: the names of the people matched by the query."

  • 74 |
75 | 76 |

The hybrid solution

77 | 78 |

While AEOM queries can be surprisingly powerful, there are still many problems too complex for the application to evaluate entirely by itself. For example, let's say that you want to obtain the name of every person who has an email addresses that uses a particular domain name. Unfortunately, this test is too complex to express as a single AEOM query; however, it can still be solved reasonably efficiently by obtaining all the data from the application up-front and processing it locally. For this we need: 1. the name of every person in the Contacts, and 2. each person's email addresses. Each request can be described in a single query, allowing all of the required data to be retrieved using just two get commands.

79 | 80 |
  let contacts = Contacts()
 81 | 
 82 |   let desiredDomain = "@example.com"
 83 | 
 84 |   // get each person's name
 85 |   let names = contacts.people.name.get() as [String]
 86 | 
 87 |   // get each person's email addresses
 88 |   let emailsByPerson = contacts.people.emails.value.get() as [[String]]
 89 | 
 90 |   var foundNames = [String]()
 91 |   for (name, emails) in zip(names, emailsByPerson) {
 92 |       for email in emails {
 93 |           if email.hasSuffix(desiredDomain) {
 94 |               foundNames.append(name)
 95 |               break
 96 |           }
 97 |       }
 98 |   }
 99 |   print(foundNames)
100 | 
101 | 102 |

This solution isn't as fast as the pure-query approach, but is still far more efficient than iterating over and manipulating every person and email element one at a time.

103 | 104 |
105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /doc-generated/relationships_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-generated/relationships_example.gif -------------------------------------------------------------------------------- /doc-generated/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwiftAutomation | Welcome 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Welcome

12 | 13 |

What is SwiftAutomation?

14 | 15 |

SwiftAutomation enables you to control "AppleScriptable" macOS applications using Apple Inc's Swift programming language. SwiftAutomation makes Swift a true alternative to AppleScript for automating your Mac.

16 | 17 |

For example, to get the value of the first paragraph of the topmost document in TextEdit:

18 | 19 |
try TextEdit().documents[1].paragraphs[1].get() as String
20 | 
21 | 22 |

This is equivalent to the AppleScript statement:

23 | 24 |
tell application "TextEdit" to get paragraph 1 of document 1
25 | 
26 | 27 |

Or to create a new "Hello World!" document in TextEdit:

28 | 29 |
try TextEdit().make(new: TED.document, 
30 |                     withProperties: [TED.text: "Hello World!"])
31 | 
32 | 33 |

which is equivalent to this:

34 | 35 |
tell app "TextEdit" to make new document ¬
36 |                             with properties {text: "Hello World!"}
37 | 
38 | 39 |

How does SwiftAutomation differ to Cocoa OOP?

40 | 41 |

[TO DO: how best to pitch this section?]

42 | 43 |

In order to use SwiftAutomation effectively, you will need to understand the differences between the Apple event and Swift/Cocoa object systems.

44 | 45 |

In contrast to the familiar object-oriented approach of other inter-process communication systems such as COM and Distributed Objects, Apple event IPC is based on a combination of remote procedure calls and first-class queries - somewhat analogous to using XPath over XML-RPC.

46 | 47 |

While SwiftAutomation uses an object-oriented-like syntax for conciseness and readability, like AppleScript, it behaves according to Apple event rules. As a result, Swift users will discover that some things work differently in SwiftAutomation from what they're used to. For example:

48 | 49 |
    50 |
  • Object elements are one-indexed, not zero-indexed like Swift Arrays.

  • 51 |
  • Referencing a property of an application object does not automatically return the property's value (you need a get command for that).

  • 52 |
  • Many applications allow a single command to operate on multiple objects at the same time, providing significant performance benefits when manipulating large numbers of application objects. (Conversely, sending lots of commands to manipulate single objects one at a time can severely degrade performance.)

  • 53 |
54 | 55 |

Chapters 2 and 3 of this manual provide further information on how Apple event IPC works and a tutorial-based introduction to the SwiftAutomation bridge. Chapters 4 and 10 explain how to generate glue files for controlling specific appications. Chapters 5 through 9 cover the SwiftAutomation API, and chapter 11 discusses techniques for optimising performance.

56 | 57 |
58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /doc-source/1 welcome.md: -------------------------------------------------------------------------------- 1 | # Welcome 2 | 3 | [TO DO: need to rejig HTML generator so that this page is the front index.html page and the chapter list appears as a sidebar or dropdown menu on each page instead of as a separate index page] 4 | 5 | ## What is SwiftAutomation? 6 | 7 | SwiftAutomation enables you to control "AppleScriptable" macOS applications using Apple Inc's [Swift programming language](https://swift.org/). SwiftAutomation makes Swift a true alternative to AppleScript for automating your Mac. 8 | 9 | For example, to get the value of the first paragraph of the topmost document in TextEdit: 10 | 11 | try TextEdit().documents[1].paragraphs[1].get() as String 12 | 13 | This is equivalent to the AppleScript statement: 14 | 15 | tell application "TextEdit" to get paragraph 1 of document 1 16 | 17 | 18 | Or to create a new "Hello World!" document in TextEdit: 19 | 20 | try TextEdit().make(new: TED.document, 21 | withProperties: [TED.text: "Hello World!"]) 22 | 23 | which is equivalent to this: 24 | 25 | tell app "TextEdit" to make new document ¬ 26 | with properties {text: "Hello World!"} 27 | 28 | 29 | ## How does SwiftAutomation differ to Cocoa OOP? 30 | 31 | [TO DO: how best to pitch this section?] 32 | 33 | In order to use SwiftAutomation effectively, you will need to understand the differences between the Apple event and Swift/Cocoa object systems. 34 | 35 | In contrast to the familiar object-oriented approach of other inter-process communication systems such as COM and Distributed Objects, Apple event IPC is based on a combination of remote procedure calls and first-class queries - somewhat analogous to using XPath over XML-RPC. 36 | 37 | While SwiftAutomation uses an object-oriented-like syntax for conciseness and readability, like AppleScript, it behaves according to Apple event rules. As a result, Swift users will discover that some things work differently in SwiftAutomation from what they're used to. For example: 38 | 39 | * Object elements are one-indexed, not zero-indexed like Swift Arrays. 40 | 41 | * Referencing a property of an application object does not automatically return the property's value (you need a get command for that). 42 | 43 | * Many applications allow a single command to operate on multiple objects at the same time, providing significant performance benefits when manipulating large numbers of application objects. (Conversely, sending lots of commands to manipulate single objects one at a time can severely degrade performance.) 44 | 45 | Chapters 2 and 3 of this manual provide further information on how Apple event IPC works and a tutorial-based introduction to the SwiftAutomation bridge. Chapters 4 and 10 explain how to generate glue files for controlling specific appications. Chapters 5 through 9 cover the SwiftAutomation API, and chapter 11 discusses techniques for optimising performance. 46 | 47 | -------------------------------------------------------------------------------- /doc-source/10 examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | // TO DO: include corresponding aeglue commands as comments 4 | 5 | ## Application objects 6 | 7 | // application id "com.apple.Finder" 8 | let finder = Finder() // (use the glue's default bundle ID) 9 | 10 | // application "Adobe InDesign CS6" 11 | let indesign = AdobeInDesign(name: "Adobe InDesign CS6") 12 | 13 | // application "Macintosh HD:Applications:TextEdit.app:" 14 | let textedit = TextEdit(name: "/Applications/TextEdit.app") 15 | 16 | // application "iTunes" of machine "eppc://jsmith@media-mac.local" 17 | let itunes = ITunes(url: URL(string: "eppc://jsmith@media-mac.local/iTunes")) 18 | 19 | // application id "com.apple.Stickies" // a non-scriptable application 20 | let stickies = AEApplication(bundleIdentifier: "com.apple.Stickies") 21 | 22 | ## Property references 23 | 24 | // a reference to startup disk of application "Finder" 25 | finder.startupDisk 26 | 27 | // a reference to name of folder 1 of home of application "Finder" 28 | finder.home.folders[1].name 29 | 30 | // a reference to name of every item of home of application "Finder" 31 | finder.home.items.name 32 | 33 | // a reference to text of every document of application "TextEdit" 34 | textedit.documents.text 35 | 36 | // a reference to color of character 1 of every paragraph of text ¬ 37 | // of document 1 of application "TextEdit" 38 | textedit.documents[1].text.paragraphs.characters[1].color 39 | 40 | 41 | ## All elements references 42 | 43 | // a reference to disks of application "Finder" 44 | finder.disks 45 | 46 | // a reference to every word of every paragraph ¬ 47 | // of text of every document of application "TextEdit" 48 | textedit.documents.text.paragraphs.words 49 | 50 | 51 | ## Single element references 52 | 53 | // a reference to disk 1 of application "Finder" 54 | finder.disks[1] 55 | 56 | // a reference to file "ReadMe.txt" of folder "Documents" of home of application "Finder" 57 | finder.home.folders["Documents"].files["ReadMe.txt"] 58 | 59 | // a reference to paragraph -1 of text of document 1 of application "TextEdit" 60 | textedit.documents[1].text.paragraphs[-1] 61 | 62 | // a reference to middle paragraph of text of last document of application "TextEdit" 63 | textedit.documents.last.text.paragraphs.middle 64 | 65 | // a reference to any file of home of application "Finder" 66 | finder.home.files.any 67 | 68 | 69 | ## Relative references 70 | 71 | // a reference to paragraph before paragraph 6 of text of document 1 of application "TextEdit" 72 | textedit.documents[1].text.paragraphs[6].previous(TED.paragraph) 73 | 74 | // a reference to paragraph after character 30 of document 1 of application "Tex-Edit Plus" 75 | texeditplus.documents[1].characters[30].next(TEP.paragraph) 76 | 77 | 78 | ## Element range references 79 | 80 | // a reference to words 1 thru 4 of text of document 1 of application "TextEdit" 81 | textedit.documents[1].text.words[1, 4] 82 | 83 | 84 | // a reference to paragraphs 2 thru -1 of text of document 1 of application "TextEdit" 85 | textedit.documents[1].text.paragraphs[2, -1] 86 | 87 | // a reference to folders "Documents" thru "Music" of home of application "Finder" 88 | finder.home.folders["Documents", "Music"] 89 | 90 | // a reference to text (word 3) thru (paragraph 7) of document 1 of application "Tex-Edit Plus" 91 | texeditplus.documents[1].text[TEPCon.words[3], TEPCon.paragraphs[7]] 92 | 93 | 94 | ## Test references 95 | 96 | // a reference to every document of application "TextEdit" whose text is "\n" 97 | textedit.documents[TEDIts.text == "\n"] 98 | 99 | // a reference to every paragraph of document 1 of application "Tex-Edit Plus" ¬ 100 | // whose first character is last character 101 | texeditplus.documents[1].paragraphs[TEPIts.characters.first == TEPIts.characters.last] 102 | 103 | // a reference to every file of folder "Documents" of home of application "Finder" ¬ 104 | // whose name extension is "txt" and size < 10240 105 | finder.home.folders["Documents"].files[FINIts.nameExtension == "txt" && FINIts.size < 10240] 106 | 107 | 108 | ## Insertion location references 109 | 110 | // a reference to end of documents of application "TextEdit" 111 | textedit.documents.end 112 | 113 | // a reference to before paragraph 1 of text of document 1 of application "TextEdit" 114 | textedit.documents[1].text.paragraphs[1].before 115 | 116 | 117 | ## `open` command 118 | 119 | Open a document in TextEdit: 120 | 121 | // tell application "TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt") 122 | try textedit.open(URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt")) 123 | // TextEdit().documents["ReadMe.txt"] 124 | 125 | 126 | ## `get` command 127 | Get the name of every folder in the user's home folder: 128 | 129 | // tell application "Finder" to get name of every folder of home 130 | try finder.get(FINApp.home.folders.name) 131 | 132 | Or, more concisely: 133 | 134 | try finder.home.folders.name.get() 135 | 136 | Remember to declare the command's return type if you intend to use the returned value: 137 | 138 | let folderNames = try finder.home.folders.name.get() as [String] 139 | print(folderNames.joined(separator: ", ")) 140 | // "Desktop, Documents, Downloads, Movies, ..." 141 | 142 | 143 | ## `set` command 144 | 145 | Set the content of a TextEdit document: 146 | 147 | // tell application "TextEdit" to set text of document 1 to "Hello World" 148 | try textedit.documents[1].text.set(to: "Hello World") 149 | 150 | 151 | ## `count` command 152 | 153 | Count the words in a TextEdit document: 154 | 155 | // tell application "TextEdit" to count words of document 1 156 | try textedit.documents[1].words.count() as Int 157 | // 42 158 | 159 | Count the items in the current user's home folder: 160 | 161 | // tell application "Finder" to count items of home 162 | try finder.home.count(each: FIN.item) as Int 163 | // 11 164 | 165 | (Note that Finder and many other Carbon applications require the `count` command's `each` parameter to be given. Cocoa-based apps should accept either form.) 166 | 167 | 168 | ## `make` command 169 | 170 | Create a new TextEdit document: 171 | 172 | // tell application "TextEdit" to make new document ¬ 173 | // with properties {text:"Hello World\n"} 174 | try textedit.make(new: TED.document, 175 | withProperties: [TED.text: "Hello World\n"]) as TEDItem 176 | // TextEdit().documents["Untitled"] 177 | 178 | Append text to a TextEdit document: 179 | 180 | // tell application "TextEdit" to make new paragraph ¬ 181 | // at end of text of document 1 ¬ 182 | // with properties {text:"Yesterday\nToday\nTomorrow\n"} 183 | try textedit.make(new: TED.paragraph, 184 | at: TEDApp.documents[1].text.end, 185 | withData: "Yesterday\nToday\nTomorrow\n") 186 | 187 | 188 | ## `duplicate` command 189 | 190 | Duplicate a folder to a disk, replacing an existing item if one exists: 191 | 192 | // tell application "Finder" 193 | // duplicate folder "Projects" of home to disk "Work" with replacing 194 | // end tell 195 | try finder.home.folders["Projects"].duplicate(to: FINApp.disks["Backup"], replacing: true) 196 | // Finder().disks["Backup"].folders["Projects"] 197 | 198 | 199 | ## `add` command 200 | 201 | Add every person with a known birthday to a group named "Birthdays": 202 | 203 | // tell application "Contacts" 204 | // add every person whose birth date is not missing value to group "Birthdays" 205 | // end tell 206 | try contacts.people[CONIts.birthDate != MissingValue].add(to: CONApp.groups["Birthdays"]) 207 | 208 | 209 | ## `quit` command 210 | 211 | Close every TextEdit document without saving: 212 | 213 | // tell application "TextEdit" to quit saving no 214 | try textedit.quit(saving: TED.no) 215 | 216 | Quit the Stickies app if it's currently running: 217 | 218 | let stickies = AEApplication(bundleIdentifier: "com.apple.Stickies") // default glue 219 | if stickies.isRunning { try? stickies.quit() } 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /doc-source/11 advanced-type-support.md: -------------------------------------------------------------------------------- 1 | # Improving type system integration 2 | 3 | The `aeglue` tool's `-e`, `-s`, and `-t` options can be used to insert custom Swift enums, structs, and typealiases in the generated glue files for improved integration between Swift's strong, static type system and the Apple Event Manager's weak, dynamic types. Each option may appear any number of times and takes a format string as argument. 4 | 5 | 6 | ## Type aliases 7 | 8 | For convenience, all glue files define the following typealias as standard as dictionary type to which Apple event records are mapped by default: 9 | 10 |
typealias PREFIXRecord = [PREFIXRecord:Any]
11 | 12 | The `aeglue`'s `-t` option can be used to add more typealiases to the glue file if needed. For example, to define a typealias for `Array` named `PREFIXStrings`: 13 | 14 | -t 'Strings=Array' 15 | 16 | The `-t` option's format string has the following structure: 17 | 18 | 'ALIASNAME=TYPE' 19 | 20 | The glue's `PREFIX` is added automatically to `ALIASNAME`, and to any reserved type names that appear within `TYPE`. For example, the following command: 21 | 22 | aeglue -t 'Strings=Array' TextEdit 23 | 24 | adds the following typealias to a TextEdit glue: 25 | 26 | typealias TEDStrings = Array 27 | 28 | 29 | ## Enumerated types 30 | 31 | Unlike the AppleScript language, which has untyped variables to which any type of value can be assigned at any time, Swift requires all variables' types to be known at compile-time (`Int`, `String`, `Optional`, `Array`, `SomeClass`, etc). This presents a challenge when sending application commands that return more than one type of value; for example: 32 | 33 | let path = try TextEdit().documents[1].path.get() as Any 34 | 35 | may return either a `String` (e.g. `"/Users/jsmith/ReadMe.txt"`) or a `MissingValue` constant, depending on whether or not the document has been saved to disk. Furthermore, the Swift compiler won't allow you to manipulate that value until `path` is cast to a specific type (e.g. `String`). The type-safe solution to this problem is to define an enumerated type (a.k.a. tagged union, or sum type) where each case holds a different type of value. For example, SwiftAutomation's built-in `MayBeMissing` enum can hold either a value of type `T` or a `MissingValue` constant, thus: 36 | 37 | let path = try TextEdit().documents[1].path.get() as MayBeMissing 38 | 39 | ensures that the command's result is always a `String` value or `MissingValue`, and subsequent code can easily use a `switch` statement to determine which and extract the value for further use. 40 | 41 | The `aeglue` tool's `-e` option makes it easy to include enumerations for other combinations of return types within a generated glue file. For example, if an application command you wish to use is known to return either a `Symbol` or a `String` value, add the following `-e` option to the `aeglue` command: 42 | 43 | -e Symbol+String 44 | 45 | The -e option must have the following format (square brackets indicate optional information): 46 | 47 | -e '[ENUMNAME=][CASE1:]TYPE1+[CASE1:]TYPE2+...' 48 | 49 | `ENUMNAME` is the name to be given to the enumerated type, e.g. `-e MyType=Symbol+String`will create an enum named PREFIXMyType. (Do not include the glue's `PREFIX` in `ENUMNAME` as one will be added automatically.) 50 | 51 | If `ENUMNAME` is omitted, a default name is automatically generated, e.g. `-e Symbol+String` will create an enum named PREFIXSymbolOrString. 52 | 53 | `TYPEn` is the name of an existing Swift type, for example, `String` or a standard SwiftAutomation type: `Symbol`, `Object`, `Insertion`, `Item`, or `Items` (the glue `PREFIX` will be added automatically). `MissingValue` may also be used, in which case a `missing` case is added so that the enumeration can also contain a `MissingValue` constant (see the `Missing Value` section in [Chapter 5](type-mappings.html) for details). 54 | 55 | `CASEn` is the name of the case to which values of that type are assigned. If `TYPE` is parameterized, a suitable `CASE` name must be given, e.g. `strings:Array`. Otherwise the `CASE` name can be omitted, in which case a default name is automatically generated; e.g. if `TYPE` is `Int` then the default `CASE` name will be `int`. 56 | 57 | Thus, the following command: 58 | 59 | aeglue -e 'Color=Symbol+values:Array' 'System Events.app' 60 | 61 | will generate a glue file for System Events containing an `SEVColor` enum that accepts either a symbol (`SEV.blue`, `SEV.gold`, etc) or an integer array representing an RGB color value (e.g. `[0, 65535, 65535]`): 62 | 63 | enum SEVColor { 64 | case symbol(SEVSymbol) 65 | case values(Array) 66 | } 67 | 68 | which can then be used as follows: 69 | 70 | let color = try SystemEvent().appearancePreferences.highlightColor.get() as SEVColor 71 | switch color { 72 | case .symbol(let colorName): 73 | ... 74 | case .values(let rgbValues): 75 | ... 76 | } 77 | 78 | The order in which the enumerated types are declared is significant. SwiftAutomation will attempt to coerce an Apple event descriptor to each type in turn, returning a result as soon as it succeeds, or throwing an error if all coercions failed. The Apple Event Manager defines a variety of single- and bi-directional coercions: for instance, all values can coerce to a list (e.g. `3.14` ➝ `[3.14]`), any number can coerce to a string (e.g. `3.14` ➝ `"3.14"`) but only numeric strings may coerce to a number (e.g. `"3.14"` ➝ `3.14`, but `"forty-two"` will fail); while symbol types will coerce to four-char-code strings (e.g. `TED.document` ➝ `"docu"`), but not vice-versa (and either way is probably not what you intended). For best results, order the types from most specific coercion to least, e.g. `Symbol+String`, not `String+Symbol` (which would always return a string). 79 | 80 | 81 | 82 | ## Record structs 83 | 84 | While SwiftAutomation packs and unpacks Apple event records as `Dictionary` values as standard, it is also possible to map part or all of a specific record structures to a Swift struct, simplifying member access and improving type safety. While it is not practical to generate these record structs automatically due to the incomplete and often inaccurate nature of application AETE/SDEF resources, `aeglue` does allow individual record structs to be manually defined, in whole or in part, using its `-s` option. For example, consider the following TextEdit command: 85 | 86 | try TextEdit.documents[1].properties.get() 87 | // [TED.class_: TED.document, TED.name: "Untitled", TED.modified: false, TED.path: MissingValue, TED.text: ""] 88 | 89 | By default, SwiftAutomation unpacks Apple event records as Swift dictionary values of type Dictionary<PREFIXSymbol,Any>. To map the TextEdit document's properties record to a Swift struct named `TEDDocumentRecord` instead, first add the following `-s` option to the `aeglue` command: 90 | 91 | -s 'Document:document=name:String+modified:Bool+path:MayBeMissing+text:String' 92 | 93 | The `-s` option must have the following format: 94 | 95 | -s 'STRUCTNAME[:CLASS]=NAME1:TYPE1+NAME2:TYPE2+...' 96 | 97 | `STRUCTNAME` is the name to be given to the record struct, e.g. `Document` ➝ `TEDDocumentRecord` (the PREFIX prefix and Record suffix are added automatically). `CLASS` is the record's terminology-defined 'class' name as it appears in the glue's SDEF documentation, e.g. `document`; if omitted, `record` is used. 98 | 99 | `NAMEn` is the name of a property to appear on the struct, and `TYPEn` is the property's type; e.g. `name:String` describes a property named `name` which holds a value of type `String`. All record properties are declared as `var`, except for the special `class_` property. 100 | 101 | The above format string, when applied to a TextEdit dictionary, defines a struct with the following name and properties: 102 | 103 | struct TEDDocumentRecord { 104 | let class_ = TED.document 105 | var name: String 106 | var modified: Bool 107 | var path: MayBeMissing 108 | var text: String 109 | } 110 | 111 | To unpack a TextEdit document's properties record as a `TEDDocumentRecord` struct: 112 | 113 | try TextEdit.documents[1].properties.get() as TEDDocumentRecord 114 | // TEDDocumentRecord(class_:TED.document, name: "Untitled", modified: false, path: MissingValue, text: "") 115 | 116 | 117 | 118 | ## MayBeMissing versus Optional 119 | 120 | 121 | [TO DO: finish this section once a final decision is made on whether or not to keep `MayBeMissing` and the special `.missing(MissingValue)` case in `aeglue`-generated enums.] 122 | 123 | [Note that AEM's `missing value` symbol is roughly analogous to Swift's `nil`; however, advantage of `MissingValue` over `nil` is that it's concrete, not generic, so can be used, compared for equality, etc. even when variable's static type is `Any`. OTOH, `Optional` is idiomatic Swift, so will always be the preferred form used in typed code. SwiftAutomation accepts both when used as command parameters; when command's return type is `Any`, `missing value` descriptor is unpacked as `MissingValue`; when return type is `Optional`, it's unpacked as `Optional.none` instead. `MayBeMissing` unpacks it as `MayBeMissing.missing(MissingValue)`; custom enums that include `MissingValue` in their `-e` format string unpack it as ENUMNAME.missing(MissingValue).] 124 | -------------------------------------------------------------------------------- /doc-source/12 optimizing-performance.md: -------------------------------------------------------------------------------- 1 | # Optimizing performance 2 | 3 | // TO DO: check code examples work 4 | 5 | ## About performance 6 | 7 | Apple event IPC is subject to a number of potential performance bottlenecks: 8 | 9 | * Sending Apple events is more expensive than calling local functions. 10 | 11 | * There may be significant overheads in how applications resolve individual object references. 12 | 13 | * Packing and unpacking large and/or complex values (e.g. a long list of object specifiers) can take an appreciable amount of time. 14 | 15 | [TO DO: include note that AEs were originally introduced in System 7, which had very high process switching costs, so were designed to compensate for that] 16 | 17 | Fortunately, it's often possible to minimise performance overheads by using fewer commands to do more work. Let's consider a typical example: obtaining the name of every person in `Contacts.app` who has a particular email address. There are several possible solutions to this, each with very different performance characteristics: 18 | 19 | 20 | ## The iterative OO-style approach 21 | 22 | While iterating over application objects and manipulating each in turn is a common technique, it's also the slowest by far: 23 | 24 | // `aeglue Contacts.app` 25 | let contacts = Contacts() 26 | 27 | let desiredEmail = "sam.brown@example.com" 28 | 29 | var foundNames = [String]() 30 | for person in contacts.people.get() as [CONItem] { 31 | for email in people.emails.get() as [CONItem] { 32 | if email.value.get() == desiredEmail { 33 | foundNames += person.name.get() 34 | } 35 | } 36 | } 37 | print(foundNames) 38 | 39 | The above code sends one Apple event to get a list of references to all people, then one Apple event for each person to get a list of references to their emails, then one Apple event for each of those emails. Thus the time taken increases directly in proportion to the number of people in Contacts. If there's hundreds of people to search, that's hundreds of Apple events to be built, sent and individually resolved, and performance suffers as a result. 40 | 41 | The solution, where possible, is to use fewer, more sophisticated commands to do the same job. 42 | 43 | 44 | ## The smart query-oriented approach 45 | 46 | While there are some situations where iterating over and manipulating each application object individually is the only option (for example, when setting a property in each object to a different value), in this case there is plenty of room for improvement. Depending on how well an application implements its AEOM support, it's possible to construct queries that identify more than one application object at a time, allowing a single command to manipulate multiple objects in a single operation. 47 | 48 | In this case, the entire search can be performed using a single complex query sent to Contacts via a single Apple event: 49 | 50 | let contacts = Contacts() 51 | 52 | let desiredEmail = "sam.brown@example.com" 53 | 54 | let foundNames = contacts.people[CONIts.emails.value.contains(desiredEmail)].name.get() as [String] 55 | 56 | print(foundNames) 57 | 58 | To explain: 59 | 60 | * The query states: "Find the name of every person object that passes a specific test." 61 | 62 | * The test is: "Does a given value, `"sam.brown@example.com"`, appear in a list that consists of the value of each email object contained by an individual person?" 63 | 64 | * The command is: "Evaluate that query against the AEOM and get (return) the result, which is a list of zero or more strings: the names of the people matched by the query." 65 | 66 | 67 | 68 | ## The hybrid solution 69 | 70 | While AEOM queries can be surprisingly powerful, there are still many problems too complex for the application to evaluate entirely by itself. For example, let's say that you want to obtain the name of every person who has an email addresses that uses a particular domain name. Unfortunately, this test is too complex to express as a single AEOM query; however, it can still be solved reasonably efficiently by obtaining all the data from the application up-front and processing it locally. For this we need: 1. the name of every person in the Contacts, and 2. each person's email addresses. Each request can be described in a single query, allowing all of the required data to be retrieved using just two `get` commands. 71 | 72 | let contacts = Contacts() 73 | 74 | let desiredDomain = "@example.com" 75 | 76 | // get each person's name 77 | let names = contacts.people.name.get() as [String] 78 | 79 | // get each person's email addresses 80 | let emailsByPerson = contacts.people.emails.value.get() as [[String]] 81 | 82 | var foundNames = [String]() 83 | for (name, emails) in zip(names, emailsByPerson) { 84 | for email in emails { 85 | if email.hasSuffix(desiredDomain) { 86 | foundNames.append(name) 87 | break 88 | } 89 | } 90 | } 91 | print(foundNames) 92 | 93 | This solution isn't as fast as the pure-query approach, but is still far more efficient than iterating over and manipulating every person and email element one at a time. 94 | 95 | -------------------------------------------------------------------------------- /doc-source/13 low-level-apis.md: -------------------------------------------------------------------------------- 1 | # Using the low-level `AEApplication` glue 2 | 3 | While glue files' terminology-based properties and methods are recommended for controlling individual "AppleScriptable" applications, SwiftAutomation also includes lower-level APIs for interacting with "non-scriptable" "applications that do not include an AETE/SDEF terminology resource, or whose terminology contains defects that render some or all of the generated glue unusable, or when sending standard commands that do not require an application-specific glue. These low-level APIs are present on all generated glues' `Application` and `ObjectSpecifier` classes if needed, and also on the default `AEApplicationGlue` that is included in SwiftAutomation as standard. 4 | 5 | 6 | ## Sending standard Apple events 7 | 8 | The following commands are defined on all `Application` and `Specifier` classes, including the default `AEApplication`, and are recognized by most/all macOS applications: 9 | 10 | run() 11 | reopen() 12 | launch() 13 | activate() 14 | open(Array) // list of file URLs 15 | openLocation(String) // a URL string (e.g. "http://apple.com") 16 | print(Array) // list of file URLs 17 | quit( [ saving: AE.yes|AE.no|AE.ask ] ) 18 | 19 | (Standard `get` and `set` commands are also defined, but will only work in apps that implement an AEOM.) 20 | 21 | As with application-specific commands, standard commands will throw a `CommandError` on failure, so remember to prefix with `try`. 22 | 23 | For example, to open a file: 24 | 25 | // tell application id "com.apple.TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt") 26 | let textedit = AEApplication(bundleIdentifier: "com.apple.TextEdit") 27 | try textedit.open(URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt")) 28 | 29 | Or to quit multiple applications without saving changes to any open documents: 30 | 31 | for appName in ["TextEdit", "Preview", "Script Editor"] { 32 | let app = AEApplication(name: appName) 33 | if app.isRunning { try? app.quit(saving: AE.no) } 34 | } 35 | 36 | 37 | ## Sending Apple events using four-char codes 38 | 39 | All specifiers implement a low-level `sendAppleEvent(...)` method, allowing Apple events to be built and sent using four-char codes (a.k.a. OSTypes): 40 | 41 | sendAppleEvent(_ eventClass: OSType/String, _ eventID: OSType/String, _ parameters: [OSType/String:Any] = [:], 42 | requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil, 43 | withTimeout: TimeInterval? = nil, considering: ConsideringOptions? = nil) throws -> T/Any 44 | 45 | Four-char codes may be given as `OSType` (`UInt32`) values or as `OSType`-encodable `String` values containing exactly four MacRoman characters. Invalid strings will cause `sendAppleEvent()` to throw a `CommandError`. 46 | 47 | For example: 48 | 49 | // tell application id "com.apple.TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt") 50 | // tell application id "com.apple.TextEdit" to «event aevtodoc» (POSIX file "/Users/jsmith/ReadMe.txt") 51 | let textedit = AEApplication(bundleIdentifier: "com.apple.TextEdit") 52 | try textedit.sendAppleEvent("aevt", "odoc", ["----": URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt")]) 53 | 54 | // tell application id "com.apple.TextEdit" to quit saving no 55 | // tell application id "com.apple.TextEdit" to «event aevtquit» given «class savo»: «constant ****ask » 56 | try textedit.sendAppleEvent("aevt", "quit", ["savo": AE.ask]) 57 | 58 |

While the Carbon AE headers define constants for common four-char codes, e.g. cDocument = 'docu' = 0x646f6375, as of Swift3/Xcode8/macOS10.12 some constants are incorrectly mapped to Int (SInt64) instead of OSType (UInt32), so their use is best avoided.

59 | 60 | 61 | ## Constructing object specifiers using four-char codes 62 | 63 | All object specifiers implement low-level methods for constructing property and all-elements specifiers 64 | 65 | * userProperty(_ name: String) -- user-defined identifier, e.g. `someProperty` (note: case-[in]sensitivity rules are target-specific) 66 | 67 | * property(_ code: OSType/String) -- four-char code, either as OSType (UInt32) or four-char string, e.g. `cDocument`/`"docu"` 68 | 69 | * elements(_ code: OSType/String) -- ditto 70 | 71 | The default `AEApplicationGlue` defines `AEApp`, `AECon`, and `AEIts` roots for constructing untargeted specifiers using four-char codes only. 72 | 73 | Insertion and element selectors are the same as in application-specific glues; see [Chapter 7](object-specifiers.html) for details. 74 | 75 | 76 | For example: 77 | 78 | // every paragraph of text of document 1 [of it] 79 | // every «class cpar» of «property ctxt» of «class docu» [of it] 80 | AEApp.elements("docu")[1].property("ctxt").elements("cpar") 81 | AEApp.elements(0x646f6375)[1].property(0x63747874).elements(0x63706172) 82 | 83 | 84 | ## Constructing symbols using four-char codes 85 | 86 | The default `AEApplicationGlue` defines an `AESymbol` class, type aliased as `AE`, for constructing `Symbol` instances using four-char codes: 87 | 88 | AESymbol(code: OSType/String, type: OSType/String = typeType/"type") 89 | 90 | For example: 91 | 92 | // document 93 | // «class docu» 94 | AE(code: "docu") 95 | AE(code: 0x646f6375) 96 | 97 | // name 98 | // «property pnam» 99 | AE(code: "pnam", type: "prop") // (note: "type" is more commonly used than "prop") 100 | AE(code: 0x706e616d, type: 0x70726f70) 101 | 102 | // ask 103 | // «constant ****ask » 104 | AE(code: "ask ", type: "enum") 105 | AE(code: 0x61736b20, type: 0x656e756d) 106 | 107 | `AESymbol` instances can be used interchangeably with glue-defined PREFIXSymbol classes. SwiftAutomation only compares `Symbol` instances' `code` and `type` properties when comparing for equality; thus the following equality test returns true: 108 | 109 | AE(code: "docu") == TED.document 110 | 111 | 112 | ## Using symbols as AERecord keys 113 | 114 | AppleScript records can contain any combination of keyword- and/or identifier-based keys, so the `Symbol` class also defines an `init(_ name: String)` initializer, allowing identifier-based record keys to be constructed as well: 115 | 116 | // {name: "Sam", age: 32, isSingle: true} 117 | [AE(code:"pnam"): "Sam", AE("age"): 32, AE("issingle"): true] 118 | 119 | Be aware that case-[in]sensitivity rules for identifier strings can vary depending on how and where the record is used; for case-insensitivity, use all-lowercase. 120 | 121 | To determine if a `Symbol` instance represents a keyword or an identifier: 122 | 123 | AE(code:"pnam").nameOnly // false 124 | AE("issingle").nameOnly // true 125 | 126 | Scriptable applications do not normally use identifier-based keys in records; however, they may be used by AppleScript-based applets and in `NSAppleScript`/`NSUserAppleScriptTask` calls. 127 | 128 | 129 | -------------------------------------------------------------------------------- /doc-source/14 notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | // TO DO: update 4 | 5 | ## Security issues 6 | 7 | If including user names and/or passwords in remote application URLs, please note that XXApplication and XXSpecifier objects will retain those URL strings over their entire lifetime. Security here is the developer's responsibility, as it's their code that creates and retains these objects. 8 | 9 | 10 | ## GUI Scripting 11 | 12 | Non-scriptable applications may in some cases be controlled from SwiftAutomation by using System Events to manipulate their graphical user interface. Note that the "Enable access for assistive devices" checkbox must be selected in the Universal Access system preferences pane for GUI Scripting to work. 13 | 14 | 15 | ## Type bridging limitations 16 | 17 | Some applications (e.g. QuarkXpress) may return values which SwiftAutomation cannot convert to equivalent Cocoa types. These values are usually of types which are defined, used and understood only by that particular application, and will be represented in Objective-C as `NSAppleEventDescriptor` instances which should generally be treated as opaque values. 18 | 19 | A few standard but rarely used AE types are also currently unbridged with Cocoa counterparts, such as image types (`typePict`, `typeTIFF`, etc.). Client code should perform any necessary conversions itself. 20 | 21 | 22 | 23 | ## SwiftAutomation and threads 24 | 25 | SwiftAutomation is thread-safe and events can be sent (and their replies received) from any thread. 26 | 27 | 28 | ## Credits 29 | 30 | Many thanks to all those who have contributed comments, suggestions and bug reports. 31 | 32 | -------------------------------------------------------------------------------- /doc-source/2 installing-swiftautomation.md: -------------------------------------------------------------------------------- 1 | # Installing SwiftAutomation 2 | 3 | [[ TO DO: include screenshots? ]] 4 | 5 | 6 | ## Get SwiftAutomation 7 | 8 | Run the following command in Terminal to clone the [SwiftAutomation repository](https://bitbucket.org/hhas/swiftae) to your Mac: 9 | 10 | git clone https://bitbucket.org/hhas/swiftae.git 11 | 12 | Minimum requirements: macOS 10.11 and Xcode 8.1/Swift 3.0.1. 13 | 14 | 15 | ## Installing for Swift "scripting" 16 | 17 | Open the SwiftAE project in Xcode. Select the Product ➞ Scheme ➞ Release menu option followed by Product ➞ Build to build the following three products: `SwiftAutomation.framework`, `aeglue` (this is automatically embedded in the framework), and `MacOSGlues.framework`. 18 | 19 | To reveal the folder containing the built products, scroll down to "Products" in the Xcode project window's Project Navigator list, right-click the "SwiftAutomation.framework" entry (its name should now be black, not red), and select Show in Finder from the contextual menu. 20 | 21 | Launch Terminal and type the following into a new window: 22 | 23 | cd /Library/Frameworks 24 | 25 | Next type the following, with a space after `-s`: 26 | 27 | sudo ln -s 28 | 29 | then drag `SwiftAutomation.framework` from the Finder window onto Terminal to insert the full path to the framework at the end of the command. On pressing Return, enter an an administrator login to allow the `ln` command to create a symlink in `/Library/Frameworks`. Repeat the process for the `MacOSGlues.framework`. (Do not create Finder aliases as those do not work with command-line tools.) 30 | 31 | To confirm this setup works, enter the following Swift "script" into a plain text or code editor of your choice: 32 | 33 | #!/usr/bin/swift -target x86_64-apple-macosx10.12 -F /Library/Frameworks 34 | 35 | import SwiftAutomation 36 | import MacOSGlues 37 | 38 | print(try Finder().home.name.get()) // output your home folder's name 39 | 40 | Save it as `automate.swift` in your home folder, type `cd ~/ && chmod +x test.swift` in Terminal to make it executable, then run it as follows: 41 | 42 | ./automate.swift 43 | 44 | All going well, the script should compile and run in well under a second, and print the name of your home folder to `stdout`. Success! 45 | 46 | (All not going well, Swift will log an error message describing why it failed. If you cannot troubleshoot it yourself, please get in touch.) 47 | 48 |

Remember to rebuild SwiftAE's Release products whenever a new version of Xcode/Swift is installed: until Swift provides a stable ABI, the compiled SwiftAutomation and MacOSGlues frameworks can only be used by scripts compiled with the exact same version of Swift as they were.

49 | 50 | 51 | ## Embedding in GUI apps 52 | 53 | Because Swift does not yet provide a stable ABI, it is not possible for a GUI application built by one version of the Swift compiler to link to a framework built by another. Instead, a Swift-based application project must compile all of its Swift framework dependencies and embed those frameworks within its .app bundle as part of its build process. Technical Note TN2435 [Embedding Frameworks In An App](https://developer.apple.com/library/content/technotes/tn2435/_index.html) explains how to embed a third-party Xcode framework project such as SwiftAE inside your own Xcode project. 54 | 55 | -------------------------------------------------------------------------------- /doc-source/5 creating-and-using-static-glues.md: -------------------------------------------------------------------------------- 1 | # Creating and using static glues 2 | 3 | The SwiftAutomation framework includes a command line `aeglue` tool for generating static glue files. Glues enable you to control "AppleScriptable" applications using human-readable property and method names derived from their built-in terminology resources. 4 | 5 | 6 | ## Generating a glue 7 | 8 | For convenience, add the following shortcut to your `~/.bash_profile`: 9 | 10 | alias aeglue=/Library/Frameworks/SwiftAutomation.framework/Resources/bin/aeglue 11 | 12 | To view the `aeglue` tool's full documentation: 13 | 14 | aeglue -h 15 | 16 | Glue files follow a standard NAMEGlue.swift naming convention, where NAME is the name of the glue's `Application` class. The following command generates a `TextEditGlue.swift` glue file in your current working directory: 17 | 18 | aeglue TextEdit 19 | 20 | If an identically named file already exists at the same location, `aeglue` will normally fail with a "path already exists" error. To overwrite the existing file with no warning, add an `-r` option: 21 | 22 | aeglue -r TextEdit 23 | 24 | To write the file to a different directory, use the `-o` option. For example, to create a new `iTunesGlue.swift` file on your desktop: 25 | 26 | aeglue -o ~/Desktop TextEdit 27 | 28 | 29 | ## Getting application documentation 30 | 31 | In addition to generating the glue file, the `aeglue` tool also creates a NAMEGlue.swift.sdef file containing the application dictionary (interface documentation), reformatted for use with SwiftAutomation. For example, to view the `TextEditGlue.swift` terminology in Script Editor: 32 | 33 | open -a 'Script Editor' TextEditGlue.swift.sdef 34 | 35 | Refer to this documentation when using SwiftAutomation glues in your own code, as it shows element, property, command, etc. names as they appear in the generated glue classes. (Make sure Script Editor's dictionary viewer is set to the "AppleScript" language option for it to display correctly.) 36 | 37 | Be aware that only 'keyword' definitions are displayed in Swift syntax; 'type' names are unchanged from their AppleScript representation, as are AppleScript terms and sample code that appear in descriptions. SDEF-based documentation is always written for AppleScript users, so unless the application developer provides external documentation for other programming languages some manual translation is required. Furthermore, most applications' SDEF documentation is far from exhaustive, and frequently lacks both detail and accuracy; for instance, the SDEF format doesn't descript precisely what types and combinations of parameters are/aren't accepted by each command, while the documented 'types' of properties, parameters, and return values may be incomplete or wrong. Supplementary documentation, example code, AppleScript user forums, educated guesswork, and trial-and-error experimentation may also be required. 38 | 39 | The SwiftAutoEdit application includes a File ➝ New ➝ Command Translator menu option that can also help when the correct AppleScript syntax for a command is already known, and all that is needed is some assistance in constructing its Swift equivalent. 40 | 41 | 42 | ## How glues are structured 43 | 44 | Each glue file contains the following classes: 45 | 46 | * Application -- represents the root application object used to send commands, e.g. `TextEdit` 47 | 48 | * PREFIXItem, PREFIXItems, PREFIXInsertion, PREFIXRoot -- represents the various forms of Apple Event Object Model queries, a.k.a. _object specifiers_, e.g. `TEDItem` 49 | 50 | * PREFIXSymbol -- represents Apple event type, enumerator, and property names, e.g. `TEDSymbol` 51 | 52 | `aeglue` automatically disambiguates each glue's class names by adding a three-letter PREFIX derived from the application's name (e.g. `TextEdit` ➝ `TED`). Thus the standard `TextEditGlue.swift` glue defines `TextEdit`, `TEDItem`, `TEDItems`, `TEDInsertion`, `TEDRoot`, and `TEDSymbol` classes, while `FinderGlue.swift` defines `Finder`, `FINItem`, `FINItems`, and so on. (Different prefixes allow multiple glues to be imported into a program without the need to fully qualify all references to those classes with the full glue name, i.e. `TEDItem` is easier to write than `TextEditGlue.Item`.) 53 | 54 | Each glue also defines: 55 | 56 | * PREFIXApp, PREFIXCon and PREFIXIts constants for constructing certain kinds of object specifiers 57 | 58 | * a PREFIXRecord typealias as a convenient shorthand for Dictionary<PREFIXSymbol:Any>, which is the default type to which Apple event records are mapped. 59 | 60 | * a PREFIX typealias as a convenient shorthand for PREFIXSymbol. 61 | 62 | Glue files may also include custom `typealias`, `enum`, and `struct` definitions that improve integration between Swift and Apple event type systems. [Chapter 10](advanced-type-support.html) explains how to add and use these features. 63 | 64 | 65 | ## Customizing glues 66 | 67 | If the default three-letter prefix is unsuitable for use, use the `-p` option to specify a custom prefix. The following command creates a new `TextEditGlue.swift` file that uses the class name prefix `TE`: 68 | 69 | aeglue -p TE TextEdit 70 | 71 | For compatibility, `aeglue` normally sends the application an `ascr/gdte` event to retrieve its terminology in AETE format. However, some Carbon-based applications (e.g. Finder) may have buggy `ascr/gdte` event handlers that return Cocoa Scripting's default terminology instead of the application's own. To work around this, add an `-S` option to retrieve the terminology in SDEF format instead: 72 | 73 | aeglue -S Finder 74 | 75 | The `-S` option may be quicker when generating glues for CocoaScripting-based apps which already contain SDEF resources. When using the `-S` option to work around buggy `ascr/gdte` event handlers in AETE-based Carbon apps, be aware that macOS's AETE-to-SDEF converter is not 100% reliable. For example, four-char code strings containing non-printing characters fail to appear in the generated SDEF XML, in which case `aeglue` will warn of their omission and you'll have to correct the glue files manually or use SwiftAutomation's lower-level `OSType`-based APIs in order to access the affected objects/commands. 76 | 77 | 78 |
79 |

Tip: When getting started, a quick way to generate standard glues for all scriptable applications in /Applications, including those in subfolders, is to run the following commands:

80 | 81 |
mkdir AllGlues && cd AllGlues && aeglue -S /Applications/*.app /Applications/*/*.app
82 | 83 |

aeglue will log error messages for problematic applications (e.g. those without dictionaries or whose dictionaries contain significant flaws). Any glues that are unsatisfactory or require extra customization can then be manually regenerated one at a time with the appropriate options.

84 |
85 | 86 | 87 | ## Using a glue 88 | 89 | To include the generated glue file in your project: 90 | 91 | 1. Right-click in the Project Navigator pane of the Xcode project window, and select Add Files to PROJECT... from the contextual menu. 92 | 93 | 2. Select the generated glue file (e.g. `TextEditGlue.swift`) and click Add. 94 | 95 | 3. In the following sheet, check the "Copy items into destination group's folder", and click Add. 96 | 97 | 98 |

Subsequent code examples in this manual assume a standard glue file has already been generated and imported; e.g. TextEdit-based examples use a TextEdit glue with the prefix TED, Finder-based examples use a Finder glue with the prefix FIN, etc.

99 | 100 | 101 | ## How keywords are converted 102 | 103 | Because scriptable applications' terminology resources supply class, property, command, etc. names in AppleScript keyword format, `aeglue` must convert these terms to valid Swift identifiers when generating the glue file and accompanying `.sdef` documentation. For reference, here are the main conversion rules used: 104 | 105 | * Characters `a-z`, `A-Z`, `0-9`, and underscores (`_`) are preserved. 106 | 107 | * Spaces, hyphens (`-`), and forward slashes (`/`) are removed, and the first character of all but the first word are capitalized, e.g. `document file` ➝ `documentFile`, `Finder window` ➝ `FinderWindow`. 108 | 109 | * Any names that match Swift keywords or properties/methods already defined by SwiftAutomation classes have an underscore (`_`) appended to avoid conflict, e.g. `class` ➝ `class_`. 110 | 111 | 112 | Some rarely encountered corner cases are dealt with by the following conversion rules: 113 | 114 | * Ampersands (`&`) are replaced by the word 'And'. 115 | 116 | * Any other characters are converted to `_0x00_`-style hexadecimal representations. 117 | 118 | * Names that begin with an underscore (`_`) have an underscore appended too. 119 | 120 | * SwiftAutomation provides default terminology for standard type classes such as `integer` and `unicodeText`, and standard commands such as `open` and `quit`. If an application-defined name matches a built-in name but has a _different Apple event code_, SwiftAutomation will append an underscore to the application-defined name to avoid conflict. 121 | 122 | -------------------------------------------------------------------------------- /doc-source/7 application-objects.md: -------------------------------------------------------------------------------- 1 | # Application objects 2 | 3 | [TO DO: The name/path/bundleID/fileURL options all currently rely on NSWorkspace's launchApplication(at:URL,...), so can't be used in a sandboxed app. In theory, the bundleID option (both explicit and default) would use launchApplication(withBundleIdentifier:,...), which avoids running into sandbox restrictions, but that method's a piece of crap so got pulled out before the sandboxing problem was known about. Once a workable solution for the latter is found, the bundleID option should work everywhere, though the by-name/path/fileURL options will need to be documented as only available for use within non-sandboxed apps] 4 | 5 | [TO DO: add a brief section on using SwiftAutomation within a sandboxed app, to ensure users are aware they'll need entitlements in order to talk to other apps. (Might also want to note that there's a potential security hole any time a non-sandboxed app can be send a run script/do script/do shell script/etc command that enables execution of arbitrary code; not sure how thoroughly plugged these are, and there's nothing to stop apps defining AE handlers that may deliberately/accidentally allow execution of arbitrary code/commands outside a sandbox. NSUserAppleScriptTask explicitly allows it, of course, though that's on the assumption that the scripts it runs will always and only be supplied by the user, i.e. scripts they've written themselves and/or scripts from a - hopefully! - trusted source that they've intentionally installed for their own use.)] 6 | 7 | ## Creating application objects 8 | 9 | Before you can communicate with a scriptable application you must create an application object. When targeting local applications, the glue's own default constructor, which locates the application by bundle identifier, is usually the best choice. For example, to target TextEdit: 10 | 11 | let textedit = TextEdit() 12 | 13 | This uses the bundle identifier of the application from which the glue was originally generated (in this case "com.apple.TextEdit"). If you have more than one version of the application installed, or wish to control the same application on another machine (via Remote Apple Events), use one of the following initializers to target it precisely: 14 | 15 | // application's name or full path (`.app` suffix is optional) 16 | Application(name: String, ...) 17 | 18 | // application's bundle ID 19 | Application(bundleIdentifier: String, ...) 20 | 21 | // `file:` URL for local application or `eppc:` URL for remote process 22 | Application(url: URL, ...) 23 | 24 | // Unix process id 25 | Application(processIdentifier: pid_t, ...) 26 | 27 | // AEAddressDesc 28 | Application(descriptor: NSAppleEventDescriptor, ...) 29 | 30 | // current (i.e. host) process 31 | Application.currentApplication() 32 | 33 | For example, to target a specific version of Adobe InDesign by its name: 34 | 35 | let indesign = AdobeInDesign(name: "Adobe InDesign CS6.app") 36 | 37 | Or to control a copy of iTunes running on another machine: 38 | 39 | let itunes = ITunes(url: URL(string: "eppc://media-mac.local/iTunes")!) 40 | 41 | Except for `currentApplication()`, the above initializers can also accept the following optional arguments: 42 | 43 | * `launchOptions: NSWorkspaceLaunchOptions` – determines behavior when launching a local application; if omitted, the `NSWorkspaceLaunchOptions.WithoutActivation` option is used. See AppKit's `NSWorkspaceLaunchOptions` documentation for a list of available options. 44 | 45 | * `relaunchMode: RelaunchMode` - determines behavior if the target process no longer exists; see Restarting applications section below. If omitted, `RelaunchMode.Limited` is used. 46 | 47 | Note that local applications will be launched if not already running when the `Application()`, `Application(name:)`, `Application(bundleIdentifier:)` or `Application(url:)` constructors are invoked, and events will be sent to the running application according to its process ID. If the process is later terminated, that process ID is no longer valid and events sent subsequently using this application object will fail as application objects currently don't provide a 'reconnect' facility. 48 | 49 | If the `Application(url:)` constructor is invoked with an `eppc://` URL, or if the `Application(processIdentifier:)` or `Application(descriptor:)` constructors are used, the caller is responsible for ensuring the target application is running before sending any events to it. 50 | 51 | 52 | ## Basic commands 53 | 54 | All applications should respond to the following commands, which are added to all glue files by default: 55 | 56 | run() // Run an application 57 | 58 | activate() // Bring the application to the front 59 | 60 | reopen() // Reactivate a running application 61 | 62 | open(Any) // Open the specified file(s) (typically URL or Array) 63 | 64 | print(Any) // Print the specified file(s) (typically URL or Array) 65 | 66 | quit( [ saving: AE.yes | AE.ask | AE.no ] ) 67 | // Quit an application, optionally saving any open documents first 68 | 69 | Some applications may provide their own definitions of some or all of these commands, so check their terminology before use. For example, many applications' `open` command will also return a `Specifier` or `Array` value identifying the newly opened documents. 70 | 71 | Standard `get` and `set` commands are also included as most scriptable applications' dictionaries don't define these commands themselves, though are only applicable to applications that define an Apple Event Object Model: 72 | 73 | get(Specifier) -> Any // Get the value of the given object specifier 74 | 75 | set(Specifier, to: Any) // Set the value of the given object specifier to the new value 76 | 77 |
78 | 79 |

Be aware that all glue-defined application commands come in two standard forms, one with a generic return type and one with an `Any` return type:

80 | 81 |
commandName<T>([_ directParameter: Any,][keywordParameter: Any,...]) throws -> T
 82 | 
 83 | commandName([_ directParameter: Any,][keywordParameter: Any,...]) throws -> Any
84 | 85 |

For brevity, this documentation omits the `throws` keyword and only indicates a return type for application commands that are expected to return a result.

86 | 87 |

Also be aware that all command parameters are typed as `Any` – it is the caller's responsibility to supply values of appropriate types for a given command. (While application dictionaries may suggest appropriate parameter types, this information is neither complete nor accurate enough for the glue generator to be any more specific.)

88 | 89 |
90 | 91 | 92 | ## Transaction support 93 | 94 | Application objects implement a `doTransaction(session:closure:)` method that allow a sequence of commands to be handled atomically by applications that support transactions, e.g. FileMaker Pro. 95 | 96 | [TO DO: document this, once it's tested (_if_ it can be tested... this is a massively neglected corner of AE handling; not even sure if FMP supports it any more, never mind any newer apps)] 97 | 98 | 99 | ## Local application launching notes 100 | 101 | Note: the following information only applies to local applications as SwiftAutomation cannot directly launch applications on a remote Mac. To control a remote application, the application must be running beforehand or else launched indirectly (e.g. by using the remote Mac's Finder to open it). 102 | 103 | 104 | ### How applications are identified 105 | 106 | When you create an Application object by application name, bundle id or creator type, SwiftAutomation uses LaunchServices to locate an application matching that description. If you have more than one copy of the same application installed, you can identify the one you want by providing its full path, otherwise LaunchServices will identify the newest copy for you. 107 | 108 | SwiftAutomation targets local running applications by process ID, so it's possible to have multiple copies/versions of an application running at the same time if their Application objects are created using process IDs (or `eppc://` URLs that include pid). 109 | 110 | 111 | ### Checking if an application is running 112 | 113 | You can check if the target application is currently running by getting the value of its `isRunning` Boolean property: 114 | 115 | Finder().isRunning 116 | 117 | For example, SwiftAutomation will automatically launch a non-running application the first time it sends a command, so if you don't want to interact with that application unless it is already running, enclose all of its commands in a conditional block that only executes if its `isRunning` property is `true`: 118 | 119 | let iTunes = iTunes() 120 | 121 | // Only perform iTunes-related commands if it's already running: 122 | if iTunes.isRunning { 123 | // all iTunes-related commands go here... 124 | } 125 | 126 | 127 | ### Launching applications via `launch()` 128 | 129 | When SwiftAutomation launches a non-running application, it normally sends it a `run` command as part of the launching process. If you wish to avoid this, you should start the application by sending it a `launch` command before doing anything else. This is useful when you want to start an application without it going through its normal startup procedure, and is equivalent to the using AppleScript's `launch` command. For example, to launch TextEdit without causing it to display a new, empty document (its usual behaviour): 130 | 131 | textedit = TextEdit() 132 | try textedit.launch() 133 | // other TextEdit-related code goes here... 134 | 135 | 136 | ### Restarting applications 137 | 138 | As soon as you start to construct a reference or command using a newly created Application objects, if the application is not already running then SwiftAutomation will automatically launch it in order to obtain its terminology. 139 | 140 | Be default, if the target application has stopped running since the Application object was created, trying to send it a command using that Application object will result in an invalid connection error (-609), unless that command is `run` or `launch`. This restriction prevents SwiftAutomation accidentally restarting an application that has unexpectedly quit while a script is controlling it. You can restart an application by sending an explicit `run` or `launch` command, or by creating a new Application object for it. 141 | 142 | To change this relaunch behavior, use one of the following `RelaunchMode` values as the initializer's `relaunchMode:` argument: 143 | 144 | * `.never` -- prevent the Application object automatically relaunching the application, even for a `run` or `launch` command 145 | * `.limited` -- allow the Application object to relaunch the application before sending a `run` or `launch` command (SwiftAutomation's default behavior) 146 | * `.always` -- allow the Application object to relaunch the application before sending any command (AppleScript's behavior) 147 | 148 | For example: 149 | 150 | let illustrator = AdobeIllustrator(relaunchMode: .never) 151 | 152 | Note that you can still use Application objects to control applications that have been quit _and_ restarted since the Application object was created. SwiftAutomation will automatically update the Application object's process ID information as needed. [TO DO: check this is correct; also check how it behaves when .never is used] 153 | 154 | 155 |

There is a known problem with quitting and immediately relaunching an application via SwiftAutomation, where the relaunch instruction is sent to the application before it has actually quit. This timing issue appears to be macOS's fault; one workaround is to send the `quit` command, wait until `isRunning` returns `false`, then send the `run`/`launch` command.

156 | 157 | 158 | -------------------------------------------------------------------------------- /doc-source/8 object-specifiers.md: -------------------------------------------------------------------------------- 1 | # Object specifiers 2 | 3 | ## How object specifiers work 4 | 5 | As explained in chapter 3, a property contains either a simple value describing an object attribute (`name`, `class`, `creationDate`, etc.) or an object specifier representing a one-to-one relationship between objects (e.g. `home`, `currentTrack`), while elements represent a one-to-many relationship between objects (`documents`, `folders`, `fileTracks`, etc). [TO DO: document class hierarchy as used in glues; note that PREFIXItem identifies a single property or element while PREFIXItems identifies zero or more elements, and summarize the selectors] 6 | 7 | [TO DO: note that all properties and elements appear as read-only properties on glue-defined ObjectSpecifier and RootSpecifier subclasses; users don't instantiate Specifier classes directly but instead construct via chained property/method calls from glue's Application class or untargeted `RootSpecifier` constants (PREFIXApp, PREFIXCon, PREFIXIts)] 8 | 9 | characters/words/paragraphs of documents by index/relative-position/range/filter 10 | 11 | [TO DO: list of supported reference forms, with links to sections below] 12 | 13 | [TO DO: following sections should include AppleScript syntax equivalents for reference] 14 | 15 | ## Reference forms 16 | 17 | ### Property 18 | 19 |
var PROPERTY: PREFIXItem {get}
20 | 21 | Examples: 22 | 23 |
textedit.version
 24 | textedit.documents[1].text
 25 | finder.home.files.name
26 | 27 | Syntax: 28 | 29 |
specifier.property
30 | 31 | ### All elements 32 | 33 |
var ELEMENTS: PREFIXItems {get}
34 | 35 | Examples: 36 | 37 |
finder.home.folders
 38 | textedit.documents
 39 | textedit.documents.paragraphs.words
40 | 41 | Syntax: 42 | 43 |
specifier.elements
44 | 45 | 46 | ### Element by index 47 | 48 |
subscript(index: Any) -> PREFIXItem
49 | 50 | Examples: 51 | 52 |
textedit.documents[1]
 53 | finder.home.folders[-2].files[1]
54 | 55 | Syntax: 56 | 57 |
elements[selector]
 58 | 
 59 |     selector : Int | Any -- the object's index (1-indexed), or other identifying value [1]
60 | 61 | [1] While element indexes are normally integers, some applications may also accept other types (e.g. Finder's file/folder/disk specifiers also accept alias values). The only exceptions are `String` and PREFIXSpecifier, which are used to construct by-name and by-test specifiers respectively. 62 | 63 |

Be aware that index-based object specifiers always use one-indexing (i.e. the first item is 1, the second is 2, etc.), not zero-indexing as in Swift (where the first item is 0, the second is 1, etc.).

64 | 65 | 66 | ### Element by name 67 | 68 |
subscript(index: String) -> PREFIXItem
 69 | func named(_ name: Any) -> PREFIXItem
70 | 71 | Examples: 72 | 73 |
textedit.documents["Untitled"]
 74 | finder.home.folders["Documents"].files["ReadMe.txt"]
75 | 76 | Specifies the first element with the given name. (The subscript syntax is preferred; the `named` method would only need used if a non-string value was required.) 77 | 78 | Syntax: 79 | 80 |
elements[selector]
 81 |         selector : String -- the object's name (as defined in its 'name' property)
82 | 83 |

Applications usually treat object names as case-insensitive. Where multiple element have the same name, a by-name specifier only identifies the first element found with that name. (To identify all elements with a particular name, use a by-test specifier instead.)

84 | 85 | [TO DO: update once a final decision is made on whether or not to include `named()` method] 86 | 87 | 88 | ### Element by ID 89 | 90 |
func ID(_ elementID: Any) -> PREFIXItem
91 | 92 | Examples: 93 | 94 |
textedit.windows.ID(4321)
95 | 96 | Syntax: 97 | 98 |
elements.ID(selector)
 99 |         selector : Any -- the object's id (as defined in its 'id' property)
100 | 101 | ### Element by absolute position 102 | 103 |
var first: PREFIXItem {get}
104 | var middle: PREFIXItem {get}
105 | var last: PREFIXItem {get}
106 | var any: PREFIXItem {get}
107 | 108 | Examples: 109 | 110 |
textedit.documents.first.text.paragraphs.last
111 | finder.desktop.files.any
112 | 113 | Syntax: 114 | 115 |
elements.first -- first element
116 | elements.middle -- middle element
117 | elements.last -- last element
118 | elements.any -- random element
119 | 120 | 121 | ### Element by relative position 122 | 123 |
func previous(_ elementClass: Symbol? = nil) -> PREFIXItem
124 | func next(_ elementClass: Symbol? = nil) -> PREFIXItem
125 | 126 | Examples: 127 | 128 |
textedit.documents[1].characters[3].next()
129 | textedit.documents[1].paragraphs[-1].previous(TED.word)
130 | 131 | Syntax: 132 | 133 |
// nearest element of a given class to appear before the specified element:
134 | element.previous(elementClass)
135 | 
136 | // nearest element of a given class to appear after the specified element
137 | element.next(elementClass)
138 | 
139 |         elementClass : Symbol -- the name of the previous/next element's class;
140 |                                  if omitted, the current element's class is used
141 | 142 | ### Elements by range 143 | 144 |
subscript(from: Any, to: Any) -> PREFIXItems
145 | 146 | Examples: 147 | 148 |
textedit.documents[1, 3]
149 | finder.home.folders["Documents", "Movies"]
150 | texeditplus.documents[1].text[TEPCon.characters[5], TEPCon.words[-2]]
151 | 152 | Caution: 153 | 154 | By-range specifiers must be constructed as elements[start,end], not elements[start...end], as Range<T> types are not supported. 155 | 156 | Syntax: 157 | 158 |
elements[start, end]
159 |         start : Int | String | PREFIXItem -- start of range
160 |         end : Int | String | PREFIXItem -- end of range
161 | 162 | Range references select all elements between and including two object specifiers indicating the start and end of the range. The start and end specifiers are normally declared relative to the container of the elements being selected. 163 | 164 | These sub-specifiers are constructed using the glue's PREFIXCon constant, e.g. `TEDCon`, as their root. For example, to indicate the third paragraph relative to the currrent container object: 165 | 166 | TEDCon.paragraphs[3] 167 | 168 | Thus, to specify all paragraphs from paragraph 3 to paragraph -1: 169 | 170 | paragraphs[TEDCon.paragraphs[3], TEDCon.paragraphs[-1]] 171 | 172 | For convenience, sub-specifiers can be written in shorthand form where their element class is the same as the elements being selected; thus the above can be written more concisely as: 173 | 174 | paragraphs[3, -1] 175 | 176 | Some applications can handle more complex range references. For example, the following will work in Tex-Edit Plus: 177 | 178 | words[TEPCon.characters[5], TEPCon.paragraphs[-2]] 179 | 180 | 181 | ### Elements by test 182 | 183 |
subscript(test: TestClause) -> PREFIXItems
184 | 185 | Examples: 186 | 187 |
textedit.documents[TEDIts.path == MissingValue]
188 | 
189 | finder.desktop.files[FINIts.nameExtension.isIn(["txt", "rtf"]) 
190 |                      && FINIts.modificationDate > (Date()-60*60*24*7)]
191 | 192 | Syntax: 193 | 194 | A specifier to each element that satisfies one or more conditions specified by a test specifier: 195 | 196 |
elements[selector]
197 |         selector : PREFIXSpecifier -- test specifier
198 | 199 | Test expressions consist of the following: 200 | 201 | * A test specifier relative to each element being tested. This specifier must be constructed using the glue's 'PREFIXIts' root, e.g. `TEDIts`. Its-based references support all valid reference forms, allowing you to construct references to its properties and elements. For example: 202 | 203 | TEDIts 204 | TEDIts.size 205 | TEDIts.words.first 206 | 207 | * One or more conditional/containment tests, implemented as operators/methods on the specifier being tested. The left-hand operand/receiver must be a PREFIXSpecifier instance. The right-hand operand/argument can be any value; its type is always `Any`. 208 | 209 | Syntax: 210 | 211 |
specifier < value
212 | specifier <= value
213 | specifier == value
214 | specifier != value
215 | specifier > value
216 | specifier >= value
217 | specifier.beginsWith(value)
218 | specifier.endsWith(value)
219 | specifier.contains(value)
220 | specifier.isIn(value)
221 | 222 | Examples: 223 | 224 | TEDIts == "" 225 | FINits.size > 1024 226 | TEDIts.words.first.beginsWith("A") 227 | TEDIts.characters.first == TEDIts.characters.last 228 | 229 | Caution: if assigning a test specifier to a variable, the variable must be explicitly typed to ensure the compiler uses the correct operator overload, e.g.: [TO DO: this sort of thing should be discouraged in practice; at most, it should be a footnote re. `==` overloading quirk] 230 | 231 | let test: TEDSpecifier = TEDIts.color == [0,0,0] 232 | let query = textedit.documents[1].words[test] 233 | 234 | * Zero or more logical tests, implemented as properties/methods on conditional tests. All operands must be conditional/containment and/or logic test specifiers. 235 | 236 | Syntax: 237 | 238 |
test && test
239 | test || test
240 | !test
241 | 242 | Examples: 243 | 244 | !(TEDIts.contains("?")) 245 | 246 | FINIts.size > 1024 && FINIts.size < 10240 247 | 248 | TEDIts.words[1].beginsWith("A") || TEDIts.words[1].contains("ce") || TEDIts.words[2] == "foo" 249 | 250 | 251 | ### Element insertion location 252 | 253 | Insertion locations can be specified at the beginning or end of all elements, or before or after a specified element or element range. 254 | 255 |
var beginning: PREFIXSpecifier
256 | var end: PREFIXSpecifier
257 | var before: PREFIXSpecifier
258 | var after: PREFIXSpecifier
259 | 260 | Examples: 261 | 262 |
textedit.documents.end
263 | textdit.documents[1].paragraphs[-1].before
264 | 265 | Syntax: 266 | 267 |
elements.beginning
268 | elements.end
269 | element.before
270 | element.after
271 | 272 | -------------------------------------------------------------------------------- /doc-source/application_architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-source/application_architecture.gif -------------------------------------------------------------------------------- /doc-source/application_architecture2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-source/application_architecture2.gif -------------------------------------------------------------------------------- /doc-source/client_app_to_itunes_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-source/client_app_to_itunes_event.gif -------------------------------------------------------------------------------- /doc-source/finder_to_textedit_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-source/finder_to_textedit_event.gif -------------------------------------------------------------------------------- /doc-source/full.css: -------------------------------------------------------------------------------- 1 | /* sticky footer code from www.cssstickyfooter.com */ 2 | 3 | html, body {height: 100%;} 4 | 5 | #wrap {min-height: 100%;} 6 | 7 | #main {overflow:auto; 8 | padding-bottom: 100px; /* must be same height as the footer */ 9 | } 10 | 11 | #footer { 12 | color:#00406e; background-color: #cedbef; 13 | position: relative; 14 | margin-top: -80px; /* negative value of footer height */ 15 | height: 80px; 16 | clear:both; 17 | } 18 | 19 | /*Opera Fix*/ 20 | body:before { 21 | content:""; 22 | height:100%; 23 | float:left; 24 | width:0; 25 | margin-top:-32767px;/ 26 | } 27 | 28 | 29 | /* general */ 30 | 31 | body { 32 | font-family:Arial,sans-serif; line-height:140%; 33 | color:#000; background-color:#fff; 34 | margin: 0 12% 0 8%; font-size: 0.9em; 35 | } 36 | 37 | /*headings*/ 38 | 39 | h1 { 40 | font-family:Arial,sans-serif; font-weight:bold; line-height:110%; 41 | color:#00457e; background-color:#cedbef; 42 | border-bottom:solid #92b4db 6px; 43 | padding:0.4em 0.6em 0.5em; margin:0; 44 | } 45 | 46 | 47 | h2, h3, h4 {font-family: Palatino, Georgia, Serif; color:#00457e; background-color:transparent; font-weight:bold;} 48 | 49 | h2 {font-size:1.6em; padding:0 0 1px; border-bottom:solid #f77700 2px; margin:1.5em 0 0.5em;} 50 | h3 {font-size:1.1em; padding:0 0 0px; border-bottom:dotted #f77700 1px; margin:0.5em 0 0.5em;} 51 | h4 {font-size:1.0em; margin:1.8em 0 0.6em 0;} 52 | 53 | 54 | /*body text*/ 55 | 56 | p {margin: 1em 0;} 57 | 58 | ul {padding-left: 0.5em; margin-left:0.5em;} 59 | 60 | var {color:#d55511;} 61 | 62 | dl, pre { 63 | line-height:130%; margin:2em 0em; 64 | } 65 | 66 | dt, code {color:#00457e; background-color:transparent;} 67 | dt {font-weight:bold;} 68 | dd {margin-left:0} 69 | 70 | .hilitebox {padding:0.5em 0 0.55em; margin:1.3em 2em 1.4em; border:solid #f77700; border-width: 2px 0 2px; } 71 | 72 | code {font-family:Courier,Monospace;} 73 | 74 | pre {color:#00406e; background-color:transparent;} 75 | 76 | 77 | dd pre, .hilitebox pre { 78 | color:#000; background-color:#cedbef; 79 | padding:1.8em 2em 2em; margin:1.2em 2em 1.2em 0; 80 | } 81 | 82 | 83 | dd+dt {margin-top:1em;} 84 | h3+p {margin-top:-0.3em;} 85 | h2+h3 {margin-top: 1.2em;} 86 | h4+p {margin-top:-0.35em;} 87 | 88 | 89 | .params {list-style-type: none;} 90 | 91 | .comment {color:#666; background-color:transparent;} 92 | 93 | hr {height: 1px; background-color: #00457e; border: 0px solid #00457e; margin-top:3em;} 94 | 95 | dl hr { 96 | color:#e8e8ff; background-color:transparent; height:1px; 97 | border:dashed #00457e; border-width: 1px 0 0; margin:0 -2em; 98 | } 99 | 100 | table { 101 | line-height:130%; width:100%; color:#00457e; background-color:#cedbef; 102 | border-bottom:solid #cedbef 10px; margin:1.2em 0em 2.4em; 103 | border-collapse:collapse; padding: 0 0 2em; 104 | } 105 | 106 | tr, th, td {padding: 0.4em 1.6em; margin: 0; border-width: 0;} 107 | 108 | th {text-align:left; font-size:0.95em; color:#00457e; background-color:#92b4db;} 109 | 110 | thead {background-color:#ccd;} 111 | 112 | .altrow {background-color:#e0e9f8;} 113 | 114 | .ruleindex tbody {font-size:90%;} 115 | 116 | 117 | /*links*/ 118 | 119 | /*a {font-style:italic;}*/ 120 | 121 | 122 | a:link {color:#00457e; background-color:transparent;} 123 | a:visited {color:#445555; background-color:transparent;} 124 | 125 | dt a:link, dt a:visited {text-decoration: none;} 126 | 127 | a img {border-width:0;} 128 | 129 | 130 | /* navigation*/ 131 | 132 | .navbar { 133 | font-size:0.9em; font-weight:bold; 134 | color:#e06000; background-color: white; 135 | padding: 0.3em 0 0.4em; margin: 0; 136 | } 137 | 138 | .navbar+h2 {margin-top: 1em} 139 | 140 | .navbar a:link, .navbar a:visited, .footer a:link, .footer a:visited {font-style:normal; text-decoration:none;} 141 | .navbar a:hover, .navbar a:active, .footer a:hover, .footer a:active {text-decoration:underline;} 142 | 143 | /*footer*/ 144 | 145 | .footer { 146 | font-size:0.9em; font-weight:bold; 147 | color:#00406e; background-color: #cedbef; 148 | border-top:solid #92b4db 6px; 149 | padding: 0.3em 13px 0.5em; margin: 0; 150 | } 151 | 152 | .ruledoc {font-size:95%; margin:2em; padding: 0 2em 1em; border:solid black 1px;} 153 | 154 | .ruledoc-excerpt {font-size:95%; margin:2em; padding: 1em 2em; border:solid black 1px;} 155 | 156 | .syntax-error {color:#fff;background-color:#c00} 157 | -------------------------------------------------------------------------------- /doc-source/relationships_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/swiftae/e41974d616602e45c5e88f19b1b38bbeef13a781/doc-source/relationships_example.gif -------------------------------------------------------------------------------- /mkdoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Generate HTML user guide (doc-html) from Markdown files (doc-source). 5 | 6 | Requires Markdown2: 7 | 8 | sudo easy_install markdown2 9 | 10 | Notes: 11 | 12 | * Use 2-space indent for
...
blocks and paragraphs within an
  • ...
  • block. 13 | 14 | * Use 2-space indent for code lines within a code block statement. Use 4-space indent for soft-wrapped code lines. 15 | 16 | """ 17 | 18 | import os, os.path, re, shutil, sys 19 | from markdown2 import markdown 20 | 21 | kPageTemplate = u'''\ 22 | 23 | 24 | 25 | {title} 26 | 27 | 28 | 29 | 30 |
    31 |
    32 |

    {heading}

    33 | 34 | {content} 35 |
    36 |
    37 | 38 | 39 | 40 | ''' 41 | kNavlink = u'{}' 42 | 43 | def esc(s): 44 | return s.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"') 45 | 46 | def render_toc(pages): # (url, title, content) 47 | navbar = kNavlink.format(pages[0][1], u'next') 48 | toc = u'

    Table of contents

    \n
      \n{}
    '.format( 49 | u'\n'.join(u'\t
  • {}
  • \n'.format(p[1], esc(p[2])) for p in pages)) 50 | return kPageTemplate.format(title=u'SwiftAutomation', heading=u'SwiftAutomation', navbar=navbar, content=toc) 51 | 52 | def render_page(links, title, content): 53 | return kPageTemplate.format(title=u'SwiftAutomation | '+esc(title), heading=esc(title), 54 | navbar=u' | '.join(kNavlink.format(url, esc(title)) for url, title in links), 55 | content=markdown(content.strip(), tab_width=2) ) 56 | 57 | def write(destdir, name, html): 58 | with open(os.path.join(destdir, name), 'w') as f: 59 | f.write(html.encode('utf-8')) 60 | 61 | 62 | def render(sourcedir, destdir): 63 | if not os.path.exists(destdir): 64 | os.mkdir(destdir) 65 | pages = [] 66 | for name in os.listdir(sourcedir): 67 | inpath = os.path.join(sourcedir, name) 68 | if name.endswith('.md'): 69 | idx, newname = name.split(' ', 1) 70 | outpath = os.path.join(destdir, ) 71 | with open(inpath) as f: 72 | title, content = f.read().decode('utf-8').strip().split('\n', 1) 73 | pages.append((int(idx), newname.rstrip('md')+'html', title.strip('# '), content.lstrip())) 74 | else: # copy other files directly 75 | shutil.copyfile(inpath, os.path.join(destdir, name)) 76 | pages.sort() 77 | write(destdir, 'index.html', render_toc(pages)) 78 | for i, (idx, newname, title, content) in enumerate(pages): 79 | if i == 0: 80 | next = pages[i+1] 81 | links = [('index.html', 'index'), (next[1], 'next')] 82 | elif i == len(pages)-1: 83 | prev = pages[i-1] 84 | links = [(prev[1], 'back'), ('index.html', 'index')] 85 | else: 86 | prev = pages[i-1] 87 | next = pages[i+1] 88 | links = [(prev[1], 'back'), ('index.html', 'index'), (next[1], 'next')] 89 | write(destdir, newname, render_page(links, title, content)) 90 | 91 | 92 | 93 | 94 | if __name__ == '__main__': 95 | if len(sys.argv) != 3: 96 | src, dest = 'doc-source', 'doc-generated' 97 | else: 98 | src, dest = sys.argv[1:] 99 | sourcedir = os.path.abspath(os.path.expanduser(src.decode('utf-8'))) 100 | destdir = os.path.abspath(os.path.expanduser(dest.decode('utf-8'))) 101 | render(sourcedir, destdir) 102 | 103 | 104 | -------------------------------------------------------------------------------- /test/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // SwiftAutomation 4 | // 5 | // tests // TO DO: move ad-hoc tests to separate target; replace this with examples of use 6 | // 7 | // TO DO: test `all` selector works correctly (should convert property specifier to all-elements specifier, return all-elements specifier as-is, and report send-time error if called on anything else) 8 | // 9 | 10 | import AppKit 11 | import Foundation 12 | import SwiftAutomation 13 | 14 | 15 | let te = TextEdit() 16 | 17 | /* 18 | do { 19 | // simple pack/unpack data test 20 | 21 | let c = te.appData 22 | do { 23 | let seq = [1, 2, 3] 24 | let desc = try c.pack(seq) 25 | print(try c.unpack(desc, returnType: [String].self)) 26 | } 27 | /* 28 | do { 29 | let seq = [AE.id:0, AE.point:4, AE.version:9] 30 | let desc = try c.pack(seq) 31 | print(try c.unpack(desc, returnType: [Symbol:Int].self)) 32 | }*/ 33 | 34 | } catch {print("ERROR: \(error)")} 35 | 36 | print("\n\n") 37 | */ 38 | 39 | do { 40 | do { 41 | let v = try te.appData.pack(true) 42 | print(v, try te.appData.unpack(v)) 43 | } 44 | do { 45 | let v = try te.appData.pack(NSNumber(value: true)) 46 | print(v,try te.appData.unpack(v)) 47 | } 48 | do { 49 | let v = try te.appData.pack(-25) 50 | print(v, try te.appData.unpack(v)) 51 | } 52 | do { 53 | let v = try te.appData.pack(NSNumber(value: -25)) 54 | print(v,try te.appData.unpack(v)) 55 | } 56 | do { 57 | let v = try te.appData.pack(4.12) 58 | print(v, try te.appData.unpack(v)) 59 | } 60 | do { 61 | let v = try te.appData.pack(NSNumber(value: 4.12)) 62 | print(v,try te.appData.unpack(v)) 63 | } 64 | 65 | 66 | let itunes = ITunes() 67 | let state: ITU = try itunes.playerState.get() 68 | print("itunes.playerState.get() -> \(state)") 69 | // try ITunes().play() 70 | 71 | 72 | // print("// Specifier.description: \(TEDApp.documents[1].text)") 73 | // print("// Specifier.description: \(te.documents[1].text)") 74 | 75 | 76 | // send `open` and `get` AEs using raw four-char codes 77 | //let result = try te.sendAppleEvent(kCoreEventClass, kAEOpenDocuments, [keyDirectObject:NSURL.fileURLWithPath("/Users/has/todos.txt")]) 78 | //print(result) 79 | 80 | 81 | print("TEST: make new document with properties {text: \"Hello World!\"}") 82 | let teDoc: TEDItem = try te.make(new: TED.document, withProperties: [TED.text: "Hello World!"]) 83 | print("=> \(teDoc)") 84 | print("TEST: get text of teDoc") 85 | print(try teDoc.text.get()) 86 | 87 | /* 88 | /* 89 | 90 | // get name of document 1 91 | 92 | // - using four-char code strings 93 | let result2 = try te.sendAppleEvent("core", "getd", ["----": te.elements("docu")[1].property("pnam")]) 94 | print(result2) 95 | 96 | // - using glue-defined terminology 97 | let result3 = try te.get(TEDApp.documents[1].name) 98 | print(result3) 99 | 100 | let result3a: Any = try te.documents[1].name.get() // convenience syntax for the above 101 | print(result3a) 102 | 103 | 104 | // get name of document 1 -- this works, returning a string value 105 | print("\nTEST: TextEdit().documents[1].name.get() as String") 106 | let result5: String = try te.documents[1].name.get() 107 | print("=> \(result5)") 108 | 109 | // get name of every document 110 | 111 | print("\nTEST: TextEdit().documents.name.get() as Any") 112 | let result4b = try te.documents.name.get() as Any 113 | print("=> \(result4b)") 114 | 115 | // get name of every document 116 | print("\nTEST: TextEdit().documents.name.get() as [String]") 117 | let result4 = try te.documents.name.get() as [String] // unpack the `get` command's result as Array of String 118 | print("=> \(result4)") 119 | 120 | // same as above 121 | let result6: [String] = try te.documents.name.get() 122 | print("=> \(result6)") 123 | 124 | 125 | // get every file of folder "Documents" of home whose name extension is "txt" and modification date > date "01:30 Jan 1, 2001 UTC" 126 | let date = Date(timeIntervalSinceReferenceDate:5400) // 1:30am on 1 Jan 2001 (UTC) 127 | print("\nTEST: Finder().home.folders[\"Documents\"].files[FINIts.nameExtension == \"txt\" && FINIts.modificationDate > DATE].name.get()") 128 | let q = Finder().home.folders["Documents"].files[FINIts.nameExtension == "txt" && FINIts.modificationDate > date].name 129 | print("// \(q)") 130 | let result4a = try q.get() 131 | print("=> \(result4a)") 132 | */ 133 | 134 | print("\nTEST: Finder().home.folders[\"Documents\"].files[FINIts.nameExtension == \"txt\"].properties.get()") 135 | let result4c = try Finder().home.folders["Documents"].files[FINIts.nameExtension == "txt"].properties.get() as [[FINSymbol:Any]] 136 | print("=> \(result4c)") 137 | 138 | print("\nTEST: duplicate file 1 of home to desktop with replacing") 139 | let myresult = try Finder().duplicate(Finder().home.files[1], to:Finder().desktop, replacing:true) 140 | print("=> \(myresult)") 141 | 142 | 143 | print("\nTEST: TextEdit().documents.properties.get() as [TEDDocumentRecord]") 144 | let result4d = try TextEdit().documents.properties.get() as [TEDDocumentRecord] 145 | print("=> \(result4d)") 146 | 147 | print("\nTEST: TextEdit().documents[1].properties.get() as TEDDocumentRecord") 148 | let result4e = try TextEdit().documents[1].properties.get() as TEDDocumentRecord 149 | print("=> \(result4e)") 150 | 151 | // try te.documents.close(saving: TED.no) // close every document saving no 152 | 153 | //struct X {}; try teDoc.text.set(to: X()) 154 | 155 | // 156 | try teDoc.close(saving: TED.no) 157 | 158 | 159 | print("\nTEST: get files 1 thru 3 of home") 160 | let r = try Finder().home.files[1, 3].get() as [FINItem] 161 | print("=> \(r)") 162 | 163 | */ 164 | } catch { 165 | print("ERROR: \(error)") 166 | } 167 | 168 | 169 | 170 | /* 171 | 172 | 173 | // test Swift<->AE type conversions 174 | let c = AEApplication.currentApplication().appData 175 | 176 | //let lst = try c.pack([1,2,3]) 177 | //print(try c.unpack(lst)) 178 | 179 | do { 180 | /* 181 | //print(try c.pack("hello")) 182 | print("PACK AS BOOLEAN") 183 | 184 | print("\(try c.pack(true)) \(formatFourCharCodeString(try c.pack(true).descriptorType))") 185 | print("\(try c.pack(NSNumber(bool: true))) \(formatFourCharCodeString(try c.pack(NSNumber(bool: true)).descriptorType))") 186 | print("") 187 | print("PACK AS INTEGER") 188 | print("\(try c.pack(3)) \(formatFourCharCodeString(try c.pack(3).descriptorType))") 189 | print("\(try c.pack(NSNumber(int: 3))) \(formatFourCharCodeString(try c.pack(NSNumber(int: 3)).descriptorType))") 190 | print("") 191 | print("PACK AS DOUBLE") 192 | print("\(try c.pack(3.1)) \(formatFourCharCodeString(try c.pack(3.1).descriptorType))") 193 | print("\(try c.pack(NSNumber(double: 3.1))) \(formatFourCharCodeString(try c.pack(NSNumber(double: 3.1)).descriptorType))") 194 | */ 195 | 196 | do { 197 | let seq = [1, 2, 3] 198 | let desc = try c.pack(seq) 199 | print(try c.unpack(desc, returnType: [Int].self)) 200 | } 201 | do { 202 | let seq = [AE.id:0, AE.point:4, AE.version:9] 203 | let desc = try c.pack(seq) 204 | print(try c.unpack(desc, returnType: [AESymbol:Int].self)) 205 | } 206 | 207 | } catch {print("ERROR: \(error)")} 208 | 209 | 210 | //NSLog("%08X", try c.pack(true).descriptorType) // 0x626F6F6C = typeBoolean 211 | 212 | //print(c) 213 | 214 | 215 | */ 216 | 217 | 218 | 219 | 220 | /* 221 | let finder = AEApplication(name: "Finder") 222 | 223 | 224 | do { 225 | let result = try finder.sendAppleEvent(kAECoreSuite, kAEGetData, 226 | [keyDirectObject:finder.property("home").elements("cobj")]) 227 | print("\n\nRESULT1: \(result)") 228 | } catch { 229 | print("\n\nERROR1: \(error)") 230 | } 231 | let f = URL(fileURLWithPath:"/Users/Shared") 232 | 233 | do { 234 | let result = try finder.sendAppleEvent(kAECoreSuite, kAEGetData, [keyDirectObject: AEApp.elements(cObject)[f]]) 235 | print("RAW: \(result)") 236 | } catch { 237 | print("\n\nERROR: \(error)") 238 | } 239 | do { 240 | let result = try finder.sendAppleEvent(kAECoreSuite, kAEGetData, [keyDirectObject: AEApp.elements(cFile)[f]]) // Finder will throw error as f is a folder, not a file 241 | print("RAW: \(result)") 242 | } catch { 243 | print("\n\nERROR: \(error)") 244 | } 245 | */ 246 | 247 | /* 248 | do { 249 | let result: AESpecifier = try finder.sendAppleEvent(kAECoreSuite, kAEGetData, 250 | [keyDirectObject:finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 251 | print("\n\nRESULT1: \(result)") 252 | } catch { 253 | print("\n\nERROR1: \(error)") 254 | } 255 | do { 256 | let result = try finder.sendAppleEvent(kAECoreSuite, kAEGetData, 257 | [keyDirectObject:finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 258 | print("\n\nRESULT2: \(result)") 259 | } catch { 260 | print("\n\nERROR2: \(error)") 261 | } 262 | 263 | 264 | do { 265 | let result: AESpecifier = try finder.sendAppleEvent("core", "getd", 266 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 267 | print("\n\nRESULT3: \(result)") 268 | } catch { 269 | print("\n\nERROR3: \(error)") 270 | } 271 | do { 272 | let result = try finder.sendAppleEvent("core", "getd", 273 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 274 | print("\n\nRESULT4: \(result)") 275 | } catch { 276 | print("\n\nERROR4: \(error)") 277 | } 278 | */ 279 | 280 | /* 281 | 282 | do { 283 | let result: AESpecifier! = try finder.sendAppleEvent("core", "getd", // Can't unpack value as ImplicitlyUnwrappedOptional 284 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 285 | print("\n\nRESULT5: \(result)") 286 | } catch { 287 | print("\n\nERROR5: \(error)") 288 | } 289 | 290 | 291 | do { 292 | let result: AESpecifier? = try finder.sendAppleEvent("core", "getd", // Can't unpack value as Optional 293 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 294 | print("\n\nRESULT6: \(result)") 295 | } catch { 296 | print("\n\nERROR6: \(error)") 297 | } 298 | 299 | */ 300 | 301 | print() 302 | 303 | 304 | --------------------------------------------------------------------------------