may_clack/
select.rs

1//! Select option
2
3use crate::{
4	error::ClackError,
5	style::{ansi, chars},
6};
7use crossterm::{
8	cursor,
9	event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
10	execute, terminal,
11};
12use owo_colors::OwoColorize;
13use std::{
14	fmt::Display,
15	io::{stdout, Write},
16};
17use unicode_truncate::UnicodeTruncateStr;
18
19/// `Select` `Opt` struct
20#[derive(Debug)]
21pub struct Opt<T: Clone, O: Display> {
22	value: T,
23	label: O,
24	hint: Option<String>,
25}
26
27impl<T: Clone, O: Display> Opt<T, O> {
28	/// Creates a new `Opt` struct.
29	///
30	/// # Examples
31	///
32	/// ```
33	/// use may_clack::select::Opt;
34	///
35	/// let option = Opt::new("value", "lavel", Some("hint"));
36	/// ```
37	pub fn new<S: ToString>(value: T, label: O, hint: Option<S>) -> Self {
38		Opt {
39			value,
40			label,
41			hint: hint.map(|hint| hint.to_string()),
42		}
43	}
44
45	/// Creates a new `Opt` struct without a hint
46	///
47	/// # Examples
48	///
49	/// ```
50	/// use may_clack::select::Opt;
51	///
52	/// let option = Opt::simple("value", "label");
53	/// ```
54	pub fn simple(value: T, label: O) -> Self {
55		Opt::new(value, label, None::<String>)
56	}
57
58	/// Creates a new `Opt` struct with a hint
59	///
60	/// # Examples
61	///
62	/// ```
63	/// use may_clack::select::Opt;
64	///
65	/// let option = Opt::hint("value", "label", "hint");
66	/// ```
67	pub fn hint<S: ToString>(value: T, label: O, hint: S) -> Self {
68		Opt::new(value, label, Some(hint))
69	}
70
71	fn trunc(&self, hint: usize) -> String {
72		let size = crossterm::terminal::size();
73		let label = format!("{}", self.label);
74
75		match size {
76			Ok((width, _height)) => label
77				.unicode_truncate(width as usize - 5 - hint)
78				.0
79				.to_owned(),
80			Err(_) => label,
81		}
82	}
83
84	fn focus(&self) -> String {
85		let hint_len = self.hint.as_deref().map_or(0, |hint| hint.len() + 3);
86		let label = self.trunc(hint_len);
87
88		let fmt = format!("{} {}", (*chars::RADIO_ACTIVE).green(), label);
89
90		if let Some(hint) = &self.hint {
91			let hint = format!("({hint})");
92			format!("{} {}", fmt, hint.dimmed())
93		} else {
94			fmt
95		}
96	}
97
98	fn unfocus(&self) -> String {
99		let label = self.trunc(0);
100		format!("{} {}", (*chars::RADIO_INACTIVE).dimmed(), label.dimmed())
101	}
102}
103
104/// `Select` struct.
105///
106/// # Examples
107///
108/// ```no_run
109/// use may_clack::select;
110///
111/// # fn main() -> Result<(), may_clack::error::ClackError> {
112/// let answer = select("message")
113///     .option("val 1", "value 1")
114///     .option("val 2", "value 2")
115///     .option_hint("val 3", "value 3", "hint")
116///     .option("val 4", "value 4")
117///     .option("val 5", "value 5")
118///     .less_amt(3)
119///     .interact()?;
120/// println!("answer {:?}", answer);
121/// # Ok(())
122/// # }
123/// ```
124pub struct Select<M: Display, T: Clone, O: Display> {
125	message: M,
126	less: bool,
127	less_amt: Option<u16>,
128	less_max: Option<u16>,
129	cancel: Option<Box<dyn Fn()>>,
130	options: Vec<Opt<T, O>>,
131}
132
133impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
134	/// Creates a new `Select` struct.
135	///
136	/// Has a shorthand version in [`select()`]
137	///
138	/// # Examples
139	///
140	/// ```no_run
141	/// use may_clack::{select, select::Select};
142	///
143	/// // these two are equivalent
144	/// let mut question = Select::new("message");
145	/// question.option("value", "hint");
146	///
147	/// let mut question = select("message");
148	/// question.option("value", "hint");
149	/// ```
150	pub fn new(message: M) -> Self {
151		Select {
152			message,
153			less: false,
154			less_amt: None,
155			less_max: None,
156			cancel: None,
157			options: vec![],
158		}
159	}
160
161	/// Add an option without a hint.
162	///
163	/// # Examples
164	///
165	/// ```no_run
166	/// use may_clack::select;
167	///
168	/// # fn main() -> Result<(), may_clack::error::ClackError> {
169	/// let answer = select("message")
170	///     .option("val1", "label 1")
171	///     .option("val2", "label 2")
172	///     .interact()?;
173	/// println!("answer {:?}", answer);
174	/// # Ok(())
175	/// # }
176	/// ```
177	pub fn option(&mut self, value: T, label: O) -> &mut Self {
178		let opt = Opt::new(value, label, None::<String>);
179		self.options.push(opt);
180		self
181	}
182
183	/// Add an option with a hint.
184	///
185	/// # Examples
186	///
187	/// ```no_run
188	/// use may_clack::select;
189	///
190	/// # fn main() -> Result<(), may_clack::error::ClackError> {
191	/// let answer = select("message")
192	///     .option("val1", "label 1")
193	///     .option_hint("val2", "label 2", "hint")
194	///     .option("val3", "label 3")
195	///     .interact()?;
196	/// println!("answer {:?}", answer);
197	/// # Ok(())
198	/// # }
199	/// ```
200	pub fn option_hint<S: ToString>(&mut self, value: T, label: O, hint: S) -> &mut Self {
201		let opt = Opt::new(value, label, Some(hint));
202		self.options.push(opt);
203		self
204	}
205
206	/// Add multiple options.
207	///
208	/// # Examples
209	///
210	/// ```no_run
211	/// use may_clack::{select, select::Opt};
212	///
213	/// # fn main() -> Result<(), may_clack::error::ClackError> {
214	/// let opts = vec![
215	///     Opt::simple("val1", "label 1"),
216	///     Opt::hint("val2", "label 2", "hint"),
217	///     Opt::simple("val3", "label 3"),
218	/// ];
219	///
220	/// let answer = select("message").options(opts).interact()?;
221	/// println!("answer {:?}", answer);
222	/// # Ok(())
223	/// # }
224	/// ```
225	pub fn options(&mut self, options: Vec<Opt<T, O>>) -> &mut Self {
226		self.options = options;
227		self
228	}
229
230	/// Enable paging with the amount of terminal rows.
231	///
232	/// # Examples
233	///
234	/// ```no_run
235	/// use may_clack::select;
236	///
237	/// # fn main() -> Result<(), may_clack::error::ClackError> {
238	/// let answer = select("message")
239	///     .option("val 1", "value 1")
240	///     .option("val 2", "value 2")
241	///     .option_hint("val 3", "value 3", "hint")
242	///     .option("val 4", "value 4")
243	///     .option("val 5", "value 5")
244	///     .less()
245	///     .interact()?;
246	/// println!("answer {:?}", answer);
247	/// # Ok(())
248	/// # }
249	/// ```
250	pub fn less(&mut self) -> &mut Self {
251		self.less = true;
252		self
253	}
254
255	/// Enable paging with the amount of terminal rows, additionally setting a maximum amount.
256	///
257	/// # Panics
258	///
259	/// Panics when the given value is 0.  
260	/// Panics when called after [`Select::less_amt`] has already been called.
261	///
262	/// # Examples
263	///
264	/// ```no_run
265	/// use may_clack::select;
266	///
267	/// # fn main() -> Result<(), may_clack::error::ClackError> {
268	/// let answer = select("message")
269	///     .option("val 1", "value 1")
270	///     .option("val 2", "value 2")
271	///     .option_hint("val 3", "value 3", "hint")
272	///     .option("val 4", "value 4")
273	///     .option("val 5", "value 5")
274	///     .less_max(3)
275	///     .interact()?;
276	/// println!("answer {:?}", answer);
277	/// # Ok(())
278	/// # }
279	/// ```
280	pub fn less_max(&mut self, max: u16) -> &mut Self {
281		assert!(max > 0, "less max value has to be greater than zero");
282		assert!(
283			self.less_amt.is_none(),
284			"cannot set both less_amt and less_max"
285		);
286		self.less = true;
287		self.less_max = Some(max);
288		self
289	}
290
291	/// Enable paging with the specified amount of lines.
292	///
293	/// # Panics
294	///
295	/// Panics when the given value is 0.  
296	/// Panics when called after [`Select::less_max`] has already been called.
297	///
298	/// # Examples
299	///
300	/// ```no_run
301	/// use may_clack::select;
302	///
303	/// # fn main() -> Result<(), may_clack::error::ClackError> {
304	/// let answer = select("message")
305	///     .option("val 1", "value 1")
306	///     .option("val 2", "value 2")
307	///     .option_hint("val 3", "value 3", "hint")
308	///     .option("val 4", "value 4")
309	///     .option("val 5", "value 5")
310	///     .less_amt(3)
311	///     .interact()?;
312	/// println!("answer {:?}", answer);
313	/// # Ok(())
314	/// # }
315	/// ```
316	pub fn less_amt(&mut self, less: u16) -> &mut Self {
317		assert!(less > 0, "less value has to be greater than zero");
318		assert!(
319			self.less_max.is_none(),
320			"cannot set both less_amt and less_max"
321		);
322		self.less = true;
323		self.less_amt = Some(less);
324		self
325	}
326
327	/// Specify function to call on cancel.
328	///
329	/// # Examples
330	///
331	/// ```no_run
332	/// use may_clack::{select, cancel};
333	///
334	/// # fn main() -> Result<(), may_clack::error::ClackError> {
335	/// let answer = select("select")
336	///     .option("val1", "value 1")
337	///     .option("val2", "value 2")
338	///     .option_hint("val 3", "value 3", "hint")
339	///     .cancel(do_cancel)
340	///     .interact()?;
341	/// println!("answer {:?}", answer);
342	/// # Ok(())
343	/// # }
344	///
345	/// fn do_cancel() {
346	///     cancel!("operation cancelled");
347	///     panic!("operation cancelled");
348	/// }
349	pub fn cancel<F>(&mut self, cancel: F) -> &mut Self
350	where
351		F: Fn() + 'static,
352	{
353		let cancel = Box::new(cancel);
354		self.cancel = Some(cancel);
355
356		self
357	}
358
359	fn mk_less(&self) -> Option<u16> {
360		if !self.less {
361			return None;
362		}
363
364		if let Some(less) = self.less_amt {
365			let is_less = self.options.len() > less as usize;
366			is_less.then_some(less)
367		} else if let Ok((_, rows)) = crossterm::terminal::size() {
368			let len = self.options.len();
369			let rows = rows.saturating_sub(4);
370			let rows = self.less_max.map_or(rows, |max| u16::min(rows, max));
371
372			let is_less = rows > 0 && len > rows as usize;
373			is_less.then_some(rows)
374		} else {
375			None
376		}
377	}
378
379	/// Wait for the user to submit an option.
380	///
381	/// # Examples
382	///
383	/// ```no_run
384	/// use may_clack::select;
385	///
386	/// # fn main() -> Result<(), may_clack::error::ClackError> {
387	/// let answer = select("select")
388	///     .option("val1", "value 1")
389	///     .option("val2", "value 2")
390	///     .option_hint("val 3", "value 3", "hint")
391	///     .interact()?;
392	/// println!("answer {:?}", answer);
393	/// # Ok(())
394	/// # }
395	/// ```
396	pub fn interact(&self) -> Result<T, ClackError> {
397		if self.options.is_empty() {
398			return Err(ClackError::NoOptions);
399		}
400
401		let max = self.options.len();
402		let is_less = self.mk_less();
403
404		let mut idx = 0;
405		let mut less_idx: u16 = 0;
406
407		if let Some(less) = is_less {
408			self.w_init_less(less);
409		} else {
410			self.w_init();
411		}
412
413		terminal::enable_raw_mode()?;
414
415		loop {
416			if let Event::Key(key) = event::read()? {
417				if key.kind == KeyEventKind::Press {
418					match (key.code, key.modifiers) {
419						(KeyCode::Up | KeyCode::Left, _) => {
420							if let Some(less) = is_less {
421								let prev_less = less_idx;
422
423								if idx > 0 {
424									idx -= 1;
425									less_idx = less_idx.saturating_sub(1);
426								} else {
427									idx = max - 1;
428									less_idx = less - 1;
429								}
430
431								self.draw_less(less, idx, less_idx, prev_less);
432							} else {
433								self.draw_unfocus(idx);
434								let mut stdout = stdout();
435
436								if idx > 0 {
437									idx -= 1;
438									let _ = execute!(stdout, cursor::MoveUp(1));
439								} else if max > 1 {
440									idx = max - 1;
441									let _ = execute!(stdout, cursor::MoveDown(max as u16 - 1));
442								}
443
444								self.draw_focus(idx);
445							}
446						}
447						(KeyCode::Down | KeyCode::Right, _) => {
448							if let Some(less) = is_less {
449								let prev_less = less_idx;
450
451								if idx < max - 1 {
452									idx += 1;
453									if less_idx < less - 1 {
454										less_idx += 1;
455									}
456								} else {
457									idx = 0;
458									less_idx = 0;
459								}
460
461								self.draw_less(less, idx, less_idx, prev_less);
462							} else {
463								self.draw_unfocus(idx);
464								let mut stdout = stdout();
465
466								if idx < max - 1 {
467									idx += 1;
468									let _ = execute!(stdout, cursor::MoveDown(1));
469								} else if idx > 0 {
470									idx = 0;
471									let _ = execute!(stdout, cursor::MoveUp(max as u16 - 1));
472								}
473
474								self.draw_focus(idx);
475							}
476						}
477						(KeyCode::PageDown, _) => {
478							if let Some(less) = is_less {
479								let prev_less = less_idx;
480
481								if idx + less as usize >= max - 1 {
482									less_idx = less - 1;
483									idx = max - 1;
484								} else {
485									idx += less as usize;
486
487									if max - idx < (less - less_idx) as usize {
488										less_idx = less - (max - idx) as u16;
489									}
490								}
491
492								self.draw_less(less, idx, less_idx, prev_less);
493							}
494						}
495						(KeyCode::PageUp, _) if idx != 0 => {
496							if let Some(less) = is_less {
497								let prev_less = less_idx;
498
499								if idx <= less as usize {
500									less_idx = 0;
501									idx = 0;
502								} else {
503									idx -= less as usize;
504									less_idx = prev_less.min(idx as u16);
505								}
506
507								self.draw_less(less, idx, less_idx, prev_less);
508							}
509						}
510						(KeyCode::Home, _) if idx != 0 => {
511							if let Some(less) = is_less {
512								let prev_less = less_idx;
513
514								idx = 0;
515								less_idx = 0;
516
517								self.draw_less(less, idx, less_idx, prev_less);
518							} else {
519								self.draw_unfocus(idx);
520
521								let mut stdout = stdout();
522								let _ = execute!(stdout, cursor::MoveUp(idx as u16));
523
524								idx = 0;
525								self.draw_focus(0);
526							}
527						}
528						(KeyCode::End, _) if idx != max - 1 => {
529							if let Some(less) = is_less {
530								let prev_less = less_idx;
531
532								idx = max - 1;
533								less_idx = less - 1;
534
535								self.draw_less(less, idx, less_idx, prev_less);
536							} else {
537								self.draw_unfocus(idx);
538
539								let mut stdout = stdout();
540								let diff = max - idx - 1;
541								let _ = execute!(stdout, cursor::MoveDown(diff as u16));
542
543								idx = max - 1;
544
545								self.draw_focus(idx);
546							}
547						}
548						(KeyCode::Enter, _) => {
549							terminal::disable_raw_mode()?;
550
551							if let Some(less) = is_less {
552								self.w_out_less(less, idx, less_idx);
553							} else {
554								self.w_out(idx);
555							}
556
557							let opt = self
558								.options
559								.get(idx)
560								.expect("idx should always be in bound");
561							let value = opt.value.clone();
562							return Ok(value);
563						}
564						(KeyCode::Char('c' | 'd'), KeyModifiers::CONTROL) => {
565							terminal::disable_raw_mode()?;
566
567							if let Some(less) = is_less {
568								self.w_cancel_less(less, idx, less_idx);
569							} else {
570								self.w_cancel(idx);
571							}
572
573							if let Some(cancel) = self.cancel.as_deref() {
574								cancel();
575							}
576
577							return Err(ClackError::Cancelled);
578						}
579						_ => {}
580					}
581				}
582			}
583		}
584	}
585}
586
587impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
588	fn draw_focus(&self, idx: usize) {
589		let opt = self
590			.options
591			.get(idx)
592			.expect("idx should always be in bound");
593		let line = opt.focus();
594		self.draw(&line);
595	}
596
597	fn draw_unfocus(&self, idx: usize) {
598		let opt = self
599			.options
600			.get(idx)
601			.expect("idx should always be in bound");
602		let line = opt.unfocus();
603		self.draw(&line);
604	}
605
606	fn draw(&self, line: &str) {
607		let mut stdout = stdout();
608		let _ = execute!(stdout, cursor::MoveToColumn(0));
609
610		print!("{}", ansi::CLEAR_LINE);
611		print!("{}  {}", (*chars::BAR).cyan(), line);
612		let _ = stdout.flush();
613	}
614
615	fn draw_less(&self, less: u16, idx: usize, less_idx: u16, prev_less: u16) {
616		let mut stdout = stdout();
617		if prev_less > 0 {
618			let _ = execute!(stdout, cursor::MoveToPreviousLine(prev_less));
619		} else {
620			let _ = execute!(stdout, cursor::MoveToColumn(0));
621		}
622
623		for i in 0..less.into() {
624			let i_idx = idx + i - less_idx as usize;
625			let opt = self
626				.options
627				.get(i_idx)
628				.expect("i_idx should always be in bound");
629			let line = opt.unfocus();
630
631			print!("{}", ansi::CLEAR_LINE);
632			println!("{}  {}\r", (*chars::BAR).cyan(), line);
633
634			let _ = execute!(stdout, cursor::MoveToColumn(0));
635		}
636
637		let max = self.options.len();
638		let amt = max.to_string().len();
639		print!("{}", ansi::CLEAR_LINE);
640		println!(
641			"{}  ......... ({:#0amt$}/{})",
642			(*chars::BAR).cyan(),
643			idx + 1,
644			max,
645			amt = amt
646		);
647
648		let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
649		if less_idx > 0 {
650			let _ = execute!(stdout, cursor::MoveToNextLine(less_idx));
651		}
652
653		self.draw_focus(idx);
654	}
655}
656
657impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
658	fn w_init(&self) {
659		let mut stdout = stdout();
660
661		println!("{}", *chars::BAR);
662		println!("{}  {}", (*chars::STEP_ACTIVE).cyan(), self.message);
663
664		for opt in &self.options {
665			let line = opt.unfocus();
666			println!("{}  {}", (*chars::BAR).cyan(), line);
667		}
668
669		print!("{}", (*chars::BAR_END).cyan());
670
671		let len = self.options.len() as u16;
672		let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
673
674		self.draw_focus(0);
675	}
676
677	fn w_init_less(&self, less: u16) {
678		println!("{}", *chars::BAR);
679		println!("{}  {}", (*chars::STEP_ACTIVE).cyan(), self.message);
680
681		self.draw_less(less, 0, 0, 0);
682
683		let mut stdout = stdout();
684		let _ = execute!(stdout, cursor::MoveToNextLine(less));
685
686		println!();
687		print!("{}", (*chars::BAR_END).cyan());
688
689		let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
690
691		self.draw_focus(0);
692	}
693
694	fn w_cancel(&self, idx: usize) {
695		let mut stdout = stdout();
696		let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
697
698		println!("{}  {}", (*chars::STEP_CANCEL).red(), self.message);
699
700		for _ in &self.options {
701			println!("{}", ansi::CLEAR_LINE);
702		}
703		print!("{}", ansi::CLEAR_LINE);
704
705		let len = self.options.len() as u16;
706		let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
707
708		let label = &self
709			.options
710			.get(idx)
711			.expect("idx should always be in bound")
712			.label;
713		println!("{}  {}", *chars::BAR, label.strikethrough().dimmed());
714	}
715
716	fn w_cancel_less(&self, less: u16, idx: usize, less_idx: u16) {
717		let mut stdout = stdout();
718		if less_idx > 0 {
719			let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
720		} else {
721			let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
722		}
723
724		println!("{}  {}", (*chars::STEP_CANCEL).red(), self.message);
725
726		for _ in 0..less.into() {
727			println!("{}", ansi::CLEAR_LINE);
728		}
729
730		println!("{}", ansi::CLEAR_LINE);
731		println!("{}", ansi::CLEAR_LINE);
732
733		let mv = less + 2;
734		let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
735
736		let label = &self
737			.options
738			.get(idx)
739			.expect("idx should always be in bound")
740			.label;
741		println!("{}  {}", *chars::BAR, label.strikethrough().dimmed());
742	}
743
744	fn w_out(&self, idx: usize) {
745		let mut stdout = stdout();
746		let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
747
748		println!("{}  {}", (*chars::STEP_SUBMIT).green(), self.message);
749
750		for _ in &self.options {
751			println!("{}", ansi::CLEAR_LINE);
752		}
753		print!("{}", ansi::CLEAR_LINE);
754
755		let len = self.options.len() as u16;
756		let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
757
758		let label = &self
759			.options
760			.get(idx)
761			.expect("idx should always be in bound")
762			.label;
763		println!("{}  {}", *chars::BAR, label.dimmed());
764	}
765
766	fn w_out_less(&self, less: u16, idx: usize, less_idx: u16) {
767		let mut stdout = stdout();
768		if less_idx > 0 {
769			let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
770		} else {
771			let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
772		}
773
774		println!("{}  {}", (*chars::STEP_SUBMIT).green(), self.message);
775
776		for _ in 0..less.into() {
777			println!("{}", ansi::CLEAR_LINE);
778		}
779
780		println!("{}", ansi::CLEAR_LINE);
781		println!("{}", ansi::CLEAR_LINE);
782
783		let mv = less + 2;
784		let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
785
786		let label = &self
787			.options
788			.get(idx)
789			.expect("idx should always be in bound")
790			.label;
791		println!("{}  {}", *chars::BAR, label.dimmed());
792	}
793}
794
795/// Shorthand for [`Select::new()`]
796pub fn select<M: Display, T: Clone, O: Display>(message: M) -> Select<M, T, O> {
797	Select::new(message)
798}