11//! New session dialog
22
3+ mod path_input;
34mod render;
45
56#[ cfg( test) ]
67mod tests;
78
89use crossterm:: event:: { KeyCode , KeyEvent , KeyModifiers } ;
10+ use std:: time:: Instant ;
911use tui_input:: backend:: crossterm:: EventHandler ;
1012use tui_input:: Input ;
1113
@@ -18,6 +20,7 @@ use crate::session::Config;
1820use crate :: session:: { civilizations, resolve_config} ;
1921use crate :: tmux:: AvailableTools ;
2022use crate :: tui:: components:: { DirPicker , DirPickerResult , ListPicker , ListPickerResult } ;
23+ use path_input:: PathCompletionCycle ;
2124
2225pub ( super ) struct FieldHelp {
2326 pub ( super ) name : & ' static str ,
@@ -99,6 +102,7 @@ pub struct NewSessionData {
99102
100103/// Spinner frames for loading animation
101104pub ( super ) const SPINNER_FRAMES : & [ & str ] = & [ "◐" , "◓" , "◑" , "◒" ] ;
105+ const PATH_FIELD : usize = 1 ;
102106
103107pub struct NewSessionDialog {
104108 pub ( super ) profile : String ,
@@ -153,6 +157,10 @@ pub struct NewSessionDialog {
153157 pub ( super ) current_hook : Option < String > ,
154158 /// Accumulated output lines from hook execution
155159 pub ( super ) hook_output : Vec < String > ,
160+ /// Temporary highlight state for invalid path input.
161+ pub ( super ) path_invalid_flash_until : Option < Instant > ,
162+ /// State for cycling path autocomplete candidates with repeated Tab.
163+ path_completion_cycle : Option < PathCompletionCycle > ,
156164}
157165
158166/// Shared logic for handling key events in an editable list (env keys or env values).
@@ -353,6 +361,8 @@ impl NewSessionDialog {
353361 has_hooks : false ,
354362 current_hook : None ,
355363 hook_output : Vec :: new ( ) ,
364+ path_invalid_flash_until : None ,
365+ path_completion_cycle : None ,
356366 }
357367 }
358368
@@ -392,9 +402,24 @@ impl NewSessionDialog {
392402 self . loading
393403 }
394404
395- /// Advance the spinner animation frame. Call this periodically when loading.
396- pub fn tick ( & mut self ) {
397- self . spinner_frame = ( self . spinner_frame + 1 ) % SPINNER_FRAMES . len ( ) ;
405+ /// Advance dialog timers (spinner and transient highlights).
406+ /// Returns true when visual state changed and the UI should redraw.
407+ pub fn tick ( & mut self ) -> bool {
408+ let mut changed = false ;
409+
410+ if self . loading {
411+ self . spinner_frame = ( self . spinner_frame + 1 ) % SPINNER_FRAMES . len ( ) ;
412+ changed = true ;
413+ }
414+
415+ if let Some ( until) = self . path_invalid_flash_until {
416+ if Instant :: now ( ) >= until {
417+ self . path_invalid_flash_until = None ;
418+ changed = true ;
419+ }
420+ }
421+
422+ changed
398423 }
399424
400425 #[ cfg( test) ]
@@ -449,6 +474,8 @@ impl NewSessionDialog {
449474 has_hooks : false ,
450475 current_hook : None ,
451476 hook_output : Vec :: new ( ) ,
477+ path_invalid_flash_until : None ,
478+ path_completion_cycle : None ,
452479 }
453480 }
454481
@@ -495,6 +522,8 @@ impl NewSessionDialog {
495522 has_hooks : false ,
496523 current_hook : None ,
497524 hook_output : Vec :: new ( ) ,
525+ path_invalid_flash_until : None ,
526+ path_completion_cycle : None ,
498527 }
499528 }
500529
@@ -537,6 +566,7 @@ impl NewSessionDialog {
537566 match self . dir_picker . handle_key ( key) {
538567 DirPickerResult :: Selected ( path) => {
539568 self . path = Input :: new ( path) ;
569+ self . clear_path_completion_cycle ( ) ;
540570 }
541571 DirPickerResult :: Cancelled | DirPickerResult :: Continue => { }
542572 }
@@ -614,7 +644,7 @@ impl NewSessionDialog {
614644
615645 // Ctrl+P opens a context-sensitive picker
616646 if key. code == KeyCode :: Char ( 'p' ) && key. modifiers . contains ( KeyModifiers :: CONTROL ) {
617- if self . focused_field == 1 {
647+ if self . focused_field == PATH_FIELD {
618648 let path_value = self . path . value ( ) . trim ( ) . to_string ( ) ;
619649 self . dir_picker . activate ( & path_value) ;
620650 return DialogResult :: Continue ;
@@ -634,6 +664,10 @@ impl NewSessionDialog {
634664 }
635665 }
636666
667+ if self . handle_path_shortcuts ( key) {
668+ return DialogResult :: Continue ;
669+ }
670+
637671 match key. code {
638672 KeyCode :: Char ( '?' ) => {
639673 self . show_help = true ;
@@ -691,10 +725,16 @@ impl NewSessionDialog {
691725 } )
692726 }
693727 KeyCode :: Tab | KeyCode :: Down => {
728+ if self . focused_field == PATH_FIELD {
729+ self . clear_path_completion_cycle ( ) ;
730+ }
694731 self . focused_field = ( self . focused_field + 1 ) % max_field;
695732 DialogResult :: Continue
696733 }
697734 KeyCode :: BackTab | KeyCode :: Up => {
735+ if self . focused_field == PATH_FIELD {
736+ self . clear_path_completion_cycle ( ) ;
737+ }
698738 self . focused_field = if self . focused_field == 0 {
699739 max_field - 1
700740 } else {
@@ -771,6 +811,10 @@ impl NewSessionDialog {
771811 self . current_input_mut ( )
772812 . handle_event ( & crossterm:: event:: Event :: Key ( key) ) ;
773813 self . error_message = None ;
814+ if self . focused_field == PATH_FIELD {
815+ self . path_invalid_flash_until = None ;
816+ self . clear_path_completion_cycle ( ) ;
817+ }
774818 }
775819 DialogResult :: Continue
776820 }
@@ -840,7 +884,7 @@ impl NewSessionDialog {
840884
841885 match self . focused_field {
842886 0 => & mut self . title ,
843- 1 => & mut self . path ,
887+ PATH_FIELD => & mut self . path ,
844888 n if n == worktree_field => & mut self . worktree_branch ,
845889 n if n == sandbox_image_field => & mut self . sandbox_image ,
846890 n if n == group_field => & mut self . group ,
0 commit comments