Skip to main content

gpg_tui/app/
prompt.rs

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