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