-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
CommandPanelCoordinator.swift
149 lines (130 loc) · 4.35 KB
/
CommandPanelCoordinator.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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import Carbon
import Cocoa
import Foundation
final class CommandPanelCoordinator: NSObject, ObservableObject, NSWindowDelegate {
private var cache = [ScriptCommand.ID: NSWindowController]()
init(cache: [ScriptCommand.ID : NSWindowController] = [ScriptCommand.ID: NSWindowController]()) {
self.cache = cache
}
@MainActor
func run(_ command: ScriptCommand) {
let windowController = cache[command.id, default: createNewWindowController(for: command)]
if windowController.window?.isVisible == false {
windowController.showWindow(nil)
}
windowController.window?.makeKeyAndOrderFront(nil)
if cache[command.id] != windowController {
cache[command.id] = windowController
}
}
@MainActor
private func createNewWindowController(for command: ScriptCommand) -> NSWindowController {
let publisher = CommandPanelViewPublisher(state: .ready)
let runner = CommandPanelRunner(plugin: ShellScriptPlugin())
var command = command
let view = CommandPanelView(publisher: publisher, command: command,
onChange: { newContents in
command.source = .inline(newContents)
}, onSubmit: { _ in
runner.run(command, for: publisher)
}, action: { [runner, publisher] in
runner.run(command, for: publisher)
})
let window = CommandPanel(identifier: command.id, runner: runner, minSize: .zero, rootView: view)
window.eventDelegate = self
window.delegate = self
let windowController = NSWindowController(window: window)
windowController.windowFrameAutosaveName = "CommandPanel-\(command.id)"
runner.run(command, for: publisher)
return windowController
}
// MARK: NSWindowDelegate
func windowShouldClose(_ sender: NSWindow) -> Bool {
clearCache(sender)
return true
}
@MainActor
func clearCache(_ window: NSWindow) {
var updatedCache = cache
for (scriptID, controller) in cache {
if controller.window == window {
updatedCache[scriptID] = nil
}
}
cache = updatedCache
}
}
extension CommandPanelCoordinator: CommandPanelEventDelegate {
// MARK: CommandPanelEventDelegate
@MainActor
func shouldConsumeEvent(_ event: NSEvent, for window: NSWindow, runner: CommandPanelRunner) -> Bool {
switch Int(event.keyCode) {
case kVK_ANSI_W:
if event.type == .keyDown, event.modifierFlags.contains(.command) {
runner.cancel()
window.close()
clearCache(window)
return true
}
return false
case kVK_Escape:
if event.type == .keyDown {
runner.cancel()
window.close()
clearCache(window)
}
return true
default:
return false
}
}
}
final class CommandPanelRunner {
let plugin: ShellScriptPlugin
var task: Task<Void, Error>?
init(plugin: ShellScriptPlugin) {
self.plugin = plugin
}
func cancel() {
task?.cancel()
}
@MainActor
func run(_ command: ScriptCommand, for publisher: CommandPanelViewPublisher) {
if publisher.state == .running {
task?.cancel()
publisher.publish(.ready)
return
}
self.task?.cancel()
let task = Task(priority: .high) { [plugin] in
publisher.publish(.running)
let snapshot = await UserSpace.shared.snapshot(resolveUserEnvironment: true)
do {
let output: String?
switch (command.kind, command.source) {
case (.shellScript, .path(let source)):
output = try await plugin.executeScript(at: source, environment: snapshot.terminalEnvironment(),
checkCancellation: true)
case (.shellScript, .inline(let script)):
output = try await plugin.executeScript(script, environment: snapshot.terminalEnvironment(),
checkCancellation: true)
default:
// This shouldn't happend.
assertionFailure("Not supported.")
return
}
publisher.publish(.done(output ?? "No output"))
} catch let error as ShellScriptPlugin.ShellScriptPluginError {
let newState: CommandPanelView.CommandState
switch error {
case .noData:
newState = .error(error.localizedDescription)
case .scriptError(let scriptError):
newState = .error(scriptError)
}
publisher.publish(newState)
}
}
self.task = task
}
}