├── 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 ├── SwiftAutomation.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── has.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── libSwiftAutomation.xcscheme └── xcuserdata │ └── has.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── Frameworks Release.xcscheme │ ├── MacOSGlues.xcscheme │ ├── Release.xcscheme │ ├── SwiftAutomation.xcscheme │ ├── SwiftAutomationLite.xcscheme │ ├── aeglue.xcscheme │ ├── test.xcscheme │ └── xcschememanagement.plist ├── SwiftAutomation ├── AEApplicationGlue.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 ├── SwiftAutomationLite ├── Info.plist └── SwiftAutomationLite.h ├── TODO.txt ├── aeglue ├── aeglue.entitlements └── 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 └── 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://github.com/hhas/SwiftAutomation.git 32 | 33 | A basic Swift "script editor" (currently under development) is also available: 34 | 35 | https://github.com/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 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/project.xcworkspace/xcuserdata/has.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/project.xcworkspace/xcuserdata/has.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/SwiftAutomation.xcodeproj/project.xcworkspace/xcuserdata/has.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcshareddata/xcschemes/libSwiftAutomation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/Frameworks Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/MacOSGlues.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/SwiftAutomation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/SwiftAutomationLite.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/aeglue.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/test.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /SwiftAutomation.xcodeproj/xcuserdata/has.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Frameworks Release.xcscheme 8 | 9 | orderHint 10 | 15 11 | 12 | Frameworks Release.xcscheme_^#shared#^_ 13 | 14 | isShown 15 | 16 | orderHint 17 | 9 18 | 19 | MacOSGlues.xcscheme 20 | 21 | orderHint 22 | 16 23 | 24 | MacOSGlues.xcscheme_^#shared#^_ 25 | 26 | isShown 27 | 28 | orderHint 29 | 8 30 | 31 | Release.xcscheme 32 | 33 | isShown 34 | 35 | orderHint 36 | 5 37 | 38 | SwiftAutomation.xcscheme 39 | 40 | orderHint 41 | 17 42 | 43 | SwiftAutomation.xcscheme_^#shared#^_ 44 | 45 | orderHint 46 | 4 47 | 48 | SwiftAutomationLite.xcscheme 49 | 50 | orderHint 51 | 18 52 | 53 | SwiftAutomationLite.xcscheme_^#shared#^_ 54 | 55 | isShown 56 | 57 | orderHint 58 | 11 59 | 60 | aeglue.xcscheme 61 | 62 | orderHint 63 | 14 64 | 65 | aeglue.xcscheme_^#shared#^_ 66 | 67 | isShown 68 | 69 | orderHint 70 | 6 71 | 72 | libSwiftAutomation.xcscheme_^#shared#^_ 73 | 74 | isShown 75 | 76 | orderHint 77 | 4 78 | 79 | test.xcscheme 80 | 81 | orderHint 82 | 19 83 | 84 | test.xcscheme_^#shared#^_ 85 | 86 | isShown 87 | 88 | orderHint 89 | 7 90 | 91 | 92 | SuppressBuildableAutocreation 93 | 94 | 4F0E39571BAA97D400FF2A96 95 | 96 | primary 97 | 98 | 99 | 4F0E39641BAA98A900FF2A96 100 | 101 | primary 102 | 103 | 104 | 4F0E399B1BAF7A7400FF2A96 105 | 106 | primary 107 | 108 | 109 | 4F1A57F91D9E81C300ACFA51 110 | 111 | primary 112 | 113 | 114 | 4F21AF3C22861DA400A1B978 115 | 116 | primary 117 | 118 | 119 | 4F63C4231B7CF06E000D74EE 120 | 121 | primary 122 | 123 | 124 | 4F9AFB351D5B58EB00A71890 125 | 126 | primary 127 | 128 | 129 | 4FACE27122723090000A04A0 130 | 131 | primary 132 | 133 | 134 | 4FFF5D231D899F0D008ED4F5 135 | 136 | primary 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /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 // TO DO: identify these and separate into legacy section which can be macro-d out 12 | // 13 | 14 | #if canImport(Carbon) 15 | import Carbon 16 | #endif 17 | 18 | import Foundation 19 | import AppleEvents 20 | 21 | // TO DO: check for any missing terms (e.g. ctxt) 22 | 23 | 24 | public class DefaultTerminology: ApplicationTerminology { 25 | 26 | // 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 27 | 28 | public let types: [KeywordTerm] 29 | public let enumerators: [KeywordTerm] 30 | public let properties: [KeywordTerm] 31 | public let elements: [ClassTerm] 32 | public let commands: [CommandTerm] 33 | 34 | public init(keywordConverter: KeywordConverterProtocol) { 35 | self.types = self._types.map{ KeywordTerm(name: keywordConverter.convertSpecifierName($0), code: $1) } 36 | self.enumerators = self._enumerators.map{ KeywordTerm(name: keywordConverter.convertSpecifierName($0), code: $1) } 37 | self.properties = self._properties.map{ KeywordTerm(name: keywordConverter.convertSpecifierName($0), code: $1) } 38 | self.elements = self._elements.map{ ClassTerm(singular: keywordConverter.convertSpecifierName($0), 39 | plural: keywordConverter.convertSpecifierName($1), code: $2) } 40 | self.commands = self._commands.map({ 41 | return CommandTerm(name: keywordConverter.convertSpecifierName($0), event: $1, 42 | parameters: $2.map{ KeywordTerm(name: keywordConverter.convertParameterName($0), code: $1) }) 43 | }) 44 | } 45 | 46 | private typealias Keywords = [(String, OSType)] 47 | private typealias Elements = [(String, String, OSType)] 48 | private typealias Commands = [(String, EventIdentifier, [(String, OSType)])] 49 | 50 | // note: AppleScript-style keyword names are automatically converted to the required format using the given keyword converter 51 | 52 | // TO DO: review list against current AppleScript AEUT 53 | 54 | private let _types: Keywords = [("anything", typeWildCard), 55 | ("boolean", typeBoolean), 56 | ("short integer", typeSInt16), 57 | ("integer", typeSInt32), 58 | ("double integer", typeSInt64), 59 | ("unsigned short integer", typeUInt16), // no AS keyword 60 | ("unsigned integer", typeUInt32), 61 | ("unsigned double integer", typeUInt64), // no AS keyword 62 | ("fixed", typeFixed), 63 | ("long fixed", typeLongFixed), 64 | ("decimal struct", typeDecimalStruct), // no AS keyword 65 | ("small real", typeIEEE32BitFloatingPoint), 66 | ("real", typeIEEE64BitFloatingPoint), 67 | //("extended real", typeExtended), 68 | ("large real", type128BitFloatingPoint), // no AS keyword 69 | ("string", typeText), 70 | ("styled text", typeStyledText), 71 | ("text style info", typeTextStyles), 72 | ("styled clipboard text", typeScrapStyles), 73 | ("encoded string", typeEncodedString), 74 | ("writing code", pScriptTag), 75 | ("international writing code", typeIntlWritingCode), 76 | ("international text", typeIntlText), 77 | ("Unicode text", typeUnicodeText), 78 | ("UTF8 text", typeUTF8Text), // no AS keyword 79 | ("UTF16 text", typeUTF16ExternalRepresentation), // no AS keyword 80 | ("version", typeVersion), 81 | ("date", typeLongDateTime), 82 | ("list", typeAEList), 83 | ("record", typeAERecord), 84 | ("data", typeData), 85 | ("script", typeScript), 86 | ("location reference", typeInsertionLoc), 87 | ("reference", typeObjectSpecifier), 88 | ("alias", typeAlias), 89 | ("file ref", typeFSRef), // no AS keyword 90 | //("file specification", typeFSS), 91 | ("bookmark data", typeBookmarkData), // no AS keyword 92 | ("file URL", typeFileURL), // no AS keyword 93 | ("point", typeQDPoint), 94 | ("bounding rectangle", typeQDRectangle), 95 | ("fixed point", typeFixedPoint), 96 | ("fixed rectangle", typeFixedRectangle), 97 | ("long point", typeLongPoint), 98 | ("long rectangle", typeLongRectangle), 99 | ("long fixed point", typeLongFixedPoint), 100 | ("long fixed rectangle", typeLongFixedRectangle), 101 | ("EPS picture", typeEPS), 102 | ("GIF picture", typeGIF), 103 | ("JPEG picture", typeJPEG), 104 | ("PICT picture", typePict), 105 | ("TIFF picture", typeTIFF), 106 | ("RGB color", typeRGBColor), 107 | ("RGB16 color", typeRGB16), 108 | ("RGB96 color", typeRGB96), 109 | ("graphic text", typeGraphicText), 110 | ("color table", typeColorTable), 111 | ("pixel map record", typePixMapMinus), 112 | ("best", typeBest), 113 | ("type class", typeType), 114 | ("constant", typeEnumeration), 115 | ("property", typeProperty), 116 | ("mach port", typeMachPort), // no AS keyword 117 | ("kernel process ID", typeKernelProcessID), // no AS keyword 118 | ("application bundle ID", typeApplicationBundleID), // no AS keyword 119 | ("process serial number", typeProcessSerialNumber), // no AS keyword 120 | ("application signature", typeApplSignature), // no AS keyword 121 | ("application URL", typeApplicationURL), // no AS keyword 122 | // ("missing value", cMissingValue), // represented as MissingValue constant, not Symbol instance 123 | ("null", typeNull), 124 | ("machine location", typeMachineLoc), 125 | ("machine", OSType(cMachine)), // OpenScripting/ASRegistry.h 126 | ("dash style", typeDashStyle), 127 | ("rotation", typeRotation), 128 | ("item", cObject), 129 | // more OpenScripting terms 130 | ("January", OSType(cJanuary)), 131 | ("February", OSType(cFebruary)), 132 | ("March", OSType(cMarch)), 133 | ("April", OSType(cApril)), 134 | ("May", OSType(cMay)), 135 | ("June", OSType(cJune)), 136 | ("July", OSType(cJuly)), 137 | ("August", OSType(cAugust)), 138 | ("September", OSType(cSeptember)), 139 | ("October", OSType(cOctober)), 140 | ("November", OSType(cNovember)), 141 | ("December", OSType(cDecember)), 142 | ("Sunday", OSType(cSunday)), 143 | ("Monday", OSType(cMonday)), 144 | ("Tuesday", OSType(cTuesday)), 145 | ("Wednesday", OSType(cWednesday)), 146 | ("Thursday", OSType(cThursday)), 147 | ("Friday", OSType(cFriday)), 148 | ("Saturday", OSType(cSaturday)), 149 | 150 | ] 151 | private let _enumerators: Keywords = [("yes", kAEYes), 152 | ("no", kAENo), 153 | ("ask", kAEAsk), 154 | ("case", OSType(kAECase)), 155 | ("diacriticals", OSType(kAEDiacritic)), 156 | ("expansion", OSType(kAEExpansion)), 157 | ("hyphens", OSType(kAEHyphens)), 158 | ("punctuation", OSType(kAEPunctuation)), 159 | ("whitespace", OSType(kAEWhiteSpace)), 160 | ("numeric strings", OSType(kASNumericStrings)), 161 | ] 162 | private let _properties: Keywords = [("class", pClass), 163 | ("id", pID), 164 | ("properties", _pALL), 165 | ] 166 | private let _elements: Elements = [("item", "items", cObject), 167 | // what about ("text",cText)? 168 | ] 169 | private let _commands: Commands = [("run", eventOpenApplication, []), 170 | ("open", eventOpenDocuments, []), 171 | ("print", eventPrintDocuments, []), 172 | ("quit", eventQuitApplication, [("saving", keyAESaveOptions)]), 173 | ("reopen", eventReopenApplication, []), 174 | //("launch", _kASAppleScriptSuite, _kASLaunchEvent, []), // this is a hardcoded method 175 | ("activate", miscEventActivate, []), 176 | ("open location", miscEventGetURL, [("window", _WIND)]), 177 | ("get", coreEventGetData, []), 178 | ("set", coreEventSetData, [("to", keyAEData)]), 179 | ] 180 | 181 | private static let _WIND = try! parseFourCharCode("WIND") 182 | private static let _pALL = try! parseFourCharCode("pALL") 183 | } 184 | 185 | -------------------------------------------------------------------------------- /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 | // a SAX parser would be faster, but SDEFs use XInclude which only [NS]XMLDocument supports as standard 7 | // 8 | // (long-term solution: replace SDEF format with a proper IDL that provides both fast machine-readable API description and slower user-readable [HTML/Markdown/whatever] documentation, in turn generating API description from API implementation and, as much as possible, double-dutying user-readable code examples as automatic unit tests as well) 9 | // 10 | 11 | // TO DO: what about synonym, xref? 12 | 13 | // note: GlueTable will resolve any conflicts between built-in and app-defined name+code definitions 14 | 15 | // TO DO: throw instead of returning SOAP SDEF when malformed/non-existent file URL is given, e.g. `URL(string:"file://Applications/TextEdit.app")` instead of `URL(string:"file:///Applications/TextEdit.app")` 16 | 17 | 18 | import Foundation 19 | import AppleEvents 20 | 21 | #if canImport(Carbon) 22 | import Carbon // OSACopyScriptingDefinitionFromURL 23 | #endif 24 | 25 | 26 | 27 | public class SDEFParser: ApplicationTerminology { 28 | 29 | // SDEF names and codes are parsed into the following tables 30 | public private(set) var types: [KeywordTerm] = [] 31 | public private(set) var enumerators: [KeywordTerm] = [] 32 | public private(set) var properties: [KeywordTerm] = [] 33 | public private(set) var elements: [ClassTerm] = [] 34 | public var commands: [CommandTerm] { return Array(self.commandsDict.values) } 35 | 36 | private var commandsDict = [String:CommandTerm]() 37 | private let keywordConverter: KeywordConverterProtocol 38 | private let errorHandler: (Error)->() 39 | 40 | public init(keywordConverter: KeywordConverterProtocol = defaultSwiftKeywordConverter, 41 | errorHandler: @escaping (Error)->()) { 42 | self.keywordConverter = keywordConverter 43 | self.errorHandler = errorHandler // TO DO: currently unused (currently parse methods always throw); also, errorHandler should probably be throwable 44 | } 45 | 46 | // parse an OSType given as 4/8-character "MacRoman" string, or 10/18-character hex string 47 | 48 | func parse(fourCharCode string: String) throws -> OSType { // class, property, enum, param, etc. code 49 | if string.count == 10 && (string.hasPrefix("0x") || string.hasPrefix("0X")) { // e.g. "0x00000001" 50 | guard let result = UInt32(string.dropFirst(2), radix: 16) else { 51 | throw AutomationError(code: 1, message: "Invalid four-char code (bad representation): \(string.debugDescription)") 52 | } 53 | return result 54 | } else { 55 | return try parseFourCharCode(string as String) 56 | } 57 | } 58 | 59 | func parse(appleEventCode string: String) throws -> EventIdentifier { 60 | if string.count == 8 { 61 | return try parseEightCharCode(string) 62 | } else if string.count == 18 && (string.hasPrefix("0x") || string.hasPrefix("0X")) { // e.g. "0x0123456789ABCDEF" // caution: this does not allow for underscores within literal number 63 | guard let event = UInt64(string.dropFirst(2), radix: 16) else { 64 | throw AutomationError(code: 1, message: "Invalid eight-char code (bad representation): \(string.debugDescription)") 65 | } 66 | return event 67 | } else { 68 | throw AutomationError(code: 1, message: "Invalid eight-char code (wrong length): \((string as String).debugDescription)") 69 | } 70 | } 71 | 72 | // extract name and code attributes from a class/enumerator/command/etc XML element 73 | 74 | private func attribute(_ name: String, of element: XMLElement) -> String? { 75 | return element.attribute(forName: name)?.stringValue 76 | } 77 | 78 | private func parse(keywordElement element: XMLElement) throws -> (String, OSType) { 79 | guard let name = self.attribute("name", of: element), let codeString = self.attribute("code", of: element), name != "" else { 80 | throw TerminologyError("Missing 'name'/'code' attribute.") 81 | } 82 | return (name, try self.parse(fourCharCode: codeString)) 83 | } 84 | 85 | private func parse(commandElement element: XMLElement) throws -> (String, EventIdentifier) { 86 | guard let name = self.attribute("name", of: element), let codeString = self.attribute("code", of: element), name != "" else { 87 | throw TerminologyError("Missing 'name'/'code' attribute.") 88 | } 89 | return (name, try self.parse(appleEventCode: codeString)) 90 | } 91 | 92 | // 93 | 94 | private func parse(typeOfElement element: XMLElement) throws -> (String, OSType) { // class, record-type, value-type 95 | let (name, code) = try self.parse(keywordElement: element) 96 | self.types.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), code: code)) 97 | return (name, code) 98 | } 99 | 100 | private func parse(propertiesOfElement element: XMLElement) throws { // class, class-extension, record-value 101 | for element in element.elements(forName: "property") { 102 | let (name, code) = try self.parse(keywordElement: element) 103 | self.properties.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), code: code)) 104 | } 105 | } 106 | 107 | // parse a class/enumerator/command/etc element of a dictionary suite 108 | 109 | func parse(definition node: XMLNode) throws { 110 | if let element = node as? XMLElement, let tagName = element.name { 111 | switch tagName { 112 | case "class": 113 | let (name, code) = try self.parse(typeOfElement: element) 114 | try self.parse(propertiesOfElement: element) 115 | // use plural class name as elements name (if not given, append "s" to singular name) 116 | // (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) 117 | let plural = element.attribute(forName: "plural")?.stringValue ?? ( 118 | (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) 119 | self.elements.append(ClassTerm(singular: self.keywordConverter.convertSpecifierName(name), 120 | plural: self.keywordConverter.convertSpecifierName(plural), code: code)) 121 | case "class-extension": 122 | try self.parse(propertiesOfElement: element) 123 | case "record-type": 124 | let _ = try self.parse(typeOfElement: element) 125 | try self.parse(propertiesOfElement: element) 126 | case "value-type": 127 | let _ = try self.parse(typeOfElement: element) 128 | case "enumeration": 129 | for element in element.elements(forName: "enumerator") { 130 | let (name, code) = try self.parse(keywordElement: element) 131 | self.enumerators.append(KeywordTerm(name: self.keywordConverter.convertSpecifierName(name), code: code)) 132 | } 133 | case "command", "event": 134 | let (name, eventIdentifier) = try self.parse(commandElement: element) 135 | // Note: overlapping command definitions (e.g. 'path to') should be processed as follows: 136 | // - If their names and codes are the same, only the last definition is used; other definitions are ignored 137 | // and will not compile. 138 | // - If their names are the same but their codes are different, only the first definition is used; other 139 | // definitions are ignored and will not compile. 140 | let previousDef = self.commandsDict[name] 141 | if previousDef == nil || (previousDef!.event == eventIdentifier) { 142 | var parameters = [KeywordTerm]() 143 | for element in element.elements(forName: "parameter") { 144 | let (paramName, paramCode) = try self.parse(keywordElement: element) 145 | parameters.append(KeywordTerm(name: self.keywordConverter.convertParameterName(paramName), code: paramCode)) 146 | } 147 | self.commandsDict[name] = CommandTerm(name: self.keywordConverter.convertSpecifierName(name), 148 | event: eventIdentifier, parameters: parameters) 149 | } // else ignore duplicate declaration 150 | default: () 151 | } 152 | } 153 | } 154 | 155 | // parse the given SDEF XML data 156 | 157 | public func parse(_ sdef: Data) throws { 158 | do { 159 | let parser = try XMLDocument(data: sdef, options: XMLNode.Options.documentXInclude) 160 | guard let dictionary = parser.rootElement() else { throw TerminologyError("Missing `dictionary` element.") } 161 | for suite in dictionary.elements(forName: "suite") { 162 | if let nodes = suite.children { 163 | for node in nodes { try self.parse(definition: node) } 164 | } 165 | } 166 | } catch { 167 | throw TerminologyError("An error occurred while parsing SDEF. \(error)") 168 | } 169 | } 170 | } 171 | 172 | 173 | // convenience function (macOS only) 174 | 175 | // TO DO: what about getting via AE? 176 | 177 | public func scriptingDefinition(for url: URL) throws -> Data { 178 | #if canImport(Carbon) 179 | var sdef: Unmanaged? 180 | let err = OSACopyScriptingDefinitionFromURL(url as NSURL, 0, &sdef) 181 | if err != 0 { 182 | throw AutomationError(code: Int(err), message: "Can't retrieve SDEF.") 183 | } 184 | return sdef!.takeRetainedValue() as Data 185 | #else 186 | throw AutomationError(code: Int(err), message: "Can't retrieve SDEF.") 187 | #endif 188 | } 189 | 190 | -------------------------------------------------------------------------------- /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 | import AppleEvents 14 | 15 | 16 | 17 | // TO DO: change to struct? 18 | // TO DO: get rid of cached `desc`; AppleEvents.framework API should be just as fast packing type and code directly 19 | 20 | open class Symbol: Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable, SelfPacking { 21 | 22 | private var desc: ScalarDescriptor 23 | public let name: String?, type: DescType, code: OSType 24 | 25 | 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) 26 | 27 | // TO DO: check `desc` vs `descriptor` naming consistency throughout code and standardize on one or other 28 | public required init(name: String?, code: OSType, type: OSType = typeType, descriptor: ScalarDescriptor? = nil) { 29 | self.name = name 30 | self.code = code 31 | self.type = type 32 | self.desc = descriptor ?? (type == noOSType && name != nil ? packAsString(name!) : packAsFourCharCode(type: type, code: code)) 33 | } 34 | 35 | // special constructor for string-based record keys (avoids the need to wrap dictionary keys in a `StringOrSymbol` enum when unpacking) 36 | // e.g. the AppleScript record `{name:"Bob", isMyUser:true}` maps to the Swift Dictionary `[Symbol.name:"Bob", Symbol("isMyUser"):true]` 37 | 38 | public convenience init(_ name: String) { 39 | self.init(name: name, code: noOSType, type: noOSType, descriptor: nil) 40 | } 41 | 42 | internal convenience init(_ name: String, descriptor: ScalarDescriptor) { // TO DO: needed? 43 | self.init(name: name, code: noOSType, type: noOSType, descriptor: descriptor) 44 | } 45 | 46 | // convenience constructors for creating Symbols using raw four-char codes 47 | 48 | public convenience init(code: String, type: String = "type") { // TO DO: use parseFourCharCode() instead, and make this throwing? (Q. why is 'type' a string, rather than enum/OSType?); might be better split into init(typeCode:String) and init(enumCode:String) 49 | self.init(name: nil, code: UTGetOSTypeFromString(code as CFString), type: UTGetOSTypeFromString(type as CFString)) // caution: UTGetOSTypeFromString silently fails (returns 0) for invalid strings 50 | } 51 | 52 | public convenience init(code: OSType, type: OSType = typeType) { 53 | self.init(name: nil, code: code, type: type) 54 | } 55 | 56 | // this is called by AppData when unpacking typeType, typeEnumerated, etc; glue-defined symbol subclasses should override to return glue-defined symbols where available 57 | open class func symbol(code: OSType, type: OSType = typeType, descriptor: ScalarDescriptor? = nil) -> Symbol { 58 | return self.init(name: nil, code: code, type: type, descriptor: descriptor) 59 | } 60 | 61 | // this is called by AppData when unpacking string-based record keys 62 | public class func symbol(string: String, descriptor: ScalarDescriptor? = nil) -> Symbol { 63 | return self.init(name: string, code: noOSType, type: noOSType, descriptor: descriptor) 64 | } 65 | 66 | // display 67 | 68 | public var description: String { 69 | if let name = self.name { 70 | return self.isNameOnly ? "\(self.typeAliasName)(\(name.debugDescription))" : "\(self.typeAliasName).\(name)" 71 | } else { 72 | return "\(self.typeAliasName)(code:\(literalFourCharCode(self.code)),type:\(literalFourCharCode(self.type)))" 73 | } 74 | } 75 | 76 | public var debugDescription: String { return self.description } 77 | 78 | public var customMirror: Mirror { 79 | let children: [Mirror.Child] = [(label: "description", value: self.description), 80 | (label: "name", value: self.name ?? ""), 81 | (label: "code", value: formatFourCharCode(self.code)), 82 | (label: "type", value: formatFourCharCode(self.type))] 83 | return Mirror(self, children: children, displayStyle: .`class`, ancestorRepresentation: .suppressed) 84 | } 85 | 86 | // packing 87 | 88 | public var descriptor: ScalarDescriptor { return self.desc } // TO DO: needed? 89 | 90 | // returns true if Symbol contains name but not code (i.e. it represents a string-based record property key) 91 | public var isNameOnly: Bool { return self.type == noOSType && self.name != nil } 92 | 93 | public func SwiftAutomation_packSelf(_ appData: AppData) throws -> Descriptor { // caller always takes ownership of pack() result 94 | return self.desc 95 | } 96 | 97 | // equatable, hashable 98 | 99 | public func hash(into hasher: inout Hasher) { // see also comments in `==()` below 100 | hasher.combine(self.isNameOnly ? self.name!.hashValue : Int(self.code)) 101 | } 102 | 103 | 104 | public static func ==(lhs: Symbol, rhs: Symbol) -> Bool { 105 | // note: operands are not required to be the same subclass as this compares for AE equality only, e.g.: 106 | // 107 | // TED.document == AESymbol(code: "docu") -> true 108 | // 109 | // 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 110 | return lhs.isNameOnly && rhs.isNameOnly ? lhs.name == rhs.name : lhs.code == rhs.code 111 | } 112 | } 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /SwiftAutomation/TermTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TermTypes.swift 3 | // SwiftAutomation 4 | // 5 | // 6 | 7 | import Foundation 8 | import AppleEvents 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? (problem with using enum/structs is that GlueTable needs to update terms' names in situ when disambiguating conflicting definitions); FWIW, all this compatibility crap is only needed when parsing AETE/SDEF due to lack of rigid spec and verifier tools; superseding these formats with a real IDL would greatly simplify glue generation 28 | 29 | 30 | public class Term { // base class for keyword and command definitions 31 | 32 | public var name: String // editable as GlueTable may need to escape names to disambiguate conflicting terms 33 | 34 | init(name: String) { 35 | self.name = name 36 | } 37 | } 38 | 39 | 40 | public class KeywordTerm: Term, Hashable, CustomStringConvertible { // type/enumerator/property/element/parameter name 41 | 42 | public let code: OSType 43 | 44 | public init(name: String, code: OSType) { 45 | self.code = code 46 | super.init(name: name) 47 | } 48 | 49 | public func hash(into hasher: inout Hasher) { 50 | hasher.combine(Int(self.code)) 51 | } 52 | 53 | public var description: String { return "<\(type(of:self)):\(self.name)=\(literalFourCharCode(self.code))>" } 54 | 55 | public static func ==(lhs: KeywordTerm, rhs: KeywordTerm) -> Bool { 56 | return lhs.code == rhs.code && lhs.name == rhs.name 57 | } 58 | } 59 | 60 | 61 | public class ClassTerm: KeywordTerm { 62 | 63 | public var singular: String 64 | public var plural: String 65 | 66 | public init(singular: String, plural: String, code: OSType) { 67 | self.singular = singular 68 | self.plural = plural 69 | super.init(name: self.singular, code: code) 70 | } 71 | } 72 | 73 | 74 | public class CommandTerm: Term, Hashable, CustomStringConvertible { 75 | 76 | public let event: EventIdentifier 77 | public let parameters: [KeywordTerm] 78 | 79 | public init(name: String, event: EventIdentifier, parameters: [KeywordTerm]) { 80 | self.event = event 81 | self.parameters = parameters 82 | super.init(name: name) 83 | } 84 | 85 | public func hash(into hasher: inout Hasher) { 86 | hasher.combine(self.event) 87 | } 88 | 89 | public static func ==(lhs: CommandTerm, rhs: CommandTerm) -> Bool { 90 | return lhs.event == rhs.event && lhs.name == rhs.name && lhs.parameters == rhs.parameters // TO DO: ignore parameters? 91 | } 92 | 93 | public var description: String { 94 | let params = self.parameters.map({"\($0.name)=\(literalFourCharCode($0.code))"}).joined(separator: ",") 95 | return "" 96 | } 97 | 98 | public func parameter(for name: String) -> KeywordTerm? { 99 | return self.parameters.first{ $0.name == name } 100 | } 101 | public func parameter(for code: OSType) -> KeywordTerm? { 102 | return self.parameters.first{ $0.code == code } 103 | } 104 | 105 | } 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /SwiftAutomation/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // SwiftAutomation 4 | // 5 | // SwiftAutomation is released into the public domain. 6 | // 7 | 8 | -------------------------------------------------------------------------------- /SwiftAutomationLite/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftAutomationLite/SwiftAutomationLite.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAutomationLite.h 3 | // 4 | 5 | #import 6 | 7 | //! Project version number for SwiftAutomationLite. 8 | FOUNDATION_EXPORT double SwiftAutomationLiteVersionNumber; 9 | 10 | //! Project version string for SwiftAutomationLite. 11 | FOUNDATION_EXPORT const unsigned char SwiftAutomationLiteVersionString[]; 12 | 13 | // In this header, you should import all the public headers of your framework using statements like #import 14 | 15 | 16 | -------------------------------------------------------------------------------- /aeglue/aeglue.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /doc-generated/application_architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/doc-generated/application_architecture.gif -------------------------------------------------------------------------------- /doc-generated/application_architecture2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/doc-generated/application_architecture2.gif -------------------------------------------------------------------------------- /doc-generated/client_app_to_itunes_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/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/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/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").isNameOnly // false 
139 | AE("issingle").isNameOnly  // 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 AEDesc 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/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/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(_ event: EventIdentifier/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. Similarly, event identifiers are eight-char codes (`UInt64`) consisting of the event’s class and ID, e.g. `"aevtquit"`. 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("aevtodoc", ["----": 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("aevtquit", ["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").isNameOnly // false 124 | AE("issingle").isNameOnly // 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 `AEDesc` 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: AEDesc, ...) 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/application_architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/doc-source/application_architecture.gif -------------------------------------------------------------------------------- /doc-source/application_architecture2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/doc-source/application_architecture2.gif -------------------------------------------------------------------------------- /doc-source/client_app_to_itunes_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/doc-source/client_app_to_itunes_event.gif -------------------------------------------------------------------------------- /doc-source/finder_to_textedit_event.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhas/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/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/SwiftAutomation/898415d1324c673b200fa992c1457effef3d4eab/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: how to bring OS permission-to-automate dialogs to front when running this test? (they tend to get hidden behind Xcode, causing target app to block until AE times out) 8 | // 9 | // 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) 10 | // 11 | 12 | import AppKit 13 | import Foundation 14 | import SwiftAutomation 15 | import MacOSGlues 16 | 17 | 18 | let textedit = TextEdit() 19 | 20 | 21 | /* 22 | do { 23 | // simple pack/unpack data test 24 | 25 | let c = textedit.appData 26 | do { 27 | let seq = [1, 2, 3] 28 | let desc = try c.pack(seq) 29 | print(try c.unpack(desc, returnType: [String].self)) 30 | } 31 | /* 32 | do { 33 | let seq = [AE.id:0, AE.point:4, AE.version:9] 34 | let desc = try c.pack(seq) 35 | print(try c.unpack(desc, returnType: [Symbol:Int].self)) 36 | }*/ 37 | 38 | } catch {print("ERROR: \(error)")} 39 | 40 | print("\n\n") 41 | */ 42 | 43 | /* 44 | do { 45 | // NSNumbers will always pack as Double as the least lossy option 46 | do { 47 | let v = try textedit.appData.pack(true) 48 | print(v, try textedit.appData.unpack(v)) 49 | } 50 | do { 51 | let v = try textedit.appData.pack(NSNumber(value: true)) 52 | print(v,try textedit.appData.unpack(v)) 53 | } 54 | do { 55 | let v = try textedit.appData.pack(-25) 56 | print(v, try textedit.appData.unpack(v)) 57 | } 58 | do { 59 | let v = try textedit.appData.pack(NSNumber(value: -25)) 60 | print(v,try textedit.appData.unpack(v)) 61 | } 62 | do { 63 | let v = try textedit.appData.pack(4.12) 64 | print(v, try textedit.appData.unpack(v)) 65 | } 66 | do { 67 | let v = try textedit.appData.pack(NSNumber(value: 4.12)) 68 | print(v,try textedit.appData.unpack(v)) 69 | } 70 | } 71 | */ 72 | 73 | 74 | do { 75 | 76 | let itunes = ITunes() 77 | print("// itunes.playerState.get()") 78 | print("=> \(try itunes.playerState.get())") 79 | 80 | print() 81 | print("// try ITunes().play()") 82 | try ITunes().play() 83 | 84 | print() 85 | print("// try itunes.currentTrack.name.get()") 86 | print("Current track:", try itunes.currentTrack.name.get()) 87 | 88 | 89 | // print("// Specifier.description: \(TEDApp.documents[1].text)") 90 | // print("// Specifier.description: \(textedit.documents[1].text)") 91 | 92 | 93 | // send `open` and `get` AEs using raw four-char codes 94 | //let result = try textedit.sendAppleEvent(coreEventClass, kAEOpenDocuments, [keyDirectObject:NSURL.fileURLWithPath("/Users/has/todos.txt")]) 95 | //print(result) 96 | 97 | print() 98 | print("get text of every document of app \"TextEdit\"") 99 | print("// try TextEdit().documents.text.get()") 100 | print(try TextEdit().documents.text.get()) 101 | 102 | print() 103 | print("TEST: make new document with properties {text: \"Hello World!\"}") 104 | print("// try textedit.make(new: TED.document, withProperties: [TED.text: \"Hello World!\"])") 105 | let doc: TEDItem = try textedit.make(new: TED.document, withProperties: [TED.text: "Hello World!"]) 106 | print("=> \(doc)") 107 | 108 | print() 109 | print("TEST: get text of doc") 110 | print("// try doc.text.get()") 111 | print(try doc.text.get()) 112 | 113 | try doc.text.color.set(to: [25186, 48058, 18246]) // green 114 | print("TEST: get the color of text of doc") 115 | print("// try doc.text.color.get()") 116 | // print(try doc.text.color.get()) // WAS: Error -1700: Can't make some data into the expected type 117 | print(try doc.text.color.get()) // NOW: [25186, 48058, 18246] 118 | print(try textedit.windows.first.bounds.get()) // [left,top,right,bottom] 119 | 120 | 121 | 122 | let finder = Finder() 123 | print("TEST: get desktop position of first item of desktop") 124 | print(try finder.desktop.items.first.desktopPosition.get()) // [left,top] 125 | 126 | 127 | /* 128 | /* 129 | 130 | // get name of document 1 131 | 132 | // - using four-char code strings 133 | let result2 = try textedit.sendAppleEvent("core", "getd", ["----": textedit.elements("docu")[1].property("pnam")]) 134 | print(result2) 135 | 136 | // - using glue-defined terminology 137 | let result3 = try textedit.get(TEDApp.documents[1].name) 138 | print(result3) 139 | 140 | let result3a: Any = try textedit.documents[1].name.get() // convenience syntax for the above 141 | print(result3a) 142 | 143 | 144 | // get name of document 1 -- this works, returning a string value 145 | print("\nTEST: TextEdit().documents[1].name.get() as String") 146 | let result5: String = try textedit.documents[1].name.get() 147 | print("=> \(result5)") 148 | 149 | // get name of every document 150 | 151 | print("\nTEST: TextEdit().documents.name.get() as Any") 152 | let result4b = try textedit.documents.name.get() as Any 153 | print("=> \(result4b)") 154 | 155 | // get name of every document 156 | print("\nTEST: TextEdit().documents.name.get() as [String]") 157 | let result4 = try textedit.documents.name.get() as [String] // unpack the `get` command's result as Array of String 158 | print("=> \(result4)") 159 | 160 | // same as above 161 | let result6: [String] = try textedit.documents.name.get() 162 | print("=> \(result6)") 163 | 164 | 165 | // get every file of folder "Documents" of home whose name extension is "txt" and modification date > date "01:30 Jan 1, 2001 UTC" 166 | let date = Date(timeIntervalSinceReferenceDate:5400) // 1:30am on 1 Jan 2001 (UTC) 167 | print("\nTEST: Finder().home.folders[\"Documents\"].files[FINIts.nameExtension == \"txt\" && FINIts.modificationDate > DATE].name.get()") 168 | let q = Finder().home.folders["Documents"].files[FINIts.nameExtension == "txt" && FINIts.modificationDate > date].name 169 | print("// \(q)") 170 | let result4a = try q.get() 171 | print("=> \(result4a)") 172 | */ 173 | 174 | print("\nTEST: Finder().home.folders[\"Documents\"].files[FINIts.nameExtension == \"txt\"].properties.get()") 175 | let result4c = try Finder().home.folders["Documents"].files[FINIts.nameExtension == "txt"].properties.get() as [[FINSymbol:Any]] 176 | print("=> \(result4c)") 177 | 178 | print("\nTEST: duplicate file 1 of home to desktop with replacing") 179 | let myresult = try Finder().duplicate(Finder().home.files[1], to:Finder().desktop, replacing:true) 180 | print("=> \(myresult)") 181 | 182 | 183 | print("\nTEST: TextEdit().documents.properties.get() as [TEDDocumentRecord]") 184 | let result4d = try TextEdit().documents.properties.get() as [TEDDocumentRecord] 185 | print("=> \(result4d)") 186 | 187 | print("\nTEST: TextEdit().documents[1].properties.get() as TEDDocumentRecord") 188 | let result4e = try TextEdit().documents[1].properties.get() as TEDDocumentRecord 189 | print("=> \(result4e)") 190 | 191 | // try textedit.documents.close(saving: TED.no) // close every document saving no 192 | 193 | //struct X {}; try teDoc.text.set(to: X()) 194 | 195 | // 196 | try teDoc.close(saving: TED.no) 197 | 198 | 199 | print("\nTEST: get files 1 thru 3 of home") 200 | let r = try Finder().home.files[1, 3].get() as [FINItem] 201 | print("=> \(r)") 202 | 203 | */ 204 | //} catch { 205 | // print("ERROR: \(error)") 206 | } 207 | 208 | 209 | 210 | /* 211 | 212 | 213 | // test Swift<->AE type conversions 214 | let c = AEApplication.currentApplication().appData 215 | 216 | //let lst = try c.pack([1,2,3]) 217 | //print(try c.unpack(lst)) 218 | 219 | do { 220 | /* 221 | //print(try c.pack("hello")) 222 | print("PACK AS BOOLEAN") 223 | 224 | print("\(try c.pack(true)) \(formatFourCharCode(try c.pack(true).descriptorType))") 225 | print("\(try c.pack(NSNumber(bool: true))) \(formatFourCharCode(try c.pack(NSNumber(bool: true)).descriptorType))") 226 | print("") 227 | print("PACK AS INTEGER") 228 | print("\(try c.pack(3)) \(formatFourCharCode(try c.pack(3).descriptorType))") 229 | print("\(try c.pack(NSNumber(int: 3))) \(formatFourCharCode(try c.pack(NSNumber(int: 3)).descriptorType))") 230 | print("") 231 | print("PACK AS DOUBLE") 232 | print("\(try c.pack(3.1)) \(formatFourCharCode(try c.pack(3.1).descriptorType))") 233 | print("\(try c.pack(NSNumber(double: 3.1))) \(formatFourCharCode(try c.pack(NSNumber(double: 3.1)).descriptorType))") 234 | */ 235 | 236 | do { 237 | let seq = [1, 2, 3] 238 | let desc = try c.pack(seq) 239 | print(try c.unpack(desc, returnType: [Int].self)) 240 | } 241 | do { 242 | let seq = [AE.id:0, AE.point:4, AE.version:9] 243 | let desc = try c.pack(seq) 244 | print(try c.unpack(desc, returnType: [AESymbol:Int].self)) 245 | } 246 | 247 | } catch {print("ERROR: \(error)")} 248 | 249 | 250 | //NSLog("%08X", try c.pack(true).descriptorType) // 0x626F6F6C = typeBoolean 251 | 252 | //print(c) 253 | 254 | 255 | */ 256 | 257 | 258 | 259 | 260 | /* 261 | let finder = AEApplication(name: "Finder") 262 | 263 | 264 | do { 265 | let result = try finder.sendAppleEvent(coreEventGetData, 266 | [keyDirectObject:finder.property("home").elements("cobj")]) 267 | print("\n\nRESULT1: \(result)") 268 | } catch { 269 | print("\n\nERROR1: \(error)") 270 | } 271 | let f = URL(fileURLWithPath:"/Users/Shared") 272 | 273 | do { 274 | let result = try finder.sendAppleEvent(coreEventGetData, [keyDirectObject: AEApp.elements(cObject)[f]]) 275 | print("RAW: \(result)") 276 | } catch { 277 | print("\n\nERROR: \(error)") 278 | } 279 | do { 280 | let result = try finder.sendAppleEvent(coreEventGetData, [keyDirectObject: AEApp.elements(cFile)[f]]) // Finder will throw error as f is a folder, not a file 281 | print("RAW: \(result)") 282 | } catch { 283 | print("\n\nERROR: \(error)") 284 | } 285 | */ 286 | 287 | /* 288 | do { 289 | let result: AESpecifier = try finder.sendAppleEvent(coreEventGetData, 290 | [keyDirectObject:finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 291 | print("\n\nRESULT1: \(result)") 292 | } catch { 293 | print("\n\nERROR1: \(error)") 294 | } 295 | do { 296 | let result = try finder.sendAppleEvent(coreEventGetData, 297 | [keyDirectObject:finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 298 | print("\n\nRESULT2: \(result)") 299 | } catch { 300 | print("\n\nERROR2: \(error)") 301 | } 302 | 303 | 304 | do { 305 | let result: AESpecifier = try finder.sendAppleEvent("coregetd", 306 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 307 | print("\n\nRESULT3: \(result)") 308 | } catch { 309 | print("\n\nERROR3: \(error)") 310 | } 311 | do { 312 | let result = try finder.sendAppleEvent("coregetd", 313 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 314 | print("\n\nRESULT4: \(result)") 315 | } catch { 316 | print("\n\nERROR4: \(error)") 317 | } 318 | */ 319 | 320 | /* 321 | 322 | do { 323 | let result: AESpecifier! = try finder.sendAppleEvent("coregetd", // Can't unpack value as ImplicitlyUnwrappedOptional 324 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 325 | print("\n\nRESULT5: \(result)") 326 | } catch { 327 | print("\n\nERROR5: \(error)") 328 | } 329 | 330 | 331 | do { 332 | let result: AESpecifier? = try finder.sendAppleEvent("coregetd", // Can't unpack value as Optional 333 | ["----":finder.elements(cFile)[NSURL.fileURLWithPath("/Users/has/entoli - defining pairs.txt")]]) 334 | print("\n\nRESULT6: \(result)") 335 | } catch { 336 | print("\n\nERROR6: \(error)") 337 | } 338 | 339 | */ 340 | 341 | print() 342 | 343 | 344 | --------------------------------------------------------------------------------