-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 48fc2ec
Showing
20 changed files
with
4,359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata |
8 changes: 8 additions & 0 deletions
8
.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key> | ||
<false/> | ||
</dict> | ||
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Int> = [procNotFound, connectionInvalid, localOnlyErr] | ||
|
||
private let launchEventSucceededErrorNumbers: Set<Int> = [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) | ||
} | ||
} |
Oops, something went wrong.