77
88import SwiftUI
99import Combine
10+ import SwiftTerm
1011
1112/// Stores the state of a task once it's executed
1213class 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 ( " \n Finished 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 ( " \n Failed 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}
0 commit comments