Skip to main content

may_clack/
select.rs

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