-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
AppleScriptPlugin.swift
117 lines (94 loc) · 3.6 KB
/
AppleScriptPlugin.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import Combine
import Cocoa
enum AppleScriptPluginError: Error {
case failedToCreateInlineScript
case failedToCreateScriptAtURL(URL)
case compileFailed(Error)
case executionFailed(Error)
}
extension NSAppleScript: @unchecked Sendable {}
actor AppleScriptCache: @unchecked Sendable {
private var storage = [String: NSAppleScript]()
func clear() {
storage = [:]
}
func set(_ appleScript: NSAppleScript, for key: String) {
storage[key] = appleScript
}
func get(_ key: String) -> NSAppleScript? {
storage[key]
}
}
final class AppleScriptPlugin: @unchecked Sendable {
private let bundleIdentifier = Bundle.main.bundleIdentifier!
private let cache = AppleScriptCache()
private var frontmostApplicationSubscription: AnyCancellable?
init(workspace: NSWorkspace) {
frontmostApplicationSubscription = workspace.publisher(for: \.frontmostApplication)
.compactMap { $0 }
.filter { $0.bundleIdentifier == self.bundleIdentifier }
.sink { [cache] _ in
Task { await cache.clear() }
}
}
func executeScript(at path: String, withId key: String, checkCancellation: Bool) async throws -> String? {
if let cachedAppleScript = await cache.get(key) {
return cachedAppleScript.executeAndReturnError(nil).stringValue
}
let filePath = path.sanitizedPath
let url = URL(fileURLWithPath: filePath)
var errorDictionary: NSDictionary?
guard let appleScript = NSAppleScript(contentsOf: url, error: &errorDictionary) else {
throw AppleScriptPluginError.failedToCreateScriptAtURL(url)
}
if checkCancellation { try Task.checkCancellation() }
let descriptor = try self.execute(appleScript)
await cache.set(appleScript, for: key)
return descriptor.stringValue
}
func execute(_ source: String, withId id: String, checkCancellation: Bool) async throws -> String? {
if let cachedAppleScript = await cache.get(id) {
do {
try Task.checkCancellation()
} catch {
throw error
}
return cachedAppleScript.executeAndReturnError(nil).stringValue
}
guard let appleScript = NSAppleScript(source: source) else {
throw AppleScriptPluginError.failedToCreateInlineScript
}
do {
if checkCancellation { try Task.checkCancellation() }
let descriptor = try self.execute(appleScript)
await cache.set(appleScript, for: id)
return descriptor.stringValue
} catch {
throw error
}
}
// MARK: Private methods
private func execute(_ appleScript: NSAppleScript) throws -> NSAppleEventDescriptor {
var errorDictionary: NSDictionary?
appleScript.compileAndReturnError(&errorDictionary)
if let errorDictionary = errorDictionary {
throw AppleScriptPluginError.compileFailed(createError(from: errorDictionary))
}
let descriptor = appleScript.executeAndReturnError(&errorDictionary)
if let errorDictionary = errorDictionary {
throw AppleScriptPluginError.executionFailed(createError(from: errorDictionary))
}
return descriptor
}
private func createError(from dictionary: NSDictionary) -> Error {
let code = dictionary[NSAppleScript.errorNumber] as? Int ?? 0
let errorMessage = dictionary[NSAppleScript.errorMessage] as? String ?? "Missing error message"
let descriptionMessage = dictionary[NSAppleScript.errorBriefMessage] ?? "Missing description"
let errorDomain = "com.zenangst.KeyboardCowboy.AppleScriptPlugin"
let error = NSError(domain: errorDomain, code: code, userInfo: [
NSLocalizedFailureReasonErrorKey: errorMessage,
NSLocalizedDescriptionKey: descriptionMessage
])
return error
}
}