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