1use crate::output::hook;
2use crate::output::themes::current_theme;
3use crossterm::style::{Color, Stylize};
4use crossterm::{
5 cursor,
6 event::{self, Event, KeyCode},
7 execute, terminal,
8};
9use std::collections::HashSet;
10use std::io::{stdin, stdout, Write};
11
12type TextValidator = dyn Fn(&str) -> Result<(), String> + Send + Sync;
14type NumberValidator = dyn Fn(f64) -> Result<(), String> + Send + Sync;
15
16pub struct TextInput<'a> {
17 label: &'a str,
18 default: Option<String>,
19 required: bool,
20 min_len: Option<usize>,
21 max_len: Option<usize>,
22 validator: Option<Box<TextValidator>>,
23 mask: Option<char>,
24}
25
26impl<'a> TextInput<'a> {
27 pub fn default(mut self, v: impl Into<String>) -> Self {
28 self.default = Some(v.into());
29 self
30 }
31 pub fn required(mut self) -> Self {
32 self.required = true;
33 self
34 }
35 pub fn min_len(mut self, n: usize) -> Self {
36 self.min_len = Some(n);
37 self
38 }
39 pub fn max_len(mut self, n: usize) -> Self {
40 self.max_len = Some(n);
41 self
42 }
43 pub fn validate<F>(mut self, f: F) -> Self
44 where
45 F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
46 {
47 self.validator = Some(Box::new(f));
48 self
49 }
50 pub fn mask(mut self, ch: char) -> Self {
51 self.mask = Some(ch);
52 self
53 }
54
55 pub fn get(self) -> Result<String, String> {
56 loop {
57 let hint = self
58 .default
59 .as_deref()
60 .map(|d| format!(" [{d}]"))
61 .unwrap_or_default();
62 print!("{}{} ", self.label, hint);
63 if let Err(e) = stdout().flush() {
64 hook::warn(&format!("flush failed: {e}"));
65 }
66
67 let input = if self.mask.is_some() {
68 let mut s = String::new();
70 if let Err(e) = stdin().read_line(&mut s) {
71 hook::error(&format!("failed to read: {e}"));
72 return Err("read error".into());
73 }
74 s
75 } else {
76 let mut s = String::new();
77 if let Err(e) = stdin().read_line(&mut s) {
78 hook::error(&format!("failed to read: {e}"));
79 return Err("read error".into());
80 }
81 s
82 };
83 let trimmed = input.trim();
84 let value = if trimmed.is_empty() {
85 self.default.as_deref().unwrap_or("")
86 } else {
87 trimmed
88 };
89
90 if self.required && value.is_empty() {
91 hook::warn("Value required");
92 continue;
93 }
94 if let Some(n) = self.min_len {
95 if value.chars().count() < n {
96 hook::warn(&format!("Min length {n}"));
97 continue;
98 }
99 }
100 if let Some(n) = self.max_len {
101 if value.chars().count() > n {
102 hook::warn(&format!("Max length {n}"));
103 continue;
104 }
105 }
106 if let Some(v) = &self.validator {
107 if let Err(msg) = v(value) {
108 hook::warn(&msg);
109 continue;
110 }
111 }
112
113 return Ok(value.to_string());
114 }
115 }
116}
117
118pub fn text(label: &str) -> TextInput<'_> {
119 TextInput {
120 label,
121 default: None,
122 required: false,
123 min_len: None,
124 max_len: None,
125 validator: None,
126 mask: None,
127 }
128}
129
130pub struct NumberInput<'a> {
131 label: &'a str,
132 default: Option<f64>,
133 min: Option<f64>,
134 max: Option<f64>,
135 step: f64,
136 validator: Option<Box<NumberValidator>>,
137}
138
139impl<'a> NumberInput<'a> {
140 pub fn default(mut self, v: f64) -> Self {
141 self.default = Some(v);
142 self
143 }
144 pub fn min(mut self, v: f64) -> Self {
145 self.min = Some(v);
146 self
147 }
148 pub fn max(mut self, v: f64) -> Self {
149 self.max = Some(v);
150 self
151 }
152 pub fn step(mut self, v: f64) -> Self {
153 self.step = v;
154 self
155 }
156 pub fn validate<F>(mut self, f: F) -> Self
157 where
158 F: Fn(f64) -> Result<(), String> + Send + Sync + 'static,
159 {
160 self.validator = Some(Box::new(f));
161 self
162 }
163
164 pub fn get(self) -> Result<f64, String> {
165 loop {
166 let hint = self
167 .default
168 .map(|d| format!(" [{}]", d))
169 .unwrap_or_default();
170 print!("{}{} ", self.label, hint);
171 if let Err(e) = stdout().flush() {
172 hook::warn(&format!("flush failed: {e}"));
173 }
174
175 let mut s = String::new();
176 if let Err(e) = stdin().read_line(&mut s) {
177 hook::error(&format!("failed to read: {e}"));
178 return Err("read error".into());
179 }
180 let trimmed = s.trim();
181 let value = if trimmed.is_empty() {
182 self.default.unwrap_or(0.0)
183 } else {
184 match trimmed.parse::<f64>() {
185 Ok(v) => v,
186 Err(_) => {
187 hook::warn("Enter a valid number");
188 continue;
189 }
190 }
191 };
192
193 if let Some(min) = self.min {
194 if value < min {
195 hook::warn(&format!("Min {min}"));
196 continue;
197 }
198 }
199 if let Some(max) = self.max {
200 if value > max {
201 hook::warn(&format!("Max {max}"));
202 continue;
203 }
204 }
205 if let Some(v) = &self.validator {
206 if let Err(msg) = v(value) {
207 hook::warn(&msg);
208 continue;
209 }
210 }
211 return Ok(value);
212 }
213 }
214}
215
216pub fn number(label: &str) -> NumberInput<'_> {
217 NumberInput {
218 label,
219 default: None,
220 min: None,
221 max: None,
222 step: 1.0,
223 validator: None,
224 }
225}
226
227pub struct ConfirmInput<'a> {
228 label: &'a str,
229 default_yes: bool,
230}
231impl<'a> ConfirmInput<'a> {
232 pub fn default_yes(mut self) -> Self {
233 self.default_yes = true;
234 self
235 }
236 pub fn default_no(mut self) -> Self {
237 self.default_yes = false;
238 self
239 }
240 pub fn get(self) -> bool {
241 let hint = if self.default_yes { "[Y/n]" } else { "[y/N]" };
242 print!("{} {} ", self.label, hint);
243 if let Err(e) = stdout().flush() {
244 hook::warn(&format!("flush failed: {e}"));
245 }
246 let mut s = String::new();
247 if let Err(e) = stdin().read_line(&mut s) {
248 hook::warn(&format!("failed to read: {e}"));
249 return self.default_yes;
250 }
251 match s.trim().to_lowercase().as_str() {
252 "y" | "yes" => true,
253 "n" | "no" => false,
254 "" => self.default_yes,
255 _ => self.default_yes,
256 }
257 }
258}
259
260pub fn confirm(label: &str) -> ConfirmInput<'_> {
261 ConfirmInput {
262 label,
263 default_yes: true,
264 }
265}
266
267pub struct SelectInput<'a> {
270 label: &'a str,
271 items: Vec<String>,
272 initial: Option<usize>,
273}
274
275impl<'a> SelectInput<'a> {
276 pub fn initial(mut self, idx: usize) -> Self {
277 self.initial = Some(idx);
278 self
279 }
280 pub fn get(self) -> Result<usize, String> {
281 println!("{}", self.label);
282 for (i, it) in self.items.iter().enumerate() {
283 println!(" {}. {}", i + 1, it);
284 }
285 print!(
286 "Enter choice [1-{}]{}: ",
287 self.items.len(),
288 self.initial
289 .map(|i| format!(" (default {})", i + 1))
290 .unwrap_or_default()
291 );
292 if let Err(e) = stdout().flush() {
293 hook::warn(&format!("flush failed: {e}"));
294 }
295 let mut s = String::new();
296 if let Err(e) = stdin().read_line(&mut s) {
297 return Err(format!("read error: {e}"));
298 }
299 let trimmed = s.trim();
300 if trimmed.is_empty() {
301 if let Some(i) = self.initial {
302 return Ok(i);
303 }
304 }
305 match trimmed.parse::<usize>() {
306 Ok(n) if n >= 1 && n <= self.items.len() => Ok(n - 1),
307 _ => Err("invalid selection".into()),
308 }
309 }
310}
311
312pub fn select(label: &str, items: impl IntoIterator<Item = impl Into<String>>) -> SelectInput<'_> {
313 SelectInput {
314 label,
315 items: items.into_iter().map(Into::into).collect(),
316 initial: None,
317 }
318}
319
320pub struct MultiSelectInput<'a> {
321 label: &'a str,
322 items: Vec<String>,
323}
324
325impl<'a> MultiSelectInput<'a> {
326 pub fn get(self) -> Result<Vec<usize>, String> {
327 println!("{}", self.label);
328 for (i, it) in self.items.iter().enumerate() {
329 println!(" {}. {}", i + 1, it);
330 }
331 println!("Enter comma-separated indexes (e.g., 1,3,4) or empty for none:");
332 print!("> ");
333 if let Err(e) = stdout().flush() {
334 hook::warn(&format!("flush failed: {e}"));
335 }
336 let mut s = String::new();
337 if let Err(e) = stdin().read_line(&mut s) {
338 return Err(format!("read error: {e}"));
339 }
340 let trimmed = s.trim();
341 if trimmed.is_empty() {
342 return Ok(Vec::new());
343 }
344 let mut out = Vec::new();
345 for part in trimmed.split(',') {
346 let t = part.trim();
347 if let Ok(n) = t.parse::<usize>() {
348 if n >= 1 && n <= self.items.len() {
349 out.push(n - 1);
350 }
351 }
352 }
353 Ok(out)
354 }
355}
356
357pub fn multi_select(
358 label: &str,
359 items: impl IntoIterator<Item = impl Into<String>>,
360) -> MultiSelectInput<'_> {
361 MultiSelectInput {
362 label,
363 items: items.into_iter().map(Into::into).collect(),
364 }
365}
366
367pub struct ButtonsInput<'a> {
368 buttons: Vec<(String, char)>,
369 default: Option<usize>,
370 label: &'a str,
371}
372
373impl<'a> ButtonsInput<'a> {
374 pub fn default(mut self, idx: usize) -> Self {
375 self.default = Some(idx);
376 self
377 }
378 pub fn get(self) -> usize {
379 println!("{}", self.label);
380 println!(
381 "{}",
382 self.buttons
383 .iter()
384 .map(|(t, k)| format!("[{}] {}", k, t))
385 .collect::<Vec<_>>()
386 .join(" ")
387 );
388 let default_hint = self
389 .default
390 .and_then(|i| {
391 self.buttons
392 .get(i)
393 .map(|(_, k)| format!(" (default {})", k))
394 })
395 .unwrap_or_default();
396 print!("Choose by hotkey{}: ", default_hint);
397 if let Err(e) = stdout().flush() {
398 hook::warn(&format!("flush failed: {e}"));
399 }
400 let mut s = String::new();
401 if let Err(e) = stdin().read_line(&mut s) {
402 hook::warn(&format!("read error: {e}"));
403 return self.default.unwrap_or(0);
404 }
405 let ch = s.trim().chars().next();
406 if let Some(c) = ch {
407 if let Some((idx, _)) = self.buttons.iter().enumerate().find(|(_, (_, k))| *k == c) {
408 return idx;
409 }
410 }
411 self.default.unwrap_or(0)
412 }
413}
414
415pub fn buttons(
416 label: &str,
417 buttons: impl IntoIterator<Item = (impl Into<String>, char)>,
418) -> ButtonsInput<'_> {
419 ButtonsInput {
420 label,
421 buttons: buttons.into_iter().map(|(t, k)| (t.into(), k)).collect(),
422 default: None,
423 }
424}
425
426#[derive(Clone)]
430pub struct KeyMap {
431 pub up: KeyCode,
432 pub down: KeyCode,
433 pub left: KeyCode,
434 pub right: KeyCode,
435 pub page_up: KeyCode,
436 pub page_down: KeyCode,
437 pub home: KeyCode,
438 pub end: KeyCode,
439 pub confirm: KeyCode,
440 pub cancel: KeyCode,
441 pub backspace: KeyCode,
442 pub toggle_char: char, }
444
445impl Default for KeyMap {
446 fn default() -> Self {
447 Self {
448 up: KeyCode::Up,
449 down: KeyCode::Down,
450 left: KeyCode::Left,
451 right: KeyCode::Right,
452 page_up: KeyCode::PageUp,
453 page_down: KeyCode::PageDown,
454 home: KeyCode::Home,
455 end: KeyCode::End,
456 confirm: KeyCode::Enter,
457 cancel: KeyCode::Esc,
458 backspace: KeyCode::Backspace,
459 toggle_char: ' ',
460 }
461 }
462}
463
464pub struct RawSelectInput<'a> {
465 label: &'a str,
466 items: Vec<String>,
467 initial: usize,
468 keymap: KeyMap,
469}
470
471impl<'a> RawSelectInput<'a> {
472 pub fn initial(mut self, idx: usize) -> Self {
473 self.initial = idx.min(self.items.len().saturating_sub(1));
474 self
475 }
476 pub fn keymap(mut self, km: KeyMap) -> Self {
477 self.keymap = km;
478 self
479 }
480 pub fn get(self) -> Option<usize> {
481 let mut stdout = stdout();
482 let mut selected = self.initial;
483 if terminal::enable_raw_mode().is_err() {
484 return None;
485 }
486 let _ = execute!(stdout, terminal::Clear(terminal::ClearType::All));
487 loop {
488 let _ = execute!(stdout, cursor::MoveTo(0, 0));
489 println!("{}\n", self.label);
490 for (i, it) in self.items.iter().enumerate() {
491 if i == selected {
492 println!(" > {}", it);
493 } else {
494 println!(" {}", it);
495 }
496 }
497 let _ = stdout.flush();
498 if let Ok(Event::Key(k)) = event::read() {
499 match k.code {
500 c if c == self.keymap.up => {
501 selected = selected.saturating_sub(1);
502 }
503 c if c == self.keymap.down => {
504 if selected + 1 < self.items.len() {
505 selected += 1;
506 }
507 }
508 c if c == self.keymap.confirm => {
509 let _ = terminal::disable_raw_mode();
510 return Some(selected);
511 }
512 c if c == self.keymap.cancel => {
513 let _ = terminal::disable_raw_mode();
514 return None;
515 }
516 _ => {}
517 }
518 }
519 }
520 }
521}
522
523pub fn raw_select(
524 label: &str,
525 items: impl IntoIterator<Item = impl Into<String>>,
526) -> RawSelectInput<'_> {
527 RawSelectInput {
528 label,
529 items: items.into_iter().map(Into::into).collect(),
530 initial: 0,
531 keymap: KeyMap::default(),
532 }
533}
534
535pub struct RawMultiSelectInput<'a> {
536 label: &'a str,
537 items: Vec<String>,
538 initial: usize,
539 keymap: KeyMap,
540}
541
542impl<'a> RawMultiSelectInput<'a> {
543 pub fn initial(mut self, idx: usize) -> Self {
544 self.initial = idx.min(self.items.len().saturating_sub(1));
545 self
546 }
547 pub fn keymap(mut self, km: KeyMap) -> Self {
548 self.keymap = km;
549 self
550 }
551 pub fn get(self) -> Option<Vec<usize>> {
552 let mut stdout = stdout();
553 let mut cursor_idx = self.initial;
554 let mut picked: Vec<bool> = vec![false; self.items.len()];
555 if terminal::enable_raw_mode().is_err() {
556 return None;
557 }
558 let _ = execute!(stdout, terminal::Clear(terminal::ClearType::All));
559 loop {
560 let _ = execute!(stdout, cursor::MoveTo(0, 0));
561 println!(
562 "{}\n(space=toggle, enter=confirm, esc=cancel)\n",
563 self.label
564 );
565 for (i, it) in self.items.iter().enumerate() {
566 let mark = if picked[i] { "[x]" } else { "[ ]" };
567 if i == cursor_idx {
568 println!(" > {} {}", mark, it);
569 } else {
570 println!(" {} {}", mark, it);
571 }
572 }
573 let _ = stdout.flush();
574 if let Ok(Event::Key(k)) = event::read() {
575 match k.code {
576 c if c == self.keymap.up => {
577 cursor_idx = cursor_idx.saturating_sub(1);
578 }
579 c if c == self.keymap.down => {
580 if cursor_idx + 1 < self.items.len() {
581 cursor_idx += 1;
582 }
583 }
584 KeyCode::Char(c) if c == self.keymap.toggle_char => {
585 if let Some(p) = picked.get_mut(cursor_idx) {
586 *p = !*p;
587 }
588 }
589 c if c == self.keymap.confirm => {
590 let _ = terminal::disable_raw_mode();
591 let res: Vec<usize> = picked
592 .iter()
593 .enumerate()
594 .filter_map(|(i, b)| if *b { Some(i) } else { None })
595 .collect();
596 return Some(res);
597 }
598 c if c == self.keymap.cancel => {
599 let _ = terminal::disable_raw_mode();
600 return None;
601 }
602 _ => {}
603 }
604 }
605 }
606 }
607}
608
609pub fn raw_multi_select(
610 label: &str,
611 items: impl IntoIterator<Item = impl Into<String>>,
612) -> RawMultiSelectInput<'_> {
613 RawMultiSelectInput {
614 label,
615 items: items.into_iter().map(Into::into).collect(),
616 initial: 0,
617 keymap: KeyMap::default(),
618 }
619}
620
621pub struct RawPagedSelectInput<'a> {
624 label: &'a str,
625 items: Vec<String>,
626 cursor: usize,
627 page_size: usize,
628 keymap: KeyMap,
629}
630
631impl<'a> RawPagedSelectInput<'a> {
632 pub fn initial(mut self, idx: usize) -> Self {
633 self.cursor = idx.min(self.items.len().saturating_sub(1));
634 self
635 }
636 pub fn page_size(mut self, n: usize) -> Self {
637 self.page_size = n.max(1);
638 self
639 }
640 pub fn keymap(mut self, km: KeyMap) -> Self {
641 self.keymap = km;
642 self
643 }
644 pub fn get(mut self) -> Option<usize> {
645 if self.items.is_empty() {
646 return None;
647 }
648 let mut stdout = stdout();
649 if terminal::enable_raw_mode().is_err() {
650 return None;
651 }
652 let _ = execute!(stdout, terminal::Clear(terminal::ClearType::All));
653 let mut query = String::new();
654 loop {
655 let ql = query.to_lowercase();
657 let filtered: Vec<usize> = self
658 .items
659 .iter()
660 .enumerate()
661 .filter(|(_, s)| s.to_lowercase().contains(&ql))
662 .map(|(i, _)| i)
663 .collect();
664 if filtered.is_empty() {
665 self.cursor = 0;
666 } else if self.cursor >= filtered.len() {
667 self.cursor = filtered.len() - 1;
668 }
669 let total_pages = filtered.len().div_ceil(self.page_size);
670 let page = if filtered.is_empty() {
671 0
672 } else {
673 self.cursor / self.page_size
674 };
675 let start = page * self.page_size;
676 let end = (start + self.page_size).min(filtered.len());
677
678 let _ = execute!(stdout, cursor::MoveTo(0, 0));
679 println!("{}\n", self.label);
680 println!("Search: {}\n", query.as_str().with(Color::DarkGrey));
681 println!(
682 "Page {}/{}\n",
683 if total_pages == 0 { 0 } else { page + 1 },
684 total_pages
685 );
686 let theme = current_theme();
687 let sel_bg = theme.get_log_color("menu_selected_bg");
688 let sel_fg = theme.get_log_color("menu_selected_fg");
689 let stripe_fg = theme.get_log_color("menu_stripe_fg");
690 for (row, fi) in (start..end).enumerate().map(|(row, i)| (row, filtered[i])) {
691 let label = &self.items[fi];
692 if (start + row) == self.cursor {
693 let line = format!(" > {}", label).with(sel_fg).on(sel_bg).bold();
694 println!("{}", line);
695 } else if row % 2 == 1 {
696 println!("{}", format!(" {}", label).with(stripe_fg));
697 } else {
698 println!(" {}", label);
699 }
700 }
701 println!(
702 "\nKeys: ↑/↓ PgUp/PgDn Home/End Enter Esc (type to search, Backspace clears)"
703 );
704 let _ = stdout.flush();
705
706 if let Ok(Event::Key(k)) = event::read() {
707 match k.code {
708 c if c == self.keymap.up => {
709 self.cursor = self.cursor.saturating_sub(1);
710 }
711 c if c == self.keymap.down => {
712 if !filtered.is_empty() && self.cursor + 1 < filtered.len() {
713 self.cursor += 1;
714 }
715 }
716 c if c == self.keymap.page_up => {
717 if self.cursor >= self.page_size {
718 self.cursor -= self.page_size;
719 } else {
720 self.cursor = 0;
721 }
722 }
723 c if c == self.keymap.page_down => {
724 if !filtered.is_empty() && self.cursor + self.page_size < filtered.len() {
725 self.cursor += self.page_size;
726 } else {
727 self.cursor = filtered.len().saturating_sub(1);
728 }
729 }
730 c if c == self.keymap.home => {
731 self.cursor = 0;
732 }
733 c if c == self.keymap.end => {
734 self.cursor = filtered.len().saturating_sub(1);
735 }
736 c if c == self.keymap.confirm => {
737 let _ = terminal::disable_raw_mode();
738 return filtered.get(self.cursor).copied();
739 }
740 c if c == self.keymap.cancel => {
741 let _ = terminal::disable_raw_mode();
742 return None;
743 }
744 c if c == self.keymap.backspace => {
745 query.pop();
746 }
747 KeyCode::Char(c) => {
748 query.push(c);
749 }
750 _ => {}
751 }
752 }
753 }
754 }
755}
756
757pub fn raw_select_paged(
758 label: &str,
759 items: impl IntoIterator<Item = impl Into<String>>,
760) -> RawPagedSelectInput<'_> {
761 RawPagedSelectInput {
762 label,
763 items: items.into_iter().map(Into::into).collect(),
764 cursor: 0,
765 page_size: 10,
766 keymap: KeyMap::default(),
767 }
768}
769
770pub struct RawPagedMultiSelectInput<'a> {
771 label: &'a str,
772 items: Vec<String>,
773 cursor: usize,
774 page_size: usize,
775 picked: Vec<bool>,
776 keymap: KeyMap,
777}
778
779impl<'a> RawPagedMultiSelectInput<'a> {
780 pub fn initial(mut self, idx: usize) -> Self {
781 self.cursor = idx.min(self.items.len().saturating_sub(1));
782 self
783 }
784 pub fn page_size(mut self, n: usize) -> Self {
785 self.page_size = n.max(1);
786 self
787 }
788 pub fn keymap(mut self, km: KeyMap) -> Self {
789 self.keymap = km;
790 self
791 }
792 pub fn get(mut self) -> Option<Vec<usize>> {
793 if self.items.is_empty() {
794 return Some(Vec::new());
795 }
796 let mut stdout = stdout();
797 if terminal::enable_raw_mode().is_err() {
798 return None;
799 }
800 let _ = execute!(stdout, terminal::Clear(terminal::ClearType::All));
801 let mut query = String::new();
802 loop {
803 let ql = query.to_lowercase();
805 let filtered: Vec<usize> = self
806 .items
807 .iter()
808 .enumerate()
809 .filter(|(_, s)| s.to_lowercase().contains(&ql))
810 .map(|(i, _)| i)
811 .collect();
812 if filtered.is_empty() {
813 self.cursor = 0;
814 } else if self.cursor >= filtered.len() {
815 self.cursor = filtered.len() - 1;
816 }
817 let total_pages = filtered.len().div_ceil(self.page_size);
818 let page = if filtered.is_empty() {
819 0
820 } else {
821 self.cursor / self.page_size
822 };
823 let start = page * self.page_size;
824 let end = (start + self.page_size).min(filtered.len());
825
826 let _ = execute!(stdout, cursor::MoveTo(0, 0));
827 println!("{}\n(space=toggle)\n", self.label);
828 println!("Search: {}\n", query.as_str().with(Color::DarkGrey));
829 println!(
830 "Page {}/{}\n",
831 if total_pages == 0 { 0 } else { page + 1 },
832 total_pages
833 );
834 let theme = current_theme();
835 let sel_bg = theme.get_log_color("menu_selected_bg");
836 let sel_fg = theme.get_log_color("menu_selected_fg");
837 let stripe_fg = theme.get_log_color("menu_stripe_fg");
838 for (row, fi) in (start..end).enumerate().map(|(row, i)| (row, filtered[i])) {
839 let mark = if *self.picked.get(fi).unwrap_or(&false) {
840 "[x]"
841 } else {
842 "[ ]"
843 };
844 if (start + row) == self.cursor {
845 let line = format!(" > {} {}", mark, self.items[fi])
846 .with(sel_fg)
847 .on(sel_bg)
848 .bold();
849 println!("{}", line);
850 } else if row % 2 == 1 {
851 println!(
852 "{}",
853 format!(" {} {}", mark, self.items[fi]).with(stripe_fg)
854 );
855 } else {
856 println!(" {} {}", mark, self.items[fi]);
857 }
858 }
859 println!("\nKeys: ↑/↓ PgUp/PgDn Home/End Space Enter Esc (type to search, Backspace clears)");
860 let _ = stdout.flush();
861
862 if let Ok(Event::Key(k)) = event::read() {
863 match k.code {
864 c if c == self.keymap.up => {
865 self.cursor = self.cursor.saturating_sub(1);
866 }
867 c if c == self.keymap.down => {
868 if !filtered.is_empty() && self.cursor + 1 < filtered.len() {
869 self.cursor += 1;
870 }
871 }
872 c if c == self.keymap.page_up => {
873 if self.cursor >= self.page_size {
874 self.cursor -= self.page_size;
875 } else {
876 self.cursor = 0;
877 }
878 }
879 c if c == self.keymap.page_down => {
880 if !filtered.is_empty() && self.cursor + self.page_size < filtered.len() {
881 self.cursor += 1;
882 } else {
883 self.cursor = filtered.len().saturating_sub(1);
884 }
885 }
886 c if c == self.keymap.home => {
887 self.cursor = 0;
888 }
889 c if c == self.keymap.end => {
890 self.cursor = filtered.len().saturating_sub(1);
891 }
892 KeyCode::Char(c1) if c1 == self.keymap.toggle_char => {
893 if let Some(fi) = filtered.get(self.cursor) {
894 if let Some(p) = self.picked.get_mut(*fi) {
895 *p = !*p;
896 }
897 }
898 }
899 c if c == self.keymap.confirm => {
900 let _ = terminal::disable_raw_mode();
901 let res: Vec<usize> = self
902 .picked
903 .iter()
904 .enumerate()
905 .filter_map(|(i, b)| if *b { Some(i) } else { None })
906 .collect();
907 return Some(res);
908 }
909 c if c == self.keymap.cancel => {
910 let _ = terminal::disable_raw_mode();
911 return None;
912 }
913 c if c == self.keymap.backspace => {
914 query.pop();
915 }
916 KeyCode::Char(c) => {
917 query.push(c);
918 }
919 _ => {}
920 }
921 }
922 }
923 }
924}
925
926pub fn raw_multi_select_paged(
927 label: &str,
928 items: impl IntoIterator<Item = impl Into<String>>,
929) -> RawPagedMultiSelectInput<'_> {
930 let v: Vec<String> = items.into_iter().map(Into::into).collect();
931 let picked = vec![false; v.len()];
932 RawPagedMultiSelectInput {
933 label,
934 items: v,
935 cursor: 0,
936 page_size: 10,
937 picked,
938 keymap: KeyMap::default(),
939 }
940}
941
942pub enum FormValue {
945 Text(String),
946 Number(f64),
947 Confirm(bool),
948}
949
950enum FormItem {
951 Text {
952 label: String,
953 cfg: Box<dyn Fn(TextInput) -> TextInput>,
954 },
955 Number {
956 label: String,
957 cfg: Box<dyn Fn(NumberInput) -> NumberInput>,
958 },
959 Confirm {
960 label: String,
961 default_yes: bool,
962 },
963}
964
965pub struct FormBuilder {
966 items: Vec<FormItem>,
967}
968
969impl FormBuilder {
970 pub fn new() -> Self {
971 Self { items: Vec::new() }
972 }
973
974 pub fn text(mut self, label: &str, cfg: impl Fn(TextInput) -> TextInput + 'static) -> Self {
975 self.items.push(FormItem::Text {
976 label: label.to_string(),
977 cfg: Box::new(cfg),
978 });
979 self
980 }
981
982 pub fn number(
983 mut self,
984 label: &str,
985 cfg: impl Fn(NumberInput) -> NumberInput + 'static,
986 ) -> Self {
987 self.items.push(FormItem::Number {
988 label: label.to_string(),
989 cfg: Box::new(cfg),
990 });
991 self
992 }
993
994 pub fn confirm(mut self, label: &str, default_yes: bool) -> Self {
995 self.items.push(FormItem::Confirm {
996 label: label.to_string(),
997 default_yes,
998 });
999 self
1000 }
1001
1002 pub fn run(self) -> Result<Vec<(String, FormValue)>, String> {
1004 let mut out = Vec::with_capacity(self.items.len());
1005 for item in self.items {
1006 match item {
1007 FormItem::Text { label, cfg } => {
1008 let v = cfg(text(&label)).get()?;
1009 out.push((label, FormValue::Text(v)));
1010 }
1011 FormItem::Number { label, cfg } => {
1012 let v = cfg(number(&label)).get()?;
1013 out.push((label, FormValue::Number(v)));
1014 }
1015 FormItem::Confirm { label, default_yes } => {
1016 let v = if default_yes {
1017 confirm(&label).default_yes().get()
1018 } else {
1019 confirm(&label).default_no().get()
1020 };
1021 out.push((label, FormValue::Confirm(v)));
1022 }
1023 }
1024 }
1025 Ok(out)
1026 }
1027}
1028
1029pub fn form() -> FormBuilder {
1030 FormBuilder::new()
1031}
1032
1033impl Default for FormBuilder {
1034 fn default() -> Self {
1035 Self::new()
1036 }
1037}
1038
1039pub struct RawButtonsInput<'a> {
1042 label: &'a str,
1043 buttons: Vec<(String, char)>,
1044 cursor: usize,
1045 disabled: HashSet<usize>,
1046 helps: Vec<Option<String>>, danger: HashSet<usize>, confirm_on_danger: bool,
1049 keymap: KeyMap,
1050}
1051
1052impl<'a> RawButtonsInput<'a> {
1053 pub fn cursor(mut self, idx: usize) -> Self {
1054 self.cursor = idx.min(self.buttons.len().saturating_sub(1));
1055 self
1056 }
1057 pub fn disabled(mut self, idxs: &[usize]) -> Self {
1058 for &i in idxs {
1059 self.disabled.insert(i);
1060 }
1061 self
1062 }
1063 pub fn helps<I, S>(mut self, helps: I) -> Self
1064 where
1065 I: IntoIterator<Item = S>,
1066 S: Into<Option<String>>,
1067 {
1068 self.helps = helps.into_iter().map(|s| s.into()).collect();
1069 self
1070 }
1071 pub fn danger(mut self, idxs: &[usize]) -> Self {
1072 for &i in idxs {
1073 self.danger.insert(i);
1074 }
1075 self
1076 }
1077 pub fn confirm_on_danger(mut self, yes: bool) -> Self {
1078 self.confirm_on_danger = yes;
1079 self
1080 }
1081 pub fn keymap(mut self, km: KeyMap) -> Self {
1082 self.keymap = km;
1083 self
1084 }
1085 pub fn get(mut self) -> Option<usize> {
1086 if self.buttons.is_empty() {
1087 return None;
1088 }
1089 let mut stdout = stdout();
1090 if terminal::enable_raw_mode().is_err() {
1091 return None;
1092 }
1093 let _ = execute!(stdout, terminal::Clear(terminal::ClearType::All));
1094 loop {
1095 let theme = current_theme();
1096 let sel_bg = theme.get_log_color("menu_selected_bg");
1097 let sel_fg = theme.get_log_color("menu_selected_fg");
1098 let _ = execute!(stdout, cursor::MoveTo(0, 0));
1099 println!("{}\n", self.label);
1100 let mut line = String::new();
1102 for (i, (title, key)) in self.buttons.iter().enumerate() {
1103 let token = format!("[{}] {}", key, title);
1104 let is_disabled = self.disabled.contains(&i);
1105 let is_danger = self.danger.contains(&i);
1106 let styled = if i == self.cursor {
1107 if is_disabled {
1109 token.as_str().with(Color::DarkGrey).on(sel_bg).to_string()
1110 } else if is_danger {
1111 token
1112 .as_str()
1113 .with(sel_fg)
1114 .on(Color::Red)
1115 .bold()
1116 .to_string()
1117 } else {
1118 token.as_str().with(sel_fg).on(sel_bg).bold().to_string()
1119 }
1120 } else if is_disabled {
1121 token.as_str().with(Color::DarkGrey).to_string()
1122 } else if is_danger {
1123 token.as_str().with(Color::Red).to_string()
1124 } else {
1125 token.clone()
1126 };
1127 line.push_str(&format!(" {}", styled));
1128 }
1129 println!("{}\n", line);
1130 if let Some(Some(help)) = self.helps.get(self.cursor) {
1132 println!("{}\n", help);
1133 }
1134 println!(
1135 "Keys: Left/Right to move, Enter select, hotkeys {}, Esc cancel",
1136 self.buttons.iter().map(|(_, k)| k).collect::<String>()
1137 );
1138 let _ = stdout.flush();
1139
1140 if let Ok(Event::Key(k)) = event::read() {
1141 match k.code {
1142 c if c == self.keymap.left => {
1143 if self.cursor > 0 {
1145 self.cursor -= 1;
1146 }
1147 while self.disabled.contains(&self.cursor) && self.cursor > 0 {
1148 self.cursor -= 1;
1149 }
1150 }
1151 c if c == self.keymap.right => {
1152 if self.cursor + 1 < self.buttons.len() {
1153 self.cursor += 1;
1154 }
1155 while self.cursor + 1 < self.buttons.len()
1156 && self.disabled.contains(&self.cursor)
1157 {
1158 self.cursor += 1;
1159 }
1160 }
1161 c if c == self.keymap.confirm => {
1162 if self.disabled.contains(&self.cursor) {
1163 continue;
1164 }
1165 if self.confirm_on_danger && self.danger.contains(&self.cursor) {
1167 let _ = terminal::disable_raw_mode();
1168 println!("Confirm '{}'? [y/N] ", self.buttons[self.cursor].0);
1169 use std::io::stdin;
1170 let mut buf = String::new();
1171 let _ = stdin().read_line(&mut buf);
1172 if !matches!(buf.trim().to_lowercase().as_str(), "y" | "yes") {
1173 let _ = terminal::enable_raw_mode();
1174 continue;
1175 }
1176 return Some(self.cursor);
1177 } else {
1178 let _ = terminal::disable_raw_mode();
1179 return Some(self.cursor);
1180 }
1181 }
1182 c if c == self.keymap.cancel => {
1183 let _ = terminal::disable_raw_mode();
1184 return None;
1185 }
1186 KeyCode::Char(c) => {
1187 if let Some((idx, _)) = self
1188 .buttons
1189 .iter()
1190 .enumerate()
1191 .find(|(i, (_, k))| *k == c && !self.disabled.contains(i))
1192 {
1193 if self.confirm_on_danger && self.danger.contains(&idx) {
1194 let _ = terminal::disable_raw_mode();
1195 println!("Confirm '{}'? [y/N] ", self.buttons[idx].0);
1196 use std::io::stdin;
1197 let mut buf = String::new();
1198 let _ = stdin().read_line(&mut buf);
1199 if !matches!(buf.trim().to_lowercase().as_str(), "y" | "yes") {
1200 let _ = terminal::enable_raw_mode();
1201 continue;
1202 }
1203 return Some(idx);
1204 }
1205 let _ = terminal::disable_raw_mode();
1206 return Some(idx);
1207 }
1208 }
1209 _ => {}
1210 }
1211 }
1212 }
1213 }
1214}
1215
1216pub fn raw_buttons(
1217 label: &str,
1218 buttons: impl IntoIterator<Item = (impl Into<String>, char)>,
1219) -> RawButtonsInput<'_> {
1220 RawButtonsInput {
1221 label,
1222 buttons: buttons.into_iter().map(|(t, k)| (t.into(), k)).collect(),
1223 cursor: 0,
1224 disabled: HashSet::new(),
1225 helps: Vec::new(),
1226 danger: HashSet::new(),
1227 confirm_on_danger: true,
1228 keymap: KeyMap::default(),
1229 }
1230}