11use std:: {
2- io,
2+ io:: { self , IsTerminal } ,
33 time:: { Duration , Instant } ,
44} ;
55
@@ -48,18 +48,29 @@ const DOT_ANIMATION: &[&str] = &["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐"
4848impl InitUI {
4949 pub fn new ( ) -> Result < Self > {
5050 enable_raw_mode ( ) ?;
51- let mut stdout = io:: stdout ( ) ;
52- execute ! ( stdout, EnterAlternateScreen ) ?;
53- let backend = CrosstermBackend :: new ( stdout) ;
54- let terminal = Terminal :: new ( backend) ?;
5551
56- Ok ( Self {
57- steps : Vec :: new ( ) ,
58- terminal,
59- animation_state : 0 ,
60- dot_animation_state : 0 ,
61- last_update : Instant :: now ( ) ,
62- } )
52+ // Try to create the UI, but clean up raw mode if anything fails
53+ let result = ( || {
54+ let mut stdout = io:: stdout ( ) ;
55+ execute ! ( stdout, EnterAlternateScreen ) ?;
56+ let backend = CrosstermBackend :: new ( stdout) ;
57+ let terminal = Terminal :: new ( backend) ?;
58+
59+ Ok ( Self {
60+ steps : Vec :: new ( ) ,
61+ terminal,
62+ animation_state : 0 ,
63+ dot_animation_state : 0 ,
64+ last_update : Instant :: now ( ) ,
65+ } )
66+ } ) ( ) ;
67+
68+ // If anything failed, disable raw mode before returning the error
69+ if result. is_err ( ) {
70+ let _ = disable_raw_mode ( ) ;
71+ }
72+
73+ result
6374 }
6475
6576 pub fn add_step ( & mut self , message : String ) {
@@ -232,17 +243,32 @@ pub struct InitUIContext {
232243
233244impl InitUIContext {
234245 pub fn new ( use_ui : bool ) -> Result < Self > {
235- let ui = if use_ui { Some ( InitUI :: new ( ) ?) } else { None } ;
246+ let ui = if use_ui {
247+ // Check if stdout is a TTY before attempting to create UI
248+ if io:: stdout ( ) . is_terminal ( ) {
249+ // Try to create the UI, but gracefully fallback if it fails
250+ InitUI :: new ( ) . ok ( )
251+ } else {
252+ // Not a TTY, use non-interactive mode
253+ None
254+ }
255+ } else {
256+ None
257+ } ;
236258 Ok ( Self {
237259 ui,
238260 current_step : 0 ,
239261 } )
240262 }
241263
264+ #[ allow( clippy:: print_stdout) ]
242265 pub fn add_step ( & mut self , message : & str ) {
243266 if let Some ( ui) = & mut self . ui {
244267 ui. add_step ( message. to_string ( ) ) ;
245268 let _ = ui. render ( ) ;
269+ } else {
270+ // Non-interactive mode: just print the step
271+ println ! ( " {}" , message) ;
246272 }
247273 }
248274
@@ -259,29 +285,41 @@ impl InitUIContext {
259285 }
260286 }
261287
288+ #[ allow( clippy:: print_stdout) ]
262289 pub fn complete_step ( & mut self ) {
263290 if let Some ( ui) = & mut self . ui {
264291 ui. update_step ( self . current_step , StepStatus :: Completed ) ;
265292 let _ = ui. render ( ) ;
266293 // Add a small delay to show the completion animation
267294 std:: thread:: sleep ( Duration :: from_millis ( 300 ) ) ;
295+ } else {
296+ // Non-interactive mode: print completion
297+ println ! ( " ✓ Done" ) ;
268298 }
269299 self . current_step += 1 ;
270300 }
271301
302+ #[ allow( clippy:: print_stderr) ]
272303 pub fn fail_step ( & mut self ) {
273304 if let Some ( ui) = & mut self . ui {
274305 ui. update_step ( self . current_step , StepStatus :: Failed ) ;
275306 let _ = ui. render ( ) ;
307+ } else {
308+ // Non-interactive mode: print failure to stderr
309+ eprintln ! ( " ✗ Failed" ) ;
276310 }
277311 self . current_step += 1 ;
278312 }
279313
314+ #[ allow( clippy:: print_stdout) ]
280315 pub fn add_completion_message ( & mut self , message : & str ) {
281316 if let Some ( ui) = & mut self . ui {
282317 ui. add_step ( message. to_string ( ) ) ;
283318 ui. update_step ( ui. steps . len ( ) - 1 , StepStatus :: Completed ) ;
284319 let _ = ui. render ( ) ;
320+ } else {
321+ // Non-interactive mode: just print the message
322+ println ! ( "\n {}" , message) ;
285323 }
286324 }
287325
@@ -298,79 +336,106 @@ impl InitUIContext {
298336 }
299337}
300338
339+ #[ allow( clippy:: print_stderr) ]
301340pub fn show_error ( message : & str ) -> Result < ( ) > {
302- enable_raw_mode ( ) ?;
303- let mut stdout = io:: stdout ( ) ;
304- execute ! ( stdout, EnterAlternateScreen ) ?;
305- let backend = CrosstermBackend :: new ( stdout) ;
306- let mut terminal = Terminal :: new ( backend) ?;
307-
308- terminal. draw ( |f| {
309- let area = f. area ( ) ;
310-
311- // Calculate popup size
312- let popup_width = message. len ( ) . min ( 60 ) as u16 + 4 ;
313- let popup_height = 7 ;
314-
315- let popup_area = Rect {
316- x : ( area. width . saturating_sub ( popup_width) ) / 2 ,
317- y : ( area. height . saturating_sub ( popup_height) ) / 2 ,
318- width : popup_width,
319- height : popup_height,
320- } ;
341+ // Check if we're in a TTY before attempting to create a fancy error UI
342+ if !io:: stdout ( ) . is_terminal ( ) {
343+ // Non-interactive mode: just print the error to stderr
344+ eprintln ! ( "Error: {}" , message) ;
345+ return Ok ( ( ) ) ;
346+ }
321347
322- // Clear the area first
323- f. render_widget ( Clear , popup_area) ;
324-
325- // Error box
326- let error_block = Block :: default ( )
327- . title ( " ⚠️ Error " )
328- . borders ( Borders :: ALL )
329- . border_style ( Style :: default ( ) . fg ( Color :: Red ) )
330- . border_type ( BorderType :: Rounded )
331- . style ( Style :: default ( ) . bg ( Color :: Black ) ) ;
332-
333- let inner = error_block. inner ( popup_area) ;
334- f. render_widget ( error_block, popup_area) ;
335-
336- // Error message
337- let error_text = vec ! [
338- Line :: from( "" ) ,
339- Line :: from( vec![
340- Span :: raw( " " ) ,
341- Span :: styled(
342- message,
343- Style :: default ( ) . fg( Color :: Red ) . add_modifier( Modifier :: BOLD ) ,
344- ) ,
345- ] ) ,
346- Line :: from( "" ) ,
347- Line :: from( vec![
348- Span :: raw( " " ) ,
349- Span :: styled(
350- "Press any key to exit" ,
351- Style :: default ( )
352- . fg( Color :: Gray )
353- . add_modifier( Modifier :: ITALIC ) ,
354- ) ,
355- ] ) ,
356- ] ;
357-
358- let paragraph = Paragraph :: new ( error_text)
359- . alignment ( Alignment :: Left )
360- . wrap ( Wrap { trim : true } ) ;
361-
362- f. render_widget ( paragraph, inner) ;
363- } ) ?;
364-
365- // Wait for user input
366- loop {
367- if let Event :: Key ( _) = event:: read ( ) ? {
368- break ;
348+ // Try to create the fancy error UI, but fallback gracefully if it fails
349+ match show_error_ui ( message) {
350+ Ok ( ( ) ) => Ok ( ( ) ) ,
351+ Err ( _) => {
352+ // Failed to create UI, fallback to simple error message
353+ eprintln ! ( "Error: {}" , message) ;
354+ Ok ( ( ) )
369355 }
370356 }
357+ }
358+
359+ fn show_error_ui ( message : & str ) -> Result < ( ) > {
360+ enable_raw_mode ( ) ?;
361+
362+ // Ensure cleanup happens regardless of success or failure
363+ let result = ( || {
364+ let mut stdout = io:: stdout ( ) ;
365+ execute ! ( stdout, EnterAlternateScreen ) ?;
366+ let backend = CrosstermBackend :: new ( stdout) ;
367+ let mut terminal = Terminal :: new ( backend) ?;
368+
369+ terminal. draw ( |f| {
370+ let area = f. area ( ) ;
371+
372+ // Calculate popup size
373+ let popup_width = message. len ( ) . min ( 60 ) as u16 + 4 ;
374+ let popup_height = 7 ;
375+
376+ let popup_area = Rect {
377+ x : ( area. width . saturating_sub ( popup_width) ) / 2 ,
378+ y : ( area. height . saturating_sub ( popup_height) ) / 2 ,
379+ width : popup_width,
380+ height : popup_height,
381+ } ;
382+
383+ // Clear the area first
384+ f. render_widget ( Clear , popup_area) ;
385+
386+ // Error box
387+ let error_block = Block :: default ( )
388+ . title ( " ⚠️ Error " )
389+ . borders ( Borders :: ALL )
390+ . border_style ( Style :: default ( ) . fg ( Color :: Red ) )
391+ . border_type ( BorderType :: Rounded )
392+ . style ( Style :: default ( ) . bg ( Color :: Black ) ) ;
393+
394+ let inner = error_block. inner ( popup_area) ;
395+ f. render_widget ( error_block, popup_area) ;
396+
397+ // Error message
398+ let error_text = vec ! [
399+ Line :: from( "" ) ,
400+ Line :: from( vec![
401+ Span :: raw( " " ) ,
402+ Span :: styled(
403+ message,
404+ Style :: default ( ) . fg( Color :: Red ) . add_modifier( Modifier :: BOLD ) ,
405+ ) ,
406+ ] ) ,
407+ Line :: from( "" ) ,
408+ Line :: from( vec![
409+ Span :: raw( " " ) ,
410+ Span :: styled(
411+ "Press any key to exit" ,
412+ Style :: default ( )
413+ . fg( Color :: Gray )
414+ . add_modifier( Modifier :: ITALIC ) ,
415+ ) ,
416+ ] ) ,
417+ ] ;
418+
419+ let paragraph = Paragraph :: new ( error_text)
420+ . alignment ( Alignment :: Left )
421+ . wrap ( Wrap { trim : true } ) ;
422+
423+ f. render_widget ( paragraph, inner) ;
424+ } ) ?;
425+
426+ // Wait for user input
427+ loop {
428+ if let Event :: Key ( _) = event:: read ( ) ? {
429+ break ;
430+ }
431+ }
432+
433+ Ok ( ( ) )
434+ } ) ( ) ;
371435
372- disable_raw_mode ( ) ?;
373- execute ! ( io:: stdout( ) , LeaveAlternateScreen ) ?;
436+ // Always clean up terminal state
437+ let _ = disable_raw_mode ( ) ;
438+ let _ = execute ! ( io:: stdout( ) , LeaveAlternateScreen ) ;
374439
375- Ok ( ( ) )
440+ result
376441}
0 commit comments