Skip to main content

may_clack/
input.rs

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