1 use crate::app::command::Command; 2 use std::cmp::Ordering; 3 use std::fmt::{Display, Formatter, Result as FmtResult}; 4 use std::time::Instant; 5 6 /// Prefix character for indicating command input. 7 pub const COMMAND_PREFIX: char = ':'; 8 /// Prefix character for indicating search input. 9 pub const SEARCH_PREFIX: char = '/'; 10 11 /// Output type of the prompt. 12 #[derive(Clone, Debug, PartialEq)] 13 pub enum OutputType { 14 /// No output. 15 None, 16 /// Successful execution. 17 Success, 18 /// Warning about execution. 19 Warning, 20 /// Failed execution. 21 Failure, 22 /// Performed an action (such as changing the mode). 23 Action, 24 } 25 26 impl Default for OutputType { default() -> Self27 fn default() -> Self { 28 Self::None 29 } 30 } 31 32 impl Display for OutputType { fmt(&self, f: &mut Formatter<'_>) -> FmtResult33 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 34 write!( 35 f, 36 "{}", 37 match self { 38 Self::Success => "(i) ", 39 Self::Warning => "(w) ", 40 Self::Failure => "(e) ", 41 _ => "", 42 } 43 ) 44 } 45 } 46 47 impl From<String> for OutputType { from(s: String) -> Self48 fn from(s: String) -> Self { 49 match s.to_lowercase().as_str() { 50 "success" => Self::Success, 51 "warning" => Self::Warning, 52 "failure" => Self::Failure, 53 "action" => Self::Action, 54 _ => Self::None, 55 } 56 } 57 } 58 59 /// Application prompt which is responsible for 60 /// handling user input ([`text`]), showing the 61 /// output of [`commands`] and ask for confirmation. 62 /// 63 /// [`text`]: Prompt::text 64 /// [`commands`]: crate::app::command::Command 65 #[derive(Clone, Debug, Default)] 66 pub struct Prompt { 67 /// Input/output text. 68 pub text: String, 69 /// Output type. 70 pub output_type: OutputType, 71 /// Clock for tracking the duration of output messages. 72 pub clock: Option<Instant>, 73 /// Command that will be confirmed for execution. 74 pub command: Option<Command>, 75 /// Command history. 76 pub history: Vec<String>, 77 /// Index of the selected command from history. 78 pub history_index: usize, 79 } 80 81 impl Prompt { 82 /// Enables the prompt. 83 /// 84 /// Available prefixes: 85 /// * `:`: command input 86 /// * `/`: search enable(&mut self, prefix: char)87 fn enable(&mut self, prefix: char) { 88 self.text = if self.text.is_empty() || self.clock.is_some() { 89 prefix.to_string() 90 } else { 91 format!("{}{}", prefix, &self.text[1..self.text.len()]) 92 }; 93 self.output_type = OutputType::None; 94 self.clock = None; 95 self.command = None; 96 self.history_index = 0; 97 } 98 99 /// Checks if the prompt is enabled. is_enabled(&self) -> bool100 pub fn is_enabled(&self) -> bool { 101 !self.text.is_empty() && self.clock.is_none() && self.command.is_none() 102 } 103 104 /// Enables the command input. enable_command_input(&mut self)105 pub fn enable_command_input(&mut self) { 106 self.enable(COMMAND_PREFIX); 107 } 108 109 /// Checks if the command input is enabled. is_command_input_enabled(&self) -> bool110 pub fn is_command_input_enabled(&self) -> bool { 111 self.text.starts_with(COMMAND_PREFIX) 112 } 113 114 /// Enables the search. enable_search(&mut self)115 pub fn enable_search(&mut self) { 116 self.enable(SEARCH_PREFIX); 117 } 118 119 /// Checks if the search is enabled. is_search_enabled(&self) -> bool120 pub fn is_search_enabled(&self) -> bool { 121 self.text.starts_with(SEARCH_PREFIX) 122 } 123 124 /// Sets the output message. set_output<S: AsRef<str>>(&mut self, output: (OutputType, S))125 pub fn set_output<S: AsRef<str>>(&mut self, output: (OutputType, S)) { 126 let (output_type, message) = output; 127 self.output_type = output_type; 128 self.text = message.as_ref().to_string(); 129 self.clock = Some(Instant::now()); 130 } 131 132 /// Sets the command that will be asked to confirm. set_command(&mut self, command: Command)133 pub fn set_command(&mut self, command: Command) { 134 self.text = format!("press 'y' to {}", command); 135 self.output_type = OutputType::Action; 136 self.command = Some(command); 137 self.clock = Some(Instant::now()); 138 } 139 140 /// Select the next command. next(&mut self)141 pub fn next(&mut self) { 142 match self.history_index.cmp(&1) { 143 Ordering::Greater => { 144 self.history_index -= 1; 145 self.text = self.history 146 [self.history.len() - self.history_index] 147 .to_string(); 148 } 149 Ordering::Equal => { 150 self.text = String::from(":"); 151 self.history_index = 0; 152 } 153 Ordering::Less => {} 154 } 155 } 156 157 /// Select the previous command. previous(&mut self)158 pub fn previous(&mut self) { 159 if self.history.len() > self.history_index { 160 self.text = self.history 161 [self.history.len() - (self.history_index + 1)] 162 .to_string(); 163 self.history_index += 1; 164 } 165 } 166 167 /// Clears the prompt. clear(&mut self)168 pub fn clear(&mut self) { 169 self.text.clear(); 170 self.output_type = OutputType::None; 171 self.clock = None; 172 self.command = None; 173 self.history_index = 0; 174 } 175 } 176 177 #[cfg(test)] 178 mod tests { 179 use super::*; 180 use pretty_assertions::{assert_eq, assert_ne}; 181 #[test] test_app_prompt()182 fn test_app_prompt() { 183 let mut prompt = Prompt::default(); 184 prompt.enable_command_input(); 185 assert!(prompt.is_command_input_enabled()); 186 prompt.enable_search(); 187 assert!(prompt.is_search_enabled()); 188 assert!(prompt.is_enabled()); 189 prompt.set_output((OutputType::from(String::from("success")), "Test")); 190 assert_eq!(String::from("Test"), prompt.text); 191 assert_eq!(OutputType::Success, prompt.output_type); 192 assert_ne!(0, prompt.clock.unwrap().elapsed().as_nanos()); 193 assert!(!prompt.is_enabled()); 194 prompt.clear(); 195 assert_eq!(String::new(), prompt.text); 196 assert_eq!(None, prompt.clock); 197 prompt.history = 198 vec![String::from("0"), String::from("1"), String::from("2")]; 199 for i in 0..prompt.history.len() { 200 prompt.previous(); 201 assert_eq!((prompt.history.len() - i - 1).to_string(), prompt.text); 202 } 203 for i in 1..prompt.history.len() { 204 prompt.next(); 205 assert_eq!(i.to_string(), prompt.text); 206 } 207 for output_type in vec![ 208 OutputType::from(String::from("warning")), 209 OutputType::from(String::from("failure")), 210 OutputType::from(String::from("action")), 211 OutputType::from(String::from("test")), 212 ] { 213 assert_eq!( 214 match output_type { 215 OutputType::Warning => "(w) ", 216 OutputType::Failure => "(e) ", 217 _ => "", 218 }, 219 &output_type.to_string() 220 ); 221 } 222 } 223 } 224