Skip to main content

may_clack/
input.rs

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