Skip to content

Commit f245eaa

Browse files
committed
Create CEActiveTaskTerminalView
1 parent ae9e7d4 commit f245eaa

5 files changed

Lines changed: 293 additions & 157 deletions

File tree

CodeEdit/Features/Tasks/Models/CEActiveTask.swift

Lines changed: 57 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77

88
import SwiftUI
99
import Combine
10+
import SwiftTerm
1011

1112
/// Stores the state of a task once it's executed
1213
class CEActiveTask: ObservableObject, Identifiable, Hashable {
1314
/// The current progress of the task.
14-
@Published private(set) var output: String = ""
15+
@Published var output: CEActiveTaskTerminalView?
16+
17+
var hasOutputBeenConfigured: Bool = false
1518

1619
/// The status of the task.
1720
@Published private(set) var status: CETaskStatus = .notRunning
@@ -22,138 +25,113 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable {
2225
/// Prevents tasks overwriting each other.
2326
/// Say a user cancels one task, then runs it immediately, the cancel message should show and then the
2427
/// starting message should show. If we don't add this modifier the starting message will be deleted.
25-
var activeTaskID: String = UUID().uuidString
28+
var activeTaskID: UUID = UUID()
2629

2730
var taskId: String {
28-
task.id.uuidString + "-" + activeTaskID
31+
task.id.uuidString + "-" + activeTaskID.uuidString
2932
}
3033

31-
var process: Process?
32-
var outputPipe: Pipe?
3334
var workspaceURL: URL?
3435

3536
private var cancellables = Set<AnyCancellable>()
3637

3738
init(task: CETask) {
3839
self.task = task
39-
self.process = Process()
40-
self.outputPipe = Pipe()
4140

4241
self.task.objectWillChange.sink { _ in
4342
self.objectWillChange.send()
4443
}.store(in: &cancellables)
4544
}
4645

47-
func run(workspaceURL: URL? = nil) {
46+
func run(workspaceURL: URL) {
4847
self.workspaceURL = workspaceURL
49-
self.activeTaskID = UUID().uuidString // generate a new ID for this run
50-
Task {
51-
// Reconstruct the full command to ensure it executes in the correct directory.
52-
// Because: CETask only contains information about the relative path.
53-
let fullCommand: String
54-
if let workspaceURL = workspaceURL {
55-
fullCommand = "cd \(workspaceURL.relativePath.escapedDirectory()) && \(task.fullCommand)"
56-
} else {
57-
fullCommand = task.fullCommand
58-
}
59-
guard let process, let outputPipe else { return }
48+
self.activeTaskID = UUID() // generate a new ID for this run
6049

61-
await updateTaskStatus(to: .running)
62-
createStatusTaskNotification()
50+
updateTaskStatus(to: .running)
51+
createStatusTaskNotification()
6352

64-
process.terminationHandler = { [weak self] capturedProcess in
65-
if let self {
66-
Task {
67-
await self.handleProcessFinished(terminationStatus: capturedProcess.terminationStatus)
68-
}
69-
}
70-
}
53+
let view = output ?? CEActiveTaskTerminalView(activeTask: self)
54+
view.startProcess(workspaceURL: workspaceURL, shell: nil)
7155

72-
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
73-
if let data = String(bytes: fileHandle.availableData, encoding: .utf8),
74-
!data.isEmpty {
75-
Task {
76-
await self.updateOutput(data)
77-
}
78-
}
79-
}
80-
81-
do {
82-
try Shell.executeCommandWithShell(
83-
process: process,
84-
command: fullCommand,
85-
environmentVariables: self.task.environmentVariablesDictionary,
86-
shell: Shell.zsh, // TODO: Let user decide which shell to use
87-
outputPipe: outputPipe
88-
)
89-
} catch { print(error) }
90-
}
56+
output = view
9157
}
9258

93-
func handleProcessFinished(terminationStatus: Int32) async {
94-
handleTerminationStatus(terminationStatus)
95-
59+
func handleProcessFinished(terminationStatus: Int32) {
9660
if terminationStatus == 0 {
97-
await updateOutput("\nFinished running \(task.name).\n\n")
98-
await updateTaskStatus(to: .finished)
61+
output?.newline()
62+
output?.sendOutputMessage("Finished running \(task.name).")
63+
output?.newline()
64+
65+
updateTaskStatus(to: .finished)
9966
updateTaskNotification(
10067
title: "Finished Running \(task.name)",
10168
message: "",
10269
isLoading: false
10370
)
10471
} else if terminationStatus == 15 {
105-
await updateOutput("\n\(task.name) cancelled.\n\n")
106-
await updateTaskStatus(to: .notRunning)
72+
output?.newline()
73+
output?.sendOutputMessage("\(task.name) cancelled.")
74+
output?.newline()
75+
76+
updateTaskStatus(to: .notRunning)
10777
updateTaskNotification(
10878
title: "\(task.name) cancelled",
10979
message: "",
11080
isLoading: false
11181
)
11282
} else {
113-
await updateOutput("\nFailed to run \(task.name).\n\n")
114-
await updateTaskStatus(to: .failed)
83+
output?.newline()
84+
output?.sendOutputMessage("Failed to run \(task.name)")
85+
output?.newline()
86+
87+
updateTaskStatus(to: .failed)
11588
updateTaskNotification(
11689
title: "Failed Running \(task.name)",
11790
message: "",
11891
isLoading: false
11992
)
12093
}
121-
outputPipe?.fileHandleForReading.readabilityHandler = nil
12294

12395
deleteStatusTaskNotification()
12496
}
12597

126-
func renew() {
127-
if let process {
128-
if process.isRunning {
129-
process.terminate()
130-
process.waitUntilExit()
98+
func suspend() {
99+
if let output, status == .running {
100+
for pid in output.runningChildProcesses() {
101+
kill(pid, SIGSTOP)
131102
}
132-
self.process = Process()
133-
outputPipe = Pipe()
103+
updateTaskStatus(to: .stopped)
134104
}
135105
}
136106

137-
func suspend() {
138-
if let process, status == .running {
139-
process.suspend()
140-
Task {
141-
await updateTaskStatus(to: .stopped)
107+
func resume() {
108+
if let output, status == .stopped {
109+
for pid in output.runningChildProcesses() {
110+
kill(pid, SIGCONT)
142111
}
112+
updateTaskStatus(to: .running)
143113
}
144114
}
145115

146-
func resume() {
147-
if let process, status == .stopped {
148-
process.resume()
149-
Task {
150-
await updateTaskStatus(to: .running)
116+
func terminate() {
117+
if let output {
118+
for pid in output.runningChildProcesses() {
119+
kill(pid, SIGTERM)
120+
}
121+
}
122+
}
123+
124+
func interrupt() {
125+
if let output {
126+
for pid in output.runningChildProcesses() {
127+
kill(pid, SIGINT)
151128
}
152129
}
153130
}
154131

155132
func clearOutput() {
156-
output = ""
133+
// TODO: - Clear Output
134+
// output?.feed(text: "\033[2J") // same command as 'clear'
157135
}
158136

159137
private func createStatusTaskNotification() {
@@ -199,23 +177,15 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable {
199177
NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo)
200178
}
201179

202-
private func updateTaskStatus(to taskStatus: CETaskStatus) async {
203-
await MainActor.run {
204-
self.status = taskStatus
205-
}
206-
}
207-
208-
/// Updates the progress and output values on the main thread`
209-
private func updateOutput(_ output: String) async {
210-
await MainActor.run {
211-
self.output += output
212-
}
180+
@MainActor
181+
private func updateTaskStatus(to taskStatus: CETaskStatus) {
182+
self.status = taskStatus
213183
}
214184

215185
static func == (lhs: CEActiveTask, rhs: CEActiveTask) -> Bool {
216186
return lhs.output == rhs.output &&
217187
lhs.status == rhs.status &&
218-
lhs.process == rhs.process &&
188+
lhs.output?.process.shellPid == rhs.output?.process.shellPid &&
219189
lhs.task == rhs.task
220190
}
221191

@@ -224,24 +194,4 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable {
224194
hasher.combine(status)
225195
hasher.combine(task)
226196
}
227-
228-
// OPTIONAL
229-
func handleTerminationStatus(_ status: Int32) {
230-
switch status {
231-
case 0:
232-
print("Process completed successfully.")
233-
case 1:
234-
print("General error.")
235-
case 2:
236-
print("Misuse of shell builtins.")
237-
case 126:
238-
print("Command invoked cannot execute.")
239-
case 127:
240-
print("Command not found.")
241-
case 128:
242-
print("Invalid argument to exit.")
243-
default:
244-
print("Process ended with exit code \(status).")
245-
}
246-
}
247197
}

CodeEdit/Features/Tasks/TaskManager.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ class TaskManager: ObservableObject {
1616

1717
@ObservedObject var workspaceSettings: CEWorkspaceSettingsData
1818

19-
private var workspaceURL: URL?
19+
private var workspaceURL: URL
2020
private var settingsListener: AnyCancellable?
2121

22-
init(workspaceSettings: CEWorkspaceSettingsData, workspaceURL: URL? = nil) {
22+
init(workspaceSettings: CEWorkspaceSettingsData, workspaceURL: URL) {
2323
self.workspaceURL = workspaceURL
2424
self.workspaceSettings = workspaceSettings
2525

@@ -71,7 +71,7 @@ class TaskManager: ObservableObject {
7171
// A process can only be started once, that means we have to renew the Process and Pipe
7272
// but don't initialize a new object.
7373
if let activeTask = activeTasks[task.id] {
74-
activeTask.renew()
74+
activeTask.terminate()
7575
// Wait until the task is no longer running.
7676
// The termination handler is asynchronous, so we avoid a race condition using this.
7777
while activeTask.status == .running {
@@ -138,10 +138,9 @@ class TaskManager: ObservableObject {
138138
///
139139
/// - Parameter taskID: The ID of the task to terminate.
140140
func terminateTask(taskID: UUID) {
141-
guard let process = activeTasks[taskID]?.process, process.isRunning else {
142-
return
141+
if let activeTask = activeTasks[taskID] {
142+
activeTask.terminate()
143143
}
144-
process.terminate()
145144
}
146145

147146
/// Interrupts the task associated with the given task ID.
@@ -156,10 +155,9 @@ class TaskManager: ObservableObject {
156155
///
157156
/// - Parameter taskID: The ID of the task to interrupt.
158157
func interruptTask(taskID: UUID) {
159-
guard let process = activeTasks[taskID]?.process, process.isRunning else {
160-
return
158+
if let activeTask = activeTasks[taskID] {
159+
activeTask.interrupt()
161160
}
162-
process.interrupt()
163161
}
164162

165163
func stopAllTasks() {

0 commit comments

Comments
 (0)