ask/
lib.rs

1pub struct Ask<T>{
2    questions: Vec<T>
3}
4
5impl <T>Ask<T>
6    where T : question::TraitQuestion {
7    pub fn new()->Ask<T> {
8        Ask{
9            questions: vec!()
10        }
11    }
12
13    pub fn question(&mut self, question_obj:T){
14        self.questions.push(question_obj);
15    }
16
17    pub fn all(&mut self){
18        for question in &mut self.questions {
19            &question.send();
20        }
21    }
22}
23
24pub mod validate{
25    pub mod group_options{
26        use crate::question::AnswerCollection;
27        
28        pub fn selections_are_gtoe(answer_collection:& AnswerCollection, num : u16)->bool{
29            let mut total_selected_answers = 0;
30            for answer_set in & answer_collection.answer_sets{
31                total_selected_answers += answer_set.answers.len() as u16;
32            }
33            total_selected_answers >= num
34        }
35
36        pub fn selections_are_ltoe(answer_collection:& AnswerCollection,  num : u16)->bool{
37            let mut total_selected_answers = 0;
38            for answer_set in & answer_collection.answer_sets{
39                total_selected_answers += answer_set.answers.len() as u16;
40            }
41            total_selected_answers <= num
42        }
43    }
44}
45
46pub mod question{
47
48    use std::io::{Write, stdout, Stdout, stdin};
49    use termion::{input::TermRead, event::Key, cursor::DetectCursorPos};
50    use termion::raw::IntoRawMode;
51    use regex::Regex;
52
53
54    // impl TraitQuestion for Confirm{
55    //     fn send(&mut self)->Result<AnswerCollection, String>{
56
57    //     }
58    // }
59
60    pub struct QuestionOption{
61        name     : String,
62        selected : bool,
63    }
64
65    impl QuestionOption{
66        pub fn new(name :String)->QuestionOption{
67            QuestionOption{
68                name     : name,
69                selected : false,
70            }
71        }
72
73        pub fn toggle_selected(mut self, switch : Option<bool>)->Self{
74            self.selected = if let Some(tf) = switch { tf }else{ !self.selected };
75            self
76        }
77    }
78
79    pub struct OptionGroup{
80        name                 : &'static str,
81        options              : Vec<QuestionOption>,
82        options_len          : u16,
83    }
84
85    impl OptionGroup {
86        pub fn new(group_name : &'static str) -> OptionGroup{
87            OptionGroup{
88                name             : group_name,
89                options          : vec!(),
90                options_len      : 0,
91            }
92        }
93
94        // fn toggle_selected_option(&mut self, selector_index: usize, io : Option<bool>){
95        //     if let Some(_io) = io{
96        //         self.options_selected[selector_index] = _io;
97        //     }else{
98        //         self.options_selected[selector_index] = !self.options_selected[selector_index];
99        //     }
100        // }
101
102        fn to_answerset(&self)-> AnswerSet{
103            AnswerSet{
104                group_name : self.name,
105                answers    : self.get_all_selected_options()
106            }
107        }
108
109        fn get_all_selected_options(&self)->Vec<String>{
110            let mut i = 0;
111            let mut result = vec!();
112            for option in &self.options{
113                if option.selected {
114                    result.push(option.name.clone());
115                }
116                i += 1;
117            }
118
119            return result;
120        }
121
122
123        fn get_all_selected_indexies(&self)->Vec<usize>{
124            let mut i = 0;
125            let mut result = vec!();
126            for options in &self.options{
127                if options.selected {
128                    result.push(i);
129                }
130                i += 1;
131            }
132
133            return result;
134        }
135
136        fn render_options(&self, stdout: &mut termion::raw::RawTerminal<Stdout>, selector_index: u16, size : (u16, u16)){
137            let mut i = 0;
138
139
140            for option in &self.options{
141                let selector_str = if i == selector_index {
142                    let selector = vec!(0xE2, 0x98, 0x9B);
143                    String::from_utf8_lossy(&selector).into_owned()
144                }else{
145                    " ".to_owned()
146                };
147
148                let selected_icon = if self.options[i as usize].selected {
149                    let large_filled_circle = vec!(0xE2, 0xAC, 0xA4);
150                    String::from_utf8_lossy(&large_filled_circle).into_owned()
151                }else{
152                    let large_circle = vec!(0xE2, 0x97, 0xAF);
153                    String::from_utf8_lossy(&large_circle).into_owned()
154                };
155                write!(stdout, "{}{}{}{}   {}  {}", termion::cursor::Goto(0,size.1 - self.options_len + i), termion::clear::CurrentLine, termion::clear::CurrentLine, selector_str, selected_icon, option.name );
156                i += 1;
157            }
158
159            let selector_str = if i == selector_index {
160                let selector = vec!(0xE2, 0x98, 0x9B);
161                String::from_utf8_lossy(&selector).into_owned()
162            }else{
163                " ".to_owned()
164            };
165            // let continue_str = if self.can_continue(){"> Continue >"}else{"[ Continue ]"};
166            let continue_str = "CONTINUE";
167            write!(stdout, "{}{}{}   {}", termion::cursor::Goto(0,size.1 - self.options_len + i), termion::clear::CurrentLine, selector_str, continue_str );
168
169            stdout.flush().unwrap();
170        }
171
172        // fn can_continue(& self)->bool{
173        //     //is between min and max . if min and max are both 0 then just continue
174        //     return if self.any_amount(){
175        //         true
176        //     }else if  self.get_selection_count() >= self.min_selections &&
177        //                 self.get_selection_count() <= self.max_selections{
178        //         true
179        //     }else{
180        //         false
181        //     }
182        // }
183
184        // fn any_amount(& self)->bool{
185        //     //is between min and max . if min and max are both 0 then just continue
186        //     return if self.min_selections == 0 && self.max_selections == 0{
187        //         true
188        //     }else{
189        //         false
190        //     }
191        // }
192
193        //=== Accessors ===
194        pub fn add_option_by_str(mut self, option:String)->Self{
195            self.options.push(QuestionOption::new(option));
196            self.options_len += 1;
197            self
198        }
199
200        pub fn add_option(mut self, option: QuestionOption)->Self{
201            self.options.push(option);
202            self.options_len += 1;
203            self
204        }
205
206        // pub fn max_selections(mut self, number:u16)->Self{
207        //     self.max_selections = number;
208        //     self
209        // }
210
211        // pub fn min_selections(mut self, number:u16)->Self{
212        //     self.min_selections = number;
213        //     self
214        // }
215
216    }
217
218    pub trait TraitQuestion{
219        fn send(&mut self)->Result<AnswerCollection, String>;
220    }
221
222    pub struct MultipleChoice{
223        message            : String,
224        prepended_message  : Option<String>,
225        option_groups      : Vec<OptionGroup>,
226        validate_fn        : Box<Fn(AnswerCollection)->bool>,
227        cursor             : Cursor,
228        continue_btn       : ContinueBtn,
229        on_select_continue : bool, //as soo as all selection are vaid
230    }
231
232    struct Cursor{
233        on_group  : u16,
234        on_option : u16,
235    }
236
237    impl TraitQuestion for MultipleChoice{
238        fn send(&mut self)->Result<AnswerCollection, String>{
239
240            let mut stdout  = stdout().into_raw_mode().unwrap();
241            let mut size    = termion::terminal_size().unwrap();
242
243            //write out question
244            write!(stdout, "{}{}",  self.prepended_message.as_ref().unwrap_or(&"".to_owned()), self.message);
245
246            stdout.flush().unwrap();
247
248            let (mut answer_collection, mut line_count) = self.render_options(size);
249
250            let stdin = stdin();
251            for c in stdin.keys(){
252                size = termion::terminal_size().unwrap();
253                match c.unwrap(){
254                    // Key::Char('\n') => {
255                    //     write!(stdout, "{}{}",
256                    //     termion::scroll::Up(1),
257                    //     termion::cursor::Goto(0,term_size.1),
258                    //     ).unwrap();
259                    // },
260                    Key::Down => {
261                        //move to next option if no more options move to next group, option 0,
262                        //if no more groups move to first if cursor wrap is on else clamp bot
263
264                        //there are more options to move to
265                        if self.cursor.on_group < self.option_groups.len() as u16 &&
266                           self.cursor.on_option + 1 < self.option_groups[self.cursor.on_group as usize].options.len() as u16{
267                            self.cursor.on_option += 1;
268                        }else if self.cursor.on_group + 1 < self.option_groups.len() as u16{
269                            self.cursor.on_group += 1;
270                            self.cursor.on_option = 0;
271                        }else{
272                            self.cursor.on_group = self.option_groups.len() as u16;
273                            self.cursor.on_option = 0;
274                        }
275                    },
276                    Key::Up =>{
277                        if self.cursor.on_option != 0{
278                            self.cursor.on_option -= 1;
279                        }else if self.cursor.on_group !=  0{
280                            self.cursor.on_option = self.option_groups.len() as u16;
281                            self.cursor.on_group  -= 1;
282                        }
283                    },
284                    Key::Char('\n') =>{
285                        if self.cursor.on_group == self.option_groups.len() as u16{ // on continue_btn
286                            //can only continue if valid
287                            if self.validate(){
288                                write!(stdout, "{}{}",  termion::scroll::Up(1), termion::cursor::Goto(0,size.1));
289                                return Ok(answer_collection);
290                            }
291                        }else{
292                            let group_i     = self.cursor.on_group;
293                            let option_i    = self.cursor.on_option;
294                            self.toggle_selected_option(group_i, option_i, None)
295                        }
296                    },
297                    Key::Ctrl('c') => {
298                        return Err("Terminated process.".to_string());
299                    },
300                    // Key::Char(c) => {
301                    //     print!("{}",c);
302                    // },
303                    _ => {}
304                }
305
306                write!(stdout, "{}",  termion::scroll::Down(line_count));
307                let (_answer_collection, _line_count) = self.render_options(size);
308                answer_collection = _answer_collection;
309                line_count  = _line_count;
310
311
312            }
313            Ok(answer_collection)
314        }
315    }
316
317    struct ContinueBtn{
318        pub valid_appearance   : String,
319        pub invalid_appearance : String,
320    }
321
322    impl ContinueBtn {
323        pub fn get_valid_appearance(&self)->String{
324            self.valid_appearance.clone()
325        }
326
327        pub fn get_invalid_appearance(&self)->String{
328            self.invalid_appearance.clone()
329        }
330
331        pub fn set_valid_appearance(&mut self, new_valid_appearance : String)->&mut Self{
332            self.valid_appearance = new_valid_appearance;
333            self
334        }
335
336        pub fn set_invalid_appearance(&mut self, new_invalid_appearance :String)-> &mut Self{
337            self.invalid_appearance = new_invalid_appearance;
338            self
339        }
340    }
341
342    impl MultipleChoice{
343        pub fn new(message: String) -> MultipleChoice{
344            MultipleChoice{
345                message            : message,
346                prepended_message  : Some("? ".to_owned()),
347                option_groups      : vec!(),
348                validate_fn        : Box::new(|_| {true}), //always validate to true if not set
349                on_select_continue : false,
350                cursor             : Cursor{
351                    on_group: 0,
352                    on_option: 0,
353                },
354                continue_btn       : ContinueBtn{
355                    valid_appearance   : "> Continue >".to_owned(),
356                    invalid_appearance : "| Continue |".to_owned()
357                }
358            }
359        }
360
361        pub fn replace_continue_btn(&mut self, valid_appearance : String , invalid_appearance : String)->&mut Self{
362            self.continue_btn.valid_appearance = valid_appearance;
363            self.continue_btn.invalid_appearance = invalid_appearance;
364            self
365        }
366
367        pub fn toggle_selected_option(&mut self,  group_index: u16, option_index :u16 , io : Option<bool>){
368            if let Some(_io) = io{
369                self.option_groups[group_index as usize].options[option_index as usize].selected = _io;
370            }else{
371                self.option_groups[group_index as usize].options[option_index as usize].selected = !self.option_groups[group_index as usize].options[option_index as usize].selected;
372            }
373        }
374
375        pub fn render_options(&mut self, size : (u16, u16) )->(AnswerCollection, u16){
376            let mut stdout  = stdout().into_raw_mode().unwrap();
377            let mut answer_collection = AnswerCollection{answer_sets : vec!()};
378            let mut line_count = 0;
379            let mut curr_grp = 0;
380
381            for grp in &mut self.option_groups{
382                let result = if grp.options.len() < 2{
383                    Err("2 or more options must be populated before sending multiple choice.".to_owned())
384                }else{
385                    //write group name
386                    write!(stdout, "{}{}    ==== {} ====", termion::scroll::Up(1), termion::cursor::Goto(0,size.1), grp.name);
387                    line_count += 1;
388
389                    let mut curr_option = 0;
390                    for option in & grp.options{
391                        let selected_icon = if grp.options[curr_option as usize].selected {
392                            let large_filled_circle = vec!(0xE2, 0xAC, 0xA4);
393                            String::from_utf8_lossy(&large_filled_circle).into_owned()
394                        }else{
395                            let large_circle = vec!(0xE2, 0x97, 0xAF);
396                            String::from_utf8_lossy(&large_circle).into_owned()
397                        };
398
399                        let selector_str = if self.cursor.on_group == curr_grp && self.cursor.on_option == curr_option{
400                            let selector = vec!(0xE2, 0x98, 0x9B);
401                            String::from_utf8_lossy(&selector).into_owned()
402                        }else{
403                            " ".to_owned()
404                        };
405
406                        write!(stdout, "{}{}{}    {}  {}", termion::scroll::Up(1), termion::cursor::Goto(0,size.1), selector_str,  selected_icon, option.name);
407                        line_count += 1;
408                        curr_option += 1;
409                    }
410
411
412
413                    Ok("".to_owned())
414                };
415
416                //render_selector
417
418                if let Err(_error) = result{ // is an error
419                }else{ //is not an error
420                    answer_collection.answer_sets.push(grp.to_answerset());
421                }
422                curr_grp += 1;
423            }
424            let is_valid = self.validate();
425            let continue_str = if is_valid {
426                self.continue_btn.get_valid_appearance()
427            }else{
428                self.continue_btn.get_invalid_appearance()
429            };
430
431            let selector_str = if self.cursor.on_group == self.option_groups.len() as u16{
432                let selector = vec!(0xE2, 0x98, 0x9B);
433                String::from_utf8_lossy(&selector).into_owned()
434            }else{
435                " ".to_owned()
436            };
437            write!(stdout, "{}{}{}    {}", termion::scroll::Up(1), termion::cursor::Goto(0,size.1), selector_str, continue_str);
438            line_count += 1;
439            stdout.flush().unwrap();
440            return (answer_collection, line_count);
441        }
442        //we want the answer collection so that the user can do complex validation.
443        pub fn set_validation(&mut self, f : Box<Fn(AnswerCollection)->bool>)->&mut Self{
444            self.validate_fn = f;
445            self
446        }
447
448        fn validate(&mut self)->bool{
449            let answer_collection = self.to_answer_collections();
450            (self.validate_fn)(answer_collection)
451        }
452
453        fn get_option_group_by_name(&mut self, option_group_name : &'static str)->Option<&mut OptionGroup>{
454            for opt_grp in &mut self.option_groups{
455                if opt_grp.name == option_group_name {
456                    return Some(opt_grp)
457                }
458            }
459            None
460        }
461
462        pub fn add_option_group(&mut self, option_group : OptionGroup)->&mut Self{
463            let mut result = None;
464            if let Some(opt_grp) = self.get_option_group_by_name(option_group.name){
465                result = Some(format!("A group with the name \"{}\" already exists under the message", opt_grp.name));
466            }
467
468            if let Some(mut err_msg) = result {
469                err_msg.push_str(&self.message);
470                panic!(err_msg);
471            }else{
472                self.option_groups.push(option_group);
473            }
474            self
475        }
476
477        pub fn to_answer_collections(&mut self)->AnswerCollection{
478            let mut answer_collections = AnswerCollection{
479                answer_sets : vec!()
480            };
481
482            for grp in self.option_groups.iter() {
483                answer_collections.answer_sets.push(grp.to_answerset());
484            }
485            answer_collections
486        }
487    }
488
489    pub struct AnswerCollection{
490        pub answer_sets : Vec<AnswerSet>,
491    }
492
493    impl AnswerSet{
494        pub fn get_first_answer(&mut self)->Option<String>{
495
496            let result = if self.answers.len() > 0{
497                    Some(self.answers[0].clone())
498                }else{
499                    None
500                };
501
502            result
503        }
504    }
505
506    impl AnswerCollection{
507        fn get_answer_set_by_group_name(&mut self,  group_name : &'static str)->Option<& AnswerSet>{
508            let mut result = None;
509            for answer_set in self.answer_sets.iter() {
510                if answer_set.group_name == group_name {
511                    result = Some(answer_set);
512                    break;
513                }
514            }
515            result
516        }
517
518        pub fn get_first_answer(&mut self)->Option<String>{
519            if self.answer_sets.len() as u16 > 0{
520                self.answer_sets[0].get_first_answer()
521            }else{
522                None
523            }
524        }
525    }
526
527    pub struct AnswerSet{
528        pub group_name : &'static str,
529        pub answers    : Vec<String>,
530    }
531
532    pub struct Confirm{
533        message : String,
534        prepended_message : Option<String>
535    }
536
537    impl Confirm{
538        pub fn new(message : String)->Confirm {
539            Confirm{
540                message : message,
541                prepended_message : Some("? ".to_owned())
542            }
543        }
544    }
545
546    impl TraitQuestion for Confirm{
547        fn send(&mut self)->Result<AnswerCollection, String>{
548            let mut stdout = stdout().into_raw_mode().unwrap();
549            let stdin      = stdin();
550            let mut size   = termion::terminal_size().unwrap();
551            write!(stdout, "{}{} [Y/n] {}{}", self.prepended_message.as_ref().unwrap_or(&"".to_owned()), self.message, termion::scroll::Up(1), termion::cursor::Goto(0, size.1));
552
553            stdout.flush().unwrap();
554            let mut data : Vec<char> = vec!();
555            let yes = Regex::new(r"(?i)^y$|^yes$$").unwrap();
556            let no = Regex::new(r"(?i)^no$|^n$").unwrap();
557            let mut was_unrecognized = false;
558            'outer: for c in stdin.keys(){
559                // let cursor_position = stdout.cursor_pos().unwrap();
560                match c.unwrap(){
561                    Key::Char('\n') => {
562                        let content = data.iter().cloned().collect::<String>();
563                        if yes.is_match(&content) {
564                            let answer_collection = AnswerCollection{
565                                answer_sets : vec!(AnswerSet{
566                                    group_name : "default",
567                                    answers    : vec!("yes".to_owned())
568                                })
569                            };
570                           return Ok(answer_collection);
571                        }else if no.is_match(&content) {
572                            let answer_collection = AnswerCollection{
573                                answer_sets : vec!(AnswerSet{
574                                    group_name : "default",
575                                    answers    : vec!("no".to_owned())
576                                })
577                            };
578                           return Ok(answer_collection);
579
580                        }else{
581                            write!(stdout, "{}{}unrecognized response.", termion::clear::CurrentLine, termion::cursor::Goto(0, size.1));
582                            was_unrecognized = true;
583                            data = vec!();
584                            stdout.flush().unwrap();
585                        }
586                    },
587                    Key::Char(c)   => {
588                        data.push(c);
589                        if was_unrecognized {
590                            write!(stdout, "{}{}{}", termion::cursor::Goto(0, size.1), termion::clear::CurrentLine, c);
591                            was_unrecognized = false;
592                        }else{
593                            write!(stdout,"{}",c);
594                        }
595
596                    },
597
598                    // Key::Down => {
599                    //     write!(stdout, "{}", termion::cursor::Goto(0,size.1));
600                    //     println!("");
601                    // },
602                    // Key::Up => {
603                    //     write!(stdout, "{}", termion::cursor::Goto(cursor_position.0, cursor_position.1 - 1));
604                    // },
605                    Key::Ctrl('c') => break,
606                    // Key::Char(c) => println!("{}",c),
607                    _ => {}
608                }
609
610                stdout.flush().unwrap();
611            }
612
613            write!(stdout, "{}{}",termion::cursor::Goto(0, size.1+1), termion::scroll::Up(1)); //new line
614            let content = data.iter().cloned().collect::<String>();
615
616            let answer_collection = AnswerCollection{
617                answer_sets : vec!(AnswerSet{
618                    group_name : "default",
619                    answers    : vec!(content.clone())
620                })
621            };
622
623            Ok(answer_collection)
624            // write!(stdout, "{}",termion::cursor::Goto(0, size.1+1)); //new line
625        }
626    }
627
628    pub struct OpenEnded{
629        message      : String,
630        prepended_message : Option<String>
631    }
632
633    impl OpenEnded{
634        pub fn new(message:String) -> OpenEnded{
635            OpenEnded{
636                message   : message,
637                prepended_message : Some("? ".to_owned()),
638            }
639        }
640    }
641
642    impl TraitQuestion for OpenEnded{
643
644        fn send(&mut self)->Result<AnswerCollection, String>{
645            let mut stdout = stdout().into_raw_mode().unwrap();
646            let stdin      = stdin();
647            let mut size   = termion::terminal_size().unwrap();
648            write!(
649                stdout, "{}{}{}{}",
650                self.prepended_message.as_ref().unwrap_or(&"".to_owned()),
651                self.message,
652                termion::scroll::Up(1),
653                termion::cursor::Goto(0,size.1)
654            );
655
656            stdout.flush().unwrap();
657            let mut data : Vec<char> = vec!();
658            for c in stdin.keys(){
659                // let cursor_position = stdout.cursor_pos().unwrap();
660                match c.unwrap(){
661                    Key::Char('\n') => {
662                        break;
663                    },
664                    Key::Char(c)   => {
665                        data.push(c);
666                        write!(stdout,"{}",c);
667                    },
668                    // Key::Down => {
669                    //     write!(stdout, "{}", termion::cursor::Goto(0,size.1));
670                    //     println!("");
671                    // },
672                    // Key::Up => {
673                    //     write!(stdout, "{}", termion::cursor::Goto(cursor_position.0, cursor_position.1 - 1));
674                    // },
675                    Key::Ctrl('c') => break,
676                    // Key::Char(c) => println!("{}",c),
677                    _ => {}
678                }
679
680                stdout.flush().unwrap();
681            }
682
683            write!(stdout, "{}{}",termion::cursor::Goto(0, size.1+1), termion::scroll::Up(1)); //new line
684            let content = data.iter().cloned().collect::<String>();
685
686            let answer_collection = AnswerCollection{
687                answer_sets : vec!(AnswerSet{
688                    group_name : "default",
689                    answers    : vec!(content.clone())
690                })
691            };
692
693            Ok(answer_collection)
694            // write!(stdout, "{}",termion::cursor::Goto(0, size.1+1)); //new line
695        }
696    }
697
698}