diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb460e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..08de0be --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0508cc4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ian Gregory + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ba8f42f --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SDEFinitely", + "repositoryURL": "https://github.com/ThatsJustCheesy/SDEFinitely", + "state": { + "branch": null, + "revision": "824fdabbc2b23c2da0b50eb2edb158d0a8ebd643", + "version": "1.2.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..3d6bfcc --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AEthereal", + platforms: [ + .macOS(.v10_11) + ], + products: [ + .library( + name: "AEthereal", + targets: ["AEthereal"] + ), + ], + dependencies: [ + .package(url: "https://github.com/ThatsJustCheesy/SDEFinitely", from: "1.2.0") + ], + targets: [ + .target( + name: "AEthereal", + dependencies: ["SDEFinitely"] + ), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a6503c --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# AEthereal + +## AppleScript-compatible AppleEvents for Swift + +AEthereal constructs, sends, and receives replies from AppleScript-compatible AppleEvents. "AppleScript-compatible" means that the entire standard AppleEvent Object Model (object and insertion specifiers, etc.) is available and supported. + +## Origin + +AEthereal consists primarily of the "dynamic bridge" portion of hhas' SwiftAutomation framework. Tweaks have been made where appropriate. + +## License + +hhas has released SwiftAutomation into the public domain. AEthereal uses a dual-licensing approach to retain the spirit of "public domain" while dealing with its often dubious legality. + +You may choose one of the following license options: + +1. AEthereal is released into the public domain, with absolutely no warranty provided. +2. AEthereal is released under the terms of the MIT License, a copy of which is provided in LICENSE.txt. diff --git a/Sources/AEthereal/AETarget.swift b/Sources/AEthereal/AETarget.swift new file mode 100644 index 0000000..b112d74 --- /dev/null +++ b/Sources/AEthereal/AETarget.swift @@ -0,0 +1,259 @@ +// Originally written by hhas. +// See README.md for licensing information. + +import AppKit + +// AE errors indicating process unavailable // TO DO: finalize +private let processNotFoundErrorNumbers: Set = [procNotFound, connectionInvalid, localOnlyErr] + +private let launchEventSucceededErrorNumbers: Set = [Int(noErr), errAEEventNotHandled] + +private let untargetedLaunchEvent = NSAppleEventDescriptor(eventClass: AE4.Events.AppleScript.eventClass, eventID: AE4.Events.AppleScript.IDs.launch, targetDescriptor: NSAppleEventDescriptor.null(), returnID: AE4.autoGenerateReturnID, transactionID: AE4.anyTransactionID) + +/// A target that can receive AppleEvents. +public enum AETarget: CustomStringConvertible { + + case current + case name(String) // application's name (.app suffix is optional) or full path + case url(URL) // "file" or "eppc" URL + case bundleIdentifier(String) + case processIdentifier(pid_t) + case descriptor(NSAppleEventDescriptor) // AEAddressDesc + case none // used in untargeted App instances; sendAppleEvent() will raise ConnectionError if called + + public var description: String { + switch self { + case .current: + return "current application" + case .name(let name): + return "application ‘\(name)’" + case .url(let url): + return "application at ‘\(url.absoluteString)’" + case .bundleIdentifier(let identifier): + return "application id ‘\(identifier)’" + case .processIdentifier(let pid): + return NSRunningApplication(processIdentifier: pid)?.localizedName.map { $0 + " (by PID)" } ?? "dead PID \(pid)" + case .descriptor(let descriptor): + return (try? RootSpecifier(addressDescriptor: descriptor).property(pName).get() as String).map { $0 + "(by address descriptor)" } ?? "(unknown address descriptor)" + case .none: + return "(empty target)" + } + } + + // support functions + + private func localRunningApplication(url: URL) throws -> NSRunningApplication? { // TO DO: rename processForLocalApplication + guard let bundleID = Bundle(url: url)?.bundleIdentifier else { + throw ConnectionError(target: self, message: "Application not found: \(url)") + } + let foundProcesses = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + if foundProcesses.count == 1 { + return foundProcesses[0] + } else if foundProcesses.count > 1 { + for process in foundProcesses { + if process.bundleURL == url { // TO DO: FIX: need to check for FS equality + /* + function idForFileURL(url) { + const fileIDRef = objc.alloc(objc.NSData, objc.NIL).ref(); + if (!url('getResourceValue', fileIDRef, 'forKey', objc.NSURLFileResourceIdentifierKey, 'error', null)) { + throw new Error(`Can't get NSURLFileResourceIdentifierKey for ${url}`); + } + return fileIDRef.deref(); + } + */ + return process + } + } + } + return nil + } + + private func sendLaunchEvent(processDescriptor: NSAppleEventDescriptor) -> Int { + do { + let event = NSAppleEventDescriptor(eventClass: AE4.Events.AppleScript.eventClass, eventID: AE4.Events.AppleScript.IDs.launch, + targetDescriptor: processDescriptor, returnID: AE4.autoGenerateReturnID, + transactionID: AE4.anyTransactionID) + let reply = try event.sendEvent(options: .waitForReply, timeout: 30) + return Int(reply.paramDescriptor(forKeyword: keyErrorNumber)?.int32Value ?? 0) // application error (errAEEventNotHandled is normal) + } catch { + return (error as Error)._code // AEM error + } + } + + private func processDescriptorForLocalApplication(url: URL, launchOptions: LaunchOptions) throws -> NSAppleEventDescriptor { + // get a typeKernelProcessID-based AEAddressDesc for the target app, finding and launch it first if not already running; + // if app can't be found/launched, throws a ConnectionError/NSError instead + let runningProcess = try (self.localRunningApplication(url: url) ?? + NSWorkspace.shared.launchApplication(at: url, options: launchOptions, configuration: [:])) + return NSAppleEventDescriptor(processIdentifier: runningProcess.processIdentifier) + } + + private func isRunning(processDescriptor: NSAppleEventDescriptor) -> Bool { + // check if process is running by sending it a 'noop' event; used by isRunning property + // this assumes app is running unless it receives an AEM error that explicitly indicates it isn't (a bit crude, but when the only identifying information for the target process is an arbitrary AEAddressDesc there isn't really a better way to check if it's running other than send it an event and see what happens) + return !processNotFoundErrorNumbers.contains(self.sendLaunchEvent(processDescriptor: processDescriptor)) + } + + private func bundleIdentifier(processDescriptor: NSAppleEventDescriptor) -> String? { + return try? RootSpecifier(addressDescriptor: processDescriptor).property(pID).get() as String + } + + /// Whether this target can be automatically relaunched. + /// Only certain targeting modes permit this. + public var isRelaunchable: Bool { + switch self { + case .name, .bundleIdentifier: + return true + case .url(let url): + return url.isFileURL + default: + return false + } + } + + /// Whether this target is running, and thus possibly able to receive + /// AppleEvents. + public var isRunning: Bool { + switch self { + case .current: + return true + case .name(let name): // application's name (.app suffix is optional) or full path + if let url = fileURLForLocalApplication(name) { + return (((try? self.localRunningApplication(url: url)) as NSRunningApplication??)) != nil + } + case .url(let url): // "file" or "eppc" URL + if url.isFileURL { + return (((try? self.localRunningApplication(url: url)) as NSRunningApplication??)) != nil + } else if url.scheme == "eppc" { + return self.isRunning(processDescriptor: NSAppleEventDescriptor(applicationURL: url)) + } + case .bundleIdentifier(let bundleID): + return NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).count > 0 + case .processIdentifier(let pid): + return NSRunningApplication(processIdentifier: pid) != nil + case .descriptor(let addressDesc): + return self.isRunning(processDescriptor: addressDesc) + case .none: // used in untargeted App instances; sendAppleEvent() will raise ConnectionError if called + break + } + return false + } + + /// Bundle ID of this target, if available. + public var bundleIdentifier: String? { + switch self { + case .current: + return NSRunningApplication.current.bundleIdentifier + case .name(let name): + return fileURLForLocalApplication(name).flatMap { Bundle(url: $0) }?.bundleIdentifier + case .url(let url): + if url.isFileURL { + return Bundle(url: url)?.bundleIdentifier + } else if url.scheme == "eppc" { + return bundleIdentifier(processDescriptor: NSAppleEventDescriptor(applicationURL: url)) + } else { + return nil + } + case .bundleIdentifier(let bundleID): + return bundleID + case .processIdentifier(let pid): + return NSRunningApplication(processIdentifier: pid)?.bundleIdentifier + case .descriptor(let addressDesc): + return bundleIdentifier(processDescriptor: addressDesc) + case .none: + return nil + } + } + + /// Launches this target. Equivalent to AppleScript's `launch` command. + /// Handles the edge case of Script Editor applets that aren't saved as + /// "stay open", which only handle the first event they receive + /// and then quit. + public func launch() throws { // called by RootSpecifier.launch() + if self.isRunning { + let errorNumber = self.sendLaunchEvent(processDescriptor: try self.descriptor()!) + if !launchEventSucceededErrorNumbers.contains(errorNumber) { + throw AutomationError(code: errorNumber, message: "Can't launch application.") + } + } else { + switch self { + case .name(let name): + if let url = fileURLForLocalApplication(name) { + try self.launch(url: url) + return + } + case .url(let url) where url.isFileURL: + try self.launch(url: url) + return + case .bundleIdentifier(let bundleID): + if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + try self.launch(url: url) + return + } + default: + () + } // fall through on failure + throw ConnectionError(target: self, message: "Can't launch application.") + } + } + + private func launch(url: URL) throws { + try NSWorkspace.shared.launchApplication(at: url, options: [.withoutActivation], configuration: [.appleEvent: untargetedLaunchEvent]) + } + + /// Makes an `NSAppleEventDescriptor` for this target, if possible. + /// + /// If the target is relaunchable and not currently running, + /// it will be launched. + /// If the target is `.current`, the result will be `.currentProcess()`. + /// If the target is local to this machine, the result will refer to a PID. + public func descriptor(_ launchOptions: LaunchOptions = DefaultLaunchOptions) throws -> NSAppleEventDescriptor? { + switch self { + case .current: + return NSAppleEventDescriptor.currentProcess() + case .name(let name): // app name or full path + guard let url = fileURLForLocalApplication(name) else { + throw ConnectionError(target: self, message: "Application not found: \(name)") + } + return try self.processDescriptorForLocalApplication(url: url, launchOptions: launchOptions) + case .url(let url): // file/eppc URL + if url.isFileURL { + return try self.processDescriptorForLocalApplication(url: url, launchOptions: launchOptions) + } else if url.scheme == "eppc" { + return NSAppleEventDescriptor(applicationURL: url) + } else { + throw ConnectionError(target: self, message: "Invalid URL scheme (not file/eppc): \(url)") + } + case .bundleIdentifier(let bundleID): + do { + let runningProcess = try NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleID, + options: launchOptions, configuration: [:]) + return NSAppleEventDescriptor(processIdentifier: runningProcess.processIdentifier) + } catch { + throw ConnectionError(target: self, message: "Can't find/launch application: \(bundleID)", cause: error) + } + case .processIdentifier(let pid): + return NSAppleEventDescriptor(processIdentifier: pid) + case .descriptor(let desc): + return desc + case .none: + throw ConnectionError(target: .none, message: "Untargeted specifiers can't send Apple events.") + } + } +} + +/// Retrieves a file URL to the app named `name` on this machine. +/// +/// `name` may be either an absolute path ending in `.app`, +/// or the name of an app registered with Launch Services (with optional +/// `.app` extension). +public func fileURLForLocalApplication(_ name: String) -> URL? { + if name.hasPrefix("/") { + return URL(fileURLWithPath: name) + } else { + guard let path = NSWorkspace.shared.fullPath(forApplication: name) else { + return nil + } + return URL(fileURLWithPath: path) + } +} diff --git a/Sources/AEthereal/App.swift b/Sources/AEthereal/App.swift new file mode 100644 index 0000000..9b05549 --- /dev/null +++ b/Sources/AEthereal/App.swift @@ -0,0 +1,807 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Swift-AE type conversion and Apple event dispatch +// + +import Foundation +import AppKit + +// TO DO: get rid of waitReply: arg and just pass .ignoreReply to sendOptions (if ignore/wait/queue option not given, add .waitReply by default) + +// NOTE: there are some inbuilt assumptions about `Int` and `UInt` always being 64-bit + +let defaultTimeout: TimeInterval = 120 // bug workaround: NSAppleEventDescriptor.sendEvent(options:timeout:) method's support for kAEDefaultTimeout=-1 and kNoTimeOut=-2 flags is buggy , so for now the default timeout is hardcoded here as 120sec (same as in AS) + +let defaultIgnoring: Considerations = [.case] + +let defaultConsidersIgnoresMask: UInt32 = 0x00010000 // AppleScript ignores case by default + +public typealias KeywordParameter = (name: String?, code: OSType, value: Any) + +/******************************************************************************/ +// App converts values between Swift and AE types, holds target process information, and provides methods for sending Apple events + +private let launchOptions: LaunchOptions = DefaultLaunchOptions +private let relaunchMode: RelaunchMode = DefaultRelaunchMode + +public final class App { + + public static var generic = App() + + // Compatibility flags; these make SwiftAutomation more closely mimic certain AppleScript behaviors that may be expected by a few older apps + + public var isInt64Compatible: Bool = true // While App.encode() always encodes integers within the SInt32.min...SInt32.max range as typeSInt32, if the isInt64Compatible flag is true then it will use typeUInt32/typeSInt64/typeUInt64 for integers outside of that range. Some older Carbon-based apps (e.g. MS Excel) may not accept these larger integer types, so set this flag false when working with those apps to encode large integers as Doubles instead, effectively emulating AppleScript which uses SInt32 and Double only. (Caution: as in AppleScript, integers beyond ±2**52 will lose precision when converted to Double.) + + // the following properties are mainly for internal use, but SpecifierFormatter may also get them when rendering app roots + public let target: AETarget + + private var _targetDescriptor: NSAppleEventDescriptor? = nil // targetDescriptor() creates an AEAddressDesc for the target process when dispatching first Apple event, caching it here for subsequent reuse + + private var _transactionID: AETransactionID = AE4.anyTransactionID + + public init(target: AETarget = .none) { + self.target = target + } + +} + +// MARK: Specifier roots +extension App { + + public var application: RootSpecifier { + return RootSpecifier(.application, app: self) + } + + public var container: RootSpecifier { + return RootSpecifier(.container, app: self) + } + + public var specimen: RootSpecifier { + return RootSpecifier(.specimen, app: self) + } + +} + +// Swift's NSNumber bridging is hopelessly ambiguous, so it's not enough to ask if `value is Bool/Int/Double` or to try casting it to Bool/Int/Double, as these ALWAYS succeed for ALL NSNumbers, regardless of what type the NSNumber actually represents, e.g. `NSNumber(value:true) is Bool` returns `true` as expected, but `NSNumber(value:2) is Bool` and `NSNumber(value:3.3) is Bool` return `true` too! Therefore, the only way to determine if `value` is a Swift Bool/Int/Double is to first eliminate the possibility that it's an NSNumber by testing for that first. This is further complicated by NSNumber being a class cluster, not a concrete class, so we can't just test if `type(of: value) == NSNumber.self`; we have to extract the underlying __NSCF… classes and test against those. This makes some assumptions based on established NSNumber implementation; if that implementation should change in future (either in Cocoa itself or in the way that Swift maps it) then these assumptions may break, resulting in incorrect/broken behavior in the encode() method below. +private let _NSBooleanType = type(of: NSNumber(value: true)) // this assumes Cocoa always represents true/false as __NSCFBoolean +private let _NSNumberType = type(of: NSNumber(value: 1)) // this assumes Cocoa always represents all integer and FP numbers as __NSCFNumber + +// MARK: Descriptor encoding +extension App { + + public func encode(_ value: Any) throws -> NSAppleEventDescriptor { + // note: Swift's Bool/Int/Double<->NSNumber bridging sucks, so NSNumber instances require special processing to ensure the underlying value's exact type (Bool/Int/Double/etc) isn't lost in translation + if type(of: value) == _NSBooleanType { // test for NSNumber(value:true/false) + // important: + // - the first test assumes NSNumber class cluster always returns an instance of __NSCFBooleanType (or at least something that can be distinguished from all other NSNumbers) + // - `value is Bool/Int/Double` always returns true for any NSNumber, so must not be used; however, checking for BooleanType returns true only for Bool (or other Swift types that implement BooleanType protocol) so should be safe + return NSAppleEventDescriptor(boolean: value as! Bool) + } else if type(of: value) == _NSNumberType { // test for any other NSNumber (but not Swift numeric types as those will be dealt with below) + let numberObj = value as! NSNumber + switch numberObj.objCType.pointee as Int8 { + case 98, 99, 67, 115, 83, 105: // (b, c, C, s, S, i) anything that will fit into SInt32 is packed as typeSInt32 for compatibility + return NSAppleEventDescriptor(int32: numberObj.int32Value) + case 73: // (I) UInt32 + var val = numberObj.uint32Value + if val <= UInt32(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: typeUInt32, bytes: &val, length: MemoryLayout.size)! + } // else encode as double + case 108, 113: // (l, q) SInt64 + var val = numberObj.int64Value + if val >= Int64(Int32.min) && val <= Int64(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: typeSInt64, bytes: &val, length: MemoryLayout.size)! + } // else encode as double, possibly with some loss of precision + case 76, 81: // (L, Q) UInt64 + var val = numberObj.uint64Value + if val <= UInt64(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: typeUInt64, bytes: &val, length: MemoryLayout.size)! + } // else encode as double, possibly with some loss of precision + default: + () + } + return NSAppleEventDescriptor(double: numberObj.doubleValue) + } + switch value { + case let val as AEEncodable: + return try val.encodeAEDescriptor(self) + case let obj as NSAppleEventDescriptor: + return obj + case var val as Int: + // Note: to maximize application compatibility, always preferentially encode integers as typeSInt32, as that's the traditional integer type recognized by all apps. (In theory, packing as typeSInt64 shouldn't be a problem as apps should coerce to whatever type they actually require before decoding, but not-so-well-designed Carbon apps sometimes explicitly typecheck instead, so will fail if the descriptor isn't the assumed typeSInt32.) + if Int(Int32.min) <= val && val <= Int(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: AE4.Types.sInt64, bytes: &val, length: MemoryLayout.size)! + } else { + return NSAppleEventDescriptor(double: Double(val)) // caution: may be some loss of precision + } + case let val as Double: + return NSAppleEventDescriptor(double: val) + case let val as String: + return NSAppleEventDescriptor(string: val) + case let obj as Date: + return NSAppleEventDescriptor(date: obj) + case let obj as URL: + if obj.isFileURL { + return NSAppleEventDescriptor(fileURL: obj) + } + + // Cocoa collection classes don't support SelfPacking (though don't require it either since they're not generics); for now, just cast to Swift type on assumption that these are less common cases and Swift's ObjC bridge won't add significant cost, though they could be packed directly here if preferred + case let obj as NSSet: + return try (obj as Set).encodeAEDescriptor(self) + case let obj as NSArray: + return try (obj as Array).encodeAEDescriptor(self) + case let obj as NSDictionary: + return try (obj as Dictionary).encodeAEDescriptor(self) + + + case var val as UInt: + if val <= UInt(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: AE4.Types.uInt32, bytes: &val, length: MemoryLayout.size)! + } else { + return NSAppleEventDescriptor(double: Double(val)) + } + case let val as Int8: + return NSAppleEventDescriptor(int32: Int32(val)) + case let val as UInt8: + return NSAppleEventDescriptor(int32: Int32(val)) + case let val as Int16: + return NSAppleEventDescriptor(int32: Int32(val)) + case let val as UInt16: + return NSAppleEventDescriptor(int32: Int32(val)) + case let val as Int32: + return NSAppleEventDescriptor(int32: Int32(val)) + case var val as UInt32: + if val <= UInt32(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: AE4.Types.uInt32, bytes: &val, length: MemoryLayout.size)! + } else { + return NSAppleEventDescriptor(double: Double(val)) + } + case var val as Int64: + if val >= Int64(Int32.min) && val <= Int64(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: AE4.Types.sInt64, bytes: &val, length: MemoryLayout.size)! + } else { + return NSAppleEventDescriptor(double: Double(val)) // caution: may be some loss of precision + } + case var val as UInt64: + if val <= UInt64(Int32.max) { + return NSAppleEventDescriptor(int32: Int32(val)) + } else if self.isInt64Compatible { + return NSAppleEventDescriptor(descriptorType: AE4.Types.uInt64, bytes: &val, length: MemoryLayout.size)! + } else { + return NSAppleEventDescriptor(double: Double(val)) // caution: may be some loss of precision + } + case let val as Float: + return NSAppleEventDescriptor(double: Double(val)) + case let val as Bool: // hopefully Swift hasn't [mis]cast `true` or `false` in one of the above cases + return NSAppleEventDescriptor(boolean: val) + case let val as Error: + throw val // if value is ErrorType, rethrow it as-is; e.g. see ObjectSpecifier.decodeParentSpecifiers(), which needs to report [rare] errors but can't throw itself; this should allow APIs that can't raise errors directly (e.g. specifier constructors) to have those errors raised if/when used in commands (which can throw) + default: + () + } + throw PackError(object: value) + } + +} + +// MARK: Descriptor decoding +extension App { + + public func decode(_ desc: NSAppleEventDescriptor) throws -> T { + if T.self == Any.self || T.self == AnyObject.self { + return try self.decodeAsAny(desc) as! T + } else if let t = T.self as? AEDecodable.Type { + if let decoded = try t.init(from: desc, app: self) as? T { + return decoded + } + } else if T.self == Query.self { + if let result = try self.decodeAsAny(desc) as? T { // specifiers can be composed of several AE types, so decode first then check type + return result + } else { + return RootSpecifier(.object(desc), app: self) as! T + } + } else if isMissingValue(desc) { + throw DecodeError(app: self, descriptor: desc, type: T.self, message: "Can't coerce 'missing value' descriptor to \(T.self).") // Important: App must not decode a 'missing value' constant as anything except `MissingValue` or `nil` (i.e. the types to which it self-decodes). AppleScript doesn't have this problem as all descriptors decode to their own preferred type, but decode() forces a descriptor to decode as a specific type or fail trying. While its role is to act as a `nil`-style placeholder when no other value is given, its descriptor type is typeType so left to its own devices it would naturally decode the same as any other typeType descriptor. e.g. One of AEM's vagaries is that it supports typeType to typeUnicodeText coercions, so while permitting cDocument to coerce to "docu" might be acceptable [if not exactly helpful], allowing cMissingValue to coerce to "msng" would defeat its whole purpose. + } + + switch T.self { + case is Bool.Type: + return desc.booleanValue as! T + case is Int.Type: // TO DO: this assumes Int will _always_ be 64-bit (on macOS); is that safe? + if desc.descriptorType == AE4.Types.sInt32 { // shortcut for common case where descriptor is already a standard 32-bit int + return Int(desc.int32Value) as! T + } else if let result = self.decodeAsInt(desc) { + return Int(result) as! T + } + case is UInt.Type: + if let result = self.decodeAsInt(desc) { + return Int(result) as! T + } + case is Double.Type: + if let doubleDesc = desc.coerce(toDescriptorType: AE4.Types.ieee64BitFloatingPoint) { + return Double(doubleDesc.doubleValue) as! T + } + case is String.Type, is NSString.Type: + if let result = desc.stringValue { + return result as! T + } + case is Symbol.Type: + if symbolDescriptorTypes.contains(desc.descriptorType) { + return Symbol(code: desc.typeCodeValue, type: desc.descriptorType) as! T + } + case is Date.Type, is NSDate.Type: + if let result = desc.dateValue { + return result as! T + } + case is URL.Type, is NSURL.Type: + if let result = desc.fileURLValue { // note: this coerces all file system-related descriptors down to typeFileURL before decoding them, so typeAlias/typeBookmarkData descriptors (which identify file system objects, not locations) won't round-trip and the resulting URL will only describe the file's location at the time the descriptor was decoded. + return result as! T + } + case is Int8.Type: // lack of common protocols on Integer types is a pain + if let n = self.decodeAsInt(desc), let result = Int8(exactly: n) { + return result as! T + } + case is Int16.Type: + if let n = self.decodeAsInt(desc), let result = Int16(exactly: n) { + return result as! T + } + case is Int32.Type: + if let n = self.decodeAsInt(desc), let result = Int32(exactly: n) { + return result as! T + } + case is Int64.Type: + if let n = self.decodeAsInt(desc), let result = Int64(exactly: n) { + return result as! T + } + case is UInt8.Type: + if let n = self.decodeAsUInt(desc), let result = UInt8(exactly: n) { + return result as! T + } + case is UInt16.Type: + if let n = self.decodeAsUInt(desc), let result = UInt16(exactly: n) { + return result as! T + } + case is UInt32.Type: + if let n = self.decodeAsUInt(desc), let result = UInt32(exactly: n) { + return result as! T + } + case is UInt64.Type: + if let n = self.decodeAsUInt(desc), let result = UInt64(exactly: n) { + return result as! T + } + case is Float.Type: + if let doubleDesc = desc.coerce(toDescriptorType: AE4.Types.ieee64BitFloatingPoint), + let result = Float(exactly: doubleDesc.doubleValue) { + return result as! T + } + case is AnyHashable.Type: // while records always decode as [Symbol:TYPE], [AnyHashable:TYPE] is a valid return type too + if let result = try self.decodeAsAny(desc) as? AnyHashable { + return result as! T + } + case is NSNumber.Type: + switch desc.descriptorType { + case AE4.Types.boolean, AE4.Types.true, AE4.Types.false: + return NSNumber(value: desc.booleanValue) as! T + case AE4.Types.sInt32, AE4.Types.sInt16: + return NSNumber(value: desc.int32Value) as! T + case AE4.Types.ieee64BitFloatingPoint, AE4.Types.ieee32BitFloatingPoint, AE4.Types._128BitFloatingPoint: + return NSNumber(value: desc.doubleValue) as! T + case AE4.Types.sInt64: + return NSNumber(value: self.decodeAsInt(desc)!) as! T + case AE4.Types.uInt32, AE4.Types.uInt16, AE4.Types.uInt64: + return NSNumber(value: self.decodeAsUInt(desc)!) as! T + default: // not a number, e.g. a string, so preferentially coerce and decode as Int64 or else Double, falling through on failure + if let doubleDesc = desc.coerce(toDescriptorType: AE4.Types.ieee64BitFloatingPoint) { + let d = doubleDesc.doubleValue + if d.truncatingRemainder(dividingBy: 1) == 0, let i = self.decodeAsInt(desc) { + return NSNumber(value: i) as! T + } else { + return NSNumber(value: doubleDesc.doubleValue) as! T + } + } + } + case is NSArray.Type: + return try self.decode(desc) as Array as! T + case is NSSet.Type: + return try self.decode(desc) as Set as! T + case is NSDictionary.Type: + return try self.decode(desc) as Dictionary as! T + case is NSAppleEventDescriptor.Type: + return desc as! T + case let t: + print(t) + } + // desc couldn't be coerced to the specified type + let symbol = Symbol(code: desc.descriptorType, type: typeType) + let typeName = String(fourCharCode: symbol.code) + throw DecodeError(app: self, descriptor: desc, type: T.self, message: "Can't coerce \(typeName) descriptor to \(T.self).") + } + + /******************************************************************************/ + // Convert an Apple event descriptor to its preferred Swift type, as determined by its descriptorType + + public func decodeAsAny(_ desc: NSAppleEventDescriptor) throws -> Any { // note: this never returns Optionals (i.e. cMissingValue AEDescs always decode as MissingValue when return type is Any) to avoid dropping user into Optional.some(Optional.none) hell. + switch desc.descriptorType { + case AE4.Types.null: + return App.generic.application + case AE4.Types.currentContainer: + return App.generic.container + case AE4.Types.objectBeingExamined: + return App.generic.specimen + // common AE types + case AE4.Types.true, AE4.Types.false, AE4.Types.boolean: + return desc.booleanValue + case AE4.Types.sInt32, AE4.Types.sInt16: + return desc.int32Value + case AE4.Types.ieee64BitFloatingPoint, AE4.Types.ieee32BitFloatingPoint: + return desc.doubleValue + case AE4.Types._128BitFloatingPoint: // coerce down lossy + guard let doubleDesc = desc.coerce(toDescriptorType: AE4.Types.ieee64BitFloatingPoint) else { + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Can't coerce 128-bit float to double.") + } + return doubleDesc.doubleValue + case AE4.Types.text, AE4.Types.intlText, AE4.Types.utf8Text, AE4.Types.utf16ExternalRepresentation, AE4.Types.styledText, AE4.Types.unicodeText, AE4.Types.version: + guard let result = desc.stringValue else { // this should never fail unless the AEDesc contains mis-encoded text data (e.g. claims to be typeUTF8Text but contains non-UTF8 byte sequences) + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Corrupt string descriptor.") + } + return result + case AE4.Classes.char: + let data = desc.data + return try data.withUnsafeBytes { bytes in + switch data.count { + case 1: + return Character(Unicode.Scalar(bytes.first!)) + case 2: + let char16s = bytes.bindMemory(to: UInt16.self) + guard let scalar = char16s.first.flatMap({ Unicode.Scalar($0) }) else { + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Corrupt UTF-16 character descriptor.") + } + return Character(scalar) + case 4: + let char32s = bytes.bindMemory(to: UInt32.self) + guard let scalar = char32s.first.flatMap({ Unicode.Scalar($0) }) else { + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Corrupt UTF-32 character descriptor.") + } + return Character(scalar) + default: + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Character descriptor has irregular byte count.") + } + } + case AE4.Types.longDateTime: + guard let result = desc.dateValue else { // this should never fail unless the AEDesc contains bad data + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Corrupt descriptor.") + } + return result + case AE4.Types.list: + return try Array(from: desc, app: self) as [Any] + case AE4.Types.record: + return try Dictionary(from: desc, app: self) as [Symbol:Any] + case AE4.Types.alias, AE4.Types.bookmarkData, AE4.Types.fileURL, AE4.Types.fsRef, AE4.Types.fss: // note: typeFSS is long defunct so shouldn't be encountered unless dealing with exceptionally old 32-bit Carbon apps, while a `file "HFS:PATH:"` object specifier (typeObjectSpecifier of cFile; basically an AppleScript kludge-around to continue supporting the `file [specifier] "HFS:PATH:"` syntax form despite typeFSS going away) is indistinguishable from any other object specifier so will decode as an explicit `APPLICATION().files["HFS:PATH:"]` or `APPLICATION().elements("file")["HFS:PATH:"]` specifier depending on whether or not the glue defines a `file[s]` keyword (TBH, not sure if there are any apps do return AEDescs that represent file system locations this way.) + guard let result = desc.fileURLValue else { // ditto + throw DecodeError(app: self, descriptor: desc, type: Any.self, message: "Corrupt descriptor.") + } + return result + case AE4.Types.type, AE4.Types.enumerated, AE4.Types.property, AE4.Types.keyword: + return isMissingValue(desc) ? MissingValue : Symbol(code: desc.typeCodeValue, type: desc.descriptorType) + // object specifiers + case AE4.Types.objectSpecifier: + return try self.decodeAsObjectSpecifier(desc) + case AE4.Types.insertionLoc: + return try self.decodeAsInsertionLoc(desc) + case AE4.Types.null: // null descriptor indicates object specifier root + return self.application + case AE4.Types.currentContainer: + return self.container + case AE4.Types.objectBeingExamined: + return self.specimen + case AE4.Types.compDescriptor: + return try self.decodeAsComparisonDescriptor(desc) + case AE4.Types.logicalDescriptor: + return try self.decodeAsLogicalDescriptor(desc) + + // less common types + case AE4.Types.sInt64: + return self.decodeAsInt(desc)! + case AE4.Types.uInt64, AE4.Types.uInt32, AE4.Types.uInt16: + return self.decodeAsUInt(desc)! + case AE4.Types.qdPoint, AE4.Types.qdRectangle, AE4.Types.rgbColor: + return try self.decode(desc) as [Int] + // note: while there are also several AEAddressDesc types used to identify applications, these are very rarely used as command results (e.g. the `choose application` OSAX) and there's little point decoding them anway as the only type they can automatically be mapped to is AEApplication, which has only minimal functionality anyway. Also unsupported are unit types as they only cover a handful of measurement types and in practice aren't really used for anything except measurement conversions in AppleScript. + default: + if desc.isRecordDescriptor { + return try Dictionary(from: desc, app: self) as [Symbol:Any] + } + return desc + } + } + +} + +private let _absoluteOrdinalCodes: Set = Set(AE4.AbsoluteOrdinal.allCases.map { $0.rawValue }) +private let _relativeOrdinalCodes: Set = Set(AE4.RelativeOrdinal.allCases.map { $0.rawValue }) + +private let _comparisonOperatorCodes: Set = Set(AE4.Comparison.allCases.map { $0.rawValue } + AE4.Containment.allCases.map { $0.rawValue } + [AE4.notEquals, AE4.isIn]) +private let _logicalOperatorCodes: Set = Set(AE4.LogicalOperator.allCases.map { $0.rawValue }) + +// MARK: Decoding helpers +extension App { + + private func decodeAsInt(_ desc: NSAppleEventDescriptor) -> Int? { + // coerce the descriptor (whatever it is - typeSInt16, typeUInt32, typeUnicodeText, etc.) to typeSIn64 (hoping the Apple Event Manager has remembered to install TYPE-to-SInt64 coercion handlers for all these types too), and decode as Int[64] + if let intDesc = desc.coerce(toDescriptorType: AE4.Types.sInt64) { + var result: Int64 = 0 + (intDesc.data as NSData).getBytes(&result, length: MemoryLayout.size) + return Int(result) // caution: this assumes Int will always be 64-bit + } else { + return nil + } + } + + private func decodeAsUInt(_ desc: NSAppleEventDescriptor) -> UInt? { + // as above, but for unsigned ints + if let intDesc = desc.coerce(toDescriptorType: AE4.Types.uInt64) { + var result: UInt64 = 0 + (intDesc.data as NSData).getBytes(&result, length: MemoryLayout.size) + return UInt(result) // caution: this assumes UInt will always be 64-bit + } else { + return nil + } + } + + func decodeAsInsertionLoc(_ desc: NSAppleEventDescriptor) throws -> Specifier { + guard + let _ = desc.forKeyword(AE4.InsertionSpecifierKeywords.object), // only used to check InsertionLoc record is correctly formed + let insertionLocation = desc.forKeyword(AE4.InsertionSpecifierKeywords.position) + else { + throw DecodeError(app: self, descriptor: desc, type: InsertionSpecifier.self, message: "Can't decode malformed insertion specifier.") + } + return InsertionSpecifier(insertionLocation: insertionLocation, parentQuery: try decode(desc), app: self) + } + + func decodeAsObjectSpecifier(_ desc: NSAppleEventDescriptor) throws -> Specifier { + guard + let parentDesc = desc.forKeyword(AE4.ObjectSpecifierKeywords.container), + let wantType = desc.forKeyword(AE4.ObjectSpecifierKeywords.desiredClass), + let selectorForm = desc.forKeyword(AE4.ObjectSpecifierKeywords.keyForm), + let selectorDesc = desc.forKeyword(AE4.ObjectSpecifierKeywords.keyData) + else { + throw DecodeError(app: self, descriptor: desc, type: ObjectSpecifier.self, message: "Can't decode malformed object specifier.") + } + do { // decode selectorData, unless it's a property code or absolute/relative ordinal (in which case use its 'prop'/'enum' descriptor as-is) + var selectorData: Any = selectorDesc // the selector won't be decoded if it's a property/relative/absolute ordinal + var objectSpecifierClass = ObjectSpecifier.self // most reference forms describe one-to-one relationships + switch AE4.IndexForm(rawValue: selectorForm.enumCodeValue) { + case .propertyID: // property + if ![AE4.Types.type, AE4.Types.property].contains(selectorDesc.descriptorType) { + throw DecodeError(app: self, descriptor: desc, type: ObjectSpecifier.self, message: "Can't decode malformed object specifier.") + } + case .relativePosition: // before/after + if !(selectorDesc.descriptorType == AE4.Types.enumerated && _relativeOrdinalCodes.contains(selectorDesc.enumCodeValue)) { + throw DecodeError(app: self, descriptor: desc, type: ObjectSpecifier.self, + message: "Can't decode malformed object specifier.") + } + case .absolutePosition: // by-index or first/middle/last/any/all ordinal + if selectorDesc.descriptorType == AE4.Types.enumerated && _absoluteOrdinalCodes.contains(selectorDesc.enumCodeValue) { // don't decode ordinals + if selectorDesc.enumCodeValue == AE4.AbsoluteOrdinal.all.rawValue { // `all` ordinal = one-to-many relationship + objectSpecifierClass = MultipleObjectSpecifier.self + } + } else { // decode index (normally Int32, though the by-index form can take any type of selector as long as the app understands it) + selectorData = try decode(selectorDesc) + } + case .range: // by-range = one-to-many relationship + objectSpecifierClass = MultipleObjectSpecifier.self + if selectorDesc.descriptorType != AE4.Types.rangeDescriptor { + throw DecodeError(app: self, descriptor: selectorDesc, type: RangeSelector.self, message: "Malformed selector in by-range specifier.") + } + guard + let startDesc = selectorDesc.forKeyword(AE4.RangeSpecifierKeywords.start), + let stopDesc = selectorDesc.forKeyword(AE4.RangeSpecifierKeywords.stop) + else { + throw DecodeError(app: self, descriptor: selectorDesc, type: RangeSelector.self, message: "Malformed selector in by-range specifier.") + } + do { + selectorData = RangeSelector(start: try decodeAsAny(startDesc), stop: try self.decodeAsAny(stopDesc), wantType: wantType) + } catch { + throw DecodeError(app: self, descriptor: selectorDesc, type: RangeSelector.self, message: "Couldn't decode start/stop selector in by-range specifier.") + } + case .test: // by-range = one-to-many relationship + objectSpecifierClass = MultipleObjectSpecifier.self + selectorData = try decode(selectorDesc) + if !(selectorData is Query) { + throw DecodeError(app: self, descriptor: selectorDesc, type: Query.self, message: "Malformed selector in by-test specifier.") + } + default: // by-name or by-ID + selectorData = try decode(selectorDesc) + } + return objectSpecifierClass.init(wantType: wantType, + selectorForm: selectorForm, selectorData: selectorData, + parentQuery: try decode(parentDesc) as Query, + app: self) + } catch { + throw DecodeError(app: self, descriptor: desc, type: ObjectSpecifier.self, message: "Can't decode object specifier's selector data.", cause: error) + } + } + + func decodeAsComparisonDescriptor(_ desc: NSAppleEventDescriptor) throws -> TestClause { + if + let operatorType = desc.forKeyword(AE4.TestPredicateKeywords.comparisonOperator), + let operand1Desc = desc.forKeyword(AE4.TestPredicateKeywords.firstObject), + let operand2Desc = desc.forKeyword(AE4.TestPredicateKeywords.secondObject), + !_comparisonOperatorCodes.contains(operatorType.enumCodeValue) + { + // don't bother with dedicated error reporting here as malformed operand descs that cause the following decode calls to fail are unlikely in practice, and will still be caught and reported further up the call chain anyway + let operand1 = try decodeAsAny(operand1Desc) + let operand2 = try decodeAsAny(operand2Desc) + if operatorType.typeCodeValue == AE4.Containment.contains.rawValue && !(operand1 is ObjectSpecifier) { + if let op2 = operand2 as? ObjectSpecifier { + return ComparisonTest(operatorType: AE4.Descriptors.ContainmentTests.isIn, operand1: op2, operand2: operand1, app: self) + } // else fall through to throw + } else if let op1 = operand1 as? ObjectSpecifier { + return ComparisonTest(operatorType: operatorType, operand1: op1, operand2: operand2, app: self) + } // else fall through to throw + } + throw DecodeError(app: self, descriptor: desc, type: TestClause.self, message: "Can't decode comparison test: malformed descriptor.") + } + + func decodeAsLogicalDescriptor(_ desc: NSAppleEventDescriptor) throws -> TestClause { + if + let operatorType = desc.forKeyword(AE4.TestPredicateKeywords.logicalOperator), + let operandsDesc = desc.forKeyword(AE4.TestPredicateKeywords.object), + !_logicalOperatorCodes.contains(operatorType.enumCodeValue) + { + let operands = try decode(operandsDesc) as [TestClause] + return LogicalTest(operatorType: operatorType, operands: operands, app: self) + } + throw DecodeError(app: self, descriptor: desc, type: TestClause.self, message: "Can't decode logical test: malformed descriptor.") + } + +} + +// MARK: Target encoding +extension App { + + public func targetDescriptor() throws -> NSAppleEventDescriptor? { + if _targetDescriptor == nil { + _targetDescriptor = try target.descriptor(launchOptions) + } + return _targetDescriptor + } + +} + +private let defaultSendMode = SendOptions.defaultOptions.union(SendOptions.canSwitchLayer) +private let defaultIgnorances = packConsideringAndIgnoringFlags([.case]) + +// if target process is no longer running, Apple Event Manager will return an error when an event is sent to it +private let RelaunchableErrorCodes: Set = [-600, -609] +// if relaunchMode = .limited, only 'launch' and 'run' are allowed to restart a local application that's been quit +private let LimitedRelaunchEvents: [(OSType,OSType)] = [(AE4.Events.Core.eventClass, AE4.Events.Core.IDs.openApplication), (AE4.Events.AppleScript.eventClass, AE4.Events.AppleScript.IDs.launch)] + +// MARK: Apple event sending +extension App { + + private func send(event: NSAppleEventDescriptor, sendMode: SendOptions, timeout: TimeInterval) throws -> NSAppleEventDescriptor { + do { + return try event.sendEvent(options: sendMode, timeout: timeout) // throws NSError on AEM errors (but not app errors) + } catch { + // 'launch' events normally return 'not handled' errors, so just ignore those + // TO DO: this is wrong; -1708 will be in reply event, not in AEM error; FIX + if + (error as NSError).code == -1708, + event.attributeDescriptor(forKeyword: AE4.Attributes.eventClass)!.typeCodeValue == AE4.Events.AppleScript.eventClass, + event.attributeDescriptor(forKeyword: AE4.Attributes.eventID)!.typeCodeValue == AE4.Events.AppleScript.IDs.launch + { + // not a full AppleEvent desc, but reply event's attributes aren't used so is equivalent to a reply event containing neither error nor result + return NSAppleEventDescriptor.record() + } else { + throw error + } + } + } + + public func sendAppleEvent(name: String?, eventClass: OSType, eventID: OSType, + parentSpecifier: Specifier, // the Specifier on which the command method was called; see special-case packing logic below + directParameter: Any = NoParameter, // the first (unnamed) parameter to the command method; see special-case packing logic below + keywordParameters: [KeywordParameter] = [], // the remaining named parameters + requestedType: Symbol? = nil, // event's `as` parameter, if any (note: while a `keyAERequestedType` parameter can be supplied via `keywordParameters:`, it will be ignored if `requestedType:` is given) + waitReply: Bool = true, // wait for application to respond before returning? + sendOptions: SendOptions? = nil, // raw send options (these are rarely needed); if given, `waitReply:` is ignored + withTimeout: TimeInterval? = nil, // no. of seconds to wait before raising timeout error (-1712); may also be default/never + ignoring ignorances: Considerations? = nil + ) throws -> Result // coerce and decode result as this type or return raw reply event if T is NSDescriptor; default is Any + { + // note: human-readable command and parameter names are only used (if known) in error messages + // note: all errors occurring within this method are caught and rethrown as CommandError, allowing error message to provide a description of the failed command as well as the error itself + var sentEvent: NSAppleEventDescriptor?, repliedEvent: NSAppleEventDescriptor? + do { + // Create a new AppleEvent descriptor (throws ConnectionError if target app isn't found) + let event = NSAppleEventDescriptor(eventClass: eventClass, eventID: eventID, targetDescriptor: try self.targetDescriptor(), + returnID: AE4.autoGenerateReturnID, transactionID: AE4.anyTransactionID) + // encode its keyword parameters + for (paramName, code, value) in keywordParameters where parameterExists(value) { + do { + event.setDescriptor(try self.encode(value), forKeyword: code) + } catch { + throw AutomationError(code: error._code, message: "Invalid '\(paramName ?? String(fourCharCode: code))' parameter.", cause: error) + } + } + // encode event's direct parameter and/or subject attribute + let hasDirectParameter = parameterExists(directParameter) + if hasDirectParameter { // if the command includes a direct parameter, encode that normally as its direct param + event.setParam(try self.encode(directParameter), forKeyword: AE4.Keywords.directObject) + } + // if command method was called on root Application (null) object, the event's subject is also null... + var subjectDesc = applicationRoot + // ... but if the command was called on a Specifier, decide if that specifier should be packed as event's subject + // or, as a special case, used as event's keyDirectObject/keyAEInsertHere parameter for user's convenience + if !(parentSpecifier is RootSpecifier) { // technically Application, but there isn't an explicit class for that + if eventClass == AE4.Suites.coreSuite && eventID == AE4.AESymbols.createElement { // for user's convenience, `make` command is treated as a special case + // if `make` command is called on a specifier, use that specifier as event's `at` parameter if not already given + if event.paramDescriptor(forKeyword: AE4.Keywords.insertHere) != nil { // an `at` parameter was already given, so encode parent specifier as event's subject attribute + subjectDesc = try self.encode(parentSpecifier) + } else { // else encode parent specifier as event's `at` parameter and use null as event's subject attribute + event.setParam(try self.encode(parentSpecifier), forKeyword: AE4.Keywords.insertHere) + } + } else { // for all other commands, check if a direct parameter was already given + if hasDirectParameter { // encode the parent specifier as the event's subject attribute + subjectDesc = try self.encode(parentSpecifier) + } else { // else encode parent specifier as event's direct parameter and use null as event's subject attribute + event.setParam(try self.encode(parentSpecifier), forKeyword: AE4.Keywords.directObject) + } + } + } + event.setAttribute(subjectDesc, forKeyword: AE4.Attributes.subject) + // encode requested type (`as`) parameter, if specified; note: most apps ignore this, but a few may recognize it (usually in `get` commands) even if they don't define it in their dictionary (another AppleScript-introduced quirk); e.g. `Finder().home.get(requestedType:FIN.alias) as URL` tells Finder to return a typeAlias descriptor instead of typeObjectSpecifier, which can then be decoded as URL + if let type = requestedType { + event.setDescriptor(NSAppleEventDescriptor(typeCode: type.code), forKeyword: AE4.Keywords.requestedType) + } + // event attributes + // (note: most apps ignore considering/ignoring attributes, and always ignore case and consider everything else) + let (ignorances, consideringIgnoring) = ignorances == nil ? defaultIgnorances : packConsideringAndIgnoringFlags(ignorances!) + event.setAttribute(ignorances, forKeyword: AE4.Attributes.considerations) + event.setAttribute(consideringIgnoring, forKeyword: AE4.Attributes.considsAndIgnores) + // send the event + let sendMode: SendOptions = [.alwaysInteract, .waitForReply] //sendOptions ?? defaultSendMode.union(waitReply ? .waitForReply : .noReply) + let timeout = withTimeout ?? defaultTimeout + var replyEvent: NSAppleEventDescriptor + sentEvent = event + do { + replyEvent = try self.send(event: event, sendMode: sendMode, timeout: timeout) // throws NSError on AEM error + } catch { // handle errors raised by Apple Event Manager (e.g. timeout, process not found) + if RelaunchableErrorCodes.contains((error as NSError).code) && self.target.isRelaunchable && (relaunchMode == .always + || (relaunchMode == .limited && LimitedRelaunchEvents.contains(where: {$0.0 == eventClass && $0.1 == eventID}))) { + // event failed as target process has quit since previous event; recreate AppleEvent with new address and resend + self._targetDescriptor = nil + let event2 = NSAppleEventDescriptor(eventClass: eventClass, eventID: eventID, targetDescriptor: try self.targetDescriptor(), + returnID: AE4.autoGenerateReturnID, transactionID: AE4.anyTransactionID) + let count = event.numberOfItems + if count > 0 { + for i in 1...count { + event2.setParam(event.atIndex(i)!, forKeyword: event.keywordForDescriptor(at: i)) + } + } + for key in [AE4.Attributes.subject, AE4.Attributes.considerations, AE4.Attributes.considsAndIgnores] { + event2.setAttribute(event.attributeDescriptor(forKeyword: key)!, forKeyword: key) + } + replyEvent = try self.send(event: event2, sendMode: sendMode, timeout: timeout) + } else { + throw error + } + } + repliedEvent = replyEvent + if sendMode.contains(.waitForReply) { + if Result.self == ReplyEventDescriptor.self { // return the entire reply event as-is + return ReplyEventDescriptor(descriptor: replyEvent) as! Result + } else if replyEvent.paramDescriptor(forKeyword: AE4.Keywords.errorNumber)?.int32Value ?? 0 != 0 { // check if an application error occurred + throw AutomationError(code: Int(replyEvent.paramDescriptor(forKeyword: AE4.Keywords.errorNumber)!.int32Value)) + } else if let resultDesc = replyEvent.paramDescriptor(forKeyword: AE4.Keywords.directObject) { + return try self.decode(resultDesc) as Result + } // no return value or error, so fall through + } else if sendMode.contains(.queueReply) { // get the return ID that will be used by the reply event so that client code's main loop can identify that reply event in its own event queue later on + guard let returnIDDesc = event.attributeDescriptor(forKeyword: AE4.Attributes.returnID) else { // sanity check + throw AutomationError(code: defaultErrorCode, message: "Can't get keyReturnIDAttr.") + } + return try self.decode(returnIDDesc) + } + // note that some Apple event handlers intentionally return a void result (e.g. `set`, `quit`), and now and again a crusty old Carbon app will forget to supply a return value where one is expected; however, rather than add `COMMAND()->void` methods to glue files (which would only cover the first case), it's simplest just to return an 'empty' value which covers both use cases + if let result = MissingValue as? Result { // this will succeed when T is Any (which it always will be when the caller ignores the command's result) + return result + } + throw AutomationError(code: defaultErrorCode, message: "Caller requested \(Result.self) result but application didn't return anything.") + } catch { + let commandDescription = CommandDescription(name: name, eventClass: eventClass, eventID: eventID, parentSpecifier: parentSpecifier, + directParameter: directParameter, keywordParameters: keywordParameters, + requestedType: requestedType, waitReply: waitReply, + withTimeout: withTimeout, considering: ignorances) + throw CommandError(commandInfo: commandDescription, app: self, event: sentEvent, reply: repliedEvent, cause: error) + } + } + + + // convenience shortcut for dispatching events using raw OSType codes only (the above method also requires human-readable command and parameter names to be supplied for error reporting purposes); users should call this via one of the `sendAppleEvent` methods on `AEApplication`/`AEItem` + + public func sendAppleEvent(eventClass: OSType, eventID: OSType, parentSpecifier: Specifier, parameters: [OSType:Any] = [:], + requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil, + withTimeout: TimeInterval? = nil, ignoring: Considerations? = nil) throws -> T { + var parameters = parameters + let directParameter = parameters.removeValue(forKey: AE4.Keywords.directObject) ?? NoParameter + let keywordParameters: [KeywordParameter] = parameters.map({(name: nil, code: $0, value: $1)}) + return try self.sendAppleEvent(name: nil, eventClass: eventClass, eventID: eventID, parentSpecifier: parentSpecifier, directParameter: directParameter, keywordParameters: keywordParameters, requestedType: requestedType, waitReply: waitReply, sendOptions: sendOptions, withTimeout: withTimeout, ignoring: ignoring) + } + +} + +// MARK: Transactions +// In practice, there are few, if any, currently available apps that support transactions, but it's included for completeness. +extension App { + + public func doTransaction(session: Any? = nil, closure: () throws -> (T)) throws -> T { + var mutex = pthread_mutex_t() + pthread_mutex_init(&mutex, nil) + pthread_mutex_lock(&mutex) + defer { + pthread_mutex_unlock(&mutex) + pthread_mutex_destroy(&mutex) + } + assert(self._transactionID == AE4.anyTransactionID, "Transaction \(self._transactionID) already active.") + self._transactionID = try self.sendAppleEvent(name: nil, eventClass: AE4.Events.Transactions.eventClass, eventID: AE4.Events.Transactions.IDs.begin, parentSpecifier: App.generic.application, directParameter: session as Any) as AETransactionID + defer { + self._transactionID = AE4.anyTransactionID + } + var result: T + do { + result = try closure() + } catch { // abort transaction, then rethrow closure error + let _ = try? self.sendAppleEvent(name: nil, eventClass: AE4.Events.Transactions.eventClass, eventID: AE4.Events.Transactions.IDs.terminated, + parentSpecifier: App.generic.application) as Any + throw error + } // else end transaction + _ = try self.sendAppleEvent(name: nil, eventClass: AE4.Events.Transactions.eventClass, eventID: AE4.Events.Transactions.IDs.end, parentSpecifier: App.generic.application) as Any + return result + } + +} + +/******************************************************************************/ + +/// Used by App.sendAppleEvent() to encode Considerations as enumConsiderations (old-style) and enumConsidsAndIgnores (new-style) attributes +let considerationsTable: [(Consideration, NSAppleEventDescriptor, UInt32, UInt32)] = [ + // note: Swift mistranslates considering/ignoring mask constants as Int, not UInt32, so redefine them here + (.case, NSAppleEventDescriptor(enumCode: AE4.Considerations.case), 0x00000001, 0x00010000), + (.diacritic, NSAppleEventDescriptor(enumCode: AE4.Considerations.diacritic), 0x00000002, 0x00020000), + (.whiteSpace, NSAppleEventDescriptor(enumCode: AE4.Considerations.whiteSpace), 0x00000004, 0x00040000), + (.hyphens, NSAppleEventDescriptor(enumCode: AE4.Considerations.hyphens), 0x00000008, 0x00080000), + (.expansion, NSAppleEventDescriptor(enumCode: AE4.Considerations.expansion), 0x00000010, 0x00100000), + (.punctuation, NSAppleEventDescriptor(enumCode: AE4.Considerations.punctuation), 0x00000020, 0x00200000), + (.numericStrings, NSAppleEventDescriptor(enumCode: AE4.Considerations.numericStrings), 0x00000080, 0x00800000), +] + +private func packConsideringAndIgnoringFlags(_ ignorances: Considerations) -> (NSAppleEventDescriptor, NSAppleEventDescriptor) { + let ignorancesListDesc = NSAppleEventDescriptor.list() + var consideringIgnoringFlags: UInt32 = 0 + for (consideration, considerationDesc, consideringMask, ignoringMask) in considerationsTable { + if ignorances.contains(consideration) { + consideringIgnoringFlags |= ignoringMask + ignorancesListDesc.insert(considerationDesc, at: 0) + } else { + consideringIgnoringFlags |= consideringMask + } + } + // old-style flags (list of enums), new-style flags (bitmask) + return (ignorancesListDesc, NSAppleEventDescriptor(uint32: consideringIgnoringFlags)) +} diff --git a/Sources/AEthereal/AppleEventFormatter.swift b/Sources/AEthereal/AppleEventFormatter.swift new file mode 100644 index 0000000..7e83834 --- /dev/null +++ b/Sources/AEthereal/AppleEventFormatter.swift @@ -0,0 +1,132 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Format an AppleEvent descriptor as Swift source code. Enables user tools to translate application commands from AppleScript to Swift syntax simply by installing a custom SendProc into an AS component instance to intercept outgoing AEs, pass them to formatAppleEvent(), and print the result. +// + +// TO DO: Application object should appear as `APPLICATION()`, not `APPLICATION(name:"/PATH/TO/APP")`, for display in SwiftAutoEdit's command translator -- probably simplest to have a boolean arg to formatAppleEvent that dictates this (since the full version is still useful for debugging work)... might be worth making this an `app/application/fullApplication` enum to cover PREFIXApp case as well + +// TO DO: Symbols aren't displaying correctly within arrays/dictionaries/specifiers (currently appear as `Symbol.NAME` instead of `PREFIX.NAME`), e.g. `TextEdit(name: "/Applications/TextEdit.app").make(new: TED.document, withProperties: [Symbol.text: "foo"])`; `tell app "textedit" to document (text)` -> `TextEdit(name: "/Applications/TextEdit.app").documents[Symbol.text].get()` -- note that a custom Symbol subclass won't work as `description` can't be parameterized with prefix name to use; one option might be a Symbol subclass whose init takes the prefix as param when it's decoded (that probably will work); that said, why isn't Formatter.formatSymbol() doing the job in the first place? (check it has correct prefix) -- it's formatValue() -- when formatting collections, it calls itself and then renders self-formatting objects as-is + +import Foundation +import AppKit + +public enum TerminologyType { + case aete // old and nasty, but reliable; can't be obtained from apps with broken `ascr/gdte` event handlers (e.g. Finder) + case sdef // reliable for Cocoa apps only; may be corrupted when auto-generated for aete-only Carbon apps due to bugs in macOS's AETE-to-SDEF converter and/or limitations in XML/SDEF format (e.g. SDEF format represents OSTypes as four-character strings, but some OSTypes can't be represented as text due to 'unprintable characters', and SDEF format doesn't provide a way to represent those as [e.g.] hex numbers so converter discards them instead) + case none // use default terminology + raw four-char codes only +} + +public func formatAppleEvent(descriptor event: NSAppleEventDescriptor, useTerminology: TerminologyType = .sdef) -> String { // TO DO: return command/reply/error enum, giving caller more choice on how to display + // Format an outgoing or reply AppleEvent (if the latter, only the return value/error description is displayed). + // Caution: if sending events to self, caller MUST use TerminologyType.SDEF or call formatAppleEvent on a background thread, otherwise formatAppleEvent will deadlock the main loop when it tries to fetch host app's AETE via ascr/gdte event. + if event.descriptorType != AE4.Types.appleEvent { // sanity check + return "Can't format Apple event: wrong type: \(formatFourCharCodeString(event.descriptorType))." + } + if + event.attributeDescriptor(forKeyword: AE4.Attributes.eventClass)!.typeCodeValue == AE4.Events.Core.eventClass, + event.attributeDescriptor(forKeyword: AE4.Attributes.eventID)!.typeCodeValue == AE4.Events.Core.IDs.answer + { // it's a reply event, so format error/return value only + let errn = event.paramDescriptor(forKeyword: AE4.Keywords.errorNumber)?.int32Value ?? 0 + if errn != 0 { // format error message + let errs = event.paramDescriptor(forKeyword: AE4.Keywords.errorString)?.stringValue + return AutomationError(code: Int(errn), message: errs).errorDescription! // TO DO: use CommandError? (need to check it's happy with only replyEvent arg) + } else if let reply = event.paramDescriptor(forKeyword: AE4.Keywords.directObject) { // format return value + return formatSAObject((try? App().decodeAsAny(reply)) ?? reply) + } else { + return MissingValue.description + } + } else { // fully format outgoing event + return formatCommand(CommandDescription(event: event, app: App()), applicationObject: App().application) + } +} + +/******************************************************************************/ +// decode AppleEvent descriptor's contents into struct, to be consumed by SpecifierFormatter.formatCommand() + +public struct CommandDescription { + + // note: even when terminology data is available, there's still no guarantee that a command won't have to use raw codes instead (typically due to dodgy terminology; while AS allows mixing of keyword and raw chevron syntax in the same command, it's such a rare defect it's best to stick solely to one or the other) + public enum Signature { + case named(name: String, directParameter: Any, keywordParameters: [(String, Any)], requestedType: Symbol?) + case codes(eventClass: OSType, eventID: OSType, parameters: [OSType:Any]) + } + + // name and parameters + public let signature: Signature // either keywords or four-char codes + + // attributes (note that waitReply and withTimeout values are unreliable when extracted from an existing AppleEvent) + public private(set) var subject: Any? = nil // TO DO: subject or parentSpecifier? (and what, if any, difference does it make?) + public private(set) var waitReply: Bool = true // note that existing AppleEvent descriptors contain keyReplyRequestedAttr, which could be either SendOptions.waitForReply or .queueReply + // TO DO: also include sendOptions for completeness + public private(set) var withTimeout: TimeInterval = defaultTimeout + public private(set) var ignoring: Considerations = [.case] + + // called by sendAppleEvent with a failed command's details + public init(name: String?, eventClass: OSType, eventID: OSType, parentSpecifier: Any?, + directParameter: Any, keywordParameters: [KeywordParameter], + requestedType: Symbol?, waitReply: Bool, withTimeout: TimeInterval?, considering: Considerations?) { + if let commandName = name { + self.signature = .named(name: commandName, directParameter: directParameter, + keywordParameters: keywordParameters.map { ($0!, $2) }, requestedType: requestedType) + } else { + var parameters = [OSType:Any]() + if parameterExists(directParameter) { parameters[AE4.Keywords.directObject] = directParameter } + for (_, code, value) in keywordParameters where parameterExists(value) { parameters[code] = value } + if let symbol = requestedType { parameters[AE4.Keywords.requestedType] = symbol } + self.signature = .codes(eventClass: eventClass, eventID: eventID, parameters: parameters) + } + self.waitReply = waitReply + self.subject = parentSpecifier + if withTimeout != nil { self.withTimeout = withTimeout! } + if considering != nil { self.ignoring = considering! } + } + + // called by [e.g.] SwiftAutoEdit.app with an intercepted AppleEvent descriptor + public init(event: NSAppleEventDescriptor, app: App) { + // decode the event's parameters + var rawParameters = [OSType:Any]() + for i in 1...event.numberOfItems { + let desc = event.atIndex(i)! + rawParameters[event.keywordForDescriptor(at:i)] = (try? app.decodeAsAny(desc)) ?? desc + } + // + let eventClass = event.attributeDescriptor(forKeyword: AE4.Attributes.eventClass)!.typeCodeValue + let eventID = event.attributeDescriptor(forKeyword: AE4.Attributes.eventID)!.typeCodeValue + self.signature = .codes(eventClass: eventClass, eventID: eventID, parameters: rawParameters) + // decode subject attribute, if given + if let desc = event.attributeDescriptor(forKeyword: AE4.Attributes.subject) { + if desc.descriptorType != AE4.Types.null { // typeNull = root application object + self.subject = (try? app.decodeAsAny(desc)) ?? desc // TO DO: double-check formatter knows how to display descriptor (or any other non-specifier) as customRoot + } + } + // decode reply requested and timeout attributes (note: these attributes are unreliable since their values are passed via AESendMessage() rather than packed directly into the AppleEvent) + if let desc = event.attributeDescriptor(forKeyword: AE4.Attributes.replyRequested) { // TO DO: attr is unreliable + // keyReplyRequestedAttr appears to be boolean value encoded as Int32 (1=wait or queue reply; 0=no reply) + if desc.int32Value == 0 { self.waitReply = false } + } + if let timeout = event.attributeDescriptor(forKeyword: AE4.Attributes.timeout) { // TO DO: attr is unreliable + let timeoutInTicks = timeout.int32Value + if timeoutInTicks == -2 { // NoTimeout // TO DO: ditto + self.withTimeout = -2 + } else if timeoutInTicks > 0 { + self.withTimeout = Double(timeoutInTicks) / 60.0 + } + } + // considering/ignoring attributes + if let considersAndIgnoresDesc = event.attributeDescriptor(forKeyword: AE4.Attributes.considsAndIgnores) { + var considersAndIgnores: UInt32 = 0 + (considersAndIgnoresDesc.data as NSData).getBytes(&considersAndIgnores, length: MemoryLayout.size) + if considersAndIgnores != defaultConsidersIgnoresMask { + for (option, _, considersFlag, ignoresFlag) in considerationsTable { + if option == .case { + if considersAndIgnores & ignoresFlag > 0 { self.ignoring.remove(option) } + } else { + if considersAndIgnores & considersFlag > 0 { self.ignoring.insert(option) } + } + } + } + } + } +} diff --git a/Sources/AEthereal/Constants.swift b/Sources/AEthereal/Constants.swift new file mode 100644 index 0000000..044389f --- /dev/null +++ b/Sources/AEthereal/Constants.swift @@ -0,0 +1,1147 @@ +// Originally written by hhas. +// See README.md for licensing information. + +import Darwin.MacTypes +import Foundation.NSAppleEventDescriptor + +public enum AE4 { + + public enum Descriptors { + } + + public enum Classes { + } + + public enum Enumerators { + } + + public enum IndexForm: OSType, CaseIterable { + + case absolutePosition = 0x696E6478 + case name = 0x6E616D65 + case propertyID = 0x70726F70 + case range = 0x72616E67 + case relativePosition = 0x72656C65 + case test = 0x74657374 + case uniqueID = 0x49442020 + case userPropertyID = 0x75737270 + case whose = 0x77686F73 + + } + + public enum LogicalOperator: OSType, CaseIterable { + + case and = 0x414e4420 + case or = 0x4f522020 + case not = 0x4e4f5420 + + } + + public enum Comparison: OSType, CaseIterable { + + case lessThan = 0x3c202020 + case lessThanEquals = 0x3c3d2020 + case greaterThan = 0x3e202020 + case greaterThanEquals = 0x3e3d2020 + case equals = 0x3d202020 + + } + + public enum Containment: OSType, CaseIterable { + + case contains = 0x636f6e74 + case beginsWith = 0x62677774 + case endsWith = 0x656e6473 + + } + + public enum AbsoluteOrdinal: OSType, CaseIterable { + + case first = 0x66697273 + case last = 0x6c617374 + case middle = 0x6d696464 + case random = 0x616e7920 + case all = 0x616c6c20 + } + + public enum RelativeOrdinal: OSType, CaseIterable { + + case next = 0x6e657874 + case previous = 0x70726576 + + } + + public enum InsertionLocation: OSType, CaseIterable { + + case beginning = 0x62676E67 + case end = 0x656E6420 + case before = 0x6265666F + case after = 0x61667465 + } + + public enum TestPredicateKeywords { + } + + public enum ObjectSpecifierKeywords { + } + + public enum RangeSpecifierKeywords { + } + + public enum InsertionSpecifierKeywords { + } + + public enum Considerations { + } + + public enum Suites { + } + + public enum AESymbols { + } + + public enum ASSymbols { + } + + public enum ASOSASymbols { + } + + public enum Keywords { + } + + public enum Attributes { + } + + public enum Events { + } + + public enum OSAErrorKeywords { + } + + public enum OSASymbols { + } + + public enum ASKeywords { + } + + public enum Properties { + } + + public enum ASProperties { + } + + public enum Types { + } + + +} + +extension AE4 { + + /// AECreateAppleEvent: Generate an ID unique to the current session. + static let autoGenerateReturnID: AEReturnID = -1 // Int16 + /// AECreateAppleEvent: Not part of a transaction. + static let anyTransactionID: AETransactionID = 0 // Int32 + + // Not defined in OpenScripting.h for some reason: + + static let inheritedProperties: OSType = 0x6340235e // 'c@#^' + + // AEM doesn't define codes for '!=' or 'in' operators in test clauses, so we define pseudo-codes to represent these: + + /// Encoded as `kAEEquals` + `kAENOT`. + static let notEquals: OSType = 0x00000001 + /// Encoded as AE4. with operands reversed. + static let isIn: OSType = 0x00000002 + +} + +/******************************************************************************/ +// MARK: AE4 symbols +// Note: Swift wrongly maps some of the original constant types to Int instead of OSType/UInt32. +// Use these instead. + +extension AE4.Classes { + + static let aeList: OSType = 0x6C697374 + static let aliasOrString: OSType = 0x73662020 + static let application: OSType = 0x63617070 + static let april: OSType = 0x61707220 + static let arc: OSType = 0x63617263 + static let august: OSType = 0x61756720 + static let boolean: OSType = 0x626F6F6C + static let cell: OSType = 0x6363656C + static let char: OSType = 0x63686120 + static let classIdentifier: OSType = 0x70636C73 + static let closure: OSType = 0x636C7372 + static let coerceLowerCase: OSType = 0x74786C6F + static let coerceOneByteToTwoByte: OSType = 0x74786578 + static let coerceRemoveDiacriticals: OSType = 0x74786463 + static let coerceRemoveHyphens: OSType = 0x74786879 + static let coerceRemovePunctuation: OSType = 0x74787063 + static let coerceRemoveWhiteSpace: OSType = 0x74787773 + static let coerceUpperCase: OSType = 0x74787570 + static let coercion: OSType = 0x636F6563 + static let colorTable: OSType = 0x636C7274 + static let column: OSType = 0x63636F6C + static let constant: OSType = 0x656E756D + static let december: OSType = 0x64656320 + static let devSpec: OSType = 0x63646576 + static let document: OSType = 0x646F6375 + static let drawingArea: OSType = 0x63647277 + static let enumeration: OSType = 0x656E756D + static let eventIdentifier: OSType = 0x65766E74 + static let february: OSType = 0x66656220 + static let file: OSType = 0x66696C65 + static let fixed: OSType = 0x66697864 + static let fixedPoint: OSType = 0x66706E74 + static let fixedRectangle: OSType = 0x66726374 + static let friday: OSType = 0x66726920 + static let html: OSType = 0x68746D6C + static let handleBreakpoint: OSType = 0x6272616B + static let handler: OSType = 0x68616E64 + static let insertionLoc: OSType = 0x696E736C + static let insertionPoint: OSType = 0x63696E73 + static let intlText: OSType = 0x69747874 + static let intlWritingCode: OSType = 0x696E746C + static let item: OSType = 0x6369746D + static let january: OSType = 0x6A616E20 + static let july: OSType = 0x6A756C20 + static let june: OSType = 0x6A756E20 + static let keyForm: OSType = 0x6B66726D + static let keyIdentifier: OSType = 0x6B796964 + static let keystroke: OSType = 0x6B707273 + static let line: OSType = 0x636C696E + static let linkedList: OSType = 0x6C6C7374 + static let list: OSType = 0x6C697374 + static let listElement: OSType = 0x63656C6D + static let listOrRecord: OSType = 0x6C722020 + static let listOrString: OSType = 0x6C732020 + static let listRecordOrString: OSType = 0x6C727320 + static let longDateTime: OSType = 0x6C647420 + static let longFixed: OSType = 0x6C667864 + static let longFixedPoint: OSType = 0x6C667074 + static let longFixedRectangle: OSType = 0x6C667263 + static let longInteger: OSType = 0x6C6F6E67 + static let longPoint: OSType = 0x6C706E74 + static let longRectangle: OSType = 0x6C726374 + static let machine: OSType = 0x6D616368 + static let machineLoc: OSType = 0x6D4C6F63 + static let march: OSType = 0x6D617220 + static let may: OSType = 0x6D617920 + static let missingValue: OSType = 0x6D736E67 + static let monday: OSType = 0x6D6F6E20 + static let month: OSType = 0x6D6E7468 + static let november: OSType = 0x6E6F7620 + static let number: OSType = 0x6E6D6272 + static let numberDateTimeOrString: OSType = 0x6E647320 + static let numberOrDateTime: OSType = 0x6E642020 + static let numberOrString: OSType = 0x6E732020 + static let object: OSType = 0x636F626A + static let objectBeingExamined: OSType = 0x65786D6E + static let objectSpecifier: OSType = 0x6F626A20 + static let october: OSType = 0x6F637420 + static let openableObject: OSType = 0x636F6F62 + static let oval: OSType = 0x636F766C + static let pict: OSType = 0x50494354 + static let preposition: OSType = 0x70726570 + static let procedure: OSType = 0x70726F63 + static let property: OSType = 0x70726F70 + static let rawData: OSType = 0x72646174 + static let real: OSType = 0x646F7562 + static let record: OSType = 0x7265636F + static let rectangle: OSType = 0x63726563 + static let reference: OSType = 0x6F626A20 + static let rotation: OSType = 0x74726F74 + static let row: OSType = 0x63726F77 + static let saturday: OSType = 0x73617420 + static let script: OSType = 0x73637074 + static let seconds: OSType = 0x73636E64 + static let selection: OSType = 0x6373656C + static let september: OSType = 0x73657020 + static let shortInteger: OSType = 0x73686F72 + static let smallReal: OSType = 0x73696E67 + static let storage: OSType = 0x73746F72 + static let string: OSType = 0x54455854 + static let stringClass: OSType = 0x54455854 + static let sunday: OSType = 0x73756E20 + static let symbol: OSType = 0x73796D62 + static let table: OSType = 0x6374626C + static let text: OSType = 0x63747874 + static let thursday: OSType = 0x74687520 + static let tuesday: OSType = 0x74756520 + static let type: OSType = 0x74797065 + static let url: OSType = 0x75726C20 + static let undefined: OSType = 0x756E6466 + static let userIdentifier: OSType = 0x75696420 + static let vector: OSType = 0x76656374 + static let version: OSType = 0x76657273 + static let wednesday: OSType = 0x77656420 + static let weekday: OSType = 0x776B6479 + static let window: OSType = 0x6377696E + static let word: OSType = 0x63776F72 + static let writingCodeInfo: OSType = 0x6369746C + static let zone: OSType = 0x7A6F6E65 + +} + +extension AE4.Enumerators { + + static let capsLockDown: OSType = 0x4B636C6B + static let clearKey: OSType = 0x6B734700 + static let commandDown: OSType = 0x4B636D64 + static let controlDown: OSType = 0x4B63746C + static let deleteKey: OSType = 0x6B733300 + static let downArrowKey: OSType = 0x6B737D00 + static let endKey: OSType = 0x6B737700 + static let enterKey: OSType = 0x6B734C00 + static let escapeKey: OSType = 0x6B733500 + static let f10Key: OSType = 0x6B736D00 + static let f11Key: OSType = 0x6B736700 + static let f12Key: OSType = 0x6B736F00 + static let f13Key: OSType = 0x6B736900 + static let f14Key: OSType = 0x6B736B00 + static let f15Key: OSType = 0x6B737100 + static let f1Key: OSType = 0x6B737A00 + static let f2Key: OSType = 0x6B737800 + static let f3Key: OSType = 0x6B736300 + static let f4Key: OSType = 0x6B737600 + static let f5Key: OSType = 0x6B736000 + static let f6Key: OSType = 0x6B736100 + static let f7Key: OSType = 0x6B736200 + static let f8Key: OSType = 0x6B736400 + static let f9Key: OSType = 0x6B736500 + static let forwardDelKey: OSType = 0x6B737500 + static let helpKey: OSType = 0x6B737200 + static let homeKey: OSType = 0x6B737300 + static let keyKind: OSType = 0x656B7374 + static let leftArrowKey: OSType = 0x6B737B00 + static let modifiers: OSType = 0x654D6473 + static let optionDown: OSType = 0x4B6F7074 + static let pageDownKey: OSType = 0x6B737900 + static let pageUpKey: OSType = 0x6B737400 + static let pointingDevice: OSType = 0x65647064 + static let postScript: OSType = 0x65707073 + static let returnKey: OSType = 0x6B732400 + static let rightArrowKey: OSType = 0x6B737C00 + static let scheme: OSType = 0x65736368 + static let shiftDown: OSType = 0x4B736674 + static let tabKey: OSType = 0x6B733000 + static let tokenRing: OSType = 0x65746F6B + static let upArrowKey: OSType = 0x6B737E00 + static let urlAFP: OSType = 0x61667020 + static let urlAT: OSType = 0x61742020 + static let urlEPPC: OSType = 0x65707063 + static let urlFTP: OSType = 0x66747020 + static let urlFile: OSType = 0x66696C65 + static let urlGopher: OSType = 0x67706872 + static let urlHTTP: OSType = 0x68747470 + static let urlHTTPS: OSType = 0x68747073 + static let urlIMAP: OSType = 0x696D6170 + static let urlLDAP: OSType = 0x756C6470 + static let urlLaunch: OSType = 0x6C61756E + static let urlMail: OSType = 0x6D61696C + static let urlMailbox: OSType = 0x6D626F78 + static let urlMessage: OSType = 0x6D657373 + static let urlMulti: OSType = 0x6D756C74 + static let urlNFS: OSType = 0x756E6673 + static let urlNNTP: OSType = 0x6E6E7470 + static let urlNews: OSType = 0x6E657773 + static let urlPOP: OSType = 0x75706F70 + static let urlRTSP: OSType = 0x72747370 + static let urlSNews: OSType = 0x736E7773 + static let urlTelnet: OSType = 0x746C6E74 + static let urlUnknown: OSType = 0x75726C3F + +} + +extension AE4.TestPredicateKeywords { + + static let comparisonOperator: OSType = 0x72656c6f + static let firstObject: OSType = 0x6f626a31 + static let secondObject: OSType = 0x6f626a32 + + static let logicalOperator: OSType = 0x6c6f6763 + static let logicalTerms: OSType = 0x7465726d + static let object: OSType = 0x6B6F626A + +} + +extension AE4.ObjectSpecifierKeywords { + + static let desiredClass: OSType = 0x77616e74 + static let container: OSType = 0x66726f6d + static let keyForm: OSType = 0x666f726d + static let keyData: OSType = 0x73656c64 + +} + +extension AE4.RangeSpecifierKeywords { + + static let start: OSType = 0x73746172 + static let stop: OSType = 0x73746f70 + +} + +extension AE4.InsertionSpecifierKeywords { + + static let object: OSType = 0x6B6F626A + static let position: OSType = 0x6B706F73 + +} + +extension AE4.Considerations { + + static let `case`: OSType = 0x63617365 + static let diacritic: OSType = 0x64696163 + static let whiteSpace: OSType = 0x77686974 + static let hyphens: OSType = 0x68797068 + static let expansion: OSType = 0x65787061 + static let punctuation: OSType = 0x70756E63 + static let numericStrings: OSType = 0x6E756D65 + +} + +extension AE4.AESymbols { + + static let about: OSType = 0x61626F75 + static let activate: OSType = 0x61637476 + static let aliasSelection: OSType = 0x73616C69 + static let all: OSType = 0x616C6C20 + static let allCaps: OSType = 0x616C6370 + static let any: OSType = 0x616E7920 + static let applicationClass: OSType = 0x6170706C + static let applicationDied: OSType = 0x6F626974 + static let ask: OSType = 0x61736B20 + static let autoDown: OSType = 0x6175746F + static let beginsWith: OSType = 0x62677774 + static let bold: OSType = 0x626F6C64 + static let caseSensEquals: OSType = 0x63736571 + static let centered: OSType = 0x63656E74 + static let changeView: OSType = 0x76696577 + static let clone: OSType = 0x636C6F6E + static let close: OSType = 0x636C6F73 + static let commandClass: OSType = 0x636D6E64 + static let condensed: OSType = 0x636F6E64 + static let contains: OSType = 0x636F6E74 + static let copy: OSType = 0x636F7079 + static let countElements: OSType = 0x636E7465 + static let createElement: OSType = 0x6372656C + static let cut: OSType = 0x63757420 + static let deactivate: OSType = 0x64616374 + static let delete: OSType = 0x64656C6F + static let diskEvent: OSType = 0x6469736B + static let doObjectsExist: OSType = 0x646F6578 + static let doScript: OSType = 0x646F7363 + static let down: OSType = 0x646F776E + static let drag: OSType = 0x64726167 + static let duplicateSelection: OSType = 0x73647570 + static let emptyTrash: OSType = 0x656D7074 + static let endsWith: OSType = 0x656E6473 + static let equals: OSType = 0x3D202020 + static let expanded: OSType = 0x70657870 + static let `false`: OSType = 0x66616C73 + static let fast: OSType = 0x66617374 + static let first: OSType = 0x66697273 + static let getClassInfo: OSType = 0x716F626A + static let getData: OSType = 0x67657464 + static let getDataSize: OSType = 0x6473697A + static let getEventInfo: OSType = 0x67746569 + static let getInfoSelection: OSType = 0x73696E66 + static let getPrivilegeSelection: OSType = 0x73707276 + static let greaterThan: OSType = 0x3E202020 + static let greaterThanEquals: OSType = 0x3E3D2020 + static let grow: OSType = 0x67726F77 + static let hidden: OSType = 0x6869646E + static let highLevel: OSType = 0x68696768 + static let isUniform: OSType = 0x6973756E + static let keyClass: OSType = 0x6B657963 + static let keyDown: OSType = 0x6B64776E + static let last: OSType = 0x6C617374 + static let logOut: OSType = 0x6C6F676F + static let lowercase: OSType = 0x6C6F7763 + static let makeObjectsVisible: OSType = 0x6D766973 + static let middle: OSType = 0x6D696464 + static let modifiable: OSType = 0x6D6F6466 + static let mouseClass: OSType = 0x6D6F7573 + static let mouseDown: OSType = 0x6D64776E + static let mouseDownInBack: OSType = 0x6D64626B + static let move: OSType = 0x6D6F7665 + static let moved: OSType = 0x6D6F7665 + static let navigationKey: OSType = 0x6E617665 + static let next: OSType = 0x6E657874 + static let no: OSType = 0x6E6F2020 + static let noArrow: OSType = 0x61726E6F + static let nonmodifiable: OSType = 0x6E6D6F64 + static let notifyRecording: OSType = 0x72656372 + static let notifyStartRecording: OSType = 0x72656331 + static let notifyStopRecording: OSType = 0x72656330 + static let nullEvent: OSType = 0x6E756C6C + static let `open`: OSType = 0x6F646F63 + static let openContents: OSType = 0x6F636F6E + static let openDocuments: OSType = 0x6F646F63 + static let openSelection: OSType = 0x736F7065 + static let outline: OSType = 0x6F75746C + static let pageSetup: OSType = 0x70677375 + static let paste: OSType = 0x70617374 + static let plain: OSType = 0x706C616E + static let previous: OSType = 0x70726576 + static let print: OSType = 0x70646F63 + static let printDocuments: OSType = 0x70646F63 + static let printSelection: OSType = 0x73707269 + static let printWindow: OSType = 0x7077696E + static let promise: OSType = 0x70726F6D + static let quitAll: OSType = 0x71756961 + static let quitApplication: OSType = 0x71756974 + static let rawKey: OSType = 0x726B6579 + static let reallyLogOut: OSType = 0x726C676F + static let redo: OSType = 0x7265646F + static let regular: OSType = 0x7265676C + static let reopenApplication: OSType = 0x72617070 + static let replace: OSType = 0x72706C63 + static let resized: OSType = 0x7273697A + static let restart: OSType = 0x72657374 + static let resume: OSType = 0x72736D65 + static let revealSelection: OSType = 0x73726576 + static let revert: OSType = 0x72767274 + static let save: OSType = 0x73617665 + static let scrapEvent: OSType = 0x73637270 + static let scriptingSizeResource: OSType = 0x7363737A + static let select: OSType = 0x736C6374 + static let setData: OSType = 0x73657464 + static let setPosition: OSType = 0x706F736E + static let shadow: OSType = 0x73686164 + static let sharedScriptHandler: OSType = 0x77736370 + static let showClipboard: OSType = 0x7368636C + static let showPreferences: OSType = 0x70726566 + static let showRestartDialog: OSType = 0x72727374 + static let showShutdownDialog: OSType = 0x7273646E + static let shutDown: OSType = 0x73687574 + static let sleep: OSType = 0x736C6570 + static let specialClassProperties: OSType = 0x63402321 + static let startRecording: OSType = 0x72656361 + static let stopRecording: OSType = 0x72656363 + static let stoppedMoving: OSType = 0x73746F70 + static let `subscript`: OSType = 0x73627363 + static let suspend: OSType = 0x73757370 + static let terminologyExtension: OSType = 0x61657465 + static let `true`: OSType = 0x74727565 + static let underline: OSType = 0x756E646C + static let undo: OSType = 0x756E646F + static let up: OSType = 0x75702020 + static let update: OSType = 0x75706474 + static let userTerminology: OSType = 0x61657574 + static let virtualKey: OSType = 0x6B657963 + static let wakeUpEvent: OSType = 0x77616B65 + static let wholeWordEquals: OSType = 0x77776571 + static let windowClass: OSType = 0x77696E64 + static let yes: OSType = 0x79657320 + static let zoom: OSType = 0x7A6F6F6D + +} + +extension AE4.ASSymbols { + + static let add: OSType = 0x2B202020 + static let comesAfter: OSType = 0x63616672 + static let comesBefore: OSType = 0x63626672 + static let comment: OSType = 0x636D6E74 + static let commentEvent: OSType = 0x636D6E74 + static let concatenate: OSType = 0x63636174 + static let considerReplies: OSType = 0x726D7465 + static let contains: OSType = 0x636F6E74 + static let currentApplication: OSType = 0x63757261 + static let divide: OSType = 0x2F202020 + static let endsWith: OSType = 0x656E6473 + static let equal: OSType = 0x3D202020 + static let errorEventCode: OSType = 0x65727220 + static let greaterThan: OSType = 0x3E202020 + static let greaterThanOrEqual: OSType = 0x3E3D2020 + static let hasOpenHandler: OSType = 0x68736F64 + static let initializeEventCode: OSType = 0x696E6974 + static let magicEndTellEvent: OSType = 0x74656E64 + static let magicTellEvent: OSType = 0x74656C6C + static let multiply: OSType = 0x2A202020 + static let negate: OSType = 0x6E656720 + static let prepositionalSubroutine: OSType = 0x70736272 + static let quotient: OSType = 0x64697620 + static let remainder: OSType = 0x6D6F6420 + static let startLogEvent: OSType = 0x6C6F6731 + static let startsWith: OSType = 0x62677774 + static let stopLogEvent: OSType = 0x6C6F6730 + static let subroutineEvent: OSType = 0x70736272 + static let subtract: OSType = 0x2D202020 + +} + +extension AE4.Suites { + + static let coreSuite: OSType = 0x636F7265 + static let getSuiteInfo: OSType = 0x67747369 + static let internetSuite: OSType = 0x6775726C + static let requiredSuite: OSType = 0x72657164 + static let tableSuite: OSType = 0x74626C73 + static let textSuite: OSType = 0x54455854 + static let scriptEditorSuite: OSType = 0x546F7953 + static let asTypeNamesSuite: OSType = 0x74706E6D + static let osaSuite: OSType = 0x61736372 + +} + +extension AE4.ASOSASymbols { + + static let _kAppleScriptSubtype: OSType = 0x61736372 + +} + +extension AE4.Keywords { + + static let directObject: OSType = 0x2D2D2D2D + static let requestedType: OSType = 0x72747970 + + static let errorNumber: OSType = 0x6572726E + static let errorString: OSType = 0x65727273 + static let processSerialNumber: OSType = 0x70736E20 + + /// "make" -> "at" parameter + static let insertHere: OSType = 0x696E7368 + +} + +extension AE4.Attributes { + + static let eventClass: OSType = 0x6576636C + static let eventID: OSType = 0x65766964 + static let eventSource: OSType = 0x65737263 + static let interactLevel: OSType = 0x696E7465 + static let optionalKeyword: OSType = 0x6F70746B + static let originalAddress: OSType = 0x66726F6D + static let replyPort: OSType = 0x72657070 + static let replyRequested: OSType = 0x72657071 + static let returnID: OSType = 0x72746964 + static let subject: OSType = 0x7375626A + static let timeout: OSType = 0x74696D6F + static let transactionID: OSType = 0x7472616E + static let considerations: OSType = 0x636F6E73 + static let considsAndIgnores: OSType = 0x63736967 + +} + +extension AE4.Events { + + public enum Core { + + static let eventClass: OSType = 0x61657674 + + public enum IDs { + + /// Event ID of reply events. + static let answer: OSType = 0x616E7372 + + static let openApplication: OSType = 0x6F617070 + + } + + } + + public enum Transactions { + + static let eventClass: OSType = 0x6D697363 + + public enum IDs { + + static let begin: OSType = 0x62656769 + static let end: OSType = 0x656E6474 + static let terminated: OSType = 0x7474726D + + } + + } + + public enum AppleScript { + + static let eventClass: OSType = 0x61736372 + + public enum IDs { + + /// Call a user-defined AppleScript subroutine. + static let callSubroutine: OSType = 0x70736272 + + /// No-op. + static let launch: OSType = 0x6E6F6F70 + + /// Request app terminology in 'aete' resource format. + /// (AETE means AppleEvent Trminology Extension.) + static let getAETE: OSType = 0x67647465 + /// Request AppleScript built-in terminology in 'aete' resource format. + /// (AEUT means AppleEvent User Terminology.) + static let getAEUT: OSType = 0x67647574 + + static let updateAETE: OSType = 0x75647465 + static let updateAEUT: OSType = 0x75647574 + + /// Sent by OSA to current application with a chunk of recorded script text. + static let recordedText: OSType = 0x72656364 + + } + + public enum Keywords { + + /// Name of the user-defined subroutine for callSubroutine. + static let subroutineName: OSType = 0x736E616D + /// Positional subroutine arguments for callSubroutine. + static let positionalArguments: OSType = 0x70617267 + + // Keywords for predefined "prepositional" subroutine parameter names. + public enum Prepositions { + + static let about: OSType = 0x61626F75 + static let above: OSType = 0x61627665 + static let against: OSType = 0x61677374 + static let apartFrom: OSType = 0x61707274 + static let around: OSType = 0x61726E64 + static let asideFrom: OSType = 0x61736466 + static let at: OSType = 0x61742020 + static let below: OSType = 0x62656C77 + static let beneath: OSType = 0x626E7468 + static let beside: OSType = 0x62736964 + static let between: OSType = 0x6274776E + static let by: OSType = 0x62792020 + static let `for`: OSType = 0x666F7220 + static let from: OSType = 0x66726F6D + static let given: OSType = 0x6769766E + static let `in`: OSType = 0x696E2020 + static let insteadOf: OSType = 0x6973746F + static let into: OSType = 0x696E746F + static let on: OSType = 0x6F6E2020 + static let onto: OSType = 0x6F6E746F + static let outOf: OSType = 0x6F75746F + static let over: OSType = 0x6F766572 + static let since: OSType = 0x736E6365 + static let through: OSType = 0x74686768 + static let thru: OSType = 0x74687275 + static let to: OSType = 0x746F2020 + static let under: OSType = 0x756E6472 + static let until: OSType = 0x74696C6C + static let with: OSType = 0x77697468 + static let without: OSType = 0x776F7574 + + } + + } + + } + + public enum DigitalHub { + + static let eventClass: OSType = 0x64687562 + + public enum IDs { + + static let blankCD: OSType = 0x62636420 + static let blankDVD: OSType = 0x62647664 + static let musicCD: OSType = 0x61756364 + static let pictureCD: OSType = 0x70696364 + static let videoDVD: OSType = 0x76647664 + + } + + } + + public enum FolderActions { + + public enum IDs { + + static let opened: OSType = 0x666F706E + static let closed: OSType = 0x66636C6F + static let itemsAdded: OSType = 0x66676574 + static let itemsRemoved: OSType = 0x666C6F73 + static let windowMoved: OSType = 0x6673697A + + } + + public enum Keywords { + + /// Size of moved window. + static let newSize: OSType = 0x666E737A + + } + + } + +} + +extension AE4.OSAErrorKeywords { + + static let app: OSType = 0x65726170 + static let args: OSType = 0x65727261 + static let briefMessage: OSType = 0x65727262 + static let expectedType: OSType = 0x65727274 + static let message: OSType = 0x65727273 + static let number: OSType = 0x6572726E + static let offendingObject: OSType = 0x65726F62 + static let partialResult: OSType = 0x70746C72 + static let range: OSType = 0x65726E67 + +} + +extension AE4.OSASymbols { + + static let genericScriptingComponentSubtype: OSType = 0x73637074 + static let scriptBestType: OSType = 0x62657374 + static let scriptIsModified: OSType = 0x6D6F6469 + static let scriptIsTypeCompiledScript: OSType = 0x63736372 + static let scriptIsTypeScriptContext: OSType = 0x636E7478 + static let scriptIsTypeScriptValue: OSType = 0x76616C75 + + static let dialectCode: OSType = 0x64636F64 + static let dialectLangCode: OSType = 0x646C6364 + static let dialectName: OSType = 0x646E616D + static let dialectScriptCode: OSType = 0x64736364 + static let sourceEnd: OSType = 0x73726365 + static let sourceStart: OSType = 0x73726373 + +} + +extension AE4.ASKeywords { + + static let userRecordFields: OSType = 0x75737266 + +} + +extension AE4.Properties { + + static let bestType: OSType = 0x70627374 + static let bounds: OSType = 0x70626E64 + static let `class`: OSType = 0x70636C73 + static let clipboard: OSType = 0x70636C69 + static let color: OSType = 0x636F6C72 + static let contents: OSType = 0x70636E74 + static let defaultType: OSType = 0x64656674 + static let enabled: OSType = 0x656E626C + static let endPoint: OSType = 0x70656E64 + static let font: OSType = 0x666F6E74 + static let hasCloseBox: OSType = 0x68636C62 + static let hasTitleBar: OSType = 0x70746974 + static let id: OSType = 0x49442020 + static let index: OSType = 0x70696478 + static let inherits: OSType = 0x6340235E + static let insertionLoc: OSType = 0x70696E73 + static let isFloating: OSType = 0x6973666C + static let isFrontProcess: OSType = 0x70697366 + static let isModal: OSType = 0x706D6F64 + static let isModified: OSType = 0x696D6F64 + static let isResizable: OSType = 0x7072737A + static let isStationeryPad: OSType = 0x70737064 + static let isZoomable: OSType = 0x69737A6D + static let isZoomed: OSType = 0x707A756D + static let itemNumber: OSType = 0x69746D6E + static let keyKind: OSType = 0x6B6B6E64 + static let keystrokeKey: OSType = 0x6B4D7367 + static let langCode: OSType = 0x706C6364 + static let length: OSType = 0x6C656E67 + static let name: OSType = 0x706E616D + static let newElementLoc: OSType = 0x706E656C + static let path: OSType = 0x46545063 + static let properties: OSType = 0x70414C4C + static let protection: OSType = 0x7070726F + static let rest: OSType = 0x72657374 + static let reverse: OSType = 0x72767365 + static let script: OSType = 0x73637074 + static let scriptCode: OSType = 0x70736364 + static let scriptTag: OSType = 0x70736374 + static let selected: OSType = 0x73656C63 + static let selection: OSType = 0x73656C65 + static let textItemDelimiters: OSType = 0x7478646C + static let url: OSType = 0x7055524C + static let version: OSType = 0x76657273 + static let visible: OSType = 0x70766973 + +} + +extension AE4.ASProperties { + + static let dateString: OSType = 0x64737472 + static let day: OSType = 0x64617920 + static let days: OSType = 0x64617973 + static let hours: OSType = 0x686F7572 + static let it: OSType = 0x69742020 + static let me: OSType = 0x6D652020 + static let minutes: OSType = 0x6D696E20 + static let month: OSType = 0x6D6E7468 + static let parent: OSType = 0x70617265 + static let pi: OSType = 0x70692020 + static let printDepth: OSType = 0x70726470 + static let printLength: OSType = 0x70726C6E + static let quote: OSType = 0x71756F74 + static let result: OSType = 0x72736C74 + static let `return`: OSType = 0x72657420 + static let seconds: OSType = 0x73656373 + static let space: OSType = 0x73706163 + static let tab: OSType = 0x74616220 + static let time: OSType = 0x74696D65 + static let timeString: OSType = 0x74737472 + static let topLevelScript: OSType = 0x61736372 + static let weekday: OSType = 0x776B6479 + static let weeks: OSType = 0x7765656B + static let year: OSType = 0x79656172 + +} + +extension AE4.Types { + + static let _128BitFloatingPoint: OSType = 0x6C64626C + static let list: OSType = 0x6C697374 + static let record: OSType = 0x7265636F + static let aete: OSType = 0x61657465 + static let aeText: OSType = 0x74545854 + static let aeut: OSType = 0x61657574 + static let asStorage: OSType = 0x61736372 + static let absoluteOrdinal: OSType = 0x6162736F + static let alias: OSType = 0x616C6973 + static let appParameters: OSType = 0x61707061 + static let applSignature: OSType = 0x7369676E + static let appleEvent: OSType = 0x61657674 + static let appleScript: OSType = 0x61736372 + static let applicationBundleID: OSType = 0x62756E64 + static let applicationURL: OSType = 0x6170726C + static let arc: OSType = 0x63617263 + static let best: OSType = 0x62657374 + static let bookmarkData: OSType = 0x626D726B + static let boolean: OSType = 0x626F6F6C + static let cfAbsoluteTime: OSType = 0x63666174 + static let cfArrayRef: OSType = 0x63666172 + static let cfAttributedStringRef: OSType = 0x63666173 + static let cfBooleanRef: OSType = 0x63667466 + static let cfDictionaryRef: OSType = 0x63666463 + static let cfMutableArrayRef: OSType = 0x63666D61 + static let cfMutableAttributedStringRef: OSType = 0x63666161 + static let cfMutableDictionaryRef: OSType = 0x63666D64 + static let cfMutableStringRef: OSType = 0x63666D73 + static let cfNumberRef: OSType = 0x63666E62 + static let cfStringRef: OSType = 0x63667374 + static let cfTypeRef: OSType = 0x63667479 + static let cString: OSType = 0x63737472 + static let cell: OSType = 0x6363656C + static let centimeters: OSType = 0x636D7472 + static let classInfo: OSType = 0x67636C69 + static let colorTable: OSType = 0x636C7274 + static let column: OSType = 0x63636F6C + static let comp: OSType = 0x636F6D70 + static let compDescriptor: OSType = 0x636D7064 + static let componentInstance: OSType = 0x636D7069 + static let cubicCentimeter: OSType = 0x63636D74 + static let cubicFeet: OSType = 0x63666574 + static let cubicInches: OSType = 0x6375696E + static let cubicMeters: OSType = 0x636D6574 + static let cubicYards: OSType = 0x63797264 + static let currentContainer: OSType = 0x63636E74 + static let dashStyle: OSType = 0x74646173 + static let data: OSType = 0x74647461 + static let decimalStruct: OSType = 0x6465636D + static let degreesC: OSType = 0x64656763 + static let degreesF: OSType = 0x64656766 + static let degreesK: OSType = 0x6465676B + static let elemInfo: OSType = 0x656C696E + static let enumerated: OSType = 0x656E756D + static let enumeration: OSType = 0x656E756D + static let eventInfo: OSType = 0x6576696E + static let eventRecord: OSType = 0x65767263 + static let eventRef: OSType = 0x65767266 + static let extended: OSType = 0x65787465 + static let fsRef: OSType = 0x66737266 + static let fss: OSType = 0x66737320 + static let `false`: OSType = 0x66616C73 + static let feet: OSType = 0x66656574 + static let fileURL: OSType = 0x6675726C + static let finderWindow: OSType = 0x6677696E + static let fixed: OSType = 0x66697864 + static let fixedPoint: OSType = 0x66706E74 + static let fixedRectangle: OSType = 0x66726374 + static let float: OSType = 0x646F7562 + static let gif: OSType = 0x47494666 + static let gallons: OSType = 0x67616C6E + static let grams: OSType = 0x6772616D + static let ieee32BitFloatingPoint: OSType = 0x73696E67 + static let ieee64BitFloatingPoint: OSType = 0x646F7562 + static let iso8601DateTime: OSType = 0x69736F74 + static let inches: OSType = 0x696E6368 + static let indexDescriptor: OSType = 0x696E6465 + static let insertionLoc: OSType = 0x696E736C + static let integer: OSType = 0x6C6F6E67 + static let intlText: OSType = 0x69747874 + static let intlWritingCode: OSType = 0x696E746C + static let jpeg: OSType = 0x4A504547 + static let kernelProcessID: OSType = 0x6B706964 + static let keyword: OSType = 0x6B657977 + static let kilograms: OSType = 0x6B67726D + static let kilometers: OSType = 0x6B6D7472 + static let liters: OSType = 0x6C697472 + static let logicalDescriptor: OSType = 0x6C6F6769 + static let longDateTime: OSType = 0x6C647420 + static let longFixed: OSType = 0x6C667864 + static let longFixedPoint: OSType = 0x6C667074 + static let longFixedRectangle: OSType = 0x6C667263 + static let longFloat: OSType = 0x646F7562 + static let longInteger: OSType = 0x6C6F6E67 + static let longPoint: OSType = 0x6C706E74 + static let longRectangle: OSType = 0x6C726374 + static let machPort: OSType = 0x706F7274 + static let machineLoc: OSType = 0x6D4C6F63 + static let magnitude: OSType = 0x6D61676E + static let meters: OSType = 0x6D657472 + static let miles: OSType = 0x6D696C65 + static let null: OSType = 0x6E756C6C + static let osaDialectInfo: OSType = 0x6469666F + static let osaErrorRange: OSType = 0x65726E67 + static let osaGenericStorage: OSType = 0x73637074 + static let objectBeingExamined: OSType = 0x65786D6E + static let objectSpecifier: OSType = 0x6F626A20 + static let offsetArray: OSType = 0x6F666179 + static let ounces: OSType = 0x6F7A7320 + static let oval: OSType = 0x636F766C + static let pString: OSType = 0x70737472 + static let paramInfo: OSType = 0x706D696E + static let pict: OSType = 0x50494354 + static let pounds: OSType = 0x6C627320 + static let processSerialNumber: OSType = 0x70736E20 + static let propInfo: OSType = 0x70696E66 + static let property: OSType = 0x70726F70 + static let ptr: OSType = 0x70747220 + static let quarts: OSType = 0x71727473 + static let rgb16: OSType = 0x74723136 + static let rgb96: OSType = 0x74723936 + static let qdPoint: OSType = 0x51447074 + static let qdRectangle: OSType = 0x71647274 + static let rgbColor: OSType = 0x63524742 + static let rangeDescriptor: OSType = 0x72616E67 + static let rectangle: OSType = 0x63726563 + static let relativeDescriptor: OSType = 0x72656C20 + static let replyPortAttr: OSType = 0x72657070 + static let rotation: OSType = 0x74726F74 + static let roundedRectangle: OSType = 0x63727263 + static let row: OSType = 0x63726F77 + static let sInt16: OSType = 0x73686F72 + static let sInt32: OSType = 0x6C6F6E67 + static let sInt64: OSType = 0x636F6D70 + static let smFloat: OSType = 0x73696E67 + static let smInt: OSType = 0x73686F72 + static let script: OSType = 0x73637074 + static let scszResource: OSType = 0x7363737A + static let sectionH: OSType = 0x73656374 + static let shortFloat: OSType = 0x73696E67 + static let shortInteger: OSType = 0x73686F72 + static let sound: OSType = 0x736E6420 + static let squareFeet: OSType = 0x73716674 + static let squareKilometers: OSType = 0x73716B6D + static let squareMeters: OSType = 0x7371726D + static let squareMiles: OSType = 0x73716D69 + static let squareYards: OSType = 0x73717964 + static let styledText: OSType = 0x53545854 + static let styledUnicodeText: OSType = 0x73757478 + static let tiff: OSType = 0x54494646 + static let table: OSType = 0x6374626C + static let text: OSType = 0x54455854 + static let textRange: OSType = 0x7478726E + static let textRangeArray: OSType = 0x74726179 + static let textStyles: OSType = 0x74737479 + static let token: OSType = 0x746F6B65 + static let `true`: OSType = 0x74727565 + static let type: OSType = 0x74797065 + static let uInt16: OSType = 0x75736872 + static let uInt32: OSType = 0x6D61676E + static let uInt64: OSType = 0x75636F6D + static let utf16ExternalRepresentation: OSType = 0x75743136 + static let utf8Text: OSType = 0x75746638 + static let unicodeText: OSType = 0x75747874 + static let userRecordFields: OSType = 0x6C697374 + static let version: OSType = 0x76657273 + static let whoseDescriptor: OSType = 0x77686F73 + static let whoseRange: OSType = 0x77726E67 + static let wildCard: OSType = 0x2A2A2A2A + static let yards: OSType = 0x79617264 + +} + +/******************************************************************************/ +// MARK: Pre-encoded symbols + +extension AE4.Descriptors { + + public enum Types { + + static let property = NSAppleEventDescriptor(typeCode: AE4.Types.property) + + } + + public enum IndexForms { + + static let property = NSAppleEventDescriptor(enumCode: AE4.IndexForm.propertyID.rawValue) + static let userProperty = NSAppleEventDescriptor(enumCode: AE4.IndexForm.userPropertyID.rawValue) + static let absolutePosition = NSAppleEventDescriptor(enumCode: AE4.IndexForm.absolutePosition.rawValue) + static let name = NSAppleEventDescriptor(enumCode: AE4.IndexForm.name.rawValue) + static let uniqueID = NSAppleEventDescriptor(enumCode: AE4.IndexForm.uniqueID.rawValue) + static let relativePosition = NSAppleEventDescriptor(enumCode: AE4.IndexForm.relativePosition.rawValue) + static let range = NSAppleEventDescriptor(enumCode: AE4.IndexForm.range.rawValue) + static let test = NSAppleEventDescriptor(enumCode: AE4.IndexForm.test.rawValue) + + } + + public enum InsertionLocations { + + static let beginning = NSAppleEventDescriptor(enumCode: AE4.InsertionLocation.beginning.rawValue) + static let end = NSAppleEventDescriptor(enumCode: AE4.InsertionLocation.end.rawValue) + static let before = NSAppleEventDescriptor(enumCode: AE4.InsertionLocation.before.rawValue) + static let after = NSAppleEventDescriptor(enumCode: AE4.InsertionLocation.after.rawValue) + + } + + public enum AbsolutePositions { + + static let first = NSAppleEventDescriptor(type: AE4.Types.absoluteOrdinal, code: AE4.AbsoluteOrdinal.first.rawValue) + static let middle = NSAppleEventDescriptor(type: AE4.Types.absoluteOrdinal, code: AE4.AbsoluteOrdinal.middle.rawValue) + static let last = NSAppleEventDescriptor(type: AE4.Types.absoluteOrdinal, code: AE4.AbsoluteOrdinal.last.rawValue) + static let any = NSAppleEventDescriptor(type: AE4.Types.absoluteOrdinal, code: AE4.AbsoluteOrdinal.random.rawValue) + static let all = NSAppleEventDescriptor(type: AE4.Types.absoluteOrdinal, code: AE4.AbsoluteOrdinal.all.rawValue) + + } + + public enum RelativePositions { + + static let previous = NSAppleEventDescriptor(enumCode: AE4.RelativeOrdinal.previous.rawValue) + static let next = NSAppleEventDescriptor(enumCode: AE4.RelativeOrdinal.next.rawValue) + + } + + public enum ComparisonTests { + + static let lessThan = NSAppleEventDescriptor(enumCode: AE4.Comparison.lessThan.rawValue) + static let lessThanEquals = NSAppleEventDescriptor(enumCode: AE4.Comparison.lessThanEquals.rawValue) + static let equals = NSAppleEventDescriptor(enumCode: AE4.Comparison.equals.rawValue) + // Encoded as !(op1 == op2) + static let notEquals = NSAppleEventDescriptor(enumCode: AE4.notEquals) + static let greaterThan = NSAppleEventDescriptor(enumCode: AE4.Comparison.greaterThan.rawValue) + static let greaterThanEquals = NSAppleEventDescriptor(enumCode: AE4.Comparison.greaterThanEquals.rawValue) + + } + + public enum ContainmentTests { + + static let beginsWith = NSAppleEventDescriptor(enumCode: AE4.Containment.beginsWith.rawValue) + static let endsWith = NSAppleEventDescriptor(enumCode: AE4.Containment.endsWith.rawValue) + static let contains = NSAppleEventDescriptor(enumCode: AE4.Containment.contains.rawValue) + // Encoded as op2.contains(op1) + static let isIn = NSAppleEventDescriptor(enumCode: AE4.isIn) + + } + + public enum LogicalTests { + + // logic tests + static let and = NSAppleEventDescriptor(enumCode: AE4.LogicalOperator.and.rawValue) + static let or = NSAppleEventDescriptor(enumCode: AE4.LogicalOperator.or.rawValue) + static let not = NSAppleEventDescriptor(enumCode: AE4.LogicalOperator.not.rawValue) + + } + +} diff --git a/Sources/AEthereal/Errors.swift b/Sources/AEthereal/Errors.swift new file mode 100644 index 0000000..88cf316 --- /dev/null +++ b/Sources/AEthereal/Errors.swift @@ -0,0 +1,293 @@ +// Originally written by hhas. +// See README.md for licensing information. + +import Foundation + +// TO DO: currently Errors are mostly opaque to client code (even inits are internal only); what (if any) properties should be made public? + +/******************************************************************************/ +// error descriptions from ASLG/MacErrors.h + +private let descriptionForError: [Int:String] = [ + // OS errors + -34: "Disk is full.", + -35: "Disk wasn't found.", + -37: "Bad name for file.", + -38: "File wasn't open.", + -39: "End of file error.", + -42: "Too many files open.", + -43: "File wasn't found.", + -44: "Disk is write protected.", + -45: "File is locked.", + -46: "Disk is locked.", + -47: "File is busy.", + -48: "Duplicate file name.", + -49: "File is already open.", + -50: "Parameter error.", + -51: "File reference number error.", + -61: "File not open with write permission.", + -108: "Out of memory.", + -120: "Folder wasn't found.", + -124: "Disk is disconnected.", + -128: "User canceled.", + -192: "A resource wasn't found.", + -600: "Application isn't running.", + -601: "Not enough room to launch application with special requirements.", + -602: "Application is not 32-bit clean.", + -605: "More memory is needed than is specified in the size resource.", + -606: "Application is background-only.", + -607: "Buffer is too small.", + -608: "No outstanding high-level event.", + -609: "Connection is invalid.", + -610: "No user interaction allowed.", + -904: "Not enough system memory to connect to remote application.", + -905: "Remote access is not allowed.", + -906: "Application isn't running or program linking isn't enabled.", + -915: "Can't find remote machine.", + -30720: "Invalid date and time.", + // AE errors + -1700: "Can't make some data into the expected type.", + -1701: "Some parameter is missing for command.", + -1702: "Some data could not be read.", + -1703: "Some data was the wrong type.", + -1704: "Some parameter was invalid.", + -1705: "Operation involving a list item failed.", + -1706: "Need a newer version of the Apple Event Manager.", + -1707: "Event isn't an Apple event.", + -1708: "Application could not handle this command.", + -1709: "AEResetTimer was passed an invalid reply.", + -1710: "Invalid sending mode was passed.", + -1711: "User canceled out of wait loop for reply or receipt.", + -1712: "Apple event timed out.", + -1713: "No user interaction allowed.", + -1714: "Wrong keyword for a special function.", + -1715: "Some parameter wasn't understood.", + -1716: "Unknown Apple event address type.", + -1717: "The handler is not defined.", + -1718: "Reply has not yet arrived.", + -1719: "Can't get reference. Invalid index.", + -1720: "Invalid range.", + -1721: "Wrong number of parameters for command.", + -1723: "Can't get reference. Access not allowed.", + -1725: "Illegal logical operator called.", + -1726: "Illegal comparison or logical.", + -1727: "Expected a reference.", + -1728: "Can't get reference.", + -1729: "Object counting procedure returned a negative count.", + -1730: "Container specified was an empty list.", + -1731: "Unknown object type.", + -1739: "Attempting to perform an invalid operation on a null descriptor.", + // Application scripting errors + -10000: "Apple event handler failed.", + -10001: "Type error.", + -10002: "Invalid key form.", + -10003: "Can't set reference to given value. Access not allowed.", + -10004: "A privilege violation occurred.", + -10005: "The read operation wasn't allowed.", + -10006: "Can't set reference to given value.", + -10007: "The index of the event is too large to be valid.", + -10008: "The specified object is a property, not an element.", + -10009: "Can't supply the requested descriptor type for the data.", + -10010: "The Apple event handler can't handle objects of this class.", + -10011: "Couldn't handle this command because it wasn't part of the current transaction.", + -10012: "The transaction to which this command belonged isn't a valid transaction.", + -10013: "There is no user selection.", + -10014: "Handler only handles single objects.", + -10015: "Can't undo the previous Apple event or user action.", + -10023: "Enumerated value is not allowed for this property.", + -10024: "Class can't be an element of container.", + -10025: "Illegal combination of properties settings." +] + + +/******************************************************************************/ + + +let defaultErrorCode = 1 +let encodeErrorCode = errAECoercionFail +let decodeErrorCode = errAECoercionFail + + +func errorMessage(_ err: Any) -> String { + switch err { + case let e as AutomationError: + return e.message ?? "Error \(e.code)." + case is NSError: + return (err as! NSError).localizedDescription + default: + return String(describing: err) + } +} + + +/******************************************************************************/ +// error classes + +// base class for all SwiftAutomation-raised errors (not including NSErrors raised by underlying Cocoa APIs) +public class AutomationError: LocalizedError { + public let _domain = "SwiftAutomation" + public let _code: Int // the OSStatus if known, or generic error code if not + public let cause: Error? // the error that triggered this failure, if any + + let _message: String? + + public init(code: Int, message: String? = nil, cause: Error? = nil) { + self._code = code + self._message = message + self.cause = cause + } + + public var code: Int { return self._code } + public var message: String? { return self._message } // TO DO: make non-optional? + + func description(_ previousCode: Int, separator: String = " ") -> String { + let msg = self.message ?? descriptionForError[self._code] + var string = self._code == previousCode ? "" : "Error \(self._code)\(msg == nil ? "." : ": ")" + if msg != nil { string += msg! } + if let error = self.cause as? AutomationError { + string += "\(separator)\(error.description(self._code))" + } else if let error = self.cause { + string += "\(separator)\(error)" + } + return string + } + + public var errorDescription: String? { + self.description(0) + } + +} + + +public class ConnectionError: AutomationError { + + public let target: AETarget + + public init(target: AETarget, message: String, cause: Error? = nil) { + self.target = target + super.init(code: defaultErrorCode, message: message, cause: cause) + } + + // TO DO: include target description in message? +} + + +public class PackError: AutomationError { + + let object: Any + + public init(object: Any, message: String? = nil, cause: Error? = nil) { + self.object = object + super.init(code: encodeErrorCode, message: message, cause: cause) + } + + public override var message: String? { + return "Can't encode unsupported \(type(of: self.object)) value:\n\n\t\(self.object)" + + (self._message != nil ? "\n\n\(self._message!)" : "") + } +} + +public class DecodeError: AutomationError { + + let type: Any.Type + let app: App + let descriptor: NSAppleEventDescriptor + + public init(app: App, descriptor: NSAppleEventDescriptor, type: Any.Type, message: String? = nil, cause: Error? = nil) { + self.app = app + self.descriptor = descriptor + self.type = type + super.init(code: decodeErrorCode, message: message, cause: cause) + } + + // TO DO: worth including a method for trying to decode desc as Any; this should be used when constructing full error message (might also be useful to caller); or what about a var that returns the type it would decode as? (caveat: that probably won't work so well for AEList/AERecord descs due to their complexity and the obvious challenges of fabricating generic type objects on the fly) + + public override var message: String? { // TO DO: how best to phrase error message? + var value: Any = self.descriptor + var string = "Can't decode value as \(self.type)" + do { + value = try self.app.decodeAsAny(self.descriptor) + } catch { + string = "Can't decode malformed descriptor" + } + return "\(string):\n\n\t\(value)" + (self._message != nil ? "\n\n\(self._message!)" : "") + } +} + + +/******************************************************************************/ +// standard command error + + +public class CommandError: AutomationError { // raised whenever an application command fails + + let commandInfo: CommandDescription // TO DO: this should always be given + let app: App + let event: NSAppleEventDescriptor? // non-nil if event was built and send + let reply: NSAppleEventDescriptor? // non-nil if reply event was received + + public init(commandInfo: CommandDescription, app: App, + event: NSAppleEventDescriptor? = nil, reply: NSAppleEventDescriptor? = nil, cause: Error? = nil) { + self.app = app + self.event = event + self.reply = reply + self.commandInfo = commandInfo + var errorNumber = 1 + if let error = cause { + errorNumber = error._code + } else if let replyEvent = reply { + if let appError = replyEvent.forKeyword(AE4.Keywords.errorNumber) { + errorNumber = Int(appError.int32Value) + // TO DO: [lazily] decode any other available error info + } + } + if errorNumber == 0 { errorNumber = defaultErrorCode } // should never be 0; TO DO: assert? + super.init(code: errorNumber, cause: cause) + } + + public override var message: String? { + return (self.reply?.forKeyword(AE4.Keywords.errorString)?.stringValue + ?? self.reply?.forKeyword(AE4.OSAErrorKeywords.briefMessage)?.stringValue + ?? descriptionForError[self._code]) + } + + public var expectedType: Symbol? { + if let desc = self.reply?.forKeyword(AE4.OSAErrorKeywords.expectedType) { + return try? self.app.decode(desc) as Symbol + } else { + return nil + } + } + + public var offendingObject: Any? { + if let desc = self.reply?.forKeyword(AE4.OSAErrorKeywords.offendingObject) { + return try? self.app.decode(desc) as Any + } else { + return nil + } + } + + public var partialResult: Any? { + if let desc = self.reply?.forKeyword(AE4.OSAErrorKeywords.partialResult) { + return try? self.app.decode(desc) as Any + } else { + return nil + } + } + + public var commandDescription: String { + formatCommand(self.commandInfo, applicationObject: self.app.application) + } + + public override var errorDescription: String? { + var string = "CommandError \(self._code): \(self.message ?? "")\n\n\t\(self.commandDescription)" + if let expectedType = self.expectedType { string += "\n\n\tExpected type: \(expectedType)" } + if let offendingObject = self.offendingObject { string += "\n\n\tOffending object: \(offendingObject)" } + if let error = self.cause as? AutomationError { + string += "\n\n" + error.description(self._code, separator: "\n\n") + } else if let error = self.cause { + string += "\n\n\(error)" + } + return string + } +} diff --git a/Sources/AEthereal/Path.swift b/Sources/AEthereal/Path.swift new file mode 100644 index 0000000..19af120 --- /dev/null +++ b/Sources/AEthereal/Path.swift @@ -0,0 +1,12 @@ +// Originally written by hhas. +// See README.md for licensing information. + +import Foundation + +public func HFSPath(fromFileURL url: URL) -> String { + return NSAppleEventDescriptor(fileURL: url).coerce(toDescriptorType: typeUnicodeText)!.stringValue! +} + +public func fileURL(fromHFSPath path: String) -> URL { + return NSAppleEventDescriptor(string: path).coerce(toDescriptorType: typeFileURL)!.fileURLValue! +} diff --git a/Sources/AEthereal/Specifier.swift b/Sources/AEthereal/Specifier.swift new file mode 100644 index 0000000..e5f471f --- /dev/null +++ b/Sources/AEthereal/Specifier.swift @@ -0,0 +1,473 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Base classes for constructing AE queries. +// +// Notes: +// +// An AE query is represented as a linked list of AEDescs, primarily AERecordDescs of typeObjectSpecifier. Each object specifier record has four properties: +// +// 'want' -- the type of element to identify (or 'prop' when identifying a property) +// 'form', 'seld' -- the reference form and selector data identifying the element(s) or property +// 'from' -- the parent descriptor in the linked list +// +// For example: +// +// name of document "ReadMe" [of application "TextEdit"] +// +// is represented by the following chain of AEDescs: +// +// {want:'prop', form:'prop', seld:'pnam', from:{want:'docu', form:'name', seld:"ReadMe", from:null}} +// +// Additional AERecord types (typeInsertionLocation, typeRangeDescriptor, typeCompDescriptor, typeLogicalDescriptor) are also used to construct specialized query forms describing insertion points before/after existing elements, element ranges, and test clauses. +// +// Atomic AEDescs of typeNull, typeCurrentContainer, and typeObjectBeingExamined are used to terminate the linked list. +// +// +// [TO DO: developer notes on Apple event query forms and Apple Event Object Model's relational object graphs (objects with attributes, one-to-one relationships, and one-to-many relationships); aka "AE IPC is simple first-class relational queries, not OOP"] +// +// +// Specifier.swift defines the base classes from which concrete Specifier classes representing each major query form are constructed. These base classes combine with various SpecifierExtensions (which provide by-index, by-name, etc selectors and Application object constructors) and glue-defined Query and Command extensions (which provide property and all-elements selectors, and commands) to form the following concrete classes: +// +// CLASS DESCRIPTION CAN CONSTRUCT +// +// Query [base class] +// ├─PREFIXInsertion insertion location specifier ├─commands +// └─PREFIXObject [object specifier base protocol] └─commands, and property and all-elements specifiers +// ├─PREFIXItem single-object specifier ├─previous/next selectors +// │ └─PREFIXItems multi-object specifier │ └─by-index/name/id/ordinal/range/test selectors +// └─PREFIXRoot App/Con/Its (untargeted roots) ├─[1] +// └─APPLICATION Application (app-targeted root) └─initializers +// +// +// (The above diagram fudges the exact inheritance hierarchy for illustrative purposes. Commands are actually provided by a PREFIXCommand protocol [not shown], which is adopted by APPLICATION and all PREFIX classes except PREFIXRoot [1] - which cannot construct working commands as it has no target information, so omits these methods for clarity. Strictly speaking, the only class which should implement commands is APPLICATION, as Apple event IPC is based on Remote *Procedure* Calls, not OOP; however, they also appear on specifier classes as a convenient shorthand when writing commands whose direct parameter is a specifier. Note that while all specifier classes provide command methods [including those used to construct relative-specifiers in by-range and by-test clauses, as omitting commands from these is more trouble than its worth] they will automatically throw if their root is an untargeted App/Con/Its object.) +// +// The following classes are also defined for use with Its-based object specifiers in by-test selectors. +// +// Query +// └─TestClause [test clause base class] +// ├─ComparisonTest comparison/containment test +// └─LogicalTest Boolean logic test +// +// +// Except for APPLICATION, users do not instantiate any of these classes directly, but instead by chained property/method calls on existing Query instances. +// + +import Foundation +import AppKit + +/******************************************************************************/ +// Common protocol for all specifier and test clause types. + +public protocol Query: AEEncodable { + + var rootSpecifier: RootSpecifier { get } + + var app: App { get set } + +} + +/******************************************************************************/ +// Abstract base class for all object and insertion specifiers + +// An object specifier is constructed as a linked list of AERecords of typeObjectSpecifier, terminated by a root descriptor (e.g. a null descriptor represents the root node of the app's Apple event object graph). The topmost node may also be an insertion location specifier, represented by an AERecord of typeInsertionLoc. The abstract Specifier class implements functionality common to both object and insertion specifiers. + +public class Specifier: Query { + + public var app: App + + public init(app: App) { + self.app = app + } + + public var rootSpecifier: RootSpecifier { + return parentQuery.rootSpecifier + } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + fatalError() + } + +} + +public protocol ChildQuery: Query { + + // note that parentQuery and rootSpecifier properties are really only intended for internal use when traversing a specifier chain; while there is nothing to prevent client code using these properties the results are not guaranteed to be valid or usable queries (once constructed, object specifiers should be treated as opaque values); the proper way to identify an object's (or objects') container is to ask the application to return a specifier (or list of specifiers) to its `container` property, if it has one, e.g. `let parentFolder:FinItem = someFinderItem.container.get()` + + // return the next ObjectSpecifier/TestClause in query chain + var parentQuery: Query { get } + +} + +extension ChildQuery { + + public var parentQuery: Query { + fatalError("ChildQuery.parentQuery must be overridden by subclasses.") + } + + public var rootSpecifier: RootSpecifier { + fatalError("ChildQuery.rootSpecifier must be overridden by subclasses.") + } + +} + +extension Specifier: ChildQuery { + + // convenience methods for sending Apple events using four-char codes (either OSTypes or Strings) + + public func sendAppleEvent(_ eventClass: OSType, _ eventID: OSType, _ parameters: [OSType: Any] = [:], + requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil, + withTimeout: TimeInterval? = nil, ignoring: Considerations? = nil) throws -> T { + return try app.sendAppleEvent(eventClass: eventClass, eventID: eventID, + parentSpecifier: self, parameters: parameters, + requestedType: requestedType, waitReply: waitReply, + sendOptions: sendOptions, withTimeout: withTimeout, ignoring: ignoring) + } + + // non-generic version of the above method; bound when T can't be inferred (either because caller doesn't use the return value or didn't declare a specific type for it, e.g. `let result = cmd.call()`), in which case Any is used + + @discardableResult public func sendAppleEvent(_ eventClass: OSType, _ eventID: OSType, _ parameters: [OSType: Any] = [:], + requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil, + withTimeout: TimeInterval? = nil, ignoring: Considerations? = nil) throws -> Any { + return try app.sendAppleEvent(eventClass: eventClass, eventID: eventID, + parentSpecifier: self, parameters: parameters, + requestedType: requestedType, waitReply: waitReply, + sendOptions: sendOptions, withTimeout: withTimeout, ignoring: ignoring) + } + +} + +/******************************************************************************/ +// Insertion location specifier + +public class InsertionSpecifier: Specifier { + + // 'insl' + public let insertionLocation: NSAppleEventDescriptor + + private(set) public var parentQuery: Query + + public required init(insertionLocation: NSAppleEventDescriptor, + parentQuery: Query, app: App) { + self.insertionLocation = insertionLocation + self.parentQuery = parentQuery + super.init(app: app) + } + + public override func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: typeInsertionLoc)! + desc.setDescriptor(try parentQuery.encodeAEDescriptor(app), forKeyword: keyAEObject) + desc.setDescriptor(insertionLocation, forKeyword: keyAEPosition) + return desc + } + + public enum Kind: OSType { + case beginning = 0x62676E67, end = 0x656E6420 + case before = 0x6265666F, after = 0x61667465 + } + + public var kind: Kind? { + return Kind(rawValue: insertionLocation.enumCodeValue) + } + +} + +/******************************************************************************/ +// Property/single-element specifiers; identifies an attribute/describes a one-to-one relationship between nodes in the app's AEOM graph + +public protocol ObjectSpecifierProtocol: ChildQuery { + + var wantType: NSAppleEventDescriptor { get } + var selectorForm: NSAppleEventDescriptor { get } + var selectorData: Any { get } + var parentQuery: Query { get } + +} + +// Represents property or single element specifier; adds property+elements vars, relative selectors, insertion specifiers +public class ObjectSpecifier: Specifier, ObjectSpecifierProtocol { + + // 'want', 'form', 'seld' + public let wantType: NSAppleEventDescriptor + public let selectorForm: NSAppleEventDescriptor + public let selectorData: Any + + private(set) public var parentQuery: Query + + public required init(wantType: NSAppleEventDescriptor, selectorForm: NSAppleEventDescriptor, selectorData: Any, + parentQuery: Query, app: App) { + self.wantType = wantType + self.selectorForm = selectorForm + self.selectorData = selectorData + self.parentQuery = parentQuery + super.init(app: app) + } + + public override func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: AE4.Types.objectSpecifier)! + desc.setDescriptor(try parentQuery.encodeAEDescriptor(app), forKeyword: AE4.ObjectSpecifierKeywords.container) + desc.setDescriptor(wantType, forKeyword: AE4.ObjectSpecifierKeywords.desiredClass) + desc.setDescriptor(selectorForm, forKeyword: AE4.ObjectSpecifierKeywords.keyForm) + desc.setDescriptor(try app.encode(selectorData), forKeyword: AE4.ObjectSpecifierKeywords.keyData) + return desc + } + + // Containment test constructors + + // note: ideally the following would only appear on objects constructed from an Its root; however, this would complicate the implementation while failing to provide any real benefit to users, who are unlikely to make such a mistake in the first place + + public func beginsWith(_ value: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ContainmentTests.beginsWith, operand1: self, operand2: value, app: app) + } + + public func endsWith(_ value: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ContainmentTests.endsWith, operand1: self, operand2: value, app: app) + } + + public func contains(_ value: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ContainmentTests.contains, operand1: self, operand2: value, app: app) + } + + public func isIn(_ value: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ContainmentTests.isIn, operand1: self, operand2: value, app: app) + } + +} + +// Comparison test constructors + +public func <(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.lessThan, operand1: lhs, operand2: rhs, app: lhs.app) +} + +public func <=(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.lessThanEquals, operand1: lhs, operand2: rhs, app: lhs.app) +} + +public func ==(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.equals, operand1: lhs, operand2: rhs, app: lhs.app) +} + +public func !=(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.notEquals, operand1: lhs, operand2: rhs, app: lhs.app) +} + +public func >(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.greaterThan, operand1: lhs, operand2: rhs, app: lhs.app) +} + +public func >=(lhs: ObjectSpecifier, rhs: Any) -> TestClause { + return ComparisonTest(operatorType: AE4.Descriptors.ComparisonTests.greaterThanEquals, operand1: lhs, operand2: rhs, app: lhs.app) +} + +/******************************************************************************/ +// Multi-element specifiers; represents a one-to-many relationship between nodes in the app's AEOM graph + +// note: each glue should define an Elements class that subclasses ObjectSpecifier and adopts MultipleObjectSpecifier (which adds by range/test/all selectors) + +// note: by-range selector doesn't confirm APP/CON-based roots for start+stop specifiers; as with ITS-based roots this would add significant complexity to class hierarchy in order to detect mistakes that are unlikely to be made in practice (most errors are likely to be made further down the chain, e.g. getting the 'containment' hierarchy for more complex specifiers incorrect) + +public struct RangeSelector: AEEncodable { // holds data for by-range selectors + // Start and stop are Con-based (i.e. relative to container) specifiers (App-based specifiers will also work, as + // long as they have the same parent specifier as the by-range specifier itself). For convenience, users can also + // pass non-specifier values (typically Strings and Ints) to represent simple by-name and by-index specifiers of + // the same element class; these will be converted to specifiers automatically when packed. + public let start: Any + public let stop: Any + public let wantType: NSAppleEventDescriptor + + public init(start: Any, stop: Any, wantType: NSAppleEventDescriptor) { + self.start = start + self.stop = stop + self.wantType = wantType + } + + private func packSelector(_ selectorData: Any, app: App) throws -> NSAppleEventDescriptor { + var selectorForm: NSAppleEventDescriptor + switch selectorData { + case is NSAppleEventDescriptor: + return selectorData as! NSAppleEventDescriptor + case is Specifier: // technically, only ObjectSpecifier makes sense here, tho AS prob. doesn't prevent insertion loc or multi-element specifier being passed instead + return try (selectorData as! Specifier).encodeAEDescriptor(app) + default: // encode anything else as a by-name or by-index specifier + selectorForm = selectorData is String ? AE4.Descriptors.IndexForms.name : AE4.Descriptors.IndexForms.absolutePosition + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: AE4.Types.objectSpecifier)! + desc.setDescriptor(containerRoot, forKeyword: AE4.ObjectSpecifierKeywords.container) + desc.setDescriptor(wantType, forKeyword: AE4.ObjectSpecifierKeywords.desiredClass) + desc.setDescriptor(selectorForm, forKeyword: AE4.ObjectSpecifierKeywords.keyForm) + desc.setDescriptor(try app.encode(selectorData), forKeyword: AE4.ObjectSpecifierKeywords.keyData) + return desc + } + } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: AE4.Types.rangeDescriptor)! + desc.setDescriptor(try packSelector(start, app: app), forKeyword: AE4.RangeSpecifierKeywords.start) + desc.setDescriptor(try packSelector(stop, app: app), forKeyword: AE4.RangeSpecifierKeywords.stop) + return desc + } +} + +/******************************************************************************/ +// Test clause; used in by-test specifiers + +// note: only TestClauses constructed from Its roots are actually valid; however, enfording this at compile-time would require a more complex class/protocol structure, while checking this at runtime would require calling Query.rootSpecifier.rootObject and checking object is 'its' descriptor. As it's highly unlikely users will use an App or Con root by accident, we'll live recklessly and let the gods of AppleScript punish any user foolish enough to do so. + +public protocol TestClause: Query { +} + +// Logical test constructors + +public func &&(lhs: TestClause, rhs: TestClause) -> TestClause { + return LogicalTest(operatorType: AE4.Descriptors.LogicalTests.and, operands: [lhs, rhs], app: lhs.app) +} + +public func ||(lhs: TestClause, rhs: TestClause) -> TestClause { + return LogicalTest(operatorType: AE4.Descriptors.LogicalTests.or, operands: [lhs, rhs], app: lhs.app) +} + +public prefix func !(op: TestClause) -> TestClause { + return LogicalTest(operatorType: AE4.Descriptors.LogicalTests.not, operands: [op], app: op.app) +} + +public class ComparisonTest: TestClause { + + public var app: App + + public let operatorType: NSAppleEventDescriptor, operand1: ObjectSpecifier, operand2: Any + + init(operatorType: NSAppleEventDescriptor, + operand1: ObjectSpecifier, operand2: Any, app: App) { + self.operatorType = operatorType + self.operand1 = operand1 + self.operand2 = operand2 + self.app = app + } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + if operatorType === AE4.Descriptors.ComparisonTests.notEquals { // AEM doesn't support a 'kAENotEqual' enum... + return try (!(operand1 == operand2)).encodeAEDescriptor(app) // so convert to kAEEquals+kAENOT + } else { + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: AE4.Types.compDescriptor)! + let opDesc1 = try app.encode(operand1) + let opDesc2 = try app.encode(operand2) + if operatorType === AE4.Descriptors.ContainmentTests.isIn { // AEM doesn't support a 'kAEIsIn' enum... + desc.setDescriptor(AE4.Descriptors.ContainmentTests.contains, forKeyword: AE4.TestPredicateKeywords.comparisonOperator) // so use kAEContains with operands reversed + desc.setDescriptor(opDesc2, forKeyword: AE4.TestPredicateKeywords.firstObject) + desc.setDescriptor(opDesc1, forKeyword: AE4.TestPredicateKeywords.secondObject) + } else { + desc.setDescriptor(operatorType, forKeyword: AE4.TestPredicateKeywords.comparisonOperator) + desc.setDescriptor(opDesc1, forKeyword: AE4.TestPredicateKeywords.firstObject) + desc.setDescriptor(opDesc2, forKeyword: AE4.TestPredicateKeywords.secondObject) + } + return desc + } + } + + public var parentQuery: Query { + return operand1 + } + + public var rootSpecifier: RootSpecifier { + return operand1.rootSpecifier + } + +} + +public class LogicalTest: TestClause, ChildQuery { + + public var app: App + + public let operatorType: NSAppleEventDescriptor + public let operands: [TestClause] // note: this doesn't have a 'parent' as such; to walk chain, just use first operand + + init(operatorType: NSAppleEventDescriptor, operands: [TestClause], app: App) { + self.operatorType = operatorType + self.operands = operands + self.app = app + } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.record().coerce(toDescriptorType: typeLogicalDescriptor)! + desc.setDescriptor(operatorType, forKeyword: AE4.TestPredicateKeywords.logicalOperator) + desc.setDescriptor(try app.encode(operands), forKeyword: AE4.TestPredicateKeywords.logicalTerms) + return desc + } + + public var parentQuery: Query { + return operands[0] + } + + public var rootSpecifier: RootSpecifier { + return operands[0].rootSpecifier + } + +} + +/******************************************************************************/ +// Specifier roots (all Specifier chains must originate from a RootSpecifier instance) + +public class RootSpecifier: Specifier, ObjectSpecifierProtocol { + + public enum Kind { + /// Root of all absolute object specifiers. + /// e.g., `document 1 of «application»`. + case application + /// Root of an object specifier specifying the start or end of a range of + /// elements in a by-range specifier. + /// e.g., `folders (folder 2 of «container») thru (folder -1 of «container»)`. + case container + /// Root of an object specifier specifying an element whose state is being + /// compared in a by-test specifier. + /// e.g., `every track where (rating of «specimen» > 50)`. + case specimen + /// Root of an object specifier that descends from a descriptor object. + /// e.g., `item 1 of {1,2,3}`. + /// (These sorts of descriptors are effectively exclusively generated + /// by AppleScript). + case object(NSAppleEventDescriptor) + } + + public var kind: Kind + + public init(_ kind: Kind, app: App) { + self.kind = kind + super.init(app: app) + } + + public var wantType: NSAppleEventDescriptor { + .null() + } + public var selectorForm: NSAppleEventDescriptor { + .null() + } + + public var selectorData: Any { + switch kind { + case .application: + return applicationRoot + case .container: + return containerRoot + case .specimen: + return specimenRoot + case let .object(descriptor): + return descriptor + } + } + + // Query/Specifier-inherited properties and methods that recursively call their parent specifiers are overridden here to ensure they terminate: + + public var parentQuery: Query { + self + } + + public override var rootSpecifier: RootSpecifier { + self + } + + public override func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + try app.encode(selectorData) + } + +} diff --git a/Sources/AEthereal/SpecifierConstruction.swift b/Sources/AEthereal/SpecifierConstruction.swift new file mode 100644 index 0000000..84b28d6 --- /dev/null +++ b/Sources/AEthereal/SpecifierConstruction.swift @@ -0,0 +1,233 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Extensions that add the standard selector vars/methods to Specifier classes. +// These allow specifiers to be built up via chained calls, e.g.: +// +// paragraphs 1 thru -2 of text of document "README" of it +// +// App.generic.application.elements(cDocument)["README"].property(cText).elements(cParagraph)[1,-2] +// + +import Foundation + +/******************************************************************************/ +// Property/single-element specifier; identifies an attribute/describes a one-to-one relationship between nodes in the app's AEOM graph + +public extension ObjectSpecifierProtocol { + + func userProperty(_ name: String) -> ObjectSpecifier { + return ObjectSpecifier(wantType: AE4.Descriptors.Types.property, selectorForm: AE4.Descriptors.IndexForms.userProperty, selectorData: NSAppleEventDescriptor(string: name), parentQuery: self, app: self.app) + } + + func property(_ code: OSType) -> ObjectSpecifier { + return ObjectSpecifier(wantType: AE4.Descriptors.Types.property, selectorForm: AE4.Descriptors.IndexForms.property, selectorData: NSAppleEventDescriptor(typeCode: code), parentQuery: self, app: self.app) + } + + func property(_ code: String) -> ObjectSpecifier { + let data: Any + do { + data = NSAppleEventDescriptor(typeCode: try FourCharCode(fourByteString: code)) + } catch { + data = error + } + return ObjectSpecifier(wantType: AE4.Descriptors.Types.property, selectorForm: AE4.Descriptors.IndexForms.property, selectorData: data, parentQuery: self, app: self.app) + } + + func elements(_ code: OSType) -> MultipleObjectSpecifier { + return MultipleObjectSpecifier(wantType: NSAppleEventDescriptor(typeCode: code), selectorForm: AE4.Descriptors.IndexForms.absolutePosition, selectorData: AE4.Descriptors.AbsolutePositions.all, parentQuery: self, app: self.app) + } + + func elements(_ code: String) -> MultipleObjectSpecifier { + let want: NSAppleEventDescriptor, data: Any + do { + want = NSAppleEventDescriptor(typeCode: try FourCharCode(fourByteString: code)) + data = AE4.Descriptors.AbsolutePositions.all + } catch { + want = NSAppleEventDescriptor.null() + data = error + } + return MultipleObjectSpecifier(wantType: want, selectorForm: AE4.Descriptors.IndexForms.absolutePosition, selectorData: data, parentQuery: self, app: self.app) + } + + // relative position selectors + func previous(_ elementClass: Symbol? = nil) -> ObjectSpecifier { + return ObjectSpecifier(wantType: elementClass == nil ? self.wantType : elementClass!.encodeAEDescriptor(), + selectorForm: AE4.Descriptors.IndexForms.relativePosition, selectorData: AE4.Descriptors.RelativePositions.previous, + parentQuery: self, app: self.app) + } + + func next(_ elementClass: Symbol? = nil) -> ObjectSpecifier { + return ObjectSpecifier(wantType: elementClass == nil ? self.wantType : elementClass!.encodeAEDescriptor(), + selectorForm: AE4.Descriptors.IndexForms.relativePosition, selectorData: AE4.Descriptors.RelativePositions.next, + parentQuery: self, app: self.app) + } + + // insertion specifiers + var beginning: InsertionSpecifier { + return InsertionSpecifier(insertionLocation: AE4.Descriptors.InsertionLocations.beginning, parentQuery: self, app: self.app) + } + var end: InsertionSpecifier { + return InsertionSpecifier(insertionLocation: AE4.Descriptors.InsertionLocations.end, parentQuery: self, app: self.app) + } + var before: InsertionSpecifier { + return InsertionSpecifier(insertionLocation: AE4.Descriptors.InsertionLocations.before, parentQuery: self, app: self.app) + } + var after: InsertionSpecifier { + return InsertionSpecifier(insertionLocation: AE4.Descriptors.InsertionLocations.after, parentQuery: self, app: self.app) + } + + var all: MultipleObjectSpecifier { // equivalent to `every REFERENCE`; applied to a property specifier, converts it to all-elements (this may be necessary when property and element names are identical, in which case [with exception of `text`] a property specifier is constructed by default); applied to an all-elements specifier, returns it as-is; applying it to any other reference form will throw an error when used + if self.selectorForm.typeCodeValue == AE4.IndexForm.propertyID.rawValue { + return MultipleObjectSpecifier(wantType: self.selectorData as! NSAppleEventDescriptor, selectorForm: AE4.Descriptors.IndexForms.absolutePosition, selectorData: AE4.Descriptors.AbsolutePositions.all, parentQuery: self.parentQuery, app: self.app) + } else if + self.selectorForm.typeCodeValue == AE4.IndexForm.absolutePosition.rawValue, + (self.selectorData as? NSAppleEventDescriptor)?.enumCodeValue == AE4.AbsoluteOrdinal.all.rawValue, + let specifier = self as? MultipleObjectSpecifier + { + return specifier + } else { + let error = AutomationError(code: 1, message: "Invalid specifier: \(self).all") + return MultipleObjectSpecifier(wantType: self.wantType, selectorForm: AE4.Descriptors.IndexForms.absolutePosition, selectorData: error, parentQuery: self.parentQuery, app: self.app) + } + } +} + +/******************************************************************************/ +// Multi-element specifier; represents a one-to-many relationship between nodes in the app's AEOM graph + +public class MultipleObjectSpecifier: ObjectSpecifier {} + +extension MultipleObjectSpecifier { + + // Note: calling an element[s] selector on an all-elements specifier effectively replaces its original gAll selector data with the new selector data, instead of extending the specifier chain. This ensures that applying any selector to `elements[all]` produces `elements[selector]` (effectively replacing the existing selector), while applying a second selector to `elements[selector]` produces `elements[selector][selector2]` (appending the second selection to the first) as normal; e.g. `first document whose modified is true` would be written as `documents[Its.modified==true].first`. + var baseQuery: Query { + if + let desc = self.selectorData as? NSAppleEventDescriptor, + desc.descriptorType == AE4.Types.absoluteOrdinal && desc.enumCodeValue == AE4.AbsoluteOrdinal.all.rawValue + { + return self.parentQuery + } else { + return self + } + } + + // by-index, by-name, by-test + public subscript(index: Any) -> ObjectSpecifier { + var form: NSAppleEventDescriptor + switch (index) { + case is TestClause: + return self[index as! TestClause] + case is String: + form = AE4.Descriptors.IndexForms.name + default: + form = AE4.Descriptors.IndexForms.absolutePosition + } + return ObjectSpecifier(wantType: self.wantType, selectorForm: form, selectorData: index, parentQuery: self.baseQuery, app: self.app) + } + + public subscript(test: TestClause) -> MultipleObjectSpecifier { + return MultipleObjectSpecifier(wantType: self.wantType, selectorForm: AE4.Descriptors.IndexForms.test, selectorData: test, parentQuery: self.baseQuery, app: self.app) + } + + // by-name, by-id, by-range + public func named(_ name: Any) -> ObjectSpecifier { // use this if name is not a String, else use subscript // TO DO: trying to think of a use case where this has ever been found necessary; DELETE? (see also TODOs on whether or not to add an explicit `all` selector property) + return ObjectSpecifier(wantType: self.wantType, selectorForm: AE4.Descriptors.IndexForms.name, selectorData: name, parentQuery: self.baseQuery, app: self.app) + } + public func id(_ id: Any) -> ObjectSpecifier { + return ObjectSpecifier(wantType: self.wantType, selectorForm: AE4.Descriptors.IndexForms.uniqueID, selectorData: id, parentQuery: self.baseQuery, app: self.app) + } + public subscript(from: Any, to: Any) -> MultipleObjectSpecifier { + // caution: by-range specifiers must be constructed as `elements[from,to]`, NOT `elements[from...to]`, as `Range` types are not supported + // Note that while the `x...y` form _could_ be supported (via the SelfPacking protocol, since Ranges are generics), the `x..(session: Any? = nil, closure: () throws -> T) throws -> T { + return try self.app.doTransaction(session: session, closure: closure) + } + +} + +// MARK: Evaluation +extension ObjectSpecifier { + + public func get(_ directParameter: Any = NoParameter, + requestedType: Symbol? = nil, waitReply: Bool = true, sendOptions: SendOptions? = nil, + withTimeout: TimeInterval? = nil, ignoring: Considerations? = nil) throws -> Result { + try self.app.sendAppleEvent( + name: "get", + eventClass: AE4.Suites.coreSuite, + eventID: AE4.AESymbols.getData, + parentSpecifier: self, + directParameter: directParameter, + keywordParameters: [], + requestedType: requestedType, + waitReply: waitReply, + sendOptions: sendOptions, + withTimeout: withTimeout, + ignoring: ignoring + ) + } + +} diff --git a/Sources/AEthereal/SpecifierFormatter.swift b/Sources/AEthereal/SpecifierFormatter.swift new file mode 100644 index 0000000..cbdb0aa --- /dev/null +++ b/Sources/AEthereal/SpecifierFormatter.swift @@ -0,0 +1,309 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Generates source code representation of Specifier. +// + +import Foundation +import AppKit + +// Ian's notes: Formats SA specifiers as Swift expressions. +// Not really necessary for our purposes, but has potential debugging use. + +// TO DO: when formatting specifiers, what info is needed? app?, isNestedSpecifier; anything else? (note, this data ought to be available throughout; e.g. given a list of specifiers, current nesting flag should be carried over; there is also the question of when to adopt specifier's own app vs use the one already provided to formatter; furthermore, it might be argued that App should do the formatting itself [although that leaves the flag problem]) + +// TO DO: how to display nested App root as shorthand? (really need separate description(nested:Bool) func, or else use visitor API - note that a simplified api only need replicate constructor calls, not individual specifier+selector method calls; it also gives cleaner approach to glue-specific hooks and dynamic use, and encapsulating general Swift type formatting) + +// TO DO: when displaying by-range specifier's start and stop, simplify by-index/by-name representations where appropriate, e.g. `TextEdit().documents[TEDCon.documents[1], TEDCon.documents[-2]]` should display as `TextEdit().documents[1, -2]` + +/******************************************************************************/ +// Formatter + +// used by a Specifier's description property to render Swift literal representation of itself; +// static glues instantiate this with their own application-specific code->name translation tables + +public func formatSAObject(_ object: Any) -> String { + switch object { + case let obj as RootSpecifier: + return formatRootSpecifier(obj) + case let obj as InsertionSpecifier: + return formatInsertionSpecifier(obj) + case let obj as ObjectSpecifier: + return formatObjectSpecifier(obj) + case let obj as ComparisonTest: + return formatComparisonTest(obj) + case let obj as LogicalTest: + return formatLogicalTest(obj) + case let obj as Symbol: + return formatSymbol(obj) + default: + return formatValue(object) + } +} + +// hooks + +func formatSymbol(_ symbol: Symbol) -> String { + return formatSymbol(code: symbol.code, type: symbol.type) +} + +func formatSymbol(code: OSType, type: OSType) -> String { + "Symbol(code:\(formatFourCharCodeString(code)), type:\(formatFourCharCodeString(type)))" +} + +func formatPropertyVar(_ code: OSType) -> String { + ".property(\(formatFourCharCodeString(code)))" +} + +func formatElementsVar(_ code: OSType) -> String { + ".elements(\(formatFourCharCodeString(code)))" +} + +// Specifier formatters + +func formatRootSpecifier(_ specifier: RootSpecifier) -> String { + switch specifier.kind { + case .application: + var result = "Application" + switch specifier.app.target { + case .none: + result = "«application»" + case .current: + result += ".currentApplication()" + case .name(let name): + result += "(name: \(formatSAObject(name)))" + case .url(let url): + result += url.isFileURL ? "(name: \(formatSAObject(url.path)))" : "(url: \(formatSAObject(url)))" + case .bundleIdentifier(let bundleID): + result += "(bundleIdentifier: \(formatSAObject(bundleID)))" + case .processIdentifier(let pid): + result += "(processIdentifier: \(pid))" + case .descriptor(let desc): + result += "(addressDescriptor: \(desc))" + } + return result + case .container: + return "«container»" + case .specimen: + return "«specimen»" + case let .object(descriptor): + return "«object root: \(descriptor)»" + } +} + +func formatInsertionSpecifier(_ specifier: InsertionSpecifier) -> String { + if let name: String = { + switch AE4.InsertionLocation(rawValue: specifier.insertionLocation.enumCodeValue) { + case .beginning: + return "beginning" + case .end: + return "end" + case .before: + return "before" + case .after: + return "after" + case nil: + return nil + } + }() { + return "\(formatSAObject(specifier.parentQuery)).\(name)" + } + return "<\(type(of: specifier))(kpos:\(specifier.insertionLocation),kobj:\(formatSAObject(specifier.parentQuery)))>" +} + +func formatObjectSpecifier(_ specifier: ObjectSpecifier) -> String { + let form = specifier.selectorForm.enumCodeValue + var result = formatSAObject(specifier.parentQuery) + switch AE4.IndexForm(rawValue: form) { + case .propertyID: + // kludge, seld is either desc or symbol, depending on whether constructed or deocded; TO DO: eliminate? + if let desc = specifier.selectorData as? NSAppleEventDescriptor, let propertyDesc = desc.coerce(toDescriptorType: AE4.Types.type) { + return result + formatPropertyVar(propertyDesc.typeCodeValue) + } else if let symbol = specifier.selectorData as? Symbol { + return result + formatPropertyVar(symbol.code) + } // else malformed desc + case .userPropertyID: + return "\(result).userProperty(\(formatValue(specifier.selectorData)))" + case .relativePosition: // specifier.previous/next(SYMBOL) + if let seld = specifier.selectorData as? NSAppleEventDescriptor, // ObjectSpecifier's self-decoding does not decode ordinals + let name = [AE4.RelativeOrdinal.previous.rawValue: "previous", AE4.RelativeOrdinal.next.rawValue: "next"][seld.enumCodeValue], + let parent = specifier.parentQuery as? ObjectSpecifier { + if specifier.wantType.typeCodeValue == parent.wantType.typeCodeValue { + return "\(result).\(name)()" // use shorthand form for neatness + } else { + let element = formatSymbol(code: specifier.wantType.typeCodeValue, type: typeType) + return "\(result).\(name)(\(element))" + } + } + default: + result += formatElementsVar(specifier.wantType.typeCodeValue) + if let desc = specifier.selectorData as? NSAppleEventDescriptor, desc.typeCodeValue == AE4.AbsoluteOrdinal.all.rawValue { + return result + } + switch AE4.IndexForm(rawValue: form) { + case .absolutePosition: // specifier[IDX] or specifier.first/middle/last/any + if + let desc = specifier.selectorData as? NSAppleEventDescriptor, // ObjectSpecifier's self-decoding does not decode ordinals + let ordinal: String = { + switch AE4.AbsoluteOrdinal(rawValue: desc.enumCodeValue) { + case .first: + return "first" + case .middle: + return "middle" + case .last: + return "last" + case .random: + return "any" + case .all, nil: + return nil + } + }() + { + return "\(result).\(ordinal)" + } else { + return "\(result)[\(formatValue(specifier.selectorData))]" + } + case .name: // specifier[NAME] or specifier.named(NAME) + return specifier.selectorData is Int ? "\(result).named(\(formatValue(specifier.selectorData)))" + : "\(result)[\(formatValue(specifier.selectorData))]" + case .uniqueID: // specifier.ID(UID) + return "\(result).ID(\(formatSAObject(specifier.selectorData)))" + case .range: // specifier[FROM,TO] + if let seld = specifier.selectorData as? RangeSelector { + return "\(result)[\(formatSAObject(seld.start)), \(formatSAObject(seld.stop))]" // TO DO: app-based specifiers should use untargeted 'App' root; con-based specifiers should be reduced to minimal representation if their wantType == specifier.wantType + } + case .test: // specifier[TEST] + return "\(result)[\(formatSAObject(specifier.selectorData))]" + default: + break + } + } + return "<\(type(of: specifier))(want:\(specifier.wantType),form:\(specifier.selectorForm),seld:\(formatValue(specifier.selectorData)),from:\(formatSAObject(specifier.parentQuery)))>" +} + +private let _comparisonOperators = [AE4.Comparison.lessThan: "<", AE4.Comparison.lessThanEquals: "<=", AE4.Comparison.equals: "==", + AE4.Comparison.greaterThan: ">", AE4.Comparison.greaterThanEquals: ">="] +private let _containmentOperators = [AE4.Containment.beginsWith: "beginsWith", AE4.Containment.endsWith: "endsWith", AE4.Containment.contains: "contains"] +private let _logicalBinaryOperators = [AE4.LogicalOperator.and: "and", AE4.LogicalOperator.or: "or"] +private let _logicalUnaryOperators = [AE4.LogicalOperator.not: "not"] + +func formatComparisonTest(_ specifier: ComparisonTest) -> String { + let operand1 = formatValue(specifier.operand1), operand2 = formatValue(specifier.operand2) + let opcode = specifier.operatorType.enumCodeValue + if + let comparison = AE4.Comparison(rawValue: opcode), + let name = _comparisonOperators[comparison] + { + return "\(operand1) \(name) \(operand2)" + } else if + let containment = AE4.Containment(rawValue: opcode), + let name = _containmentOperators[containment] + { + return "\(operand1).\(name)(\(operand2))" + } + return "<\(type(of: specifier))(relo:\(specifier.operatorType),obj1:\(formatValue(operand1)),obj2:\(formatValue(operand2)))>" +} + +func formatLogicalTest(_ specifier: LogicalTest) -> String { + let operands = specifier.operands.map({formatValue($0)}) + let opcode = specifier.operatorType.enumCodeValue + if let logical = AE4.LogicalOperator(rawValue: opcode) { + if let name = _logicalBinaryOperators[logical], operands.count > 1 { + return operands.joined(separator: " \(name) ") + } else if let name = _logicalUnaryOperators[logical], operands.count == 1 { + return "\(name) (\(operands[0]))" + } + } + return "<\(type(of: specifier))(logc:\(specifier.operatorType),term:\(formatValue(operands)))>" +} + +// general formatting functions + +func formatValue(_ value: Any) -> String { // TO DO: this should probably be a method on SpecifierFormatter so that it can be overridden to generate representations for other languages + // formats AE-bridged Swift types as literal syntax; other Swift types will show their default description (unfortunately debugDescription doesn't provide usable literal representations - e.g. String doesn't show tabs in escaped form, Cocoa classes return their [non-literal] description string instead, and reliable representations of Bool/Int/Double are a dead loss as soon as NSNumber gets involved, so custom implementation is needed) + switch value { + case let obj as NSArray: // HACK (since `obj as Array` won't work); see also App.encode() // TO DO: implement SelfFormatting protocol on Array, Set, Dictionary + return "[" + obj.map({formatValue($0)}).joined(separator: ", ") + "]" + case let obj as NSDictionary: // HACK; see also App.encode() + return "[" + obj.map({"\(formatValue($0)): \(formatValue($1))"}).joined(separator: ", ") + "]" + case let obj as String: + return obj.debugDescription + case let obj as Date: + return "Date(timeIntervalSinceReferenceDate:\(obj.timeIntervalSinceReferenceDate)) /*\(obj.description)*/" + case let obj as URL: + if obj.isFileURL { + return "URL(fileURLWithPath:\(formatValue(obj.path)))" + } else { + return "URL(string:\(formatValue(obj.absoluteString)))" + } + case let obj as NSNumber: + // note: matching Bool, Int, Double types can be glitchy due to Swift's crappy bridging of ObjC's crappy NSNumber class, + // so just match NSNumber (which also matches corresponding Swift types) and figure out appropriate representation + if CFBooleanGetTypeID() == CFGetTypeID(obj) { // voodoo: NSNumber class cluster uses __NSCFBoolean + return obj == 0 ? "false" : "true" + } else { + return "\(value)" + } + default: + return "\(value)" // SwiftAutomation objects (specifiers, symbols) are self-formatting; any other value will use its own default description (which may or may not be the same as its literal representation, but that's Swift's problem, not ours) + } +} + +public func formatCommand(_ description: CommandDescription, applicationObject: RootSpecifier? = nil) -> String { + var parentSpecifier = applicationObject != nil ? String(describing: applicationObject!) : "Application()" + var args: [String] = [] + switch description.signature { + case .named(let name, let directParameter, let keywordParameters, let requestedType): + if description.subject != nil && parameterExists(directParameter) { + parentSpecifier = formatSAObject(description.subject!) + args.append(formatSAObject(directParameter)) + //} else if eventClass == _kAECoreSuite && eventID == _kAECreateElement { // TO DO: format make command as special case (for convenience, sendAppleEvent should allow user to call `make` directly on a specifier, in which case the specifier is used as its `at` parameter if not already given) + } else if description.subject == nil && parameterExists(directParameter) { + parentSpecifier = formatSAObject(directParameter) + } else if description.subject != nil && !parameterExists(directParameter) { + parentSpecifier = formatSAObject(description.subject!) + } + parentSpecifier += ".\(name)" + for (key, value) in keywordParameters { args.append("\(key): \(formatSAObject(value))") } + if let symbol = requestedType { args.append("requestedType: \(symbol)") } + case .codes(let eventClass, let eventID, let parameters): + if let subject = description.subject { + parentSpecifier = formatSAObject(subject) + } + parentSpecifier += ".sendAppleEvent" + args.append("\(formatFourCharCodeString(eventClass)), \(formatFourCharCodeString(eventID))") + if parameters.count > 0 { + let params = parameters.map({ "\(formatFourCharCodeString($0)): \(formatSAObject($1)))" }).joined(separator: ", ") + args.append("[\(params)]") + } + } + // TO DO: AE's representation of AESendMessage args (waitReply and withTimeout) is unreliable; may be best to ignore these entirely + /* + if !eventDescription.waitReply { + args.append("waitReply: false") + } + //sendOptions: NSAppleEventSendOptions? = nil + if eventDescription.withTimeout != defaultTimeout { + args.append("withTimeout: \(eventDescription.withTimeout)") // TO DO: if -2, use NoTimeout constant (except 10.11 hasn't defined one yet, and is still buggy in any case) + } + */ + if description.ignoring != defaultIgnoring { + args.append("considering: \(description.ignoring)") + } + return "try \(parentSpecifier)(\(args.joined(separator: ", ")))" +} + +/******************************************************************************/ + +// convert an OSType to its String literal representation, e.g. 'docu' -> "\"docu\"" +func formatFourCharCodeString(_ code: OSType) -> String { + var n = CFSwapInt32HostToBig(code) + var result = "" + for _ in 1...4 { + let c = n % 256 + result += String(format: (c == 0x21 || 0x23 <= c && c <= 0x7e) ? "%c" : "\\0x%02X", c) + n >>= 8 + } + return "\"\(result)\"" +} diff --git a/Sources/AEthereal/Support.swift b/Sources/AEthereal/Support.swift new file mode 100644 index 0000000..138471c --- /dev/null +++ b/Sources/AEthereal/Support.swift @@ -0,0 +1,155 @@ +// Originally written by hhas. +// See README.md for licensing information. + +import Foundation +import AppKit + +/******************************************************************************/ +// KLUDGE: NSWorkspace provides a good method for launching apps by file URL, and a crap one for launching by bundle ID - unfortunately, only the latter can be used in sandboxed apps. This extension adds a launchApplication(withBundleIdentifier:options:configuration:)throws->NSRunningApplication method that has a good API and the least compromised behavior, insulating AETarget code from the crappiness that hides within. If/when Apple adds a real, robust version of this method to NSWorkspace , this extension can (and should) go away. + +extension NSWorkspace { + + // caution: the configuration parameter is ignored in sandboxed apps; this is unavoidable + @objc func launchApplication(withBundleIdentifier bundleID: String, options: NSWorkspace.LaunchOptions = [], + configuration: [NSWorkspace.LaunchConfigurationKey : Any]) throws -> NSRunningApplication { + // if one or more processes with the given bundle ID is already running, return the first one found + let foundProcesses = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + if foundProcesses.count > 0 { + return foundProcesses[0] + } + // first try to get the app's file URL, as this lets us use the better launchApplication(at:options:configuration:) method… + if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + do { + return try NSWorkspace.shared.launchApplication(at: url, options: options, configuration: configuration) + } catch {} // for now, we're not sure if urlForApplication(withBundleIdentifier:) will always return nil if blocked by sandbox; if it returns garbage URL instead then hopefully that'll cause launchApplication(at:...) to throw + } + // …else fall back to the inferior launchApplication(withBundleIdentifier:options:additionalEventParamDescriptor:launchIdentifier:) + var options = options + options.remove(NSWorkspace.LaunchOptions.async) + if NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleID, options: options, + additionalEventParamDescriptor: nil, launchIdentifier: nil) { + // TO DO: confirm that launchApplication() never returns before process is available (otherwise the following will need to be in a loop that blocks until it is available or the loop times out) + let foundProcesses = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + if foundProcesses.count > 0 { + return foundProcesses[0] + } + } + throw NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: + [NSLocalizedDescriptionKey: "Can't find/launch application \(bundleID.debugDescription)"]) // TO DO: what error to report here, since launchApplication(withBundleIdentifier:options:additionalEventParamDescriptor:launchIdentifier:) doesn't provide any error info itself? + } +} + +/******************************************************************************/ +// logging + +struct StderrStream: TextOutputStream { + public mutating func write(_ string: String) { fputs(string, stderr) } +} +var errStream = StderrStream() + +/******************************************************************************/ +// convert between 4-character strings and OSTypes (use these instead of calling UTGetOSTypeFromString/UTCopyStringFromOSType directly) + +extension FourCharCode { + + public init(fourByteString string: String) throws { + // convert four-character string containing MacOSRoman characters to OSType + // (this is safer than using UTGetOSTypeFromString, which silently fails if string is malformed) + guard let data = string.data(using: .macOSRoman) else { + throw AutomationError(code: 1, message: "Invalid four-char code (bad encoding): \(string.debugDescription)") + } + guard data.count == 4 else { + throw AutomationError(code: 1, message: "Invalid four-char code (wrong length): \(string.debugDescription)") + } + let reinterpreted = data.withUnsafeBytes { $0.bindMemory(to: FourCharCode.self).first! } + self.init(reinterpreted.bigEndian) + } + +} + +extension String { + + public init(fourCharCode: FourCharCode) { + // convert an OSType to four-character string containing MacOSRoman characters + self.init(UTCreateStringForOSType(fourCharCode).takeRetainedValue() as String) + } + +} + +public func eightCharCode(_ eventClass: OSType, _ eventID: OSType) -> UInt64 { + return UInt64(eventClass) << 32 | UInt64(eventID) +} + +// misc AEDesc packing functions + +extension NSAppleEventDescriptor { + + convenience init(type: OSType, code: OSType) { + var data = code + self.init(descriptorType: type, bytes: &data, length: MemoryLayout.size)! + } + + convenience init(uint32 data: UInt32) { + var data = data + self.init(descriptorType: AE4.Types.uInt32, bytes: &data, length: MemoryLayout.size)! + } +} + +// the following AEDesc types will be mapped to Symbol instances +let symbolDescriptorTypes: Set = [typeType, typeEnumerated, typeProperty, typeKeyword] + +/******************************************************************************/ +// consids/ignores options are defined in ASRegistry.h (they're a crappy design and a complete mess, and most apps completely ignore them, but we support them anyway in order to ensure feature parity with AS) + +public enum Consideration { + case `case` + case diacritic + case whiteSpace + case hyphens + case expansion + case punctuation +// case replies // TO DO: check if this is ever supplied by AS; if it is, might be an idea to add it; if not, delete + case numericStrings +} + +public typealias Considerations = Set + +public typealias SendOptions = NSAppleEventDescriptor.SendOptions + +/******************************************************************************/ +// launch and relaunch options used in Application initializers + +public typealias LaunchOptions = NSWorkspace.LaunchOptions + +public let DefaultLaunchOptions: LaunchOptions = NSWorkspace.LaunchOptions.withoutActivation + +public enum RelaunchMode { // if [local] target process has terminated, relaunch it automatically when sending next command to it + case always + case limited + case never +} + +public let DefaultRelaunchMode: RelaunchMode = .limited + +// Indicates omitted command parameter + +public enum OptionalParameter { + case none +} + +public let NoParameter = OptionalParameter.none + +func parameterExists(_ value: Any) -> Bool { + return value as? OptionalParameter != NoParameter +} + +/******************************************************************************/ +// Apple event descriptors used to terminate nested AERecord (of typeObjectSpecifier, etc) chains + +public let applicationRoot = NSAppleEventDescriptor.null() + +public let containerRoot = NSAppleEventDescriptor(descriptorType: typeCurrentContainer, data: nil)! + +// root descriptor for an object specifier describing an element whose state is being compared in a by-test specifier +// e.g. `every track where (rating of «typeObjectBeingExamined» > 50)` +public let specimenRoot = NSAppleEventDescriptor(descriptorType: typeObjectBeingExamined, data: nil)! diff --git a/Sources/AEthereal/Symbol.swift b/Sources/AEthereal/Symbol.swift new file mode 100644 index 0000000..529500a --- /dev/null +++ b/Sources/AEthereal/Symbol.swift @@ -0,0 +1,49 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// Ian's notes: SA's method of representing ae4 codes. + +import Foundation + +public struct Symbol { + + public let code: OSType + public let type: OSType + + public init(code: OSType, type: OSType) { + self.code = code + self.type = type + } + +} + +// MARK: AEEncodable +extension Symbol: AEEncodable { + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + encodeAEDescriptor() + } + + public func encodeAEDescriptor() -> NSAppleEventDescriptor { + NSAppleEventDescriptor(type: type, code: code) + } + +} + +// MARK: Hashable +extension Symbol: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(code) + } + + public static func ==(lhs: Symbol, rhs: Symbol) -> Bool { + // note: operands are not required to be the same subclass as this compares for AE equality only, e.g.: + // + // TED.document == AESymbol(code: "docu") -> true + // + // 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 + lhs.code == rhs.code + } + +} diff --git a/Sources/AEthereal/TypeExtensions.swift b/Sources/AEthereal/TypeExtensions.swift new file mode 100644 index 0000000..bdfc69d --- /dev/null +++ b/Sources/AEthereal/TypeExtensions.swift @@ -0,0 +1,301 @@ +// Originally written by hhas. +// See README.md for licensing information. + +// +// Extends Swift's generic Optional and collection types so that they encode and decode themselves (since Swift lacks the dynamic introspection capabilities for App to determine how to encode and decode them itself) +// + +import Foundation + + +/******************************************************************************/ +// Specifier and Symbol subclasses encode themselves +// Set, Array, Dictionary structs encode and decode themselves +// Optional and MayBeMissing enums encode and decode themselves + +public protocol AEEncodable { + func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor +} + +public protocol AEDecodable { + init?(from descriptor: NSAppleEventDescriptor, app: App) throws +} + +public typealias AECodable = AEEncodable & AEDecodable + + +/******************************************************************************/ +// `missing value` constant + +// note: this design is not yet finalized (ideally we'd just map cMissingValue to nil, but returning nil for commands whose return type is `Any` is a PITA as all of Swift's normal unboxing techniques break, and the only way to unbox is to cast from Any to Optional first, which in turn requires that T is known in advance, in which case what's the point of returning Any in the first place?) + +let missingValueDesc = NSAppleEventDescriptor(typeCode: AE4.Classes.missingValue) + + +// unlike Swift's `nil` (which is actually an infinite number of values since Optional.none is generic), there is only ever one `MissingValue`, which means it should behave sanely when cast to and from `Any` + +public enum MissingValueType: CustomStringConvertible, AECodable { + + case missingValue + + init() { self = .missingValue } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + return missingValueDesc + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + self.init() + } + + public var description: String { return "MissingValue" } +} + +public let MissingValue = MissingValueType() // the `missing value` constant; serves a similar purpose to `Optional.none` (`nil`), except that it's non-generic so isn't a giant PITA to deal with when casting to/from `Any` + + +// TO DO: define `==`/`!=` operators that treat MayBeMissing.missing(…) and MissingValue and Optional.none as equivalent? Or get rid of `MayBeMissing` enum and (if possible/practical) support `Optional as? MissingValueType` and vice-versa? + +// define a generic type for use in command's return type that allows the value to be missing, e.g. `Contacts().people.birthDate.get() as [MayBeMissing]` + +// TO DO: it may be simpler for users if commands always return Optional.none when an Optional return type is specified, and MissingValue when one is not + +public enum MayBeMissing: AECodable { // TO DO: rename 'MissingOr'? this'd be more in keeping with TypeSupportSpec-generated enum names (e.g. 'IntOrStringOrMissing') + case value(T) + case missing(MissingValueType) + + public init(_ value: T) { + switch value { + case is MissingValueType: + self = .missing(MissingValue) + default: + self = .value(value) + } + } + + public init() { + self = .missing(MissingValue) + } + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + switch self { + case .value(let value): + return try app.encode(value) + case .missing(_): + return missingValueDesc + } + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + if isMissingValue(descriptor) { + self = .missing(MissingValue) + } else { + self = .value(try app.decode(descriptor) as T) + } + } + + public var value: T? { // unbox the actual value, or return `nil` if it was MissingValue; this should allow users to bridge safely from MissingValue to nil + switch self { + case .value(let value): + return value + case .missing(_): + return nil + } + } +} + + +func isMissingValue(_ desc: NSAppleEventDescriptor) -> Bool { // check if the given AEDesc is the `missing value` constant + return desc.descriptorType == AE4.Types.type && desc.typeCodeValue == AE4.Classes.missingValue +} + +// allow optionals to be used in place of MayBeMissing… arguably, MayBeMissing won't be needed if this works + +extension Optional: AECodable { + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + switch self { + case .some(let value): + return try app.encode(value) + case .none: + return missingValueDesc + } + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + if isMissingValue(descriptor) { + self = .none + } else { + self = .some(try app.decode(descriptor)) + } + } + +} + + +/******************************************************************************/ +// extend Swift's standard collection types to encode and decode themselves + + +extension Set: AECodable { // note: AEM doesn't define a standard AE type for Sets, so encode/decode as typeAEList (we'll assume client code has its own reasons for suppling/requesting Set instead of Array) + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.list() + for item in self { desc.insert(try app.encode(item), at: 0) } + return desc + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + var result = Set() + switch descriptor.descriptorType { + case AE4.Types.list: + for i in 1..<(descriptor.numberOfItems+1) { // bug workaround for zero-length range: 1...0 throws error, but 1..<1 doesn't + do { + result.insert(try app.decode(descriptor.atIndex(i)!) as Element) + } catch { + throw DecodeError(app: app, descriptor: descriptor, type: Self.self, message: "Can't decode item \(i) as \(Element.self).") + } + } + default: + result.insert(try app.decode(descriptor) as Element) + } + self = result + } + +} + + +extension Array: AECodable { + + // TO DO: protocol hierarchy for Swift's various numeric types is both complicated and useless; see about factoring out `Int(n) as! Element` as a block, in which case copy-paste can be replaced with generic + + private static func decodeInt16Array(_ desc: NSAppleEventDescriptor, app: App, indexes: [Int]) throws -> [Element] { + if Element.self == Int.self { // common case + var result = [Element]() + let data = desc.data + for i in indexes { // QDPoint is YX, so swap to give [X,Y] + var n: Int16 = 0 + (data as NSData).getBytes(&n, range: NSRange(location: i*MemoryLayout.size, length: MemoryLayout.size)) + result.append(Int(n) as! Element) // note: can't use Element(n) here as Swift doesn't define integer constructors in IntegerType protocol (but does for FloatingPointType) + } + return result + } else { // for any other Element, decode as Int then repack as AEList of typeSInt32, and [try to] decode that as [Element] (bit lazy, but will do) + return try self.init(from: try app.encode(app.decode(desc) as [Int]), app: app) + } + } + + private static func decodeUInt16Array(_ desc: NSAppleEventDescriptor, app: App, indexes:[Int]) throws -> [Element] { + if Element.self == Int.self { // common case + var result = [Element]() + let data = desc.data + for i in indexes { // QDPoint is YX, so swap to give [X,Y] + var n: UInt16 = 0 + (data as NSData).getBytes(&n, range: NSRange(location: i*MemoryLayout.size, length: MemoryLayout.size)) + result.append(Int(n) as! Element) // note: can't use Element(n) here as Swift doesn't define integer constructors in IntegerType protocol (but does for FloatingPointType) + } + return result + } else { // for any other Element, decode as Int then repack as AEList of typeSInt32, and [try to] decode that as [Element] (bit lazy, but will do) + return try self.init(from: try app.encode(app.decode(desc) as [Int]), app: app) + } + } + + // + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + let desc = NSAppleEventDescriptor.list() + for item in self { desc.insert(try app.encode(item), at: 0) } + return desc + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + switch descriptor.descriptorType { + case AE4.Types.list: + var result = [Element]() + for i in 1..<(descriptor.numberOfItems+1) { // bug workaround for zero-length range: 1...0 throws error, but 1..<1 doesn't + do { + result.append(try app.decode(descriptor.atIndex(i)!) as Element) + } catch { + throw DecodeError(app: app, descriptor: descriptor, type: Self.self, message: "Can't decode item \(i) as \(Element.self).") + } + } + self = result + // note: coercing QD types to typeAEList and decoding those would be simpler, but while AEM provides coercion handlers for coercing e.g. typeAEList to typeQDPoint, it doesn't provide handlers for the reverse (coercing a typeQDPoint desc to typeAEList merely produces a single-item AEList containing the original typeQDPoint, not a 2-item AEList of typeSInt16) + case AE4.Types.qdPoint: // SInt16[2] + self = try Array.decodeInt16Array(descriptor, app: app, indexes: [1,0]) // QDPoint is YX; swap to give [X,Y] + case AE4.Types.qdRectangle: // SInt16[4] + self = try Array.decodeInt16Array(descriptor, app: app, indexes: [1,0,3,2]) // QDRectangle is Y0X0Y1X1; swap to give [X0,Y0,X1,Y1] + case AE4.Types.rgbColor: // UInt16[3] (used by older Carbon apps; Cocoa apps use lists) + self = try Array.decodeUInt16Array(descriptor, app: app, indexes: [0,1,2]) + default: + self = [try app.decode(descriptor) as Element] + } + } + +} + + +extension Dictionary: AECodable { + + public func encodeAEDescriptor(_ app: App) throws -> NSAppleEventDescriptor { + var desc = NSAppleEventDescriptor.record() + var isCustomRecordType: Bool = false + if let key = Symbol(code: AE4.Properties.class, type: typeType) as? Key, let recordClass = self[key] as? Symbol { // TO DO: confirm this works + desc = desc.coerce(toDescriptorType: recordClass.code)! + isCustomRecordType = true + } + for (key, value) in self { + guard let keySymbol = key as? Symbol else { + throw PackError(object: key, message: "Can't encode non-Symbol dictionary key of type: \(type(of: key))") + } + if !(keySymbol.code == AE4.Properties.class && isCustomRecordType) { + desc.setDescriptor(try app.encode(value), forKeyword: keySymbol.code) + } + } + return desc + } + + public init(from descriptor: NSAppleEventDescriptor, app: App) throws { + if !descriptor.isRecordDescriptor { + throw DecodeError(app: app, descriptor: descriptor, type: Self.self, message: "Not a record.") + } + var result = [Key:Value]() + if descriptor.descriptorType != AE4.Types.record { + if let key = Symbol(code: AE4.Properties.class, type: typeType) as? Key, + let value = Symbol(code: descriptor.descriptorType, type: typeType) as? Value { + result[key] = value + } + } + for i in 1..<(descriptor.numberOfItems + 1) { + let property = descriptor.keywordForDescriptor(at: i) + // decode record property whose key is a four-char code (typically corresponding to a dictionary-defined property name) + guard let key = Symbol(code: property, type: AE4.Types.property) as? Key else { + throw DecodeError(app: app, descriptor: descriptor, type: Key.self, + message: "Can't decode record keys as non-Symbol type: \(Key.self)") + } + do { + result[key] = try app.decode(descriptor.atIndex(i)!) as Value + } catch { + throw DecodeError(app: app, descriptor: descriptor, type: Value.self, + message: "Can't decode value of record's \(key) property as Swift type: \(Value.self)") + } + } + self = result + } + +} + +// specialized return type for use in commands to return the _entire_ reply AppleEvent as a raw AppleEvent descriptor + +public struct ReplyEventDescriptor { + + let descriptor: NSAppleEventDescriptor // the reply AppleEvent + + public var result: NSAppleEventDescriptor? { // the application-returned result value, if any + return descriptor.paramDescriptor(forKeyword: keyDirectObject) + } + + public var errorNumber: Int { // the application-returned error number, if any; 0 = noErr + return Int(descriptor.paramDescriptor(forKeyword: keyErrorNumber)?.int32Value ?? 0) + } +} + diff --git a/SwiftAutomation.h b/SwiftAutomation.h new file mode 100644 index 0000000..d61187a --- /dev/null +++ b/SwiftAutomation.h @@ -0,0 +1,16 @@ +// +// SwiftAutomation.h +// SwiftAutomation +// +// + +#import + +//! Project version number for SwiftAutomation. +FOUNDATION_EXPORT double SwiftAutomationVersionNumber; + +//! Project version string for SwiftAutomation. +FOUNDATION_EXPORT const unsigned char SwiftAutomationVersionString[]; + + + diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..2751de3 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,77 @@ +This file started as hhas' SwiftAutomation to-do list as of early 2019 or so. +Anything left here is still relevant to deal with. + +TO DO: + +- AutoReleasePools? + + +- there is a serious bug when comparing Any result to MissingValue: + + print(try Finder().home.name.get() == MissingValue) // returns true!!! (should be false as result is a String value) + +- another problem will be when using `==` on a result that may be a specifier, since == constructs a test clause; going to have to give these overloads more thought; at worst, switch to `eq`, `ne`, `lt`, etc, or custom operators which won't clash with builtins + + +- one problem with overriding `==` for use in test clauses is that swiftc is rather dopey about resolving ambiguous overloads; dunno if there's any way to tell it to ignore this override if LHS is not explicitly ObjectSpecifier, e.g.: + + print(("" as Any) == ("" as Any)) + + error: cannot convert value of type 'Any' to expected argument type 'ObjectSpecifier' + print(("" as Any) == ("" as Any)) + ~~~^~~~~~ + + +- amusing translation error: + + tell application "Keynote" + --get font of object text of every text item of master slides of document 1 + set font of object text of (every text item whose object text's font starts with "IowanOldStyle-") of master slides of document 1 to "Century Schoolbook" + end tell + + gives: + + try Keynote(name: "/Applications/Keynote.app").customRoot(Error -1700: Can't decode malformed descriptor: + + + + Can't decode object specifier's selector data. Can't decode malformed descriptor: + + + + Can't decode comparison test: malformed descriptor.).objectText.font.set(to: "Century Schoolbook") + + 1. SpecifierFormatter.formatValue() needs to check for Error, and show opaque description string (e.g. "«malformed-descriptor»") + + 2. The above is due to a bug in the decode code rather than a malformed AEDesc, so need to track down and fix that as well. + + +- Define standard structs for representing color, position, etc.? (And/Or support encoding/decoding Cocoa equivalents, e.g. NSColor?) The default behavior is to encode/decode as Array, but type-safe alternatives could also be provided that ensure whatever type of descriptor an app returns (typeAEList/typeQDRectangle) the final result is normalized to the semantic type specified by the user (e.g. SwiftAutomation.RectangleRecord). Also, how practical to support encoding/decoding image types (a known mess), e.g. bridging typeTIFF, typePICT, etc to NSImage? + + +- check Symbol.symbol() lookup performance on on very large switches [e.g. InDesign]; if noticeable, a dictionary could be used to cache previous lookups + + +- the `isInt64Compatible` flag's' default is `true` on the assumption that most apps will do the right thing upon receiving `typeUInt32`/`typeSInt64`/`typeUInt64` descriptors (i.e. coerce them to whatever type[s] they actually need, e.g. Double), and apps like Excel which only accept `SInt32` and `Double` (which are what AppleScript uses) and fail on anything else are in the minority. If that assumption turns out to be wrong, this flag will need to be made `false` by default (i.e. emulate AppleScript's behavior). Note that this only affects integers beyond ±2**31; integers than can be packed into SInt32 always will be. + + +- currently Application.customRoot(object) allows the creation of individual object specifiers with a non-typeNull terminator, e.g. `Finder().customRoot(aURL).name` is equivalent to `tell app "Finder" to name of POSIX file "..."`. In practice, this is almost never neeeded (or supported by apps), but it's included for maximum AppleScript 'quirk-for-quirk' compatibility. Should App also provide an option to set the default app root object to be used in building and decoding _all_ object specifiers? The only place this might be useful is in Automator when packing fully qualified object specifiers; OTOH, fully qualified object specifiers always were a bad solution to the problem of binding AEAddressDescs to objspec descriptors, and Automator's a dead-end and largely moribund product anyway. + + +- note that file system specifiers (typeAlias, typeFileURL, typeFSRef, etc) currently don't roundtrip (they all get coerced down to typeFileURL, regardless of their original type, and URL instances always encode as typeFileURL); this may or may not be a problem when dealing with older Carbon apps. One solution may be an `isFileURLCompatible` flag that, if set to false, packs URLs as as typeAlias if the path identifies an existing FS object and typeFileURL if it does not. (Packing as typeFSRef is redundant and typeFSS is defunct, so those should not need supported.) -- TBH, probably best forget about roundtripping these types, encode all file: URLs as typeFileURL, and hope for the best: 1. there's no way to attach their AEDescs to a URL struct, and 2. URL structs don't support isFileReferenceURL so there's no way to tell if they're a bookmark (in which case encode as typeAlias) or a path (encode as typeFileURL) + + - typeBookmark/typeAlias descriptors identify file system *objects* [e.g. by inode], whereas typeFSRef/typeFileURL identify file system locations; however, only NSURL can distinguish the two so for now any Bookmark/Alias information will be lost on conversion to Swift `URL` instances, while `URL` will always encode as typeFileURL. This may change in future depending on how many compatibility issues with older Carbon apps this lack of roundtripping throws up.] + + +- worth including convenience API for interacting with `NSAppleScript`/`NSUserAppleScriptTask`/`OSAScript`? Given that SwiftAutomation provides automatic Swift<->AE type bridging, it seems a pity not to take advantage of it as it'll greatly reduce the amount of effort needed to make Swift apps attachable. Or should that be spun off into its own library to avoid crudding up SwiftAutomation's main API? (Probably the latter, as it allows that code to evolve independently later on. That may be particularly significant if the app also implements its own AEOM which attached scripts will want to interact with; doubly so if Swift ever gets a native AEOM framework [as a non-sucky alternative to CocoaScripting]; triply so if it can get a non-sucky replacement to the awful, archaic, and barely supported/supportable ComponentManager-based OpenScripting [OSA] framework.) + + +- default SendMode is currently [.canSwitchLayer,.waitReply]; confirm this is same as AppleScript + + +- can custom mirrors be improved? what info should they show? (one problem is that playgrounds tend to display array of specifier as mirror, not description/debugDescription, which is unhelpful) + + +- need to check both AETE and SDEF parsers work correctly with apps that have multiple terminologies (e.g. if app also has scriptable plugins) + +- also find out if macOS's SDEF-to-AETE converter now handles 'unprintable' OSTypes correctly (i.e. uses hex representation)