may_clack/
input.rs

1//! Text input
2
3use crate::{
4	error::ClackError,
5	style::{ansi, chars},
6};
7use crossterm::{cursor, QueueableCommand};
8use owo_colors::OwoColorize;
9use rustyline::{highlight::Highlighter, Completer, Editor, Helper, Hinter, Validator};
10use std::{
11	borrow::{Borrow, Cow},
12	error::Error,
13	fmt::Display,
14	io::{stdout, Write},
15	str::FromStr,
16};
17
18#[derive(Completer, Helper, Hinter, Validator)]
19pub(super) struct PlaceholderHighlighter<'a> {
20	placeholder: Option<&'a str>,
21	pub is_val: bool,
22}
23
24impl<'a> PlaceholderHighlighter<'a> {
25	pub fn new(placeholder: Option<&'a str>) -> Self {
26		PlaceholderHighlighter {
27			placeholder,
28			is_val: false,
29		}
30	}
31}
32
33impl Highlighter for PlaceholderHighlighter<'_> {
34	fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
35		if let Some(placeholder) = self.placeholder {
36			if line.is_empty() {
37				Cow::Owned(placeholder.dimmed().to_string())
38			} else {
39				Cow::Borrowed(line)
40			}
41		} else {
42			Cow::Borrowed(line)
43		}
44	}
45
46	fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
47		true
48	}
49
50	fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
51		&'s self,
52		prompt: &'p str,
53		default: bool,
54	) -> Cow<'b, str> {
55		if !default {
56			// i honestly don't know what this even does
57			Cow::Borrowed(prompt)
58		} else if self.is_val {
59			Cow::Owned(prompt.yellow().to_string())
60		} else {
61			Cow::Owned(prompt.cyan().to_string())
62		}
63	}
64}
65
66pub(super) type ValidateFn = dyn Fn(&str) -> Result<(), Cow<'static, str>>;
67
68/// `Input` struct
69///
70/// # Examples
71///
72/// ```no_run
73/// use may_clack::{cancel, input};
74/// # use std::borrow::Cow;
75///
76/// # fn main() -> Result<(), may_clack::error::ClackError> {
77/// let answer = input("message")
78///     .initial_value("initial_value")
79///     .validate(|x| {
80///         if x.find(char::is_uppercase).is_some() {
81///             Err(Cow::Borrowed("only use lowercase characters"))
82///         } else {
83///             Ok(())
84///         }
85///     })
86///     .cancel(do_cancel)
87///     .interact()?;
88/// println!("answer {:?}", answer);
89/// # Ok(())
90/// # }
91///
92/// fn do_cancel() {
93///     cancel!("operation cancelled");
94///     std::process::exit(1);
95/// }
96/// ````
97pub struct Input<M: Display> {
98	message: M,
99	initial_value: Option<String>,
100	placeholder: Option<String>,
101	validate: Option<Box<ValidateFn>>,
102	cancel: Option<Box<dyn Fn()>>,
103}
104
105impl<M: Display> Input<M> {
106	/// Creates a new `Input` struct.
107	///
108	/// Has a shorthand version in [`input()`]
109	///
110	/// # Examples
111	///
112	/// ```no_run
113	/// use may_clack::{input, input::Input};
114	///
115	/// // these two are equivalent
116	/// let question = Input::new("message");
117	/// let question = input("message");
118	/// ```
119	pub fn new(message: M) -> Self {
120		Input {
121			message,
122			initial_value: None,
123			placeholder: None,
124			validate: None,
125			cancel: None,
126		}
127	}
128
129	/// Specify a placeholder.
130	///
131	/// # Examples
132	///
133	/// ```no_run
134	/// use may_clack::input;
135	///
136	/// # fn main() -> Result<(), may_clack::error::ClackError> {
137	/// let answer = input("message").placeholder("placeholder").required()?;
138	/// println!("answer {:?}", answer);
139	/// # Ok(())
140	/// # }
141	/// ```
142	pub fn placeholder<S: ToString>(&mut self, placeholder: S) -> &mut Self {
143		self.placeholder = Some(placeholder.to_string());
144		self
145	}
146
147	/// Maybe specify an initial value.
148	///
149	/// # Examples
150	///
151	/// ```no_run
152	/// use may_clack::input;
153	///
154	/// # fn main() -> Result<(), may_clack::error::ClackError> {
155	/// let answer = input("message").maybe_initial(Some("initial")).required()?;
156	/// let answer = input("message").maybe_initial(None::<&str>).required()?;
157	/// # Ok(())
158	/// # }
159	/// ```
160	pub fn maybe_initial<T: Borrow<Option<S>>, S: ToString>(&mut self, initial: T) -> &mut Self {
161		if let Some(initial) = initial.borrow() {
162			self.initial_value = Some(initial.to_string());
163		}
164
165		self
166	}
167
168	/// Specify the initial value.
169	///
170	/// # Examples
171	///
172	/// ```no_run
173	/// use may_clack::input;
174	///
175	/// # fn main() -> Result<(), may_clack::error::ClackError> {
176	/// let answer = input("message").initial_value("initial_value").interact()?;
177	/// println!("answer {:?}", answer);
178	/// # Ok(())
179	/// # }
180	/// ```
181	pub fn initial_value<S: ToString>(&mut self, initial_value: S) -> &mut Self {
182		self.initial_value = Some(initial_value.to_string());
183		self
184	}
185
186	/// Specify a validation function.
187	///
188	/// On a successful validation, return a `None` from the closure,
189	/// and on an unsuccessful validation return a `Some<&'static str>` with the error message.
190	///
191	/// # Examples
192	///
193	/// ```no_run
194	/// use may_clack::input;
195	/// # use std::borrow::Cow;
196	///
197	/// let answer = input("message")
198	///     .validate(|x| {
199	///         if x.is_ascii() {
200	///             Ok(())
201	///         } else {
202	///             Err(Cow::Borrowed("only use ascii characters"))
203	///         }
204	///     })
205	///     .interact()?;
206	/// println!("answer {:?}", answer);
207	/// # Ok::<(), may_clack::error::ClackError>(())
208	/// ```
209	pub fn validate<F>(&mut self, validate: F) -> &mut Self
210	where
211		F: Fn(&str) -> Result<(), Cow<'static, str>> + 'static,
212	{
213		let validate = Box::new(validate);
214		self.validate = Some(validate);
215		self
216	}
217
218	fn do_validate(&self, input: &str) -> Result<(), Cow<'static, str>> {
219		if let Some(validate) = self.validate.as_deref() {
220			validate(input)
221		} else {
222			Ok(())
223		}
224	}
225
226	/// Specify function to call on cancel.
227	///
228	/// # Examples
229	///
230	/// ```no_run
231	/// use may_clack::{input, cancel};
232	///
233	/// # fn main() -> Result<(), may_clack::error::ClackError> {
234	/// let answer = input("message").cancel(do_cancel).interact()?;
235	/// println!("answer {:?}", answer);
236	/// # Ok(())
237	/// # }
238	///
239	/// fn do_cancel() {
240	///     cancel!("operation cancelled");
241	///     panic!("operation cancelled");
242	/// }
243	pub fn cancel<F>(&mut self, cancel: F) -> &mut Self
244	where
245		F: Fn() + 'static,
246	{
247		let cancel = Box::new(cancel);
248		self.cancel = Some(cancel);
249		self
250	}
251
252	fn interact_once<T: FromStr>(&self, enforce_non_empty: bool) -> Result<Option<T>, ClackError>
253	where
254		T::Err: Error,
255	{
256		let prompt = format!("{}  ", *chars::BAR);
257
258		let mut editor = Editor::new()?;
259		let helper = PlaceholderHighlighter::new(self.placeholder.as_deref());
260		editor.set_helper(Some(helper));
261
262		let mut initial_value = self.initial_value.as_deref().map(Cow::Borrowed);
263		loop {
264			let line = if let Some(ref init) = initial_value {
265				editor.readline_with_initial(&prompt, (init, ""))
266			} else {
267				editor.readline(&prompt)
268			};
269
270			// todo this looks refactor-able
271			if let Ok(value) = line {
272				if value.is_empty() {
273					if enforce_non_empty {
274						initial_value = None;
275
276						if let Some(helper) = editor.helper_mut() {
277							helper.is_val = true;
278						}
279
280						self.w_val("value is required");
281					} else {
282						break Ok(None);
283					}
284				} else if let Err(text) = self.do_validate(&value) {
285					initial_value = Some(Cow::Owned(value));
286
287					if let Some(helper) = editor.helper_mut() {
288						helper.is_val = true;
289					}
290
291					self.w_val(&text);
292				} else {
293					match value.parse::<T>() {
294						Ok(val) => break Ok(Some(val)),
295						Err(err) => {
296							initial_value = Some(Cow::Owned(value));
297
298							if let Some(helper) = editor.helper_mut() {
299								helper.is_val = true;
300							}
301
302							self.w_val(&err.to_string());
303						}
304					}
305				}
306			} else {
307				break Err(ClackError::Cancelled);
308			}
309		}
310	}
311
312	/// Like [`Input::required()`], but parses the value before returning.
313	///
314	/// Useful for getting number inputs.
315	///
316	/// # Examples
317	///
318	/// ```no_run
319	/// use may_clack::input;
320	///
321	/// # fn main() -> Result<(), may_clack::error::ClackError> {
322	/// let answer: i32 = input("message").parse::<i32>()?;
323	/// println!("answer {:?}", answer);
324	/// # Ok(())
325	/// # }
326	/// ```
327	pub fn parse<T: FromStr + Display>(&self) -> Result<T, ClackError>
328	where
329		T::Err: Error,
330	{
331		self.w_init();
332
333		let interact = self.interact_once::<T>(true);
334		match interact {
335			Ok(Some(value)) => {
336				self.w_out(&value);
337				Ok(value)
338			}
339			Ok(None) => unreachable!(),
340			Err(ClackError::Cancelled) => {
341				self.w_cancel();
342				if let Some(cancel) = self.cancel.as_deref() {
343					cancel();
344				}
345
346				Err(ClackError::Cancelled)
347			}
348			Err(err) => Err(err),
349		}
350	}
351
352	/// Like [`Input::parse()`], but it also allows empty line submits like [`Input::interact()`].
353	///
354	/// ```no_run
355	/// use may_clack::input;
356	/// use std::net::Ipv4Addr;
357	///
358	/// # fn main() -> Result<(), may_clack::error::ClackError> {
359	/// let answer = input("message").maybe_parse::<Ipv4Addr>()?;
360	/// println!("answer {:?}", answer);
361	/// # Ok(())
362	/// # }
363	/// ```
364	pub fn maybe_parse<T: FromStr + Display>(&self) -> Result<Option<T>, ClackError>
365	where
366		T::Err: Error,
367	{
368		self.w_init();
369
370		let interact = self.interact_once::<T>(false);
371		match interact {
372			Ok(val) => {
373				if let Some(val) = &val {
374					self.w_out(val);
375				} else {
376					self.w_out("");
377				}
378
379				Ok(val)
380			}
381			Err(ClackError::Cancelled) => {
382				self.w_cancel();
383				if let Some(cancel) = self.cancel.as_deref() {
384					cancel();
385				}
386
387				Err(ClackError::Cancelled)
388			}
389			Err(err) => Err(err),
390		}
391	}
392
393	/// Like [`Input::interact()`], but does not return an empty line.
394	///
395	/// # Examples
396	///
397	/// ```no_run
398	/// use may_clack::input;
399	///
400	/// # fn main() -> Result<(), may_clack::error::ClackError> {
401	/// let answer = input("message").required()?;
402	/// println!("answer {:?}", answer);
403	/// # Ok(())
404	/// # }
405	/// ```
406	pub fn required(&self) -> Result<String, ClackError> {
407		self.w_init();
408
409		let interact = self.interact_once::<String>(true);
410		match interact {
411			Ok(Some(value)) => {
412				self.w_out(&value);
413				Ok(value)
414			}
415			Ok(None) => unreachable!(),
416			Err(ClackError::Cancelled) => {
417				self.w_cancel();
418				if let Some(cancel) = self.cancel.as_deref() {
419					cancel();
420				}
421
422				Err(ClackError::Cancelled)
423			}
424			Err(err) => Err(err),
425		}
426	}
427
428	/// Waits for the user to submit a line of text.
429	///
430	/// Returns [`None`] on an empty line and [`Some::<String>`] otherwise.
431	///
432	/// # Examples
433	///
434	/// ```no_run
435	/// use may_clack::{cancel, input};
436	/// # use std::borrow::Cow;
437	///
438	/// # fn main() -> Result<(), may_clack::error::ClackError> {
439	/// let answer = input("message")
440	///     .initial_value("initial_value")
441	///     .validate(|x| {
442	///         x.parse::<u32>()
443	///             .map(|_| ())
444	///             .map_err(|_| Cow::Borrowed("invalid u32"))
445	///     })
446	///     .cancel(do_cancel)
447	///     .interact()?;
448	/// println!("answer {:?}", answer);
449	/// # Ok(())
450	/// # }
451	///
452	/// fn do_cancel() {
453	///     cancel!("operation cancelled");
454	///     std::process::exit(1);
455	/// }
456	/// ```
457	pub fn interact(&self) -> Result<Option<String>, ClackError> {
458		self.w_init();
459
460		let interact = self.interact_once(false);
461		match interact {
462			Ok(val) => {
463				let v = val.as_deref().unwrap_or("");
464				self.w_out(v);
465				Ok(val)
466			}
467			Err(ClackError::Cancelled) => {
468				self.w_cancel();
469				if let Some(cancel) = self.cancel.as_deref() {
470					cancel();
471				}
472
473				Err(ClackError::Cancelled)
474			}
475			Err(err) => Err(err),
476		}
477	}
478}
479
480impl<M: Display> Input<M> {
481	fn w_init(&self) {
482		let mut stdout = stdout();
483
484		println!("{}", *chars::BAR);
485		println!("{}  {}", (*chars::STEP_ACTIVE).cyan(), self.message);
486		println!("{}", (*chars::BAR).cyan());
487		print!("{}", (*chars::BAR_END).cyan());
488
489		let _ = stdout.queue(cursor::MoveToPreviousLine(1));
490		let _ = stdout.flush();
491
492		print!("{}  ", (*chars::BAR).cyan());
493		let _ = stdout.flush();
494	}
495
496	fn w_val(&self, text: &str) {
497		let mut stdout = stdout();
498		let _ = stdout.queue(cursor::MoveToPreviousLine(2));
499		let _ = stdout.flush();
500
501		println!("{}  {}", (*chars::STEP_ERROR).yellow(), self.message);
502		println!("{}", (*chars::BAR).yellow());
503
504		print!("{}", ansi::CLEAR_LINE);
505		print!("{}  {}", (*chars::BAR_END).yellow(), text.yellow());
506
507		let _ = stdout.queue(cursor::MoveToPreviousLine(1));
508		let _ = stdout.flush();
509	}
510
511	fn w_out<D: Display>(&self, value: D) {
512		let mut stdout = stdout();
513		let _ = stdout.queue(cursor::MoveToPreviousLine(2));
514		let _ = stdout.flush();
515
516		println!("{}  {}", (*chars::STEP_SUBMIT).green(), self.message);
517		print!("{}", ansi::CLEAR_LINE);
518		println!("{}  {}", *chars::BAR, value.dimmed());
519
520		print!("{}", ansi::CLEAR_LINE);
521	}
522
523	fn w_cancel(&self) {
524		let mut stdout = stdout();
525		let _ = stdout.queue(cursor::MoveToPreviousLine(2));
526		let _ = stdout.flush();
527
528		println!("{}  {}", (*chars::STEP_CANCEL).red(), self.message);
529
530		print!("{}", ansi::CLEAR_LINE);
531		println!("{}  {}", *chars::BAR, "cancelled".strikethrough().dimmed());
532
533		print!("{}", ansi::CLEAR_LINE);
534	}
535}
536
537/// Shorthand for [`Input::new()`]
538pub fn input<M: Display>(message: M) -> Input<M> {
539	Input::new(message)
540}