@@ -10,25 +10,29 @@ use ratatui::{
1010 text:: Line ,
1111 widgets:: { Block , Paragraph , Widget as _} ,
1212} ;
13+ use regex:: Regex ;
14+ use tracing:: debug;
1315use tui_input:: Input ;
1416use tui_input:: backend:: crossterm:: EventHandler as _;
1517
1618use crate :: ui:: widget:: BgClear ;
1719use crate :: ui:: { KeyboardHandler , ModalResult , Theme , UiModal , input_value_and_pos, is_portrait} ;
1820
1921#[ derive( Debug , PartialEq ) ]
20- enum Overwrite {
22+ enum ValidationResult {
2123 Reset ,
22- Prompted ,
23- Confirmed ,
24+ Ok ,
25+ Invalid ( String ) ,
26+ Exists ,
2427}
2528
2629#[ derive( Debug ) ]
2730pub ( crate ) struct SaveAsFileModal {
2831 filename : Input ,
32+ original_filename : Box < str > ,
2933 folder : Box < str > ,
3034 ext : Box < str > ,
31- overwrite : Overwrite ,
35+ validation : ValidationResult ,
3236}
3337
3438impl UiModal for SaveAsFileModal {
@@ -45,7 +49,7 @@ impl UiModal for SaveAsFileModal {
4549 . vertical_margin ( 1 )
4650 . areas ( modal_area) ;
4751
48- let ( display_value, x) = input_value_and_pos ( & self . filename , input_area. width ) ;
52+ let ( display_value, x) = input_value_and_pos ( & self . filename , input_area. width - 2 ) ;
4953
5054 frame. render_widget ( BgClear :: new ( theme. background_color ( ) ) , modal_area) ;
5155 Block :: bordered ( )
@@ -71,60 +75,96 @@ impl KeyboardHandler for SaveAsFileModal {
7175 if key. code == KeyCode :: Esc {
7276 ModalResult :: Close
7377 } else if key. code == KeyCode :: Enter {
74- if self . overwrite == Overwrite :: Prompted {
75- self . overwrite = Overwrite :: Confirmed ;
76- }
7778 let filename = self . filename . value ( ) . trim ( ) ;
78- let valid = !filename. is_empty ( ) && !self . is_file_exists ( filename) ;
79- if valid || self . overwrite == Overwrite :: Confirmed {
80- ModalResult :: Filename ( filename. to_owned ( ) )
81- } else {
82- self . overwrite = Overwrite :: Prompted ;
83- ModalResult :: None
79+ // Enter pressed after the overwrite prompt
80+ if self . validation == ValidationResult :: Exists {
81+ debug ! ( filename, "Save as. Overwrite" ) ;
82+ return ModalResult :: Filename ( filename. to_owned ( ) ) ;
83+ }
84+ self . validation = self . validate ( filename) ;
85+ match self . validation {
86+ ValidationResult :: Ok => {
87+ debug ! ( filename, "Save as" ) ;
88+ ModalResult :: Filename ( filename. to_owned ( ) )
89+ }
90+ _ => ModalResult :: None ,
8491 }
8592 } else {
86- self . overwrite = Overwrite :: Reset ;
93+ self . validation = ValidationResult :: Reset ;
8794 self . filename . handle_event ( & Event :: Key ( key) ) ;
8895 ModalResult :: None
8996 }
9097 }
9198}
9299
93100impl SaveAsFileModal {
94- pub ( crate ) fn new ( folder : & str , filename : & str , ext : & str ) -> Self {
101+ pub ( crate ) fn new ( original_filename : & str , folder : & str , filename : & str , ext : & str ) -> Self {
95102 Self {
96103 filename : Input :: new ( filename. to_owned ( ) ) ,
104+ original_filename : original_filename. into ( ) ,
97105 folder : folder. into ( ) ,
98106 ext : ext. into ( ) ,
99- overwrite : Overwrite :: Reset ,
107+ validation : ValidationResult :: Reset ,
100108 }
101109 }
102110
103111 fn render_input_hints ( & self , area : Rect , frame : & mut Frame , theme : & Theme ) {
104- let line = if self . overwrite == Overwrite :: Prompted {
105- let error = theme. error_style ( ) . bold ( ) ;
106- Line :: from ( vec ! [
107- Span :: styled( "File already exists. Press " , error) ,
108- Span :: styled( "Enter" , theme. key_style( ) ) ,
109- Span :: styled( " again to overwrite" , error) ,
110- ] )
111- . centered ( )
112- } else {
113- let key_style = theme. key_style ( ) ;
114- let text_style = theme. text_color ( ) ;
115- Line :: from ( vec ! [
116- Span :: styled( "Enter" , key_style) ,
117- Span :: styled( ": confirm " , text_style) ,
118- Span :: styled( "Esc" , key_style) ,
119- Span :: styled( ": close" , text_style) ,
120- ] )
112+ let line = match & self . validation {
113+ ValidationResult :: Exists => {
114+ let error_style = theme. error_style ( ) . bold ( ) ;
115+ Line :: from ( vec ! [
116+ Span :: styled( "File already exists. Press " , error_style) ,
117+ Span :: styled( "Enter" , theme. key_style( ) ) ,
118+ Span :: styled( " again to overwrite" , error_style) ,
119+ ] )
120+ . centered ( )
121+ }
122+ ValidationResult :: Invalid ( reason) => {
123+ let error_style = theme. error_style ( ) . bold ( ) ;
124+ Line :: from ( vec ! [ Span :: styled( reason, error_style) ] ) . centered ( )
125+ }
126+ _ => {
127+ let key_style = theme. key_style ( ) ;
128+ let text_style = theme. text_color ( ) ;
129+ Line :: from ( vec ! [
130+ Span :: styled( "Enter" , key_style) ,
131+ Span :: styled( ": confirm " , text_style) ,
132+ Span :: styled( "Esc" , key_style) ,
133+ Span :: styled( ": close" , text_style) ,
134+ ] )
135+ }
121136 } ;
122137 frame. render_widget ( Paragraph :: new ( line) , area) ;
123138 }
124139
140+ fn validate ( & self , filename : & str ) -> ValidationResult {
141+ if filename. is_empty ( ) {
142+ ValidationResult :: Invalid ( "Filename is empty" . into ( ) )
143+ } else if filename == self . original_filename . as_ref ( ) {
144+ ValidationResult :: Invalid ( "Filename is the same as original" . into ( ) )
145+ } else if Self :: contains_invalid_chars ( filename) {
146+ ValidationResult :: Invalid ( "Filename contains invalid characters" . into ( ) )
147+ } else if filename. len ( ) > 200 {
148+ ValidationResult :: Invalid ( "Filename is too long" . into ( ) )
149+ } else if self . is_file_exists ( filename) {
150+ ValidationResult :: Exists
151+ } else {
152+ ValidationResult :: Ok
153+ }
154+ }
155+
156+ fn contains_invalid_chars ( filename : & str ) -> bool {
157+ filename. starts_with ( '-' )
158+ || filename. starts_with ( '~' )
159+ || std:: str:: from_utf8 ( filename. as_bytes ( ) ) . is_err ( )
160+ || Regex :: new ( r"[/\\|<>$:\x00-\x1F\x7F\x80-\x9F]+" )
161+ . unwrap ( )
162+ . is_match ( filename)
163+ }
164+
125165 fn is_file_exists ( & self , filename : & str ) -> bool {
126- let mut path = PathBuf :: new ( ) . join ( & * self . folder ) . join ( filename) ;
127- path. set_extension ( & * self . ext ) ;
166+ let mut path = PathBuf :: new ( ) . join ( self . folder . as_ref ( ) ) . join ( filename) ;
167+ path. add_extension ( self . ext . as_ref ( ) ) ;
128168 path. exists ( )
129169 }
130170}
0 commit comments