@@ -24,16 +24,20 @@ impl ScriptManager {
24
24
cursor, execute,
25
25
style:: { Color , Print , SetForegroundColor } ,
26
26
terminal:: { self , Clear , ClearType } ,
27
+ event:: { self , Event , KeyCode , KeyEvent } ,
27
28
} ;
28
29
use parking_lot:: Mutex ;
29
30
use std:: io:: stdout;
30
- use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
31
+ use std:: sync:: atomic:: { AtomicBool , AtomicUsize , Ordering } ;
31
32
use std:: sync:: Arc ;
32
33
use std:: thread:: { self , JoinHandle } ;
33
- use std:: time:: Instant ;
34
+ use std:: time:: { Duration , Instant } ;
34
35
35
36
// Create a mutex-wrapped stdout for thread-safe access
36
37
let stdout = Arc :: new ( Mutex :: new ( stdout ( ) ) ) ;
38
+
39
+ // Create cancellation flag for Ctrl+C handling
40
+ let cancelled = Arc :: new ( AtomicBool :: new ( false ) ) ;
37
41
38
42
// Initialize terminal
39
43
{
@@ -60,7 +64,7 @@ impl ScriptManager {
60
64
}
61
65
62
66
let commands = Arc :: new ( script_config. commands . clone ( ) ) ;
63
- let mut handles: Vec < JoinHandle < Result < ( ) > > > = vec ! [ ] ; // Add type annotation here
67
+ let mut handles: Vec < JoinHandle < Result < ( ) > > > = vec ! [ ] ;
64
68
65
69
// Calculate screen layout
66
70
let ( term_width, term_height) = terminal:: size ( ) ?;
@@ -95,15 +99,46 @@ impl ScriptManager {
95
99
// For parallel execution with max threads control
96
100
let active_threads = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
97
101
98
- // Execute commands
102
+ // Start Ctrl+C detection thread
103
+ let cancelled_clone = Arc :: clone ( & cancelled) ;
104
+ let stdout_clone = Arc :: clone ( & stdout) ;
105
+ let ctrl_c_handle = thread:: spawn ( move || {
106
+ loop {
107
+ if event:: poll ( Duration :: from_millis ( 100 ) ) . unwrap_or ( false ) {
108
+ if let Ok ( Event :: Key ( KeyEvent { code : KeyCode :: Char ( 'c' ) , modifiers, .. } ) ) = event:: read ( ) {
109
+ if modifiers. contains ( crossterm:: event:: KeyModifiers :: CONTROL ) {
110
+ cancelled_clone. store ( true , Ordering :: SeqCst ) ;
111
+ let mut stdout = stdout_clone. lock ( ) ;
112
+ let _ = execute ! (
113
+ stdout,
114
+ cursor:: MoveTo ( 0 , 0 ) ,
115
+ SetForegroundColor ( Color :: Red ) ,
116
+ Print ( "⚠️ Cancelling execution... (Ctrl+C detected)" )
117
+ ) ;
118
+ break ;
119
+ }
120
+ }
121
+ }
122
+ if cancelled_clone. load ( Ordering :: SeqCst ) {
123
+ break ;
124
+ }
125
+ }
126
+ } ) ;
127
+
99
128
// Execute commands
100
129
for ( idx, cmd) in commands. iter ( ) . enumerate ( ) {
130
+ // Check for cancellation before starting new commands
131
+ if cancelled. load ( Ordering :: SeqCst ) {
132
+ break ;
133
+ }
134
+
101
135
let cmd = cmd. clone ( ) ;
102
136
let name = name. to_string ( ) ;
103
137
let stdout = Arc :: clone ( & stdout) ;
104
138
let y_pos = idx as u16 * section_height;
105
139
// let term_width = term_width;
106
140
let active_threads = Arc :: clone ( & active_threads) ;
141
+ let cancelled_flag = Arc :: clone ( & cancelled) ;
107
142
108
143
// If parallel execution is disabled, wait for previous command to complete
109
144
if !script_config. parallel {
@@ -146,6 +181,15 @@ impl ScriptManager {
146
181
let handle = thread:: spawn ( move || -> Result < ( ) > {
147
182
let command_start = Instant :: now ( ) ;
148
183
184
+ // Check for cancellation before starting
185
+ if cancelled_flag. load ( Ordering :: SeqCst ) {
186
+ active_threads. fetch_sub ( 1 , Ordering :: SeqCst ) ;
187
+ return Err ( HookError :: ScriptExecutionError {
188
+ script_name : name,
189
+ reason : "Execution cancelled" . to_string ( ) ,
190
+ } ) ;
191
+ }
192
+
149
193
// Build command with working directory and environment variables
150
194
let mut child = std:: process:: Command :: new ( "sh" ) ;
151
195
child. arg ( "-c" ) . arg ( & cmd. command ) ;
@@ -173,10 +217,19 @@ impl ScriptManager {
173
217
let mut current_line = y_pos + 1 ;
174
218
175
219
// Process output in real-time
176
-
177
220
if let Some ( child_stdout) = child. stdout . take ( ) {
178
221
let reader = std:: io:: BufReader :: new ( child_stdout) ;
179
222
for line in std:: io:: BufRead :: lines ( reader) . flatten ( ) {
223
+ // Check for cancellation during output processing
224
+ if cancelled_flag. load ( Ordering :: SeqCst ) {
225
+ let _ = child. kill ( ) ;
226
+ active_threads. fetch_sub ( 1 , Ordering :: SeqCst ) ;
227
+ return Err ( HookError :: ScriptExecutionError {
228
+ script_name : name,
229
+ reason : "Execution cancelled" . to_string ( ) ,
230
+ } ) ;
231
+ }
232
+
180
233
let mut stdout = stdout. lock ( ) ;
181
234
execute ! (
182
235
stdout,
@@ -188,12 +241,23 @@ impl ScriptManager {
188
241
}
189
242
}
190
243
244
+ // Check for cancellation before waiting for process completion
245
+ if cancelled_flag. load ( Ordering :: SeqCst ) {
246
+ let _ = child. kill ( ) ;
247
+ active_threads. fetch_sub ( 1 , Ordering :: SeqCst ) ;
248
+ return Err ( HookError :: ScriptExecutionError {
249
+ script_name : name,
250
+ reason : "Execution cancelled" . to_string ( ) ,
251
+ } ) ;
252
+ }
253
+
191
254
let status = child. wait ( ) . map_err ( |e| HookError :: ScriptExecutionError {
192
255
script_name : name. clone ( ) ,
193
256
reason : e. to_string ( ) ,
194
257
} ) ?;
195
258
196
259
if !status. success ( ) {
260
+ active_threads. fetch_sub ( 1 , Ordering :: SeqCst ) ;
197
261
return Err ( HookError :: ScriptExecutionError {
198
262
script_name : name,
199
263
reason : format ! ( "Command '{}' failed with status {}" , cmd. command, status) ,
@@ -218,6 +282,8 @@ impl ScriptManager {
218
282
219
283
// Wait for all remaining threads to complete
220
284
let mut all_successful = true ;
285
+ let was_cancelled = cancelled. load ( Ordering :: SeqCst ) ;
286
+
221
287
while let Some ( handle) = handles. pop ( ) {
222
288
match handle. join ( ) . map_err ( |_| HookError :: ScriptExecutionError {
223
289
script_name : name. to_string ( ) ,
@@ -240,21 +306,29 @@ impl ScriptManager {
240
306
}
241
307
}
242
308
309
+ // Signal the Ctrl+C thread to stop and wait for it
310
+ cancelled. store ( true , Ordering :: SeqCst ) ;
311
+ let _ = ctrl_c_handle. join ( ) ;
312
+
243
313
// Show final status
244
314
let total_duration = start_time. elapsed ( ) ;
245
315
{
246
316
let mut stdout = stdout. lock ( ) ;
247
317
execute ! (
248
318
stdout,
249
319
cursor:: MoveTo ( 0 , term_height - 1 ) ,
250
- SetForegroundColor ( if all_successful {
320
+ SetForegroundColor ( if was_cancelled {
321
+ Color :: Yellow
322
+ } else if all_successful {
251
323
Color :: Green
252
324
} else {
253
325
Color :: Red
254
326
} ) ,
255
327
Print ( format!(
256
328
"Execution {} in {:.2?} ({})" ,
257
- if all_successful {
329
+ if was_cancelled {
330
+ "cancelled"
331
+ } else if all_successful {
258
332
"completed"
259
333
} else {
260
334
"failed"
@@ -278,10 +352,14 @@ impl ScriptManager {
278
352
execute ! ( stdout, terminal:: LeaveAlternateScreen ) ?;
279
353
}
280
354
281
- if !all_successful {
355
+ if was_cancelled || !all_successful {
282
356
return Err ( HookError :: ScriptExecutionError {
283
357
script_name : name. to_string ( ) ,
284
- reason : "One or more commands failed" . to_string ( ) ,
358
+ reason : if was_cancelled {
359
+ "Execution was cancelled by user" . to_string ( )
360
+ } else {
361
+ "One or more commands failed" . to_string ( )
362
+ } ,
285
363
} ) ;
286
364
}
287
365
0 commit comments