@@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
99
1010use crate :: config:: TaskConfig ;
1111use crate :: keychain;
12+ use crate :: logger:: Logger ;
1213use crate :: notifications:: NotificationManager ;
1314
1415/// Task execution result
@@ -33,20 +34,97 @@ pub enum TaskStatus {
3334/// Task executor with progress tracking
3435#[ derive( Clone ) ]
3536pub struct TaskExecutor {
36- pub multi_progress : Arc < MultiProgress > ,
37+ pub multi_progress : Option < Arc < MultiProgress > > ,
3738 pub dry_run : bool ,
3839 pub verbose : bool ,
3940 pub notifier : Arc < NotificationManager > ,
41+ logger : Option < Arc < Logger > > ,
42+ show_progress : bool ,
4043}
4144
4245impl TaskExecutor {
4346 /// Create a new task executor
44- pub fn new ( dry_run : bool , verbose : bool , notifications_enabled : bool ) -> Self {
47+ pub fn new (
48+ dry_run : bool ,
49+ verbose : bool ,
50+ notifications_enabled : bool ,
51+ show_progress : bool ,
52+ logger : Option < Arc < Logger > > ,
53+ ) -> Self {
4554 Self {
46- multi_progress : Arc :: new ( MultiProgress :: new ( ) ) ,
55+ multi_progress : show_progress . then ( || Arc :: new ( MultiProgress :: new ( ) ) ) ,
4756 dry_run,
4857 verbose,
4958 notifier : Arc :: new ( NotificationManager :: new ( notifications_enabled) ) ,
59+ logger,
60+ show_progress,
61+ }
62+ }
63+
64+ fn update_progress ( & self , pb : & ProgressBar , message : & str ) {
65+ if self . show_progress {
66+ pb. set_message ( message. to_string ( ) ) ;
67+ } else {
68+ println ! ( "{}" , message) ;
69+ }
70+ }
71+
72+ fn finish_progress ( & self , pb : & ProgressBar , message : & str ) {
73+ if self . show_progress {
74+ pb. finish_with_message ( message. to_string ( ) ) ;
75+ } else {
76+ println ! ( "{}" , message) ;
77+ }
78+ }
79+
80+ fn log_line ( & self , message : String ) {
81+ if let Some ( logger) = & self . logger {
82+ if let Err ( err) = logger. log_line ( & message) {
83+ if self . verbose {
84+ eprintln ! ( "{}" , format!( "Failed to write log entry: {}" , err) . yellow( ) ) ;
85+ }
86+ }
87+ }
88+ }
89+
90+ fn log_task_completion (
91+ & self ,
92+ group_label : & str ,
93+ task_label : & str ,
94+ status : TaskStatus ,
95+ duration : Duration ,
96+ output : Option < & str > ,
97+ ) {
98+ if self . logger . is_none ( ) {
99+ return ;
100+ }
101+
102+ let status_prefix = match status {
103+ TaskStatus :: Success => "✓ SUCCESS" ,
104+ TaskStatus :: Failed => "✗ FAILED" ,
105+ TaskStatus :: Skipped => "○ SKIPPED" ,
106+ } ;
107+ self . log_line ( format ! (
108+ "{} [{}] {} ({})" ,
109+ status_prefix,
110+ group_label,
111+ task_label,
112+ format_duration( duration)
113+ ) ) ;
114+
115+ if let Some ( output) = output {
116+ let trimmed = output. trim ( ) ;
117+ if trimmed. is_empty ( ) {
118+ return ;
119+ }
120+ if let Some ( logger) = & self . logger {
121+ let header = format ! ( "└ output [{}] {}" , group_label, task_label) ;
122+ if let Err ( err) = logger. log_block ( & header, trimmed) {
123+ if self . verbose {
124+ eprintln ! ( "{}" , format!( "Failed to write log entry: {}" , err) . yellow( ) ) ;
125+ }
126+ }
127+ }
50128 }
51129 }
52130
@@ -141,14 +219,18 @@ impl TaskExecutor {
141219
142220 /// Create a configured spinner progress bar
143221 pub fn new_spinner ( & self ) -> ProgressBar {
144- let pb = self . multi_progress . add ( ProgressBar :: new_spinner ( ) ) ;
145- pb. set_style (
146- ProgressStyle :: with_template ( "{spinner:.cyan} {msg}" )
147- . unwrap ( )
148- . tick_strings ( & [ "⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" ] ) ,
149- ) ;
150- pb. enable_steady_tick ( Duration :: from_millis ( 120 ) ) ;
151- pb
222+ if let Some ( multi) = & self . multi_progress {
223+ let pb = multi. add ( ProgressBar :: new_spinner ( ) ) ;
224+ pb. set_style (
225+ ProgressStyle :: with_template ( "{spinner:.cyan} {msg}" )
226+ . unwrap ( )
227+ . tick_strings ( & [ "⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" ] ) ,
228+ ) ;
229+ pb. enable_steady_tick ( Duration :: from_millis ( 120 ) ) ;
230+ pb
231+ } else {
232+ ProgressBar :: hidden ( )
233+ }
152234 }
153235
154236 /// Execute a single task
@@ -165,76 +247,109 @@ impl TaskExecutor {
165247 let group_label = format_group_label ( & group_name, & group_icon) ;
166248 let task_label = format_task_label ( & task_name, & task. icon ) ;
167249 let progress_label = format ! ( "[{}] {}" , group_label, task_label) ;
250+ let running_message = format ! ( "{} {}" , progress_label. bold( ) , "Running…" . bright_white( ) ) ;
251+ self . update_progress ( & pb, & running_message) ;
168252
169- // Update progress bar
170- pb. set_message ( format ! (
171- "{} {}" ,
172- progress_label. clone( ) . bold( ) ,
173- "Running…" . bright_white( )
253+ let mut cmd = task. command . clone ( ) ;
254+ if task. sudo && !cmd. is_empty ( ) && cmd[ 0 ] != "sudo" {
255+ cmd. insert ( 0 , "sudo" . to_string ( ) ) ;
256+ }
257+ let command_display = if cmd. is_empty ( ) {
258+ "<empty command>" . to_string ( )
259+ } else {
260+ cmd. join ( " " )
261+ } ;
262+ self . log_line ( format ! (
263+ "▶ [{}] {} :: {}" ,
264+ group_label, task_label, command_display
174265 ) ) ;
175266
176267 if self . dry_run {
177268 tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
178- pb . finish_with_message ( format ! (
269+ let dry_run_msg = format ! (
179270 "{} {} {}" ,
180- progress_label. clone ( ) . bold( ) ,
271+ progress_label. bold( ) ,
181272 "○" . yellow( ) ,
182273 "[dry run]" . dimmed( )
183- ) ) ;
274+ ) ;
275+ self . finish_progress ( & pb, & dry_run_msg) ;
276+ let duration = start. elapsed ( ) ;
277+ let reason = "Dry run - command not executed" . to_string ( ) ;
278+ self . log_task_completion (
279+ & group_label,
280+ & task_label,
281+ TaskStatus :: Skipped ,
282+ duration,
283+ Some ( reason. as_str ( ) ) ,
284+ ) ;
184285 return TaskResult {
185286 name : task_name. clone ( ) ,
186287 group : group_name,
187288 group_icon,
188289 status : TaskStatus :: Skipped ,
189- duration : start . elapsed ( ) ,
190- output : Some ( "Dry run - command not executed" . to_string ( ) ) ,
290+ duration,
291+ output : Some ( reason ) ,
191292 } ;
192293 }
193294
194295 // Check preconditions
195296 if let Some ( check_cmd) = & task. check_command
196297 && !keychain:: command_exists ( check_cmd)
197298 {
198- pb . finish_with_message ( format ! (
299+ let skip_msg = format ! (
199300 "{} {}" ,
200- progress_label. clone ( ) . bold( ) ,
301+ progress_label. bold( ) ,
201302 "[skipped: command not found]" . dimmed( )
202- ) ) ;
303+ ) ;
304+ self . finish_progress ( & pb, & skip_msg) ;
305+ let duration = start. elapsed ( ) ;
306+ let reason = format ! ( "Command '{}' not found" , check_cmd) ;
307+ self . log_task_completion (
308+ & group_label,
309+ & task_label,
310+ TaskStatus :: Skipped ,
311+ duration,
312+ Some ( reason. as_str ( ) ) ,
313+ ) ;
203314 return TaskResult {
204315 name : task_name. clone ( ) ,
205316 group : group_name,
206317 group_icon,
207318 status : TaskStatus :: Skipped ,
208- duration : start . elapsed ( ) ,
209- output : Some ( format ! ( "Command '{}' not found" , check_cmd ) ) ,
319+ duration,
320+ output : Some ( reason ) ,
210321 } ;
211322 }
212323
213324 if let Some ( check_path) = & task. check_path {
214325 let expanded = shellexpand:: tilde ( check_path) ;
215326 if !Path :: new ( expanded. as_ref ( ) ) . exists ( ) {
216- pb . finish_with_message ( format ! (
327+ let skip_msg = format ! (
217328 "{} {}" ,
218- progress_label. clone ( ) . bold( ) ,
329+ progress_label. bold( ) ,
219330 "[skipped: path not found]" . dimmed( )
220- ) ) ;
331+ ) ;
332+ self . finish_progress ( & pb, & skip_msg) ;
333+ let duration = start. elapsed ( ) ;
334+ let reason = format ! ( "Path '{}' not found" , check_path) ;
335+ self . log_task_completion (
336+ & group_label,
337+ & task_label,
338+ TaskStatus :: Skipped ,
339+ duration,
340+ Some ( reason. as_str ( ) ) ,
341+ ) ;
221342 return TaskResult {
222343 name : task_name. clone ( ) ,
223344 group : group_name,
224345 group_icon,
225346 status : TaskStatus :: Skipped ,
226- duration : start . elapsed ( ) ,
227- output : Some ( format ! ( "Path '{}' not found" , check_path ) ) ,
347+ duration,
348+ output : Some ( reason ) ,
228349 } ;
229350 }
230351 }
231352
232- // Build command
233- let mut cmd = task. command . clone ( ) ;
234- if task. sudo && !cmd. is_empty ( ) && cmd[ 0 ] != "sudo" {
235- cmd. insert ( 0 , "sudo" . to_string ( ) ) ;
236- }
237-
238353 // Warn if command might internally call sudo (heuristic check)
239354 if !task. sudo && self . verbose && !cmd. is_empty ( ) {
240355 let cmd_str = cmd. join ( " " ) . to_lowercase ( ) ;
@@ -276,12 +391,20 @@ impl TaskExecutor {
276391 TaskStatus :: Skipped => "○" . yellow ( ) ,
277392 } ;
278393
279- pb . finish_with_message ( format ! (
394+ let completion_message = format ! (
280395 "{} {} {}" ,
281396 progress_label. bold( ) ,
282397 status_icon,
283398 format!( "({})" , format_duration( duration) ) . dimmed( )
284- ) ) ;
399+ ) ;
400+ self . finish_progress ( & pb, & completion_message) ;
401+ self . log_task_completion (
402+ & group_label,
403+ & task_label,
404+ status,
405+ duration,
406+ output. as_deref ( ) ,
407+ ) ;
285408
286409 TaskResult {
287410 name : task_name,
0 commit comments