1#![forbid(unsafe_code)]
2#![allow(clippy::bool_to_int_with_if)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::doc_markdown)]
6#![allow(clippy::format_collect)]
7#![allow(clippy::format_push_string)]
8#![allow(clippy::map_unwrap_or)]
9#![allow(clippy::missing_const_for_fn)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::no_effect_underscore_binding)]
12#![allow(clippy::option_if_let_else)]
13#![allow(clippy::redundant_clone)]
14#![allow(clippy::redundant_closure_for_method_calls)]
15#![allow(clippy::return_self_not_must_use)]
16#![allow(clippy::struct_excessive_bools)]
17#![allow(clippy::too_many_lines)]
18#![allow(clippy::uninlined_format_args)]
19#![allow(clippy::used_underscore_binding)]
20
21use std::any::Any;
77use std::sync::atomic::{AtomicUsize, Ordering};
78
79use thiserror::Error;
80
81use bubbles::key::Binding;
82use bubbletea::{Cmd, KeyMsg, KeyType, Message, Model};
83use lipgloss::{Border, Style};
84
85static LAST_ID: AtomicUsize = AtomicUsize::new(0);
90
91fn next_id() -> usize {
92 LAST_ID.fetch_add(1, Ordering::SeqCst)
93}
94
95#[derive(Error, Debug, Clone, PartialEq, Eq)]
150pub enum FormError {
151 #[error("user aborted")]
169 UserAborted,
170
171 #[error("timeout")]
182 Timeout,
183
184 #[error("validation error: {0}")]
208 Validation(String),
209
210 #[error("io error: {0}")]
224 Io(String),
225}
226
227impl FormError {
228 pub fn validation(message: impl Into<String>) -> Self {
230 Self::Validation(message.into())
231 }
232
233 pub fn io(message: impl Into<String>) -> Self {
235 Self::Io(message.into())
236 }
237
238 pub fn is_user_abort(&self) -> bool {
240 matches!(self, Self::UserAborted)
241 }
242
243 pub fn is_timeout(&self) -> bool {
245 matches!(self, Self::Timeout)
246 }
247
248 pub fn is_recoverable(&self) -> bool {
250 matches!(self, Self::Validation(_))
251 }
252}
253
254pub type Result<T> = std::result::Result<T, FormError>;
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum FormState {
281 #[default]
283 Normal,
284 Completed,
286 Aborted,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct SelectOption<T: Clone + PartialEq> {
297 pub key: String,
299 pub value: T,
301 pub selected: bool,
303}
304
305impl<T: Clone + PartialEq> SelectOption<T> {
306 pub fn new(key: impl Into<String>, value: T) -> Self {
308 Self {
309 key: key.into(),
310 value,
311 selected: false,
312 }
313 }
314
315 pub fn selected(mut self, selected: bool) -> Self {
317 self.selected = selected;
318 self
319 }
320}
321
322impl<T: Clone + PartialEq + std::fmt::Display> SelectOption<T> {
323 pub fn from_values(values: impl IntoIterator<Item = T>) -> Vec<Self> {
325 values
326 .into_iter()
327 .map(|v| Self::new(v.to_string(), v))
328 .collect()
329 }
330}
331
332pub fn new_options<S: Into<String> + Clone>(
334 values: impl IntoIterator<Item = S>,
335) -> Vec<SelectOption<String>> {
336 values
337 .into_iter()
338 .map(|v| {
339 let s: String = v.clone().into();
340 SelectOption::new(s.clone(), s)
341 })
342 .collect()
343}
344
345#[derive(Debug, Clone)]
351pub struct Theme {
352 pub form: FormStyles,
354 pub group: GroupStyles,
356 pub field_separator: Style,
358 pub blurred: FieldStyles,
360 pub focused: FieldStyles,
362 pub help: Style,
364}
365
366impl Default for Theme {
367 fn default() -> Self {
368 theme_charm()
369 }
370}
371
372#[derive(Debug, Clone, Default)]
374pub struct FormStyles {
375 pub base: Style,
377}
378
379#[derive(Debug, Clone, Default)]
381pub struct GroupStyles {
382 pub base: Style,
384 pub title: Style,
386 pub description: Style,
388}
389
390#[derive(Debug, Clone, Default)]
392pub struct FieldStyles {
393 pub base: Style,
395 pub title: Style,
397 pub description: Style,
399 pub error_indicator: Style,
401 pub error_message: Style,
403
404 pub select_selector: Style,
407 pub option: Style,
409 pub next_indicator: Style,
411 pub prev_indicator: Style,
413
414 pub multi_select_selector: Style,
417 pub selected_option: Style,
419 pub selected_prefix: Style,
421 pub unselected_option: Style,
423 pub unselected_prefix: Style,
425
426 pub text_input: TextInputStyles,
429
430 pub focused_button: Style,
433 pub blurred_button: Style,
435
436 pub note_title: Style,
439}
440
441#[derive(Debug, Clone, Default)]
443pub struct TextInputStyles {
444 pub cursor: Style,
446 pub cursor_text: Style,
448 pub placeholder: Style,
450 pub prompt: Style,
452 pub text: Style,
454}
455
456#[allow(clippy::field_reassign_with_default)]
458pub fn theme_base() -> Theme {
459 let button = Style::new().padding((0, 2)).margin_right(1);
460
461 let mut focused = FieldStyles::default();
462 focused.base = Style::new()
463 .padding_left(1)
464 .border(Border::thick())
465 .border_left(true);
466 focused.error_indicator = Style::new().set_string(" *");
467 focused.error_message = Style::new().set_string(" *");
468 focused.select_selector = Style::new().set_string("> ");
469 focused.next_indicator = Style::new().margin_left(1).set_string("→");
470 focused.prev_indicator = Style::new().margin_right(1).set_string("←");
471 focused.multi_select_selector = Style::new().set_string("> ");
472 focused.selected_prefix = Style::new().set_string("[•] ");
473 focused.unselected_prefix = Style::new().set_string("[ ] ");
474 focused.focused_button = button.clone().foreground("0").background("7");
475 focused.blurred_button = button.foreground("7").background("0");
476 focused.text_input.placeholder = Style::new().foreground("8");
477
478 let mut blurred = focused.clone();
479 blurred.base = blurred.base.border(Border::hidden());
480 blurred.multi_select_selector = Style::new().set_string(" ");
481 blurred.next_indicator = Style::new();
482 blurred.prev_indicator = Style::new();
483
484 Theme {
485 form: FormStyles { base: Style::new() },
486 group: GroupStyles::default(),
487 field_separator: Style::new().set_string("\n\n"),
488 focused,
489 blurred,
490 help: Style::new().foreground("241").margin_top(1),
491 }
492}
493
494pub fn theme_charm() -> Theme {
496 let mut t = theme_base();
497
498 let indigo = "#7571F9";
499 let fuchsia = "#F780E2";
500 let green = "#02BF87";
501 let red = "#ED567A";
502 let normal_fg = "252";
503
504 t.focused.base = t.focused.base.border_foreground("238");
505 t.focused.title = t.focused.title.foreground(indigo).bold();
506 t.focused.note_title = t
507 .focused
508 .note_title
509 .foreground(indigo)
510 .bold()
511 .margin_bottom(1);
512 t.focused.description = t.focused.description.foreground("243");
513 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
514 t.focused.error_message = t.focused.error_message.foreground(red);
515 t.focused.select_selector = t.focused.select_selector.foreground(fuchsia);
516 t.focused.next_indicator = t.focused.next_indicator.foreground(fuchsia);
517 t.focused.prev_indicator = t.focused.prev_indicator.foreground(fuchsia);
518 t.focused.option = t.focused.option.foreground(normal_fg);
519 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(fuchsia);
520 t.focused.selected_option = t.focused.selected_option.foreground(green);
521 t.focused.selected_prefix = Style::new().foreground("#02A877").set_string("✓ ");
522 t.focused.unselected_prefix = Style::new().foreground("243").set_string("• ");
523 t.focused.unselected_option = t.focused.unselected_option.foreground(normal_fg);
524 t.focused.focused_button = t
525 .focused
526 .focused_button
527 .foreground("#FFFDF5")
528 .background(fuchsia);
529 t.focused.blurred_button = t
530 .focused
531 .blurred_button
532 .foreground(normal_fg)
533 .background("237");
534 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(green);
535 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground("238");
536 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(fuchsia);
537
538 t.blurred = t.focused.clone();
539 t.blurred.base = t.focused.base.clone().border(Border::hidden());
540 t.blurred.next_indicator = Style::new();
541 t.blurred.prev_indicator = Style::new();
542
543 t.group.title = t.focused.title.clone();
544 t.group.description = t.focused.description.clone();
545 t.help = Style::new().foreground("241").margin_top(1);
546
547 t
548}
549
550pub fn theme_dracula() -> Theme {
552 let mut t = theme_base();
553
554 let selection = "#44475a";
555 let foreground = "#f8f8f2";
556 let comment = "#6272a4";
557 let green = "#50fa7b";
558 let purple = "#bd93f9";
559 let red = "#ff5555";
560 let yellow = "#f1fa8c";
561
562 t.focused.base = t.focused.base.border_foreground(selection);
563 t.focused.title = t.focused.title.foreground(purple);
564 t.focused.note_title = t.focused.note_title.foreground(purple);
565 t.focused.description = t.focused.description.foreground(comment);
566 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
567 t.focused.error_message = t.focused.error_message.foreground(red);
568 t.focused.select_selector = t.focused.select_selector.foreground(yellow);
569 t.focused.next_indicator = t.focused.next_indicator.foreground(yellow);
570 t.focused.prev_indicator = t.focused.prev_indicator.foreground(yellow);
571 t.focused.option = t.focused.option.foreground(foreground);
572 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(yellow);
573 t.focused.selected_option = t.focused.selected_option.foreground(green);
574 t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
575 t.focused.unselected_option = t.focused.unselected_option.foreground(foreground);
576 t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(comment);
577 t.focused.focused_button = t
578 .focused
579 .focused_button
580 .foreground(yellow)
581 .background(purple)
582 .bold();
583 t.focused.blurred_button = t
584 .focused
585 .blurred_button
586 .foreground(foreground)
587 .background("#282a36");
588 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(yellow);
589 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(comment);
590 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(yellow);
591
592 t.blurred = t.focused.clone();
593 t.blurred.base = t.blurred.base.border(Border::hidden());
594 t.blurred.next_indicator = Style::new();
595 t.blurred.prev_indicator = Style::new();
596
597 t.group.title = t.focused.title.clone();
598 t.group.description = t.focused.description.clone();
599 t.help = Style::new().foreground(comment).margin_top(1);
600
601 t
602}
603
604pub fn theme_base16() -> Theme {
606 let mut t = theme_base();
607
608 t.focused.base = t.focused.base.border_foreground("8");
609 t.focused.title = t.focused.title.foreground("6");
610 t.focused.note_title = t.focused.note_title.foreground("6");
611 t.focused.description = t.focused.description.foreground("8");
612 t.focused.error_indicator = t.focused.error_indicator.foreground("9");
613 t.focused.error_message = t.focused.error_message.foreground("9");
614 t.focused.select_selector = t.focused.select_selector.foreground("3");
615 t.focused.next_indicator = t.focused.next_indicator.foreground("3");
616 t.focused.prev_indicator = t.focused.prev_indicator.foreground("3");
617 t.focused.option = t.focused.option.foreground("7");
618 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground("3");
619 t.focused.selected_option = t.focused.selected_option.foreground("2");
620 t.focused.selected_prefix = t.focused.selected_prefix.foreground("2");
621 t.focused.unselected_option = t.focused.unselected_option.foreground("7");
622 t.focused.focused_button = t.focused.focused_button.foreground("7").background("5");
623 t.focused.blurred_button = t.focused.blurred_button.foreground("7").background("0");
624
625 t.blurred = t.focused.clone();
626 t.blurred.base = t.blurred.base.border(Border::hidden());
627 t.blurred.note_title = t.blurred.note_title.foreground("8");
628 t.blurred.title = t.blurred.title.foreground("8");
629 t.blurred.text_input.prompt = t.blurred.text_input.prompt.foreground("8");
630 t.blurred.text_input.text = t.blurred.text_input.text.foreground("7");
631 t.blurred.next_indicator = Style::new();
632 t.blurred.prev_indicator = Style::new();
633
634 t.group.title = t.focused.title.clone();
635 t.group.description = t.focused.description.clone();
636 t.help = Style::new().foreground("8").margin_top(1);
637
638 t
639}
640
641pub fn theme_catppuccin() -> Theme {
646 let mut t = theme_base();
647
648 let base = "#1e1e2e";
650 let text = "#cdd6f4";
651 let subtext1 = "#bac2de";
652 let subtext0 = "#a6adc8";
653 let _overlay1 = "#7f849c";
654 let overlay0 = "#6c7086";
655 let green = "#a6e3a1";
656 let red = "#f38ba8";
657 let pink = "#f5c2e7";
658 let mauve = "#cba6f7";
659 let rosewater = "#f5e0dc";
660
661 t.focused.base = t.focused.base.border_foreground(subtext1);
662 t.focused.title = t.focused.title.foreground(mauve);
663 t.focused.note_title = t.focused.note_title.foreground(mauve);
664 t.focused.description = t.focused.description.foreground(subtext0);
665 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
666 t.focused.error_message = t.focused.error_message.foreground(red);
667 t.focused.select_selector = t.focused.select_selector.foreground(pink);
668 t.focused.next_indicator = t.focused.next_indicator.foreground(pink);
669 t.focused.prev_indicator = t.focused.prev_indicator.foreground(pink);
670 t.focused.option = t.focused.option.foreground(text);
671 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(pink);
672 t.focused.selected_option = t.focused.selected_option.foreground(green);
673 t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
674 t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(text);
675 t.focused.unselected_option = t.focused.unselected_option.foreground(text);
676 t.focused.focused_button = t.focused.focused_button.foreground(base).background(pink);
677 t.focused.blurred_button = t.focused.blurred_button.foreground(text).background(base);
678
679 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(rosewater);
680 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(overlay0);
681 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(pink);
682
683 t.blurred = t.focused.clone();
684 t.blurred.base = t.blurred.base.border(Border::hidden());
685 t.blurred.next_indicator = Style::new();
686 t.blurred.prev_indicator = Style::new();
687
688 t.group.title = t.focused.title.clone();
689 t.group.description = t.focused.description.clone();
690 t.help = Style::new().foreground(subtext0).margin_top(1);
691
692 t
693}
694
695#[derive(Debug, Clone)]
701pub struct KeyMap {
702 pub quit: Binding,
704 pub input: InputKeyMap,
706 pub select: SelectKeyMap,
708 pub multi_select: MultiSelectKeyMap,
710 pub confirm: ConfirmKeyMap,
712 pub note: NoteKeyMap,
714 pub text: TextKeyMap,
716 pub file_picker: FilePickerKeyMap,
718}
719
720impl Default for KeyMap {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726impl KeyMap {
727 pub fn new() -> Self {
729 Self {
730 quit: Binding::new().keys(&["ctrl+c"]),
731 input: InputKeyMap::default(),
732 select: SelectKeyMap::default(),
733 multi_select: MultiSelectKeyMap::default(),
734 confirm: ConfirmKeyMap::default(),
735 note: NoteKeyMap::default(),
736 text: TextKeyMap::default(),
737 file_picker: FilePickerKeyMap::default(),
738 }
739 }
740}
741
742#[derive(Debug, Clone)]
744pub struct InputKeyMap {
745 pub accept_suggestion: Binding,
747 pub next: Binding,
749 pub prev: Binding,
751 pub submit: Binding,
753}
754
755impl Default for InputKeyMap {
756 fn default() -> Self {
757 Self {
758 accept_suggestion: Binding::new().keys(&["ctrl+e"]).help("ctrl+e", "complete"),
759 prev: Binding::new()
760 .keys(&["shift+tab"])
761 .help("shift+tab", "back"),
762 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
763 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
764 }
765 }
766}
767
768#[derive(Debug, Clone)]
770pub struct SelectKeyMap {
771 pub next: Binding,
773 pub prev: Binding,
775 pub up: Binding,
777 pub down: Binding,
779 pub left: Binding,
781 pub right: Binding,
783 pub filter: Binding,
785 pub set_filter: Binding,
787 pub clear_filter: Binding,
789 pub half_page_up: Binding,
791 pub half_page_down: Binding,
793 pub goto_top: Binding,
795 pub goto_bottom: Binding,
797 pub submit: Binding,
799}
800
801impl Default for SelectKeyMap {
802 fn default() -> Self {
803 Self {
804 prev: Binding::new()
805 .keys(&["shift+tab"])
806 .help("shift+tab", "back"),
807 next: Binding::new()
808 .keys(&["enter", "tab"])
809 .help("enter", "select"),
810 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
811 up: Binding::new()
812 .keys(&["up", "k", "ctrl+k", "ctrl+p"])
813 .help("↑", "up"),
814 down: Binding::new()
815 .keys(&["down", "j", "ctrl+j", "ctrl+n"])
816 .help("↓", "down"),
817 left: Binding::new()
818 .keys(&["h", "left"])
819 .help("←", "left")
820 .set_enabled(false),
821 right: Binding::new()
822 .keys(&["l", "right"])
823 .help("→", "right")
824 .set_enabled(false),
825 filter: Binding::new().keys(&["/"]).help("/", "filter"),
826 set_filter: Binding::new()
827 .keys(&["escape"])
828 .help("esc", "set filter")
829 .set_enabled(false),
830 clear_filter: Binding::new()
831 .keys(&["escape"])
832 .help("esc", "clear filter")
833 .set_enabled(false),
834 half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
835 half_page_down: Binding::new()
836 .keys(&["ctrl+d"])
837 .help("ctrl+d", "½ page down"),
838 goto_top: Binding::new()
839 .keys(&["home", "g"])
840 .help("g/home", "go to start"),
841 goto_bottom: Binding::new()
842 .keys(&["end", "G"])
843 .help("G/end", "go to end"),
844 }
845 }
846}
847
848#[derive(Debug, Clone)]
850pub struct MultiSelectKeyMap {
851 pub next: Binding,
853 pub prev: Binding,
855 pub up: Binding,
857 pub down: Binding,
859 pub toggle: Binding,
861 pub filter: Binding,
863 pub set_filter: Binding,
865 pub clear_filter: Binding,
867 pub half_page_up: Binding,
869 pub half_page_down: Binding,
871 pub goto_top: Binding,
873 pub goto_bottom: Binding,
875 pub select_all: Binding,
877 pub select_none: Binding,
879 pub submit: Binding,
881}
882
883impl Default for MultiSelectKeyMap {
884 fn default() -> Self {
885 Self {
886 prev: Binding::new()
887 .keys(&["shift+tab"])
888 .help("shift+tab", "back"),
889 next: Binding::new()
890 .keys(&["enter", "tab"])
891 .help("enter", "confirm"),
892 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
893 toggle: Binding::new().keys(&[" ", "x"]).help("x", "toggle"),
894 up: Binding::new().keys(&["up", "k", "ctrl+p"]).help("↑", "up"),
895 down: Binding::new()
896 .keys(&["down", "j", "ctrl+n"])
897 .help("↓", "down"),
898 filter: Binding::new().keys(&["/"]).help("/", "filter"),
899 set_filter: Binding::new()
900 .keys(&["enter", "escape"])
901 .help("esc", "set filter")
902 .set_enabled(false),
903 clear_filter: Binding::new()
904 .keys(&["escape"])
905 .help("esc", "clear filter")
906 .set_enabled(false),
907 half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
908 half_page_down: Binding::new()
909 .keys(&["ctrl+d"])
910 .help("ctrl+d", "½ page down"),
911 goto_top: Binding::new()
912 .keys(&["home", "g"])
913 .help("g/home", "go to start"),
914 goto_bottom: Binding::new()
915 .keys(&["end", "G"])
916 .help("G/end", "go to end"),
917 select_all: Binding::new()
918 .keys(&["ctrl+a"])
919 .help("ctrl+a", "select all"),
920 select_none: Binding::new()
921 .keys(&["ctrl+a"])
922 .help("ctrl+a", "select none")
923 .set_enabled(false),
924 }
925 }
926}
927
928#[derive(Debug, Clone)]
930pub struct ConfirmKeyMap {
931 pub next: Binding,
933 pub prev: Binding,
935 pub toggle: Binding,
937 pub submit: Binding,
939 pub accept: Binding,
941 pub reject: Binding,
943}
944
945impl Default for ConfirmKeyMap {
946 fn default() -> Self {
947 Self {
948 prev: Binding::new()
949 .keys(&["shift+tab"])
950 .help("shift+tab", "back"),
951 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
952 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
953 toggle: Binding::new()
954 .keys(&["h", "l", "right", "left"])
955 .help("←/→", "toggle"),
956 accept: Binding::new().keys(&["y", "Y"]).help("y", "Yes"),
957 reject: Binding::new().keys(&["n", "N"]).help("n", "No"),
958 }
959 }
960}
961
962#[derive(Debug, Clone)]
964pub struct NoteKeyMap {
965 pub next: Binding,
967 pub prev: Binding,
969 pub submit: Binding,
971}
972
973impl Default for NoteKeyMap {
974 fn default() -> Self {
975 Self {
976 prev: Binding::new()
977 .keys(&["shift+tab"])
978 .help("shift+tab", "back"),
979 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
980 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
981 }
982 }
983}
984
985#[derive(Debug, Clone)]
987pub struct TextKeyMap {
988 pub next: Binding,
990 pub prev: Binding,
992 pub new_line: Binding,
994 pub editor: Binding,
996 pub submit: Binding,
998 pub uppercase_word_forward: Binding,
1000 pub lowercase_word_forward: Binding,
1002 pub capitalize_word_forward: Binding,
1004 pub transpose_character_backward: Binding,
1006}
1007
1008impl Default for TextKeyMap {
1009 fn default() -> Self {
1010 Self {
1011 prev: Binding::new()
1012 .keys(&["shift+tab"])
1013 .help("shift+tab", "back"),
1014 next: Binding::new().keys(&["tab", "enter"]).help("enter", "next"),
1015 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1016 new_line: Binding::new()
1017 .keys(&["alt+enter", "ctrl+j"])
1018 .help("alt+enter / ctrl+j", "new line"),
1019 editor: Binding::new()
1020 .keys(&["ctrl+e"])
1021 .help("ctrl+e", "open editor"),
1022 uppercase_word_forward: Binding::new()
1023 .keys(&["alt+u"])
1024 .help("alt+u", "uppercase word"),
1025 lowercase_word_forward: Binding::new()
1026 .keys(&["alt+l"])
1027 .help("alt+l", "lowercase word"),
1028 capitalize_word_forward: Binding::new()
1029 .keys(&["alt+c"])
1030 .help("alt+c", "capitalize word"),
1031 transpose_character_backward: Binding::new()
1032 .keys(&["ctrl+t"])
1033 .help("ctrl+t", "transpose"),
1034 }
1035 }
1036}
1037
1038#[derive(Debug, Clone)]
1040pub struct FilePickerKeyMap {
1041 pub next: Binding,
1043 pub prev: Binding,
1045 pub submit: Binding,
1047 pub up: Binding,
1049 pub down: Binding,
1051 pub open: Binding,
1053 pub close: Binding,
1055 pub back: Binding,
1057 pub select: Binding,
1059 pub goto_top: Binding,
1061 pub goto_bottom: Binding,
1063 pub page_up: Binding,
1065 pub page_down: Binding,
1067}
1068
1069impl Default for FilePickerKeyMap {
1070 fn default() -> Self {
1071 Self {
1072 prev: Binding::new()
1073 .keys(&["shift+tab"])
1074 .help("shift+tab", "back"),
1075 next: Binding::new().keys(&["tab"]).help("tab", "next"),
1076 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1077 up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
1078 down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
1079 open: Binding::new().keys(&["enter", "l"]).help("enter", "open"),
1080 close: Binding::new().keys(&["esc", "q"]).help("esc", "close"),
1081 back: Binding::new().keys(&["backspace", "h"]).help("h", "back"),
1082 select: Binding::new().keys(&["enter"]).help("enter", "select"),
1083 goto_top: Binding::new().keys(&["g"]).help("g", "first"),
1084 goto_bottom: Binding::new().keys(&["G"]).help("G", "last"),
1085 page_up: Binding::new().keys(&["pgup", "K"]).help("pgup", "page up"),
1086 page_down: Binding::new()
1087 .keys(&["pgdown", "J"])
1088 .help("pgdown", "page down"),
1089 }
1090 }
1091}
1092
1093#[derive(Debug, Clone, Copy, Default)]
1099pub struct FieldPosition {
1100 pub group: usize,
1102 pub field: usize,
1104 pub first_field: usize,
1106 pub last_field: usize,
1108 pub group_count: usize,
1110 pub first_group: usize,
1112 pub last_group: usize,
1114}
1115
1116impl FieldPosition {
1117 pub fn is_first(&self) -> bool {
1119 self.field == self.first_field && self.group == self.first_group
1120 }
1121
1122 pub fn is_last(&self) -> bool {
1124 self.field == self.last_field && self.group == self.last_group
1125 }
1126}
1127
1128fn binding_matches(binding: &Binding, key: &KeyMsg) -> bool {
1134 if !binding.enabled() {
1135 return false;
1136 }
1137 let key_str = key.to_string();
1138 binding.get_keys().iter().any(|k| k == &key_str)
1139}
1140
1141pub trait Field: Send + Sync {
1147 fn get_key(&self) -> &str;
1149
1150 fn get_value(&self) -> Box<dyn Any>;
1152
1153 fn skip(&self) -> bool {
1155 false
1156 }
1157
1158 fn zoom(&self) -> bool {
1160 false
1161 }
1162
1163 fn error(&self) -> Option<&str>;
1165
1166 fn init(&mut self) -> Option<Cmd>;
1168
1169 fn update(&mut self, msg: &Message) -> Option<Cmd>;
1171
1172 fn view(&self) -> String;
1174
1175 fn focus(&mut self) -> Option<Cmd>;
1177
1178 fn blur(&mut self) -> Option<Cmd>;
1180
1181 fn key_binds(&self) -> Vec<Binding>;
1183
1184 fn with_theme(&mut self, theme: &Theme);
1186
1187 fn with_keymap(&mut self, keymap: &KeyMap);
1189
1190 fn with_width(&mut self, width: usize);
1192
1193 fn with_height(&mut self, height: usize);
1195
1196 fn with_position(&mut self, position: FieldPosition);
1198}
1199
1200#[derive(Debug, Clone)]
1206pub struct NextFieldMsg;
1207
1208#[derive(Debug, Clone)]
1210pub struct PrevFieldMsg;
1211
1212#[derive(Debug, Clone)]
1214pub struct NextGroupMsg;
1215
1216#[derive(Debug, Clone)]
1218pub struct PrevGroupMsg;
1219
1220#[derive(Debug, Clone)]
1222pub struct UpdateFieldMsg;
1223
1224pub struct Input {
1230 id: usize,
1231 key: String,
1232 value: String,
1233 title: String,
1234 description: String,
1235 placeholder: String,
1236 prompt: String,
1237 char_limit: usize,
1238 echo_mode: EchoMode,
1239 inline: bool,
1240 focused: bool,
1241 error: Option<String>,
1242 validate: Option<fn(&str) -> Option<String>>,
1243 width: usize,
1244 _height: usize,
1245 theme: Option<Theme>,
1246 keymap: InputKeyMap,
1247 _position: FieldPosition,
1248 cursor_pos: usize,
1249 suggestions: Vec<String>,
1250 show_suggestions: bool,
1251}
1252
1253#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1255pub enum EchoMode {
1256 #[default]
1258 Normal,
1259 Password,
1261 None,
1263}
1264
1265impl Default for Input {
1266 fn default() -> Self {
1267 Self::new()
1268 }
1269}
1270
1271impl Input {
1272 pub fn new() -> Self {
1274 Self {
1275 id: next_id(),
1276 key: String::new(),
1277 value: String::new(),
1278 title: String::new(),
1279 description: String::new(),
1280 placeholder: String::new(),
1281 prompt: "> ".to_string(),
1282 char_limit: 0,
1283 echo_mode: EchoMode::Normal,
1284 inline: false,
1285 focused: false,
1286 error: None,
1287 validate: None,
1288 width: 80,
1289 _height: 0,
1290 theme: None,
1291 keymap: InputKeyMap::default(),
1292 _position: FieldPosition::default(),
1293 cursor_pos: 0,
1294 suggestions: Vec::new(),
1295 show_suggestions: false,
1296 }
1297 }
1298
1299 pub fn key(mut self, key: impl Into<String>) -> Self {
1301 self.key = key.into();
1302 self
1303 }
1304
1305 pub fn value(mut self, value: impl Into<String>) -> Self {
1307 self.value = value.into();
1308 self.cursor_pos = self.value.chars().count();
1309 self
1310 }
1311
1312 pub fn title(mut self, title: impl Into<String>) -> Self {
1314 self.title = title.into();
1315 self
1316 }
1317
1318 pub fn description(mut self, description: impl Into<String>) -> Self {
1320 self.description = description.into();
1321 self
1322 }
1323
1324 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1326 self.placeholder = placeholder.into();
1327 self
1328 }
1329
1330 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
1332 self.prompt = prompt.into();
1333 self
1334 }
1335
1336 pub fn char_limit(mut self, limit: usize) -> Self {
1338 self.char_limit = limit;
1339 self
1340 }
1341
1342 pub fn echo_mode(mut self, mode: EchoMode) -> Self {
1344 self.echo_mode = mode;
1345 self
1346 }
1347
1348 pub fn password(self, password: bool) -> Self {
1350 if password {
1351 self.echo_mode(EchoMode::Password)
1352 } else {
1353 self.echo_mode(EchoMode::Normal)
1354 }
1355 }
1356
1357 pub fn inline(mut self, inline: bool) -> Self {
1359 self.inline = inline;
1360 self
1361 }
1362
1363 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
1365 self.validate = Some(validate);
1366 self
1367 }
1368
1369 pub fn suggestions(mut self, suggestions: Vec<String>) -> Self {
1371 self.suggestions = suggestions;
1372 self.show_suggestions = !self.suggestions.is_empty();
1373 self
1374 }
1375
1376 fn get_theme(&self) -> Theme {
1377 self.theme.clone().unwrap_or_else(theme_charm)
1378 }
1379
1380 fn active_styles(&self) -> FieldStyles {
1381 let theme = self.get_theme();
1382 if self.focused {
1383 theme.focused
1384 } else {
1385 theme.blurred
1386 }
1387 }
1388
1389 fn run_validation(&mut self) {
1390 if let Some(validate) = self.validate {
1391 self.error = validate(&self.value);
1392 }
1393 }
1394
1395 fn display_value(&self) -> String {
1396 match self.echo_mode {
1397 EchoMode::Normal => self.value.clone(),
1398 EchoMode::Password => "•".repeat(self.value.chars().count()),
1399 EchoMode::None => String::new(),
1400 }
1401 }
1402
1403 pub fn get_string_value(&self) -> &str {
1405 &self.value
1406 }
1407
1408 pub fn id(&self) -> usize {
1410 self.id
1411 }
1412}
1413
1414impl Field for Input {
1415 fn get_key(&self) -> &str {
1416 &self.key
1417 }
1418
1419 fn get_value(&self) -> Box<dyn Any> {
1420 Box::new(self.value.clone())
1421 }
1422
1423 fn error(&self) -> Option<&str> {
1424 self.error.as_deref()
1425 }
1426
1427 fn init(&mut self) -> Option<Cmd> {
1428 None
1429 }
1430
1431 fn update(&mut self, msg: &Message) -> Option<Cmd> {
1432 if !self.focused {
1433 return None;
1434 }
1435
1436 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1437 self.error = None;
1438
1439 if binding_matches(&self.keymap.prev, key_msg) {
1441 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1442 }
1443
1444 if binding_matches(&self.keymap.next, key_msg)
1446 || binding_matches(&self.keymap.submit, key_msg)
1447 {
1448 self.run_validation();
1449 if self.error.is_some() {
1450 return None;
1451 }
1452 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1453 }
1454
1455 match key_msg.key_type {
1458 KeyType::Runes => {
1459 let chars_to_insert: Vec<char> = if key_msg.paste {
1461 key_msg
1462 .runes
1463 .iter()
1464 .map(|&c| {
1465 if c == '\n' || c == '\r' || c == '\t' {
1466 ' '
1467 } else {
1468 c
1469 }
1470 })
1471 .fold(Vec::new(), |mut acc, c| {
1473 if c == ' ' && acc.last() == Some(&' ') {
1474 } else {
1476 acc.push(c);
1477 }
1478 acc
1479 })
1480 } else {
1481 key_msg.runes.clone()
1482 };
1483
1484 let current_count = self.value.chars().count();
1486 let available = if self.char_limit == 0 {
1487 usize::MAX
1488 } else {
1489 self.char_limit.saturating_sub(current_count)
1490 };
1491 let chars_to_add: Vec<char> =
1492 chars_to_insert.into_iter().take(available).collect();
1493
1494 if !chars_to_add.is_empty() {
1495 let byte_pos = self
1497 .value
1498 .char_indices()
1499 .nth(self.cursor_pos)
1500 .map(|(i, _)| i)
1501 .unwrap_or(self.value.len());
1502
1503 let insert_str: String = chars_to_add.iter().collect();
1505 self.value.insert_str(byte_pos, &insert_str);
1506 self.cursor_pos += chars_to_add.len();
1507 }
1508 }
1509 KeyType::Backspace => {
1510 if self.cursor_pos > 0 {
1511 self.cursor_pos -= 1;
1512 if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1514 {
1515 self.value.remove(byte_pos);
1516 }
1517 }
1518 }
1519 KeyType::Delete => {
1520 let char_count = self.value.chars().count();
1521 if self.cursor_pos < char_count {
1522 if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1524 {
1525 self.value.remove(byte_pos);
1526 }
1527 }
1528 }
1529 KeyType::Left => {
1530 if self.cursor_pos > 0 {
1531 self.cursor_pos -= 1;
1532 }
1533 }
1534 KeyType::Right => {
1535 let char_count = self.value.chars().count();
1536 if self.cursor_pos < char_count {
1537 self.cursor_pos += 1;
1538 }
1539 }
1540 KeyType::Home => {
1541 self.cursor_pos = 0;
1542 }
1543 KeyType::End => {
1544 self.cursor_pos = self.value.chars().count();
1545 }
1546 _ => {}
1547 }
1548 }
1549
1550 None
1551 }
1552
1553 fn view(&self) -> String {
1554 let styles = self.active_styles();
1555 let mut output = String::new();
1556
1557 if !self.title.is_empty() {
1559 output.push_str(&styles.title.render(&self.title));
1560 if !self.inline {
1561 output.push('\n');
1562 }
1563 }
1564
1565 if !self.description.is_empty() {
1567 output.push_str(&styles.description.render(&self.description));
1568 if !self.inline {
1569 output.push('\n');
1570 }
1571 }
1572
1573 output.push_str(&styles.text_input.prompt.render(&self.prompt));
1575
1576 let display = self.display_value();
1577 if display.is_empty() && !self.placeholder.is_empty() {
1578 output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
1579 } else {
1580 output.push_str(&styles.text_input.text.render(&display));
1581 }
1582
1583 if self.error.is_some() {
1585 output.push_str(&styles.error_indicator.render(""));
1586 }
1587
1588 styles
1589 .base
1590 .width(self.width.try_into().unwrap_or(u16::MAX))
1591 .render(&output)
1592 }
1593
1594 fn focus(&mut self) -> Option<Cmd> {
1595 self.focused = true;
1596 None
1597 }
1598
1599 fn blur(&mut self) -> Option<Cmd> {
1600 self.focused = false;
1601 self.run_validation();
1602 None
1603 }
1604
1605 fn key_binds(&self) -> Vec<Binding> {
1606 if self.show_suggestions {
1607 vec![
1608 self.keymap.accept_suggestion.clone(),
1609 self.keymap.prev.clone(),
1610 self.keymap.submit.clone(),
1611 self.keymap.next.clone(),
1612 ]
1613 } else {
1614 vec![
1615 self.keymap.prev.clone(),
1616 self.keymap.submit.clone(),
1617 self.keymap.next.clone(),
1618 ]
1619 }
1620 }
1621
1622 fn with_theme(&mut self, theme: &Theme) {
1623 if self.theme.is_none() {
1624 self.theme = Some(theme.clone());
1625 }
1626 }
1627
1628 fn with_keymap(&mut self, keymap: &KeyMap) {
1629 self.keymap = keymap.input.clone();
1630 }
1631
1632 fn with_width(&mut self, width: usize) {
1633 self.width = width;
1634 }
1635
1636 fn with_height(&mut self, height: usize) {
1637 self._height = height;
1638 }
1639
1640 fn with_position(&mut self, position: FieldPosition) {
1641 self._position = position;
1642 }
1643}
1644
1645pub struct Select<T: Clone + PartialEq + Send + Sync + 'static> {
1651 id: usize,
1652 key: String,
1653 options: Vec<SelectOption<T>>,
1654 selected: usize,
1655 title: String,
1656 description: String,
1657 inline: bool,
1658 focused: bool,
1659 error: Option<String>,
1660 validate: Option<fn(&T) -> Option<String>>,
1661 width: usize,
1662 height: usize,
1663 theme: Option<Theme>,
1664 keymap: SelectKeyMap,
1665 _position: FieldPosition,
1666 filtering: bool,
1667 filter_value: String,
1668 offset: usize,
1669}
1670
1671impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for Select<T> {
1672 fn default() -> Self {
1673 Self::new()
1674 }
1675}
1676
1677impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Select<T> {
1678 pub fn new() -> Self {
1680 Self {
1681 id: next_id(),
1682 key: String::new(),
1683 options: Vec::new(),
1684 selected: 0,
1685 title: String::new(),
1686 description: String::new(),
1687 inline: false,
1688 focused: false,
1689 error: None,
1690 validate: None,
1691 width: 80,
1692 height: 5,
1693 theme: None,
1694 keymap: SelectKeyMap::default(),
1695 _position: FieldPosition::default(),
1696 filtering: false,
1697 filter_value: String::new(),
1698 offset: 0,
1699 }
1700 }
1701
1702 pub fn key(mut self, key: impl Into<String>) -> Self {
1704 self.key = key.into();
1705 self
1706 }
1707
1708 pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
1710 self.options = options;
1711 for (i, opt) in self.options.iter().enumerate() {
1713 if opt.selected {
1714 self.selected = i;
1715 break;
1716 }
1717 }
1718 self
1719 }
1720
1721 pub fn title(mut self, title: impl Into<String>) -> Self {
1723 self.title = title.into();
1724 self
1725 }
1726
1727 pub fn description(mut self, description: impl Into<String>) -> Self {
1729 self.description = description.into();
1730 self
1731 }
1732
1733 pub fn inline(mut self, inline: bool) -> Self {
1735 self.inline = inline;
1736 self
1737 }
1738
1739 pub fn validate(mut self, validate: fn(&T) -> Option<String>) -> Self {
1741 self.validate = Some(validate);
1742 self
1743 }
1744
1745 pub fn height_options(mut self, height: usize) -> Self {
1747 self.height = height;
1748 self
1749 }
1750
1751 pub fn filterable(mut self, enabled: bool) -> Self {
1757 self.filtering = enabled;
1758 self
1759 }
1760
1761 fn update_filter(&mut self, new_value: String) {
1765 let current_item_idx = self.selected;
1767
1768 self.filter_value = new_value;
1770
1771 let filtered_indices: Vec<usize> = self.filtered_indices();
1773
1774 if filtered_indices.contains(¤t_item_idx) {
1776 self.adjust_offset_from_indices(&filtered_indices);
1778 return;
1779 }
1780
1781 if let Some(&first_idx) = filtered_indices.first() {
1783 self.selected = first_idx;
1784 }
1785 self.adjust_offset_from_indices(&filtered_indices);
1786 }
1787
1788 fn filtered_indices(&self) -> Vec<usize> {
1791 if self.filter_value.is_empty() {
1792 (0..self.options.len()).collect()
1793 } else {
1794 let filter_lower = self.filter_value.to_lowercase();
1795 self.options
1796 .iter()
1797 .enumerate()
1798 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1799 .map(|(i, _)| i)
1800 .collect()
1801 }
1802 }
1803
1804 fn adjust_offset_from_indices(&mut self, filtered_indices: &[usize]) {
1807 let pos = filtered_indices
1808 .iter()
1809 .position(|&idx| idx == self.selected)
1810 .unwrap_or(0);
1811 if pos < self.offset {
1812 self.offset = pos;
1813 } else if pos >= self.offset + self.height {
1814 self.offset = pos.saturating_sub(self.height.saturating_sub(1));
1815 }
1816 }
1817
1818 fn get_theme(&self) -> Theme {
1819 self.theme.clone().unwrap_or_else(theme_charm)
1820 }
1821
1822 fn active_styles(&self) -> FieldStyles {
1823 let theme = self.get_theme();
1824 if self.focused {
1825 theme.focused
1826 } else {
1827 theme.blurred
1828 }
1829 }
1830
1831 fn run_validation(&mut self) {
1832 if let Some(validate) = self.validate
1833 && let Some(opt) = self.options.get(self.selected)
1834 {
1835 self.error = validate(&opt.value);
1836 }
1837 }
1838
1839 fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
1840 if self.filter_value.is_empty() {
1841 self.options.iter().enumerate().collect()
1842 } else {
1843 let filter_lower = self.filter_value.to_lowercase();
1844 self.options
1845 .iter()
1846 .enumerate()
1847 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1848 .collect()
1849 }
1850 }
1851
1852 pub fn get_selected_value(&self) -> Option<&T> {
1854 self.options.get(self.selected).map(|o| &o.value)
1855 }
1856
1857 pub fn id(&self) -> usize {
1859 self.id
1860 }
1861}
1862
1863impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for Select<T> {
1864 fn get_key(&self) -> &str {
1865 &self.key
1866 }
1867
1868 fn get_value(&self) -> Box<dyn Any> {
1869 if let Some(opt) = self.options.get(self.selected) {
1870 Box::new(opt.value.clone())
1871 } else {
1872 Box::new(T::default())
1873 }
1874 }
1875
1876 fn error(&self) -> Option<&str> {
1877 self.error.as_deref()
1878 }
1879
1880 fn init(&mut self) -> Option<Cmd> {
1881 None
1882 }
1883
1884 fn update(&mut self, msg: &Message) -> Option<Cmd> {
1885 if !self.focused {
1886 return None;
1887 }
1888
1889 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1890 self.error = None;
1891
1892 if self.filtering {
1894 if key_msg.key_type == KeyType::Esc {
1896 self.update_filter(String::new());
1897 return None;
1898 }
1899
1900 if key_msg.key_type == KeyType::Backspace {
1902 if !self.filter_value.is_empty() {
1903 let mut new_filter = self.filter_value.clone();
1904 new_filter.pop();
1905 self.update_filter(new_filter);
1906 }
1907 return None;
1908 }
1909
1910 if key_msg.key_type == KeyType::Runes {
1912 let mut new_filter = self.filter_value.clone();
1913 for c in &key_msg.runes {
1914 match c {
1916 'j' | 'k' | 'g' | 'G' | '/' => continue,
1917 _ => {}
1918 }
1919 if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
1920 new_filter.push(*c);
1921 }
1922 }
1923 if new_filter != self.filter_value {
1924 self.update_filter(new_filter);
1925 return None;
1926 }
1927 }
1928 }
1929
1930 if binding_matches(&self.keymap.prev, key_msg) {
1932 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1933 }
1934
1935 if binding_matches(&self.keymap.next, key_msg)
1937 || binding_matches(&self.keymap.submit, key_msg)
1938 {
1939 self.run_validation();
1940 if self.error.is_some() {
1941 return None;
1942 }
1943 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1944 }
1945
1946 let filtered_indices = self.filtered_indices();
1949 let current_pos = filtered_indices
1950 .iter()
1951 .position(|&idx| idx == self.selected);
1952
1953 if binding_matches(&self.keymap.up, key_msg)
1954 && let Some(pos) = current_pos
1955 && pos > 0
1956 {
1957 self.selected = filtered_indices[pos - 1];
1958 self.adjust_offset_from_indices(&filtered_indices);
1959 } else if binding_matches(&self.keymap.down, key_msg)
1960 && let Some(pos) = current_pos
1961 && pos < filtered_indices.len().saturating_sub(1)
1962 {
1963 self.selected = filtered_indices[pos + 1];
1964 self.adjust_offset_from_indices(&filtered_indices);
1965 } else if binding_matches(&self.keymap.goto_top, key_msg)
1966 && let Some(&idx) = filtered_indices.first()
1967 {
1968 self.selected = idx;
1969 self.offset = 0;
1970 } else if binding_matches(&self.keymap.goto_bottom, key_msg)
1971 && let Some(&idx) = filtered_indices.last()
1972 {
1973 self.selected = idx;
1974 let last_pos = filtered_indices.len().saturating_sub(1);
1975 self.offset = last_pos.saturating_sub(self.height.saturating_sub(1));
1976 }
1977 }
1978
1979 None
1980 }
1981
1982 fn view(&self) -> String {
1983 let styles = self.active_styles();
1984 let mut output = String::new();
1985
1986 if !self.title.is_empty() {
1988 output.push_str(&styles.title.render(&self.title));
1989 output.push('\n');
1990 }
1991
1992 if !self.description.is_empty() {
1994 output.push_str(&styles.description.render(&self.description));
1995 output.push('\n');
1996 }
1997
1998 if self.filtering && !self.filter_value.is_empty() {
2000 let filter_display = format!("Filter: {}_", self.filter_value);
2001 output.push_str(&styles.description.render(&filter_display));
2002 output.push('\n');
2003 }
2004
2005 let filtered = self.filtered_options();
2007 let visible: Vec<_> = filtered
2008 .iter()
2009 .skip(self.offset)
2010 .take(self.height)
2011 .collect();
2012
2013 if self.inline {
2014 let mut inline_output = String::new();
2016 inline_output.push_str(&styles.prev_indicator.render(""));
2017 for (i, (idx, opt)) in visible.iter().enumerate() {
2018 if *idx == self.selected {
2019 inline_output.push_str(&styles.selected_option.render(&opt.key));
2020 } else {
2021 inline_output.push_str(&styles.option.render(&opt.key));
2022 }
2023 if i < visible.len() - 1 {
2024 inline_output.push_str(" ");
2025 }
2026 }
2027 inline_output.push_str(&styles.next_indicator.render(""));
2028 output.push_str(&inline_output);
2029 } else {
2030 let has_visible = !visible.is_empty();
2032 for (idx, opt) in &visible {
2033 if *idx == self.selected {
2034 output.push_str(&styles.select_selector.render(""));
2035 output.push_str(&styles.selected_option.render(&opt.key));
2036 } else {
2037 output.push_str(" ");
2038 output.push_str(&styles.option.render(&opt.key));
2039 }
2040 output.push('\n');
2041 }
2042 if has_visible {
2044 output.pop();
2045 }
2046 }
2047
2048 if self.error.is_some() {
2050 output.push_str(&styles.error_indicator.render(""));
2051 }
2052
2053 styles
2054 .base
2055 .width(self.width.try_into().unwrap_or(u16::MAX))
2056 .render(&output)
2057 }
2058
2059 fn focus(&mut self) -> Option<Cmd> {
2060 self.focused = true;
2061 None
2062 }
2063
2064 fn blur(&mut self) -> Option<Cmd> {
2065 self.focused = false;
2066 self.run_validation();
2067 None
2068 }
2069
2070 fn key_binds(&self) -> Vec<Binding> {
2071 vec![
2072 self.keymap.up.clone(),
2073 self.keymap.down.clone(),
2074 self.keymap.prev.clone(),
2075 self.keymap.submit.clone(),
2076 self.keymap.next.clone(),
2077 ]
2078 }
2079
2080 fn with_theme(&mut self, theme: &Theme) {
2081 if self.theme.is_none() {
2082 self.theme = Some(theme.clone());
2083 }
2084 }
2085
2086 fn with_keymap(&mut self, keymap: &KeyMap) {
2087 self.keymap = keymap.select.clone();
2088 }
2089
2090 fn with_width(&mut self, width: usize) {
2091 self.width = width;
2092 }
2093
2094 fn with_height(&mut self, height: usize) {
2095 self.height = height;
2096 }
2097
2098 fn with_position(&mut self, position: FieldPosition) {
2099 self._position = position;
2100 }
2101}
2102
2103pub struct MultiSelect<T: Clone + PartialEq + Send + Sync + 'static> {
2109 id: usize,
2110 key: String,
2111 options: Vec<SelectOption<T>>,
2112 selected: Vec<usize>,
2113 cursor: usize,
2114 title: String,
2115 description: String,
2116 focused: bool,
2117 error: Option<String>,
2118 #[allow(clippy::type_complexity)]
2119 validate: Option<fn(&[T]) -> Option<String>>,
2120 width: usize,
2121 height: usize,
2122 limit: Option<usize>,
2123 theme: Option<Theme>,
2124 keymap: MultiSelectKeyMap,
2125 _position: FieldPosition,
2126 filtering: bool,
2127 filter_value: String,
2128 offset: usize,
2129}
2130
2131impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for MultiSelect<T> {
2132 fn default() -> Self {
2133 Self::new()
2134 }
2135}
2136
2137impl<T: Clone + PartialEq + Send + Sync + Default + 'static> MultiSelect<T> {
2138 pub fn new() -> Self {
2140 Self {
2141 id: next_id(),
2142 key: String::new(),
2143 options: Vec::new(),
2144 selected: Vec::new(),
2145 cursor: 0,
2146 title: String::new(),
2147 description: String::new(),
2148 focused: false,
2149 error: None,
2150 validate: None,
2151 width: 80,
2152 height: 5,
2153 limit: None,
2154 theme: None,
2155 keymap: MultiSelectKeyMap::default(),
2156 _position: FieldPosition::default(),
2157 filtering: false,
2158 filter_value: String::new(),
2159 offset: 0,
2160 }
2161 }
2162
2163 pub fn key(mut self, key: impl Into<String>) -> Self {
2165 self.key = key.into();
2166 self
2167 }
2168
2169 pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
2171 self.options = options;
2172 self.selected = self
2174 .options
2175 .iter()
2176 .enumerate()
2177 .filter(|(_, opt)| opt.selected)
2178 .map(|(i, _)| i)
2179 .collect();
2180 self
2181 }
2182
2183 pub fn title(mut self, title: impl Into<String>) -> Self {
2185 self.title = title.into();
2186 self
2187 }
2188
2189 pub fn description(mut self, description: impl Into<String>) -> Self {
2191 self.description = description.into();
2192 self
2193 }
2194
2195 pub fn validate(mut self, validate: fn(&[T]) -> Option<String>) -> Self {
2197 self.validate = Some(validate);
2198 self
2199 }
2200
2201 pub fn height_options(mut self, height: usize) -> Self {
2203 self.height = height;
2204 self
2205 }
2206
2207 pub fn limit(mut self, limit: usize) -> Self {
2209 self.limit = Some(limit);
2210 self
2211 }
2212
2213 pub fn filterable(mut self, enabled: bool) -> Self {
2217 self.filtering = enabled;
2218 self
2219 }
2220
2221 fn update_filter(&mut self, new_value: String) {
2226 let old_filtered = self.filtered_options();
2228 let current_item_idx = old_filtered.get(self.cursor).map(|(idx, _)| *idx);
2229
2230 self.filter_value = new_value;
2232
2233 let new_filtered = self.filtered_options();
2235
2236 if let Some(item_idx) = current_item_idx
2238 && let Some(new_pos) = new_filtered.iter().position(|(idx, _)| *idx == item_idx)
2239 {
2240 self.cursor = new_pos;
2241 self.adjust_offset();
2242 return;
2243 }
2244
2245 self.cursor = self.cursor.min(new_filtered.len().saturating_sub(1));
2247 self.adjust_offset();
2248 }
2249
2250 fn adjust_offset(&mut self) {
2252 if self.cursor < self.offset {
2254 self.offset = self.cursor;
2255 } else if self.cursor >= self.offset + self.height {
2256 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2257 }
2258 }
2259
2260 fn get_theme(&self) -> Theme {
2261 self.theme.clone().unwrap_or_else(theme_charm)
2262 }
2263
2264 fn active_styles(&self) -> FieldStyles {
2265 let theme = self.get_theme();
2266 if self.focused {
2267 theme.focused
2268 } else {
2269 theme.blurred
2270 }
2271 }
2272
2273 fn run_validation(&mut self) {
2274 if let Some(validate) = self.validate {
2275 let values: Vec<T> = self
2276 .selected
2277 .iter()
2278 .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2279 .collect();
2280 self.error = validate(&values);
2281 }
2282 }
2283
2284 fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
2285 if self.filter_value.is_empty() {
2286 self.options.iter().enumerate().collect()
2287 } else {
2288 let filter_lower = self.filter_value.to_lowercase();
2289 self.options
2290 .iter()
2291 .enumerate()
2292 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
2293 .collect()
2294 }
2295 }
2296
2297 fn toggle_current(&mut self) {
2298 let filtered = self.filtered_options();
2299 if let Some((idx, _)) = filtered.get(self.cursor) {
2300 if let Some(pos) = self.selected.iter().position(|&i| i == *idx) {
2301 self.selected.remove(pos);
2303 } else if self.limit.is_none_or(|l| self.selected.len() < l) {
2304 self.selected.push(*idx);
2306 }
2307 }
2308 }
2309
2310 fn select_all(&mut self) {
2311 if let Some(limit) = self.limit {
2312 self.selected = self
2314 .options
2315 .iter()
2316 .enumerate()
2317 .take(limit)
2318 .map(|(i, _)| i)
2319 .collect();
2320 } else {
2321 self.selected = (0..self.options.len()).collect();
2322 }
2323 }
2324
2325 fn select_none(&mut self) {
2326 self.selected.clear();
2327 }
2328
2329 pub fn get_selected_values(&self) -> Vec<&T> {
2331 self.selected
2332 .iter()
2333 .filter_map(|&i| self.options.get(i).map(|o| &o.value))
2334 .collect()
2335 }
2336
2337 pub fn id(&self) -> usize {
2339 self.id
2340 }
2341}
2342
2343impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for MultiSelect<T> {
2344 fn get_key(&self) -> &str {
2345 &self.key
2346 }
2347
2348 fn get_value(&self) -> Box<dyn Any> {
2349 let values: Vec<T> = self
2350 .selected
2351 .iter()
2352 .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2353 .collect();
2354 Box::new(values)
2355 }
2356
2357 fn error(&self) -> Option<&str> {
2358 self.error.as_deref()
2359 }
2360
2361 fn init(&mut self) -> Option<Cmd> {
2362 None
2363 }
2364
2365 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2366 if !self.focused {
2367 return None;
2368 }
2369
2370 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2371 self.error = None;
2372
2373 if self.filtering {
2375 if key_msg.key_type == KeyType::Esc {
2377 self.update_filter(String::new());
2378 return None;
2379 }
2380
2381 if key_msg.key_type == KeyType::Backspace {
2383 if !self.filter_value.is_empty() {
2384 let mut new_filter = self.filter_value.clone();
2385 new_filter.pop();
2386 self.update_filter(new_filter);
2387 }
2388 return None;
2389 }
2390
2391 if key_msg.key_type == KeyType::Runes {
2393 let mut new_filter = self.filter_value.clone();
2394 for c in &key_msg.runes {
2395 match c {
2398 'j' | 'k' | 'g' | 'G' | ' ' | 'x' | '/' => continue,
2399 _ => {}
2400 }
2401 if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
2402 new_filter.push(*c);
2403 }
2404 }
2405 if new_filter != self.filter_value {
2406 self.update_filter(new_filter);
2407 return None;
2408 }
2409 }
2410 }
2411
2412 if binding_matches(&self.keymap.prev, key_msg) {
2414 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2415 }
2416
2417 if binding_matches(&self.keymap.next, key_msg)
2419 || binding_matches(&self.keymap.submit, key_msg)
2420 {
2421 self.run_validation();
2422 if self.error.is_some() {
2423 return None;
2424 }
2425 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2426 }
2427
2428 if binding_matches(&self.keymap.toggle, key_msg) {
2430 self.toggle_current();
2431 }
2432
2433 if binding_matches(&self.keymap.select_all, key_msg) {
2435 if self.selected.len() == self.options.len() {
2436 self.select_none();
2437 } else {
2438 self.select_all();
2439 }
2440 }
2441
2442 if binding_matches(&self.keymap.up, key_msg) {
2444 if self.cursor > 0 {
2445 self.cursor -= 1;
2446 if self.cursor < self.offset {
2447 self.offset = self.cursor;
2448 }
2449 }
2450 } else if binding_matches(&self.keymap.down, key_msg) {
2451 let filtered = self.filtered_options();
2452 if self.cursor < filtered.len().saturating_sub(1) {
2453 self.cursor += 1;
2454 if self.cursor >= self.offset + self.height {
2455 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2456 }
2457 }
2458 } else if binding_matches(&self.keymap.goto_top, key_msg) {
2459 self.cursor = 0;
2460 self.offset = 0;
2461 } else if binding_matches(&self.keymap.goto_bottom, key_msg) {
2462 let filtered = self.filtered_options();
2463 self.cursor = filtered.len().saturating_sub(1);
2464 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2465 }
2466 }
2467
2468 None
2469 }
2470
2471 fn view(&self) -> String {
2472 let styles = self.active_styles();
2473 let mut output = String::new();
2474
2475 if !self.title.is_empty() {
2477 output.push_str(&styles.title.render(&self.title));
2478 output.push('\n');
2479 }
2480
2481 if !self.description.is_empty() {
2483 output.push_str(&styles.description.render(&self.description));
2484 output.push('\n');
2485 }
2486
2487 if self.filtering && !self.filter_value.is_empty() {
2489 let filter_display = format!("Filter: {}_", self.filter_value);
2490 output.push_str(&styles.description.render(&filter_display));
2491 output.push('\n');
2492 }
2493
2494 let filtered = self.filtered_options();
2496 let visible: Vec<_> = filtered
2497 .iter()
2498 .skip(self.offset)
2499 .take(self.height)
2500 .collect();
2501
2502 for (i, (idx, opt)) in visible.iter().enumerate() {
2504 let is_cursor = self.offset + i == self.cursor;
2505 let is_selected = self.selected.contains(idx);
2506
2507 if is_cursor {
2509 output.push_str(&styles.select_selector.render(""));
2510 } else {
2511 output.push_str(" ");
2512 }
2513
2514 let checkbox = if is_selected { "[x] " } else { "[ ] " };
2516 output.push_str(checkbox);
2517
2518 if is_cursor {
2520 output.push_str(&styles.selected_option.render(&opt.key));
2521 } else {
2522 output.push_str(&styles.option.render(&opt.key));
2523 }
2524
2525 output.push('\n');
2526 }
2527
2528 if !visible.is_empty() {
2530 output.pop();
2531 }
2532
2533 if self.error.is_some() {
2535 output.push_str(&styles.error_indicator.render(""));
2536 }
2537
2538 styles
2539 .base
2540 .width(self.width.try_into().unwrap_or(u16::MAX))
2541 .render(&output)
2542 }
2543
2544 fn focus(&mut self) -> Option<Cmd> {
2545 self.focused = true;
2546 None
2547 }
2548
2549 fn blur(&mut self) -> Option<Cmd> {
2550 self.focused = false;
2551 self.run_validation();
2552 None
2553 }
2554
2555 fn key_binds(&self) -> Vec<Binding> {
2556 vec![
2557 self.keymap.up.clone(),
2558 self.keymap.down.clone(),
2559 self.keymap.toggle.clone(),
2560 self.keymap.prev.clone(),
2561 self.keymap.submit.clone(),
2562 self.keymap.next.clone(),
2563 ]
2564 }
2565
2566 fn with_theme(&mut self, theme: &Theme) {
2567 if self.theme.is_none() {
2568 self.theme = Some(theme.clone());
2569 }
2570 }
2571
2572 fn with_keymap(&mut self, keymap: &KeyMap) {
2573 self.keymap = keymap.multi_select.clone();
2574 }
2575
2576 fn with_width(&mut self, width: usize) {
2577 self.width = width;
2578 }
2579
2580 fn with_height(&mut self, height: usize) {
2581 self.height = height;
2582 }
2583
2584 fn with_position(&mut self, position: FieldPosition) {
2585 self._position = position;
2586 }
2587}
2588
2589pub struct Confirm {
2595 id: usize,
2596 key: String,
2597 value: bool,
2598 title: String,
2599 description: String,
2600 affirmative: String,
2601 negative: String,
2602 focused: bool,
2603 width: usize,
2604 theme: Option<Theme>,
2605 keymap: ConfirmKeyMap,
2606 _position: FieldPosition,
2607}
2608
2609impl Default for Confirm {
2610 fn default() -> Self {
2611 Self::new()
2612 }
2613}
2614
2615impl Confirm {
2616 pub fn new() -> Self {
2618 Self {
2619 id: next_id(),
2620 key: String::new(),
2621 value: false,
2622 title: String::new(),
2623 description: String::new(),
2624 affirmative: "Yes".to_string(),
2625 negative: "No".to_string(),
2626 focused: false,
2627 width: 80,
2628 theme: None,
2629 keymap: ConfirmKeyMap::default(),
2630 _position: FieldPosition::default(),
2631 }
2632 }
2633
2634 pub fn key(mut self, key: impl Into<String>) -> Self {
2636 self.key = key.into();
2637 self
2638 }
2639
2640 pub fn value(mut self, value: bool) -> Self {
2642 self.value = value;
2643 self
2644 }
2645
2646 pub fn title(mut self, title: impl Into<String>) -> Self {
2648 self.title = title.into();
2649 self
2650 }
2651
2652 pub fn description(mut self, description: impl Into<String>) -> Self {
2654 self.description = description.into();
2655 self
2656 }
2657
2658 pub fn affirmative(mut self, text: impl Into<String>) -> Self {
2660 self.affirmative = text.into();
2661 self
2662 }
2663
2664 pub fn negative(mut self, text: impl Into<String>) -> Self {
2666 self.negative = text.into();
2667 self
2668 }
2669
2670 fn get_theme(&self) -> Theme {
2671 self.theme.clone().unwrap_or_else(theme_charm)
2672 }
2673
2674 fn active_styles(&self) -> FieldStyles {
2675 let theme = self.get_theme();
2676 if self.focused {
2677 theme.focused
2678 } else {
2679 theme.blurred
2680 }
2681 }
2682
2683 pub fn get_bool_value(&self) -> bool {
2685 self.value
2686 }
2687
2688 pub fn id(&self) -> usize {
2690 self.id
2691 }
2692}
2693
2694impl Field for Confirm {
2695 fn get_key(&self) -> &str {
2696 &self.key
2697 }
2698
2699 fn get_value(&self) -> Box<dyn Any> {
2700 Box::new(self.value)
2701 }
2702
2703 fn error(&self) -> Option<&str> {
2704 None
2705 }
2706
2707 fn init(&mut self) -> Option<Cmd> {
2708 None
2709 }
2710
2711 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2712 if !self.focused {
2713 return None;
2714 }
2715
2716 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2717 if binding_matches(&self.keymap.prev, key_msg) {
2719 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2720 }
2721
2722 if binding_matches(&self.keymap.next, key_msg)
2724 || binding_matches(&self.keymap.submit, key_msg)
2725 {
2726 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2727 }
2728
2729 if binding_matches(&self.keymap.toggle, key_msg) {
2731 self.value = !self.value;
2732 }
2733
2734 if binding_matches(&self.keymap.accept, key_msg) {
2736 self.value = true;
2737 }
2738 if binding_matches(&self.keymap.reject, key_msg) {
2739 self.value = false;
2740 }
2741 }
2742
2743 None
2744 }
2745
2746 fn view(&self) -> String {
2747 let styles = self.active_styles();
2748 let mut output = String::new();
2749
2750 if !self.title.is_empty() {
2752 output.push_str(&styles.title.render(&self.title));
2753 output.push('\n');
2754 }
2755
2756 if !self.description.is_empty() {
2758 output.push_str(&styles.description.render(&self.description));
2759 output.push('\n');
2760 }
2761
2762 if self.value {
2764 output.push_str(&styles.focused_button.render(&self.affirmative));
2765 output.push_str(&styles.blurred_button.render(&self.negative));
2766 } else {
2767 output.push_str(&styles.blurred_button.render(&self.affirmative));
2768 output.push_str(&styles.focused_button.render(&self.negative));
2769 }
2770
2771 styles
2772 .base
2773 .width(self.width.try_into().unwrap_or(u16::MAX))
2774 .render(&output)
2775 }
2776
2777 fn focus(&mut self) -> Option<Cmd> {
2778 self.focused = true;
2779 None
2780 }
2781
2782 fn blur(&mut self) -> Option<Cmd> {
2783 self.focused = false;
2784 None
2785 }
2786
2787 fn key_binds(&self) -> Vec<Binding> {
2788 vec![
2789 self.keymap.toggle.clone(),
2790 self.keymap.accept.clone(),
2791 self.keymap.reject.clone(),
2792 self.keymap.prev.clone(),
2793 self.keymap.submit.clone(),
2794 self.keymap.next.clone(),
2795 ]
2796 }
2797
2798 fn with_theme(&mut self, theme: &Theme) {
2799 if self.theme.is_none() {
2800 self.theme = Some(theme.clone());
2801 }
2802 }
2803
2804 fn with_keymap(&mut self, keymap: &KeyMap) {
2805 self.keymap = keymap.confirm.clone();
2806 }
2807
2808 fn with_width(&mut self, width: usize) {
2809 self.width = width;
2810 }
2811
2812 fn with_height(&mut self, _height: usize) {
2813 }
2815
2816 fn with_position(&mut self, position: FieldPosition) {
2817 self._position = position;
2818 }
2819}
2820
2821pub struct Note {
2827 id: usize,
2828 key: String,
2829 title: String,
2830 description: String,
2831 focused: bool,
2832 width: usize,
2833 theme: Option<Theme>,
2834 keymap: NoteKeyMap,
2835 _position: FieldPosition,
2836 next_label: String,
2837}
2838
2839impl Default for Note {
2840 fn default() -> Self {
2841 Self::new()
2842 }
2843}
2844
2845impl Note {
2846 pub fn new() -> Self {
2848 Self {
2849 id: next_id(),
2850 key: String::new(),
2851 title: String::new(),
2852 description: String::new(),
2853 focused: false,
2854 width: 80,
2855 theme: None,
2856 keymap: NoteKeyMap::default(),
2857 _position: FieldPosition::default(),
2858 next_label: "Next".to_string(),
2859 }
2860 }
2861
2862 pub fn key(mut self, key: impl Into<String>) -> Self {
2864 self.key = key.into();
2865 self
2866 }
2867
2868 pub fn title(mut self, title: impl Into<String>) -> Self {
2870 self.title = title.into();
2871 self
2872 }
2873
2874 pub fn description(mut self, description: impl Into<String>) -> Self {
2876 self.description = description.into();
2877 self
2878 }
2879
2880 pub fn next_label(mut self, label: impl Into<String>) -> Self {
2882 self.next_label = label.into();
2883 self
2884 }
2885
2886 pub fn next(self, label: impl Into<String>) -> Self {
2890 self.next_label(label)
2891 }
2892
2893 fn get_theme(&self) -> Theme {
2894 self.theme.clone().unwrap_or_else(theme_charm)
2895 }
2896
2897 fn active_styles(&self) -> FieldStyles {
2898 let theme = self.get_theme();
2899 if self.focused {
2900 theme.focused
2901 } else {
2902 theme.blurred
2903 }
2904 }
2905
2906 pub fn id(&self) -> usize {
2908 self.id
2909 }
2910}
2911
2912impl Field for Note {
2913 fn get_key(&self) -> &str {
2914 &self.key
2915 }
2916
2917 fn get_value(&self) -> Box<dyn Any> {
2918 Box::new(())
2919 }
2920
2921 fn error(&self) -> Option<&str> {
2922 None
2923 }
2924
2925 fn init(&mut self) -> Option<Cmd> {
2926 None
2927 }
2928
2929 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2930 if !self.focused {
2931 return None;
2932 }
2933
2934 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2935 if binding_matches(&self.keymap.prev, key_msg) {
2937 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2938 }
2939
2940 if binding_matches(&self.keymap.next, key_msg)
2942 || binding_matches(&self.keymap.submit, key_msg)
2943 {
2944 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2945 }
2946 }
2947
2948 None
2949 }
2950
2951 fn view(&self) -> String {
2952 let styles = self.active_styles();
2953 let mut output = String::new();
2954
2955 if !self.title.is_empty() {
2957 output.push_str(&styles.note_title.render(&self.title));
2958 output.push('\n');
2959 }
2960
2961 if !self.description.is_empty() {
2963 output.push_str(&styles.description.render(&self.description));
2964 }
2965
2966 styles
2967 .base
2968 .width(self.width.try_into().unwrap_or(u16::MAX))
2969 .render(&output)
2970 }
2971
2972 fn focus(&mut self) -> Option<Cmd> {
2973 self.focused = true;
2974 None
2975 }
2976
2977 fn blur(&mut self) -> Option<Cmd> {
2978 self.focused = false;
2979 None
2980 }
2981
2982 fn key_binds(&self) -> Vec<Binding> {
2983 vec![
2984 self.keymap.prev.clone(),
2985 self.keymap.submit.clone(),
2986 self.keymap.next.clone(),
2987 ]
2988 }
2989
2990 fn with_theme(&mut self, theme: &Theme) {
2991 if self.theme.is_none() {
2992 self.theme = Some(theme.clone());
2993 }
2994 }
2995
2996 fn with_keymap(&mut self, keymap: &KeyMap) {
2997 self.keymap = keymap.note.clone();
2998 }
2999
3000 fn with_width(&mut self, width: usize) {
3001 self.width = width;
3002 }
3003
3004 fn with_height(&mut self, _height: usize) {
3005 }
3007
3008 fn with_position(&mut self, position: FieldPosition) {
3009 self._position = position;
3010 }
3011}
3012
3013pub struct Text {
3035 id: usize,
3036 key: String,
3037 value: String,
3038 title: String,
3039 description: String,
3040 placeholder: String,
3041 lines: usize,
3042 char_limit: usize,
3043 show_line_numbers: bool,
3044 focused: bool,
3045 error: Option<String>,
3046 validate: Option<fn(&str) -> Option<String>>,
3047 width: usize,
3048 height: usize,
3049 theme: Option<Theme>,
3050 keymap: TextKeyMap,
3051 _position: FieldPosition,
3052 cursor_row: usize,
3053 cursor_col: usize,
3054}
3055
3056impl Default for Text {
3057 fn default() -> Self {
3058 Self::new()
3059 }
3060}
3061
3062impl Text {
3063 pub fn new() -> Self {
3065 Self {
3066 id: next_id(),
3067 key: String::new(),
3068 value: String::new(),
3069 title: String::new(),
3070 description: String::new(),
3071 placeholder: String::new(),
3072 lines: 5,
3073 char_limit: 0,
3074 show_line_numbers: false,
3075 focused: false,
3076 error: None,
3077 validate: None,
3078 width: 80,
3079 height: 0,
3080 theme: None,
3081 keymap: TextKeyMap::default(),
3082 _position: FieldPosition::default(),
3083 cursor_row: 0,
3084 cursor_col: 0,
3085 }
3086 }
3087
3088 pub fn key(mut self, key: impl Into<String>) -> Self {
3090 self.key = key.into();
3091 self
3092 }
3093
3094 pub fn value(mut self, value: impl Into<String>) -> Self {
3096 self.value = value.into();
3097 self
3098 }
3099
3100 pub fn title(mut self, title: impl Into<String>) -> Self {
3102 self.title = title.into();
3103 self
3104 }
3105
3106 pub fn description(mut self, description: impl Into<String>) -> Self {
3108 self.description = description.into();
3109 self
3110 }
3111
3112 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
3114 self.placeholder = placeholder.into();
3115 self
3116 }
3117
3118 pub fn lines(mut self, lines: usize) -> Self {
3120 self.lines = lines;
3121 self
3122 }
3123
3124 pub fn char_limit(mut self, limit: usize) -> Self {
3126 self.char_limit = limit;
3127 self
3128 }
3129
3130 pub fn show_line_numbers(mut self, show: bool) -> Self {
3132 self.show_line_numbers = show;
3133 self
3134 }
3135
3136 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3138 self.validate = Some(validate);
3139 self
3140 }
3141
3142 fn get_theme(&self) -> Theme {
3143 self.theme.clone().unwrap_or_else(theme_charm)
3144 }
3145
3146 fn active_styles(&self) -> FieldStyles {
3147 let theme = self.get_theme();
3148 if self.focused {
3149 theme.focused
3150 } else {
3151 theme.blurred
3152 }
3153 }
3154
3155 fn run_validation(&mut self) {
3156 if let Some(validate) = self.validate {
3157 self.error = validate(&self.value);
3158 }
3159 }
3160
3161 pub fn get_string_value(&self) -> &str {
3163 &self.value
3164 }
3165
3166 pub fn id(&self) -> usize {
3168 self.id
3169 }
3170
3171 fn visible_lines(&self) -> Vec<&str> {
3172 let lines: Vec<&str> = self.value.lines().collect();
3173 if lines.is_empty() { vec![""] } else { lines }
3174 }
3175
3176 fn transpose_left(&mut self) {
3182 let lines: Vec<String> = self.value.lines().map(String::from).collect();
3183 if self.cursor_row >= lines.len() {
3184 return;
3185 }
3186
3187 let line_chars: Vec<char> = lines[self.cursor_row].chars().collect();
3188
3189 if self.cursor_col == 0 || line_chars.len() < 2 {
3191 return;
3192 }
3193
3194 let mut col = self.cursor_col;
3195
3196 if col >= line_chars.len() {
3198 col = line_chars.len() - 1;
3199 self.cursor_col = col;
3200 }
3201
3202 let mut new_chars = line_chars;
3204 new_chars.swap(col - 1, col);
3205
3206 let mut new_lines = lines;
3208 new_lines[self.cursor_row] = new_chars.into_iter().collect();
3209 self.value = new_lines.join("\n");
3210
3211 let new_line_len = self
3213 .value
3214 .lines()
3215 .nth(self.cursor_row)
3216 .map(|l| l.chars().count())
3217 .unwrap_or(0);
3218 if self.cursor_col < new_line_len {
3219 self.cursor_col += 1;
3220 }
3221 }
3222
3223 fn do_word_right<F>(&mut self, mut f: F)
3228 where
3229 F: FnMut(usize, char) -> char,
3230 {
3231 let lines: Vec<String> = self.value.lines().map(String::from).collect();
3232 if self.cursor_row >= lines.len() {
3233 return;
3234 }
3235
3236 let mut chars: Vec<char> = lines[self.cursor_row].chars().collect();
3237 let len = chars.len();
3238
3239 while self.cursor_col < len && chars[self.cursor_col].is_whitespace() {
3241 self.cursor_col += 1;
3242 }
3243
3244 let mut char_idx = 0;
3246 while self.cursor_col < len && !chars[self.cursor_col].is_whitespace() {
3247 chars[self.cursor_col] = f(char_idx, chars[self.cursor_col]);
3248 self.cursor_col += 1;
3249 char_idx += 1;
3250 }
3251
3252 let mut new_lines = lines;
3254 new_lines[self.cursor_row] = chars.into_iter().collect();
3255 self.value = new_lines.join("\n");
3256 }
3257
3258 fn uppercase_right(&mut self) {
3260 self.do_word_right(|_, c| c.to_uppercase().next().unwrap_or(c));
3261 }
3262
3263 fn lowercase_right(&mut self) {
3265 self.do_word_right(|_, c| c.to_lowercase().next().unwrap_or(c));
3266 }
3267
3268 fn capitalize_right(&mut self) {
3270 self.do_word_right(|idx, c| {
3271 if idx == 0 {
3272 c.to_uppercase().next().unwrap_or(c)
3273 } else {
3274 c
3275 }
3276 });
3277 }
3278}
3279
3280impl Field for Text {
3281 fn get_key(&self) -> &str {
3282 &self.key
3283 }
3284
3285 fn get_value(&self) -> Box<dyn Any> {
3286 Box::new(self.value.clone())
3287 }
3288
3289 fn error(&self) -> Option<&str> {
3290 self.error.as_deref()
3291 }
3292
3293 fn init(&mut self) -> Option<Cmd> {
3294 None
3295 }
3296
3297 fn update(&mut self, msg: &Message) -> Option<Cmd> {
3298 if !self.focused {
3299 return None;
3300 }
3301
3302 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3303 self.error = None;
3304
3305 if binding_matches(&self.keymap.prev, key_msg) {
3307 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3308 }
3309
3310 if binding_matches(&self.keymap.next, key_msg)
3312 || binding_matches(&self.keymap.submit, key_msg)
3313 {
3314 self.run_validation();
3315 if self.error.is_some() {
3316 return None;
3317 }
3318 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3319 }
3320
3321 if binding_matches(&self.keymap.new_line, key_msg) {
3323 if self.char_limit == 0 || self.value.len() < self.char_limit {
3324 self.value.push('\n');
3325 self.cursor_row += 1;
3326 self.cursor_col = 0;
3327 }
3328 return None;
3329 }
3330
3331 if binding_matches(&self.keymap.uppercase_word_forward, key_msg) {
3333 self.uppercase_right();
3334 return None;
3335 }
3336 if binding_matches(&self.keymap.lowercase_word_forward, key_msg) {
3337 self.lowercase_right();
3338 return None;
3339 }
3340 if binding_matches(&self.keymap.capitalize_word_forward, key_msg) {
3341 self.capitalize_right();
3342 return None;
3343 }
3344 if binding_matches(&self.keymap.transpose_character_backward, key_msg) {
3345 self.transpose_left();
3346 return None;
3347 }
3348
3349 match key_msg.key_type {
3351 KeyType::Runes => {
3352 let current_count = self.value.chars().count();
3354 let available = if self.char_limit == 0 {
3355 usize::MAX
3356 } else {
3357 self.char_limit.saturating_sub(current_count)
3358 };
3359
3360 let chars_to_add: Vec<char> =
3363 key_msg.runes.iter().copied().take(available).collect();
3364
3365 for c in chars_to_add {
3366 self.value.push(c);
3367 if c == '\n' {
3368 self.cursor_row += 1;
3369 self.cursor_col = 0;
3370 } else {
3371 self.cursor_col += 1;
3372 }
3373 }
3374 }
3375 KeyType::Backspace => {
3376 if !self.value.is_empty() {
3377 let removed = self.value.pop();
3378 if removed == Some('\n') {
3379 self.cursor_row = self.cursor_row.saturating_sub(1);
3380 let lines = self.visible_lines();
3381 self.cursor_col =
3382 lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3383 } else {
3384 self.cursor_col = self.cursor_col.saturating_sub(1);
3385 }
3386 }
3387 }
3388 KeyType::Enter => {
3389 if self.char_limit == 0 || self.value.len() < self.char_limit {
3391 self.value.push('\n');
3392 self.cursor_row += 1;
3393 self.cursor_col = 0;
3394 }
3395 }
3396 KeyType::Up => {
3397 self.cursor_row = self.cursor_row.saturating_sub(1);
3398 }
3399 KeyType::Down => {
3400 let line_count = self.visible_lines().len();
3401 if self.cursor_row < line_count.saturating_sub(1) {
3402 self.cursor_row += 1;
3403 }
3404 }
3405 KeyType::Left => {
3406 if self.cursor_col > 0 {
3407 self.cursor_col -= 1;
3408 }
3409 }
3410 KeyType::Right => {
3411 let lines = self.visible_lines();
3412 let current_line_len = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3413 if self.cursor_col < current_line_len {
3414 self.cursor_col += 1;
3415 }
3416 }
3417 KeyType::Home => {
3418 self.cursor_col = 0;
3419 }
3420 KeyType::End => {
3421 let lines = self.visible_lines();
3422 self.cursor_col = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3423 }
3424 _ => {}
3425 }
3426 }
3427
3428 None
3429 }
3430
3431 fn view(&self) -> String {
3432 let styles = self.active_styles();
3433 let mut output = String::new();
3434
3435 if !self.title.is_empty() {
3437 output.push_str(&styles.title.render(&self.title));
3438 if self.error.is_some() {
3439 output.push_str(&styles.error_indicator.render(""));
3440 }
3441 output.push('\n');
3442 }
3443
3444 if !self.description.is_empty() {
3446 output.push_str(&styles.description.render(&self.description));
3447 output.push('\n');
3448 }
3449
3450 let lines = self.visible_lines();
3452 let visible_lines = self.lines.min(lines.len().max(1));
3453
3454 for (i, line) in lines.iter().take(visible_lines).enumerate() {
3455 if self.show_line_numbers {
3456 let line_num = format!("{:3} ", i + 1);
3457 output.push_str(&styles.description.render(&line_num));
3458 }
3459
3460 if line.is_empty() && i == 0 && self.value.is_empty() && !self.placeholder.is_empty() {
3461 output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
3462 } else {
3463 output.push_str(&styles.text_input.text.render(line));
3464 }
3465
3466 if i < visible_lines - 1 {
3467 output.push('\n');
3468 }
3469 }
3470
3471 for i in lines.len()..visible_lines {
3473 output.push('\n');
3474 if self.show_line_numbers {
3475 let line_num = format!("{:3} ", i + 1);
3476 output.push_str(&styles.description.render(&line_num));
3477 }
3478 }
3479
3480 if let Some(ref err) = self.error {
3482 output.push('\n');
3483 output.push_str(&styles.error_message.render(err));
3484 }
3485
3486 styles
3487 .base
3488 .width(self.width.try_into().unwrap_or(u16::MAX))
3489 .render(&output)
3490 }
3491
3492 fn focus(&mut self) -> Option<Cmd> {
3493 self.focused = true;
3494 None
3495 }
3496
3497 fn blur(&mut self) -> Option<Cmd> {
3498 self.focused = false;
3499 self.run_validation();
3500 None
3501 }
3502
3503 fn key_binds(&self) -> Vec<Binding> {
3504 vec![
3505 self.keymap.new_line.clone(),
3506 self.keymap.prev.clone(),
3507 self.keymap.submit.clone(),
3508 self.keymap.next.clone(),
3509 self.keymap.uppercase_word_forward.clone(),
3510 self.keymap.lowercase_word_forward.clone(),
3511 self.keymap.capitalize_word_forward.clone(),
3512 self.keymap.transpose_character_backward.clone(),
3513 ]
3514 }
3515
3516 fn with_theme(&mut self, theme: &Theme) {
3517 if self.theme.is_none() {
3518 self.theme = Some(theme.clone());
3519 }
3520 }
3521
3522 fn with_keymap(&mut self, keymap: &KeyMap) {
3523 self.keymap = keymap.text.clone();
3524 }
3525
3526 fn with_width(&mut self, width: usize) {
3527 self.width = width;
3528 }
3529
3530 fn with_height(&mut self, height: usize) {
3531 self.height = height;
3532 let adjust = if self.title.is_empty() { 0 } else { 1 }
3534 + if self.description.is_empty() { 0 } else { 1 };
3535 if height > adjust {
3536 self.lines = height - adjust;
3537 }
3538 }
3539
3540 fn with_position(&mut self, position: FieldPosition) {
3541 self._position = position;
3542 }
3543}
3544
3545pub struct FilePicker {
3568 id: usize,
3569 key: String,
3570 selected_path: Option<String>,
3571 title: String,
3572 description: String,
3573 current_directory: String,
3574 allowed_types: Vec<String>,
3575 show_hidden: bool,
3576 show_size: bool,
3577 show_permissions: bool,
3578 file_allowed: bool,
3579 dir_allowed: bool,
3580 picking: bool,
3581 focused: bool,
3582 error: Option<String>,
3583 validate: Option<fn(&str) -> Option<String>>,
3584 width: usize,
3585 height: usize,
3586 theme: Option<Theme>,
3587 keymap: FilePickerKeyMap,
3588 _position: FieldPosition,
3589 files: Vec<FileEntry>,
3591 selected_index: usize,
3592 offset: usize,
3593}
3594
3595#[derive(Debug, Clone)]
3597struct FileEntry {
3598 name: String,
3599 path: String,
3600 is_dir: bool,
3601 size: u64,
3602 #[allow(dead_code)]
3603 mode: String,
3604}
3605
3606impl Default for FilePicker {
3607 fn default() -> Self {
3608 Self::new()
3609 }
3610}
3611
3612impl FilePicker {
3613 pub fn new() -> Self {
3615 Self {
3616 id: next_id(),
3617 key: String::new(),
3618 selected_path: None,
3619 title: String::new(),
3620 description: String::new(),
3621 current_directory: ".".to_string(),
3622 allowed_types: Vec::new(),
3623 show_hidden: false,
3624 show_size: false,
3625 show_permissions: false,
3626 file_allowed: true,
3627 dir_allowed: false,
3628 picking: false,
3629 focused: false,
3630 error: None,
3631 validate: None,
3632 width: 80,
3633 height: 10,
3634 theme: None,
3635 keymap: FilePickerKeyMap::default(),
3636 _position: FieldPosition::default(),
3637 files: Vec::new(),
3638 selected_index: 0,
3639 offset: 0,
3640 }
3641 }
3642
3643 pub fn key(mut self, key: impl Into<String>) -> Self {
3645 self.key = key.into();
3646 self
3647 }
3648
3649 pub fn title(mut self, title: impl Into<String>) -> Self {
3651 self.title = title.into();
3652 self
3653 }
3654
3655 pub fn description(mut self, description: impl Into<String>) -> Self {
3657 self.description = description.into();
3658 self
3659 }
3660
3661 pub fn current_directory(mut self, dir: impl Into<String>) -> Self {
3663 self.current_directory = dir.into();
3664 self
3665 }
3666
3667 pub fn allowed_types(mut self, types: Vec<String>) -> Self {
3669 self.allowed_types = types;
3670 self
3671 }
3672
3673 pub fn show_hidden(mut self, show: bool) -> Self {
3675 self.show_hidden = show;
3676 self
3677 }
3678
3679 pub fn show_size(mut self, show: bool) -> Self {
3681 self.show_size = show;
3682 self
3683 }
3684
3685 pub fn show_permissions(mut self, show: bool) -> Self {
3687 self.show_permissions = show;
3688 self
3689 }
3690
3691 pub fn file_allowed(mut self, allowed: bool) -> Self {
3693 self.file_allowed = allowed;
3694 self
3695 }
3696
3697 pub fn dir_allowed(mut self, allowed: bool) -> Self {
3699 self.dir_allowed = allowed;
3700 self
3701 }
3702
3703 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3705 self.validate = Some(validate);
3706 self
3707 }
3708
3709 pub fn height_entries(mut self, height: usize) -> Self {
3711 self.height = height;
3712 self
3713 }
3714
3715 fn get_theme(&self) -> Theme {
3716 self.theme.clone().unwrap_or_else(theme_charm)
3717 }
3718
3719 fn active_styles(&self) -> FieldStyles {
3720 let theme = self.get_theme();
3721 if self.focused {
3722 theme.focused
3723 } else {
3724 theme.blurred
3725 }
3726 }
3727
3728 fn run_validation(&mut self) {
3729 if let Some(validate) = self.validate
3730 && let Some(ref path) = self.selected_path
3731 {
3732 self.error = validate(path);
3733 }
3734 }
3735
3736 fn read_directory(&mut self) {
3737 self.files.clear();
3738 self.selected_index = 0;
3739 self.offset = 0;
3740
3741 if self.current_directory != "/" {
3743 self.files.push(FileEntry {
3744 name: "..".to_string(),
3745 path: "..".to_string(),
3746 is_dir: true,
3747 size: 0,
3748 mode: String::new(),
3749 });
3750 }
3751
3752 if let Ok(entries) = std::fs::read_dir(&self.current_directory) {
3754 let mut entries: Vec<_> = entries
3755 .filter_map(|e| e.ok())
3756 .filter_map(|entry| {
3757 let name = entry.file_name().to_string_lossy().to_string();
3758
3759 if !self.show_hidden && name.starts_with('.') {
3761 return None;
3762 }
3763
3764 let metadata = entry.metadata().ok()?;
3765 let is_dir = metadata.is_dir();
3766 let size = metadata.len();
3767
3768 if !is_dir && !self.allowed_types.is_empty() {
3770 let matches = self.allowed_types.iter().any(|ext| {
3771 name.ends_with(ext)
3772 || name.ends_with(&ext.trim_start_matches('.').to_string())
3773 });
3774 if !matches {
3775 return None;
3776 }
3777 }
3778
3779 let path = entry.path().to_string_lossy().to_string();
3780
3781 Some(FileEntry {
3782 name,
3783 path,
3784 is_dir,
3785 size,
3786 mode: String::new(),
3787 })
3788 })
3789 .collect();
3790
3791 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
3793 (true, false) => std::cmp::Ordering::Less,
3794 (false, true) => std::cmp::Ordering::Greater,
3795 _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
3796 });
3797
3798 self.files.extend(entries);
3799 }
3800 }
3801
3802 fn is_selectable(&self, entry: &FileEntry) -> bool {
3803 if entry.is_dir {
3804 self.dir_allowed
3805 } else {
3806 self.file_allowed
3807 }
3808 }
3809
3810 fn format_size(size: u64) -> String {
3811 const KB: u64 = 1024;
3812 const MB: u64 = KB * 1024;
3813 const GB: u64 = MB * 1024;
3814
3815 if size >= GB {
3816 format!("{:.1}G", size as f64 / GB as f64)
3817 } else if size >= MB {
3818 format!("{:.1}M", size as f64 / MB as f64)
3819 } else if size >= KB {
3820 format!("{:.1}K", size as f64 / KB as f64)
3821 } else {
3822 format!("{}B", size)
3823 }
3824 }
3825
3826 pub fn get_selected_path(&self) -> Option<&str> {
3828 self.selected_path.as_deref()
3829 }
3830
3831 pub fn id(&self) -> usize {
3833 self.id
3834 }
3835}
3836
3837impl Field for FilePicker {
3838 fn get_key(&self) -> &str {
3839 &self.key
3840 }
3841
3842 fn get_value(&self) -> Box<dyn Any> {
3843 Box::new(self.selected_path.clone().unwrap_or_default())
3844 }
3845
3846 fn error(&self) -> Option<&str> {
3847 self.error.as_deref()
3848 }
3849
3850 fn init(&mut self) -> Option<Cmd> {
3851 self.read_directory();
3852 None
3853 }
3854
3855 fn update(&mut self, msg: &Message) -> Option<Cmd> {
3856 if !self.focused {
3857 return None;
3858 }
3859
3860 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3861 self.error = None;
3862
3863 if binding_matches(&self.keymap.prev, key_msg) {
3865 self.picking = false;
3866 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3867 }
3868
3869 if binding_matches(&self.keymap.next, key_msg) {
3871 self.picking = false;
3872 self.run_validation();
3873 if self.error.is_some() {
3874 return None;
3875 }
3876 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3877 }
3878
3879 if binding_matches(&self.keymap.close, key_msg) {
3881 if self.picking {
3882 self.picking = false;
3883 } else {
3884 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3885 }
3886 return None;
3887 }
3888
3889 if binding_matches(&self.keymap.open, key_msg) {
3891 if !self.picking {
3892 self.picking = true;
3893 self.read_directory();
3894 return None;
3895 }
3896
3897 if let Some(entry) = self.files.get(self.selected_index) {
3899 if entry.name == ".." {
3900 if let Some(parent) = std::path::Path::new(&self.current_directory).parent()
3902 {
3903 self.current_directory = parent.to_string_lossy().to_string();
3904 if self.current_directory.is_empty() {
3905 self.current_directory = "/".to_string();
3906 }
3907 self.read_directory();
3908 }
3909 } else if entry.is_dir {
3910 self.current_directory = entry.path.clone();
3912 self.read_directory();
3913 } else if self.is_selectable(entry) {
3914 self.selected_path = Some(entry.path.clone());
3916 self.picking = false;
3917 self.run_validation();
3918 if self.error.is_some() {
3919 return None;
3920 }
3921 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3922 }
3923 }
3924 return None;
3925 }
3926
3927 if self.picking && binding_matches(&self.keymap.back, key_msg) {
3929 if let Some(parent) = std::path::Path::new(&self.current_directory).parent() {
3930 self.current_directory = parent.to_string_lossy().to_string();
3931 if self.current_directory.is_empty() {
3932 self.current_directory = "/".to_string();
3933 }
3934 self.read_directory();
3935 }
3936 return None;
3937 }
3938
3939 if self.picking {
3941 if binding_matches(&self.keymap.up, key_msg) {
3942 if self.selected_index > 0 {
3943 self.selected_index -= 1;
3944 if self.selected_index < self.offset {
3945 self.offset = self.selected_index;
3946 }
3947 }
3948 } else if binding_matches(&self.keymap.down, key_msg) {
3949 if !self.files.is_empty()
3950 && self.selected_index < self.files.len().saturating_sub(1)
3951 {
3952 self.selected_index += 1;
3953 if self.height > 0 && self.selected_index >= self.offset + self.height {
3954 self.offset = self
3955 .selected_index
3956 .saturating_sub(self.height.saturating_sub(1));
3957 }
3958 }
3959 } else if binding_matches(&self.keymap.goto_top, key_msg) {
3960 self.selected_index = 0;
3961 self.offset = 0;
3962 } else if binding_matches(&self.keymap.goto_bottom, key_msg)
3963 && !self.files.is_empty()
3964 {
3965 self.selected_index = self.files.len().saturating_sub(1);
3966 self.offset = self
3967 .selected_index
3968 .saturating_sub(self.height.saturating_sub(1));
3969 }
3970 }
3971 }
3972
3973 None
3974 }
3975
3976 fn view(&self) -> String {
3977 let styles = self.active_styles();
3978 let mut output = String::new();
3979
3980 if !self.title.is_empty() {
3982 output.push_str(&styles.title.render(&self.title));
3983 if self.error.is_some() {
3984 output.push_str(&styles.error_indicator.render(""));
3985 }
3986 output.push('\n');
3987 }
3988
3989 if !self.description.is_empty() {
3991 output.push_str(&styles.description.render(&self.description));
3992 output.push('\n');
3993 }
3994
3995 if self.picking {
3996 let visible: Vec<_> = self
3998 .files
3999 .iter()
4000 .skip(self.offset)
4001 .take(self.height)
4002 .collect();
4003
4004 for (i, entry) in visible.iter().enumerate() {
4005 let idx = self.offset + i;
4006 let is_selected = idx == self.selected_index;
4007 let is_selectable = self.is_selectable(entry);
4008
4009 if is_selected {
4011 output.push_str(&styles.select_selector.render(""));
4012 } else {
4013 output.push_str(" ");
4014 }
4015
4016 let mut entry_str = String::new();
4018
4019 if entry.is_dir {
4021 entry_str.push_str("📁 ");
4022 } else {
4023 entry_str.push_str(" ");
4024 }
4025
4026 entry_str.push_str(&entry.name);
4027
4028 if self.show_size && !entry.is_dir {
4030 entry_str.push_str(&format!(" ({})", Self::format_size(entry.size)));
4031 }
4032
4033 if is_selected && is_selectable {
4034 output.push_str(&styles.selected_option.render(&entry_str));
4035 } else if !is_selectable && !entry.is_dir && entry.name != ".." {
4036 output.push_str(&styles.text_input.placeholder.render(&entry_str));
4037 } else {
4038 output.push_str(&styles.option.render(&entry_str));
4039 }
4040
4041 output.push('\n');
4042 }
4043
4044 if !visible.is_empty() {
4046 output.pop();
4047 }
4048
4049 output.push('\n');
4051 output.push_str(
4052 &styles
4053 .description
4054 .render(&format!("📂 {}", self.current_directory)),
4055 );
4056 } else {
4057 if let Some(ref path) = self.selected_path {
4059 output.push_str(&styles.selected_option.render(path));
4060 } else {
4061 output.push_str(
4062 &styles
4063 .text_input
4064 .placeholder
4065 .render("No file selected. Press Enter to browse."),
4066 );
4067 }
4068 }
4069
4070 if let Some(ref err) = self.error {
4072 output.push('\n');
4073 output.push_str(&styles.error_message.render(err));
4074 }
4075
4076 styles
4077 .base
4078 .width(self.width.try_into().unwrap_or(u16::MAX))
4079 .render(&output)
4080 }
4081
4082 fn focus(&mut self) -> Option<Cmd> {
4083 self.focused = true;
4084 None
4085 }
4086
4087 fn blur(&mut self) -> Option<Cmd> {
4088 self.focused = false;
4089 self.picking = false;
4090 self.run_validation();
4091 None
4092 }
4093
4094 fn key_binds(&self) -> Vec<Binding> {
4095 if self.picking {
4096 vec![
4097 self.keymap.up.clone(),
4098 self.keymap.down.clone(),
4099 self.keymap.open.clone(),
4100 self.keymap.back.clone(),
4101 self.keymap.close.clone(),
4102 ]
4103 } else {
4104 vec![
4105 self.keymap.open.clone(),
4106 self.keymap.prev.clone(),
4107 self.keymap.next.clone(),
4108 ]
4109 }
4110 }
4111
4112 fn with_theme(&mut self, theme: &Theme) {
4113 if self.theme.is_none() {
4114 self.theme = Some(theme.clone());
4115 }
4116 }
4117
4118 fn with_keymap(&mut self, keymap: &KeyMap) {
4119 self.keymap = keymap.file_picker.clone();
4120 }
4121
4122 fn with_width(&mut self, width: usize) {
4123 self.width = width;
4124 }
4125
4126 fn with_height(&mut self, height: usize) {
4127 self.height = height;
4128 }
4129
4130 fn with_position(&mut self, position: FieldPosition) {
4131 self._position = position;
4132 }
4133}
4134
4135pub struct Group {
4141 fields: Vec<Box<dyn Field>>,
4142 current: usize,
4143 title: String,
4144 description: String,
4145 width: usize,
4146 #[allow(dead_code)]
4147 height: usize,
4148 theme: Option<Theme>,
4149 keymap: Option<KeyMap>,
4150 hide: Option<Box<dyn Fn() -> bool + Send + Sync>>,
4151}
4152
4153impl Default for Group {
4154 fn default() -> Self {
4155 Self::new(Vec::new())
4156 }
4157}
4158
4159impl Group {
4160 pub fn new(fields: Vec<Box<dyn Field>>) -> Self {
4162 Self {
4163 fields,
4164 current: 0,
4165 title: String::new(),
4166 description: String::new(),
4167 width: 80,
4168 height: 0,
4169 theme: None,
4170 keymap: None,
4171 hide: None,
4172 }
4173 }
4174
4175 pub fn title(mut self, title: impl Into<String>) -> Self {
4177 self.title = title.into();
4178 self
4179 }
4180
4181 pub fn description(mut self, description: impl Into<String>) -> Self {
4183 self.description = description.into();
4184 self
4185 }
4186
4187 pub fn hide(mut self, hide: bool) -> Self {
4189 self.hide = Some(Box::new(move || hide));
4190 self
4191 }
4192
4193 pub fn hide_func<F: Fn() -> bool + Send + Sync + 'static>(mut self, f: F) -> Self {
4195 self.hide = Some(Box::new(f));
4196 self
4197 }
4198
4199 pub fn is_hidden(&self) -> bool {
4201 self.hide.as_ref().map(|f| f()).unwrap_or(false)
4202 }
4203
4204 pub fn current(&self) -> usize {
4206 self.current
4207 }
4208
4209 pub fn len(&self) -> usize {
4211 self.fields.len()
4212 }
4213
4214 pub fn is_empty(&self) -> bool {
4216 self.fields.is_empty()
4217 }
4218
4219 pub fn current_field(&self) -> Option<&dyn Field> {
4221 self.fields.get(self.current).map(|f| f.as_ref())
4222 }
4223
4224 pub fn current_field_mut(&mut self) -> Option<&mut Box<dyn Field>> {
4226 self.fields.get_mut(self.current)
4227 }
4228
4229 pub fn errors(&self) -> Vec<&str> {
4231 self.fields.iter().filter_map(|f| f.error()).collect()
4232 }
4233
4234 fn get_theme(&self) -> Theme {
4235 self.theme.clone().unwrap_or_else(theme_charm)
4236 }
4237
4238 pub fn header(&self) -> String {
4243 let theme = self.get_theme();
4244 let mut output = String::new();
4245
4246 if !self.title.is_empty() {
4247 output.push_str(&theme.group.title.render(&self.title));
4248 output.push('\n');
4249 }
4250
4251 if !self.description.is_empty() {
4252 output.push_str(&theme.group.description.render(&self.description));
4253 output.push('\n');
4254 }
4255
4256 output
4257 }
4258
4259 pub fn content(&self) -> String {
4264 let theme = self.get_theme();
4265 let mut output = String::new();
4266
4267 for (i, field) in self.fields.iter().enumerate() {
4268 output.push_str(&field.view());
4269 if i < self.fields.len() - 1 {
4270 output.push_str(&theme.field_separator.render(""));
4271 }
4272 }
4273
4274 output
4275 }
4276
4277 pub fn footer(&self) -> String {
4282 let theme = self.get_theme();
4283 let errors = self.errors();
4284
4285 if errors.is_empty() {
4286 return String::new();
4287 }
4288
4289 let error_text = errors.join(", ");
4290 theme.focused.error_message.render(&error_text)
4291 }
4292}
4293
4294impl Model for Group {
4295 fn init(&self) -> Option<Cmd> {
4296 None
4297 }
4298
4299 fn update(&mut self, msg: Message) -> Option<Cmd> {
4300 if msg.is::<NextFieldMsg>() {
4302 if self.current < self.fields.len().saturating_sub(1) {
4303 if let Some(field) = self.fields.get_mut(self.current) {
4304 field.blur();
4305 }
4306 self.current += 1;
4307 if let Some(field) = self.fields.get_mut(self.current) {
4308 return field.focus();
4309 }
4310 } else {
4311 return Some(Cmd::new(|| Message::new(NextGroupMsg)));
4312 }
4313 } else if msg.is::<PrevFieldMsg>() {
4314 if self.current > 0 {
4315 if let Some(field) = self.fields.get_mut(self.current) {
4316 field.blur();
4317 }
4318 self.current -= 1;
4319 if let Some(field) = self.fields.get_mut(self.current) {
4320 return field.focus();
4321 }
4322 } else {
4323 return Some(Cmd::new(|| Message::new(PrevGroupMsg)));
4324 }
4325 }
4326
4327 if let Some(field) = self.fields.get_mut(self.current) {
4329 return field.update(&msg);
4330 }
4331
4332 None
4333 }
4334
4335 fn view(&self) -> String {
4336 let theme = self.get_theme();
4337 let mut output = String::new();
4338
4339 if !self.title.is_empty() {
4341 output.push_str(&theme.group.title.render(&self.title));
4342 output.push('\n');
4343 }
4344
4345 if !self.description.is_empty() {
4347 output.push_str(&theme.group.description.render(&self.description));
4348 output.push('\n');
4349 }
4350
4351 for (i, field) in self.fields.iter().enumerate() {
4353 output.push_str(&field.view());
4354 if i < self.fields.len() - 1 {
4355 output.push_str(&theme.field_separator.render(""));
4356 }
4357 }
4358
4359 theme
4360 .group
4361 .base
4362 .width(self.width.try_into().unwrap_or(u16::MAX))
4363 .render(&output)
4364 }
4365}
4366
4367pub trait Layout: Send + Sync {
4379 fn view(&self, form: &Form) -> String;
4381
4382 fn group_width(&self, form: &Form, group_index: usize, total_width: usize) -> usize;
4384}
4385
4386#[derive(Debug, Clone, Default)]
4391pub struct LayoutDefault;
4392
4393impl Layout for LayoutDefault {
4394 fn view(&self, form: &Form) -> String {
4395 if let Some(group) = form.groups.get(form.current_group) {
4396 if group.is_hidden() {
4397 return String::new();
4398 }
4399 form.theme
4400 .form
4401 .base
4402 .clone()
4403 .width(form.width.try_into().unwrap_or(u16::MAX))
4404 .render(&group.view())
4405 } else {
4406 String::new()
4407 }
4408 }
4409
4410 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4411 form.width
4412 }
4413}
4414
4415#[derive(Debug, Clone, Default)]
4420pub struct LayoutStack;
4421
4422impl Layout for LayoutStack {
4423 fn view(&self, form: &Form) -> String {
4424 let mut output = String::new();
4425 let visible_groups: Vec<_> = form
4426 .groups
4427 .iter()
4428 .enumerate()
4429 .filter(|(_, g)| !g.is_hidden())
4430 .collect();
4431
4432 for (i, (_, group)) in visible_groups.iter().enumerate() {
4433 output.push_str(&group.view());
4434 if i < visible_groups.len() - 1 {
4435 output.push('\n');
4436 }
4437 }
4438
4439 form.theme
4440 .form
4441 .base
4442 .clone()
4443 .width(form.width.try_into().unwrap_or(u16::MAX))
4444 .render(&output)
4445 }
4446
4447 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4448 form.width
4449 }
4450}
4451
4452#[derive(Debug, Clone)]
4456pub struct LayoutColumns {
4457 columns: usize,
4458}
4459
4460impl LayoutColumns {
4461 pub fn new(columns: usize) -> Self {
4463 Self {
4464 columns: columns.max(1),
4465 }
4466 }
4467}
4468
4469impl Default for LayoutColumns {
4470 fn default() -> Self {
4471 Self::new(2)
4472 }
4473}
4474
4475impl Layout for LayoutColumns {
4476 fn view(&self, form: &Form) -> String {
4477 let visible_groups: Vec<_> = form
4478 .groups
4479 .iter()
4480 .enumerate()
4481 .filter(|(_, g)| !g.is_hidden())
4482 .collect();
4483
4484 if visible_groups.is_empty() {
4485 return String::new();
4486 }
4487
4488 let column_width = form.width / self.columns;
4489 let mut rows: Vec<String> = Vec::new();
4490
4491 for chunk in visible_groups.chunks(self.columns) {
4492 let mut row_parts: Vec<String> = Vec::new();
4493 for (_, group) in chunk {
4494 let group_view = group.view();
4496 let lines: Vec<&str> = group_view.lines().collect();
4498 let padded: Vec<String> = lines
4499 .iter()
4500 .map(|line| {
4501 let visual_width = lipgloss::width(line);
4502 if visual_width < column_width {
4503 format!("{}{}", line, " ".repeat(column_width - visual_width))
4504 } else {
4505 line.to_string()
4506 }
4507 })
4508 .collect();
4509 row_parts.push(padded.join("\n"));
4510 }
4511
4512 if row_parts.len() == 1 {
4514 rows.push(row_parts.into_iter().next().unwrap_or_default());
4516 } else {
4517 let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4518 rows.push(lipgloss::join_horizontal(
4519 lipgloss::Position::Top,
4520 &row_refs,
4521 ));
4522 }
4523 }
4524
4525 let output = rows.join("\n");
4526 form.theme
4527 .form
4528 .base
4529 .clone()
4530 .width(form.width.try_into().unwrap_or(u16::MAX))
4531 .render(&output)
4532 }
4533
4534 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4535 form.width / self.columns
4536 }
4537}
4538
4539#[derive(Debug, Clone)]
4544pub struct LayoutGrid {
4545 rows: usize,
4546 columns: usize,
4547}
4548
4549impl LayoutGrid {
4550 pub fn new(rows: usize, columns: usize) -> Self {
4552 Self {
4553 rows: rows.max(1),
4554 columns: columns.max(1),
4555 }
4556 }
4557}
4558
4559impl Default for LayoutGrid {
4560 fn default() -> Self {
4561 Self::new(2, 2)
4562 }
4563}
4564
4565impl Layout for LayoutGrid {
4566 fn view(&self, form: &Form) -> String {
4567 let visible_groups: Vec<_> = form
4568 .groups
4569 .iter()
4570 .enumerate()
4571 .filter(|(_, g)| !g.is_hidden())
4572 .collect();
4573
4574 if visible_groups.is_empty() {
4575 return String::new();
4576 }
4577
4578 let column_width = form.width / self.columns;
4579 let max_cells = self.rows * self.columns;
4580 let mut rows: Vec<String> = Vec::new();
4581
4582 for row_idx in 0..self.rows {
4583 let start = row_idx * self.columns;
4584 if start >= visible_groups.len() || start >= max_cells {
4585 break;
4586 }
4587 let end = (start + self.columns)
4588 .min(visible_groups.len())
4589 .min(max_cells);
4590
4591 let mut row_parts: Vec<String> = Vec::new();
4592 for (_, group) in &visible_groups[start..end] {
4593 let group_view = group.view();
4594 let lines: Vec<&str> = group_view.lines().collect();
4595 let padded: Vec<String> = lines
4596 .iter()
4597 .map(|line| {
4598 let visual_width = lipgloss::width(line);
4599 if visual_width < column_width {
4600 format!("{}{}", line, " ".repeat(column_width - visual_width))
4601 } else {
4602 line.to_string()
4603 }
4604 })
4605 .collect();
4606 row_parts.push(padded.join("\n"));
4607 }
4608
4609 if row_parts.len() == 1 {
4610 rows.push(row_parts.into_iter().next().unwrap_or_default());
4612 } else {
4613 let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4614 rows.push(lipgloss::join_horizontal(
4615 lipgloss::Position::Top,
4616 &row_refs,
4617 ));
4618 }
4619 }
4620
4621 let output = rows.join("\n");
4622 form.theme
4623 .form
4624 .base
4625 .clone()
4626 .width(form.width.try_into().unwrap_or(u16::MAX))
4627 .render(&output)
4628 }
4629
4630 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4631 form.width / self.columns
4632 }
4633}
4634
4635pub struct Form {
4641 groups: Vec<Group>,
4642 current_group: usize,
4643 state: FormState,
4644 width: usize,
4645 theme: Theme,
4646 keymap: KeyMap,
4647 layout: Box<dyn Layout>,
4648 show_help: bool,
4649 show_errors: bool,
4650 accessible: bool,
4651}
4652
4653impl Default for Form {
4654 fn default() -> Self {
4655 Self::new(Vec::new())
4656 }
4657}
4658
4659impl Form {
4660 pub fn new(groups: Vec<Group>) -> Self {
4662 Self {
4663 groups,
4664 current_group: 0,
4665 state: FormState::Normal,
4666 width: 80,
4667 theme: theme_charm(),
4668 keymap: KeyMap::default(),
4669 layout: Box::new(LayoutDefault),
4670 show_help: true,
4671 show_errors: true,
4672 accessible: false,
4673 }
4674 }
4675
4676 pub fn width(mut self, width: usize) -> Self {
4678 self.width = width;
4679 self
4680 }
4681
4682 pub fn theme(mut self, theme: Theme) -> Self {
4684 self.theme = theme;
4685 self
4686 }
4687
4688 pub fn keymap(mut self, keymap: KeyMap) -> Self {
4690 self.keymap = keymap;
4691 self
4692 }
4693
4694 pub fn layout<L: Layout + 'static>(mut self, layout: L) -> Self {
4705 self.layout = Box::new(layout);
4706 self
4707 }
4708
4709 pub fn show_help(mut self, show: bool) -> Self {
4711 self.show_help = show;
4712 self
4713 }
4714
4715 pub fn show_errors(mut self, show: bool) -> Self {
4717 self.show_errors = show;
4718 self
4719 }
4720
4721 pub fn with_accessible(mut self, accessible: bool) -> Self {
4737 self.accessible = accessible;
4738 self
4739 }
4740
4741 pub fn is_accessible(&self) -> bool {
4743 self.accessible
4744 }
4745
4746 pub fn state(&self) -> FormState {
4748 self.state
4749 }
4750
4751 pub fn current_group(&self) -> usize {
4753 self.current_group
4754 }
4755
4756 pub fn len(&self) -> usize {
4758 self.groups.len()
4759 }
4760
4761 pub fn is_empty(&self) -> bool {
4763 self.groups.is_empty()
4764 }
4765
4766 fn init_fields(&mut self) {
4768 for group in &mut self.groups {
4769 group.theme = Some(self.theme.clone());
4770 group.keymap = Some(self.keymap.clone());
4771 group.width = self.width;
4772 for field in &mut group.fields {
4773 field.with_theme(&self.theme);
4774 field.with_keymap(&self.keymap);
4775 field.with_width(self.width);
4776 }
4777 }
4778 }
4779
4780 fn next_group(&mut self) -> Option<Cmd> {
4781 loop {
4783 if self.current_group >= self.groups.len().saturating_sub(1) {
4784 self.state = FormState::Completed;
4785 return Some(bubbletea::quit());
4786 }
4787 self.current_group += 1;
4788 if !self.groups[self.current_group].is_hidden() {
4789 break;
4790 }
4791 }
4792 if let Some(group) = self.groups.get_mut(self.current_group) {
4794 group.current = 0;
4795 if let Some(field) = group.fields.get_mut(0) {
4796 return field.focus();
4797 }
4798 }
4799 None
4800 }
4801
4802 fn prev_group(&mut self) -> Option<Cmd> {
4803 loop {
4805 if self.current_group == 0 {
4806 return None;
4807 }
4808 self.current_group -= 1;
4809 if !self.groups[self.current_group].is_hidden() {
4810 break;
4811 }
4812 }
4813 if let Some(group) = self.groups.get_mut(self.current_group) {
4815 group.current = group.fields.len().saturating_sub(1);
4816 if let Some(field) = group.fields.last_mut() {
4817 return field.focus();
4818 }
4819 }
4820 None
4821 }
4822
4823 pub fn get_value(&self, key: &str) -> Option<Box<dyn Any>> {
4825 for group in &self.groups {
4826 for field in &group.fields {
4827 if field.get_key() == key {
4828 return Some(field.get_value());
4829 }
4830 }
4831 }
4832 None
4833 }
4834
4835 pub fn get_string(&self, key: &str) -> Option<String> {
4837 self.get_value(key)
4838 .and_then(|v| v.downcast::<String>().ok())
4839 .map(|v| *v)
4840 }
4841
4842 pub fn get_bool(&self, key: &str) -> Option<bool> {
4844 self.get_value(key)
4845 .and_then(|v| v.downcast::<bool>().ok())
4846 .map(|v| *v)
4847 }
4848
4849 pub fn all_errors(&self) -> Vec<String> {
4851 self.groups
4852 .iter()
4853 .flat_map(|g| g.errors())
4854 .map(|s| s.to_string())
4855 .collect()
4856 }
4857
4858 fn errors_view(&self) -> String {
4860 let errors = self.all_errors();
4861 if errors.is_empty() {
4862 return String::new();
4863 }
4864
4865 let error_text = errors.join(", ");
4866 self.theme.focused.error_message.render(&error_text)
4867 }
4868
4869 fn help_view(&self) -> String {
4871 let mut help_parts = Vec::new();
4873
4874 if let Some(group) = self.groups.get(self.current_group)
4876 && let Some(field) = group.fields.get(group.current)
4877 {
4878 for binding in field.key_binds() {
4879 let help = binding.get_help();
4880 if binding.enabled() && !help.desc.is_empty() {
4881 let keys = binding.get_keys();
4882 if !keys.is_empty() {
4883 help_parts.push(format!("{}: {}", keys.join("/"), help.desc));
4884 }
4885 }
4886 }
4887 }
4888
4889 let quit_help = self.keymap.quit.get_help();
4891 if self.keymap.quit.enabled() && !quit_help.desc.is_empty() {
4892 let keys = self.keymap.quit.get_keys();
4893 if !keys.is_empty() {
4894 help_parts.push(format!("{}: {}", keys.join("/"), quit_help.desc));
4895 }
4896 }
4897
4898 if help_parts.is_empty() {
4899 return String::new();
4900 }
4901
4902 let help_text = help_parts.join(" • ");
4904 self.theme.help.render(&help_text)
4905 }
4906
4907 pub fn group_width(&self, group_index: usize) -> usize {
4909 self.layout.group_width(self, group_index, self.width)
4910 }
4911}
4912
4913impl Model for Form {
4914 fn init(&self) -> Option<Cmd> {
4915 None
4916 }
4917
4918 fn update(&mut self, msg: Message) -> Option<Cmd> {
4919 if self.state == FormState::Normal && self.current_group == 0 {
4921 self.init_fields();
4922 if let Some(group) = self.groups.get_mut(0)
4924 && let Some(field) = group.fields.get_mut(0)
4925 {
4926 field.focus();
4927 }
4928 }
4929
4930 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>()
4932 && binding_matches(&self.keymap.quit, key_msg)
4933 {
4934 self.state = FormState::Aborted;
4935 return Some(bubbletea::quit());
4936 }
4937
4938 if msg.is::<NextGroupMsg>() {
4940 return self.next_group();
4941 } else if msg.is::<PrevGroupMsg>() {
4942 return self.prev_group();
4943 }
4944
4945 if let Some(group) = self.groups.get_mut(self.current_group) {
4947 return group.update(msg);
4948 }
4949
4950 None
4951 }
4952
4953 fn view(&self) -> String {
4954 let mut output = self.layout.view(self);
4955
4956 if self.show_help {
4958 let help_text = self.help_view();
4959 if !help_text.is_empty() {
4960 output.push('\n');
4961 output.push_str(&help_text);
4962 }
4963 }
4964
4965 if self.show_errors {
4967 let errors = self.errors_view();
4968 if !errors.is_empty() {
4969 output.push('\n');
4970 output.push_str(&errors);
4971 }
4972 }
4973
4974 output
4975 }
4976}
4977
4978pub fn validate_required(_field_name: &'static str) -> fn(&str) -> Option<String> {
5006 |s| {
5007 if s.trim().is_empty() {
5008 Some("field is required".to_string())
5009 } else {
5010 None
5011 }
5012 }
5013}
5014
5015pub fn validate_required_name() -> fn(&str) -> Option<String> {
5017 |s| {
5018 if s.trim().is_empty() {
5019 Some("name is required".to_string())
5020 } else {
5021 None
5022 }
5023 }
5024}
5025
5026pub fn validate_min_length_8() -> fn(&str) -> Option<String> {
5030 |s| {
5031 if s.chars().count() < 8 {
5032 Some("password must be at least 8 characters".to_string())
5033 } else {
5034 None
5035 }
5036 }
5037}
5038
5039pub fn validate_email() -> fn(&str) -> Option<String> {
5042 |s| {
5043 if s.is_empty() {
5044 return Some("email is required".to_string());
5045 }
5046 let parts: Vec<&str> = s.split('@').collect();
5049 if parts.len() != 2 {
5050 return Some("invalid email address".to_string());
5051 }
5052 let (local, domain) = (parts[0], parts[1]);
5053 if local.is_empty() || domain.is_empty() || !domain.contains('.') {
5054 return Some("invalid email address".to_string());
5055 }
5056 let domain_parts: Vec<&str> = domain.split('.').collect();
5058 if domain_parts.len() < 2 || domain_parts.iter().any(|p| p.is_empty()) {
5059 return Some("invalid email address".to_string());
5060 }
5061 None
5062 }
5063}
5064
5065#[cfg(test)]
5070mod tests {
5071 use super::*;
5072
5073 #[test]
5074 fn test_form_error_display() {
5075 let err = FormError::UserAborted;
5076 assert_eq!(format!("{}", err), "user aborted");
5077
5078 let err = FormError::Validation("invalid input".to_string());
5079 assert_eq!(format!("{}", err), "validation error: invalid input");
5080 }
5081
5082 #[test]
5083 fn test_form_state_default() {
5084 let state = FormState::default();
5085 assert_eq!(state, FormState::Normal);
5086 }
5087
5088 #[test]
5089 fn test_select_option() {
5090 let opt = SelectOption::new("Red", "red".to_string());
5091 assert_eq!(opt.key, "Red");
5092 assert_eq!(opt.value, "red");
5093 assert!(!opt.selected);
5094
5095 let opt = opt.selected(true);
5096 assert!(opt.selected);
5097 }
5098
5099 #[test]
5100 fn test_new_options() {
5101 let opts = new_options(["apple", "banana", "cherry"]);
5102 assert_eq!(opts.len(), 3);
5103 assert_eq!(opts[0].key, "apple");
5104 assert_eq!(opts[0].value, "apple");
5105 }
5106
5107 #[test]
5108 fn test_input_builder() {
5109 let input = Input::new()
5110 .key("name")
5111 .title("Name")
5112 .description("Enter your name")
5113 .placeholder("John Doe")
5114 .value("Jane");
5115
5116 assert_eq!(input.get_key(), "name");
5117 assert_eq!(input.get_string_value(), "Jane");
5118 }
5119
5120 #[test]
5121 fn test_confirm_builder() {
5122 let confirm = Confirm::new()
5123 .key("agree")
5124 .title("Terms")
5125 .affirmative("I Agree")
5126 .negative("I Disagree")
5127 .value(true);
5128
5129 assert_eq!(confirm.get_key(), "agree");
5130 assert!(confirm.get_bool_value());
5131 }
5132
5133 #[test]
5134 fn test_note_builder() {
5135 let note = Note::new()
5136 .key("info")
5137 .title("Information")
5138 .description("This is an informational note.");
5139
5140 assert_eq!(note.get_key(), "info");
5141 }
5142
5143 #[test]
5144 fn test_text_builder() {
5145 let text = Text::new()
5146 .key("bio")
5147 .title("Biography")
5148 .description("Tell us about yourself")
5149 .placeholder("Enter your bio...")
5150 .lines(10)
5151 .value("Hello world");
5152
5153 assert_eq!(text.get_key(), "bio");
5154 assert_eq!(text.get_string_value(), "Hello world");
5155 }
5156
5157 #[test]
5158 fn test_text_char_limit() {
5159 let text = Text::new().char_limit(50).show_line_numbers(true);
5160
5161 assert_eq!(text.char_limit, 50);
5162 assert!(text.show_line_numbers);
5163 }
5164
5165 #[test]
5166 fn test_filepicker_builder() {
5167 let picker = FilePicker::new()
5168 .key("config_file")
5169 .title("Select Configuration")
5170 .description("Choose a file")
5171 .current_directory("/tmp")
5172 .show_hidden(true)
5173 .file_allowed(true)
5174 .dir_allowed(false);
5175
5176 assert_eq!(picker.get_key(), "config_file");
5177 assert!(picker.file_allowed);
5178 assert!(!picker.dir_allowed);
5179 assert!(picker.show_hidden);
5180 }
5181
5182 #[test]
5183 fn test_filepicker_allowed_types() {
5184 let picker = FilePicker::new()
5185 .allowed_types(vec![".toml".to_string(), ".json".to_string()])
5186 .show_size(true);
5187
5188 assert_eq!(picker.allowed_types.len(), 2);
5189 assert!(picker.show_size);
5190 }
5191
5192 #[test]
5193 fn test_select_builder() {
5194 let select: Select<String> =
5195 Select::new()
5196 .key("color")
5197 .title("Favorite Color")
5198 .options(vec![
5199 SelectOption::new("Red", "red".to_string()),
5200 SelectOption::new("Green", "green".to_string()).selected(true),
5201 SelectOption::new("Blue", "blue".to_string()),
5202 ]);
5203
5204 assert_eq!(select.get_key(), "color");
5205 assert_eq!(select.get_selected_value(), Some(&"green".to_string()));
5206 }
5207
5208 #[test]
5209 fn test_theme_base() {
5210 let theme = theme_base();
5211 assert!(!theme.focused.title.value().is_empty() || theme.focused.title.value().is_empty());
5212 }
5213
5214 #[test]
5215 fn test_theme_charm() {
5216 let theme = theme_charm();
5217 let _ = theme.focused.title.render("Test");
5219 }
5220
5221 #[test]
5222 fn test_theme_dracula() {
5223 let theme = theme_dracula();
5224 let _ = theme.focused.title.render("Test");
5225 }
5226
5227 #[test]
5228 fn test_theme_base16() {
5229 let theme = theme_base16();
5230 let _ = theme.focused.title.render("Test");
5231 }
5232
5233 #[test]
5234 fn test_theme_catppuccin() {
5235 let theme = theme_catppuccin();
5236 let _ = theme.focused.title.render("Test");
5238 let _ = theme.focused.selected_option.render("Selected");
5239 let _ = theme.focused.focused_button.render("OK");
5240 let _ = theme.blurred.title.render("Blurred");
5241 }
5242
5243 #[test]
5244 fn test_keymap_default() {
5245 let keymap = KeyMap::default();
5246 assert!(keymap.quit.enabled());
5247 assert!(keymap.input.next.enabled());
5248 }
5249
5250 #[test]
5251 fn test_field_position() {
5252 let pos = FieldPosition {
5253 group: 0,
5254 field: 0,
5255 first_field: 0,
5256 last_field: 2,
5257 group_count: 2,
5258 first_group: 0,
5259 last_group: 1,
5260 };
5261 assert!(pos.is_first());
5262 assert!(!pos.is_last());
5263 }
5264
5265 #[test]
5266 fn test_group_basic() {
5267 let group = Group::new(vec![
5268 Box::new(Input::new().key("name").title("Name")),
5269 Box::new(Input::new().key("email").title("Email")),
5270 ]);
5271
5272 assert_eq!(group.len(), 2);
5273 assert!(!group.is_empty());
5274 assert_eq!(group.current(), 0);
5275 }
5276
5277 #[test]
5278 fn test_group_hide() {
5279 let group = Group::new(Vec::new()).hide(true);
5280 assert!(group.is_hidden());
5281
5282 let group = Group::new(Vec::new()).hide(false);
5283 assert!(!group.is_hidden());
5284 }
5285
5286 #[test]
5287 fn test_form_basic() {
5288 let form = Form::new(vec![Group::new(vec![Box::new(Input::new().key("name"))])]);
5289
5290 assert_eq!(form.len(), 1);
5291 assert!(!form.is_empty());
5292 assert_eq!(form.state(), FormState::Normal);
5293 }
5294
5295 #[test]
5296 fn test_input_echo_mode() {
5297 let input = Input::new().password(true);
5298 assert_eq!(input.echo_mode, EchoMode::Password);
5299
5300 let input = Input::new().echo_mode(EchoMode::None);
5301 assert_eq!(input.echo_mode, EchoMode::None);
5302 }
5303
5304 #[test]
5305 fn test_key_to_string() {
5306 let key = KeyMsg {
5307 key_type: KeyType::Enter,
5308 runes: vec![],
5309 alt: false,
5310 paste: false,
5311 };
5312 assert_eq!(key.to_string(), "enter");
5313
5314 let key = KeyMsg {
5315 key_type: KeyType::Runes,
5316 runes: vec!['a'],
5317 alt: false,
5318 paste: false,
5319 };
5320 assert_eq!(key.to_string(), "a");
5321
5322 let key = KeyMsg {
5323 key_type: KeyType::CtrlC,
5324 runes: vec![],
5325 alt: false,
5326 paste: false,
5327 };
5328 assert_eq!(key.to_string(), "ctrl+c");
5329 }
5330
5331 #[test]
5332 fn test_input_view() {
5333 let input = Input::new()
5334 .title("Name")
5335 .placeholder("Enter name")
5336 .value("");
5337
5338 let view = input.view();
5339 assert!(view.contains("Name"));
5340 }
5341
5342 #[test]
5343 fn test_confirm_view() {
5344 let confirm = Confirm::new()
5345 .title("Proceed?")
5346 .affirmative("Yes")
5347 .negative("No");
5348
5349 let view = confirm.view();
5350 assert!(view.contains("Proceed"));
5351 }
5352
5353 #[test]
5354 fn test_select_view() {
5355 let select: Select<String> = Select::new().title("Choose").options(vec![
5356 SelectOption::new("A", "a".to_string()),
5357 SelectOption::new("B", "b".to_string()),
5358 ]);
5359
5360 let view = select.view();
5361 assert!(view.contains("Choose"));
5362 }
5363
5364 #[test]
5365 fn test_note_view() {
5366 let note = Note::new().title("Info").description("Some information");
5367
5368 let view = note.view();
5369 assert!(view.contains("Info"));
5370 }
5371
5372 #[test]
5373 fn test_multiselect_view() {
5374 let multi: MultiSelect<String> = MultiSelect::new().title("Select items").options(vec![
5375 SelectOption::new("A", "a".to_string()),
5376 SelectOption::new("B", "b".to_string()).selected(true),
5377 SelectOption::new("C", "c".to_string()),
5378 ]);
5379
5380 let view = multi.view();
5381 assert!(view.contains("Select items"));
5382 }
5383
5384 #[test]
5385 fn test_multiselect_initial_selection() {
5386 let multi: MultiSelect<String> = MultiSelect::new().options(vec![
5387 SelectOption::new("A", "a".to_string()),
5388 SelectOption::new("B", "b".to_string()).selected(true),
5389 SelectOption::new("C", "c".to_string()).selected(true),
5390 ]);
5391
5392 let selected = multi.get_selected_values();
5393 assert_eq!(selected.len(), 2);
5394 assert!(selected.contains(&&"b".to_string()));
5395 assert!(selected.contains(&&"c".to_string()));
5396 }
5397
5398 #[test]
5399 fn test_multiselect_limit() {
5400 let mut multi: MultiSelect<String> = MultiSelect::new().limit(2).options(vec![
5401 SelectOption::new("A", "a".to_string()),
5402 SelectOption::new("B", "b".to_string()),
5403 SelectOption::new("C", "c".to_string()),
5404 ]);
5405
5406 multi.focus();
5408
5409 let toggle_msg = Message::new(KeyMsg {
5411 key_type: KeyType::Runes,
5412 runes: vec![' '],
5413 alt: false,
5414 paste: false,
5415 });
5416 multi.update(&toggle_msg);
5417 assert_eq!(multi.get_selected_values().len(), 1);
5418
5419 let down_msg = Message::new(KeyMsg {
5421 key_type: KeyType::Down,
5422 runes: vec![],
5423 alt: false,
5424 paste: false,
5425 });
5426 multi.update(&down_msg);
5427 multi.update(&toggle_msg);
5428 assert_eq!(multi.get_selected_values().len(), 2);
5429
5430 multi.update(&down_msg);
5432 multi.update(&toggle_msg);
5433 assert_eq!(multi.get_selected_values().len(), 2);
5435 }
5436
5437 #[test]
5438 fn test_input_unicode_cursor_handling() {
5439 let mut input = Input::new().value("café"); input.focus();
5444
5445 assert_eq!(input.cursor_pos, 4);
5447 assert_eq!(input.value.chars().count(), 4);
5448
5449 let end_msg = Message::new(KeyMsg {
5451 key_type: KeyType::End,
5452 runes: vec![],
5453 alt: false,
5454 paste: false,
5455 });
5456 input.update(&end_msg);
5457 assert_eq!(input.cursor_pos, 4);
5458
5459 let left_msg = Message::new(KeyMsg {
5461 key_type: KeyType::Left,
5462 runes: vec![],
5463 alt: false,
5464 paste: false,
5465 });
5466 input.update(&left_msg);
5467 assert_eq!(input.cursor_pos, 3);
5468
5469 let backspace_msg = Message::new(KeyMsg {
5471 key_type: KeyType::Backspace,
5472 runes: vec![],
5473 alt: false,
5474 paste: false,
5475 });
5476 input.update(&backspace_msg);
5477 assert_eq!(input.get_string_value(), "caé");
5478 assert_eq!(input.cursor_pos, 2);
5479
5480 let insert_msg = Message::new(KeyMsg {
5482 key_type: KeyType::Runes,
5483 runes: vec!['ñ'], alt: false,
5485 paste: false,
5486 });
5487 input.update(&insert_msg);
5488 assert_eq!(input.get_string_value(), "cañé");
5489 assert_eq!(input.cursor_pos, 3);
5490
5491 let delete_msg = Message::new(KeyMsg {
5493 key_type: KeyType::Delete,
5494 runes: vec![],
5495 alt: false,
5496 paste: false,
5497 });
5498 input.update(&delete_msg);
5499 assert_eq!(input.get_string_value(), "cañ");
5500
5501 let home_msg = Message::new(KeyMsg {
5503 key_type: KeyType::Home,
5504 runes: vec![],
5505 alt: false,
5506 paste: false,
5507 });
5508 input.update(&home_msg);
5509 assert_eq!(input.cursor_pos, 0);
5510 }
5511
5512 #[test]
5513 fn test_input_char_limit_with_unicode() {
5514 let mut input = Input::new().char_limit(5);
5516 input.focus();
5517
5518 let chars = ['日', '本', '語', '文', '字']; for c in chars {
5521 let msg = Message::new(KeyMsg {
5522 key_type: KeyType::Runes,
5523 runes: vec![c],
5524 alt: false,
5525 paste: false,
5526 });
5527 input.update(&msg);
5528 }
5529
5530 assert_eq!(input.value.chars().count(), 5);
5532 assert_eq!(input.get_string_value(), "日本語文字");
5533
5534 let msg = Message::new(KeyMsg {
5536 key_type: KeyType::Runes,
5537 runes: vec!['!'],
5538 alt: false,
5539 paste: false,
5540 });
5541 input.update(&msg);
5542
5543 assert_eq!(input.value.chars().count(), 5);
5545 }
5546
5547 #[test]
5548 fn test_layout_default() {
5549 let _layout = LayoutDefault;
5550 }
5552
5553 #[test]
5554 fn test_layout_stack() {
5555 let _layout = LayoutStack;
5556 }
5558
5559 #[test]
5560 fn test_layout_columns() {
5561 let layout = LayoutColumns::new(3);
5562 assert_eq!(layout.columns, 3);
5563
5564 let layout = LayoutColumns::new(0);
5566 assert_eq!(layout.columns, 1);
5567 }
5568
5569 #[test]
5570 fn test_layout_grid() {
5571 let layout = LayoutGrid::new(2, 3);
5572 assert_eq!(layout.rows, 2);
5573 assert_eq!(layout.columns, 3);
5574
5575 let layout = LayoutGrid::new(0, 0);
5577 assert_eq!(layout.rows, 1);
5578 assert_eq!(layout.columns, 1);
5579 }
5580
5581 #[test]
5582 fn test_layout_columns_view_single_empty_group_no_panic() {
5583 let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutColumns::new(1));
5584 let _ = form.view();
5585 }
5586
5587 #[test]
5588 fn test_layout_grid_view_single_empty_group_no_panic() {
5589 let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutGrid::new(1, 1));
5590 let _ = form.view();
5591 }
5592
5593 #[test]
5594 fn test_form_with_layout() {
5595 let form = Form::new(vec![
5596 Group::new(vec![Box::new(Input::new().key("a"))]),
5597 Group::new(vec![Box::new(Input::new().key("b"))]),
5598 ])
5599 .layout(LayoutColumns::new(2));
5600
5601 assert_eq!(form.len(), 2);
5603 }
5604
5605 #[test]
5606 fn test_form_show_help() {
5607 let form = Form::new(Vec::new()).show_help(false).show_errors(false);
5608
5609 assert!(!form.show_help);
5611 assert!(!form.show_errors);
5612 }
5613
5614 #[test]
5615 fn test_group_header_footer_content() {
5616 let group = Group::new(vec![Box::new(Input::new().key("test").title("Test Input"))])
5617 .title("Group Title")
5618 .description("Group Description");
5619
5620 let header = group.header();
5621 assert!(header.contains("Group Title"));
5622 assert!(header.contains("Group Description"));
5623
5624 let content = group.content();
5625 assert!(content.contains("Test Input"));
5626
5627 let footer = group.footer();
5628 assert_eq!(footer, "");
5630 }
5631
5632 #[test]
5633 fn test_form_all_errors() {
5634 let form = Form::new(vec![Group::new(Vec::new())]);
5635
5636 let errors = form.all_errors();
5638 assert_eq!(errors, Vec::<String>::new());
5639 }
5640
5641 #[test]
5644 fn test_text_transpose_left() {
5645 let mut text = Text::new().value("hello");
5646 text.cursor_row = 0;
5647 text.cursor_col = 5; text.transpose_left();
5650
5651 assert_eq!(text.get_string_value(), "helol");
5653 assert_eq!(text.cursor_col, 5); }
5655
5656 #[test]
5657 fn test_text_transpose_left_middle() {
5658 let mut text = Text::new().value("hello");
5659 text.cursor_row = 0;
5660 text.cursor_col = 2; text.transpose_left();
5663
5664 assert_eq!(text.get_string_value(), "hlelo");
5666 assert_eq!(text.cursor_col, 3); }
5668
5669 #[test]
5670 fn test_text_transpose_left_at_beginning() {
5671 let mut text = Text::new().value("hello");
5672 text.cursor_row = 0;
5673 text.cursor_col = 0; text.transpose_left();
5676
5677 assert_eq!(text.get_string_value(), "hello");
5679 assert_eq!(text.cursor_col, 0);
5680 }
5681
5682 #[test]
5683 fn test_text_uppercase_right() {
5684 let mut text = Text::new().value("hello world");
5685 text.cursor_row = 0;
5686 text.cursor_col = 0; text.uppercase_right();
5689
5690 assert_eq!(text.get_string_value(), "HELLO world");
5691 assert_eq!(text.cursor_col, 5); }
5693
5694 #[test]
5695 fn test_text_uppercase_right_with_spaces() {
5696 let mut text = Text::new().value(" hello world");
5697 text.cursor_row = 0;
5698 text.cursor_col = 0; text.uppercase_right();
5701
5702 assert_eq!(text.get_string_value(), " HELLO world");
5704 assert_eq!(text.cursor_col, 7); }
5706
5707 #[test]
5708 fn test_text_lowercase_right() {
5709 let mut text = Text::new().value("HELLO WORLD");
5710 text.cursor_row = 0;
5711 text.cursor_col = 0;
5712
5713 text.lowercase_right();
5714
5715 assert_eq!(text.get_string_value(), "hello WORLD");
5716 assert_eq!(text.cursor_col, 5);
5717 }
5718
5719 #[test]
5720 fn test_text_capitalize_right() {
5721 let mut text = Text::new().value("hello world");
5722 text.cursor_row = 0;
5723 text.cursor_col = 0;
5724
5725 text.capitalize_right();
5726
5727 assert_eq!(text.get_string_value(), "Hello world");
5729 assert_eq!(text.cursor_col, 5);
5730 }
5731
5732 #[test]
5733 fn test_text_capitalize_right_already_upper() {
5734 let mut text = Text::new().value("HELLO WORLD");
5735 text.cursor_row = 0;
5736 text.cursor_col = 0;
5737
5738 text.capitalize_right();
5739
5740 assert_eq!(text.get_string_value(), "HELLO WORLD");
5742 assert_eq!(text.cursor_col, 5);
5743 }
5744
5745 #[test]
5746 fn test_text_word_ops_multiline() {
5747 let mut text = Text::new().value("hello\nworld");
5748 text.cursor_row = 1;
5749 text.cursor_col = 0;
5750
5751 text.uppercase_right();
5752
5753 assert_eq!(text.get_string_value(), "hello\nWORLD");
5755 assert_eq!(text.cursor_row, 1);
5756 assert_eq!(text.cursor_col, 5);
5757 }
5758
5759 #[test]
5760 fn test_text_transpose_multiline() {
5761 let mut text = Text::new().value("ab\ncd");
5762 text.cursor_row = 1;
5763 text.cursor_col = 2; text.transpose_left();
5766
5767 assert_eq!(text.get_string_value(), "ab\ndc");
5769 }
5770
5771 #[test]
5772 fn test_text_word_ops_unicode() {
5773 let mut text = Text::new().value("café résumé");
5774 text.cursor_row = 0;
5775 text.cursor_col = 0;
5776
5777 text.uppercase_right();
5778
5779 assert_eq!(text.get_string_value(), "CAFÉ résumé");
5780 assert_eq!(text.cursor_col, 4);
5781 }
5782
5783 #[test]
5784 fn test_text_keymap_has_word_ops() {
5785 let keymap = TextKeyMap::default();
5786
5787 assert!(keymap.uppercase_word_forward.enabled());
5789 assert!(keymap.lowercase_word_forward.enabled());
5790 assert!(keymap.capitalize_word_forward.enabled());
5791 assert!(keymap.transpose_character_backward.enabled());
5792
5793 assert!(
5795 keymap
5796 .uppercase_word_forward
5797 .get_keys()
5798 .contains(&"alt+u".to_string())
5799 );
5800 assert!(
5801 keymap
5802 .lowercase_word_forward
5803 .get_keys()
5804 .contains(&"alt+l".to_string())
5805 );
5806 assert!(
5807 keymap
5808 .capitalize_word_forward
5809 .get_keys()
5810 .contains(&"alt+c".to_string())
5811 );
5812 assert!(
5813 keymap
5814 .transpose_character_backward
5815 .get_keys()
5816 .contains(&"ctrl+t".to_string())
5817 );
5818 }
5819
5820 mod paste_tests {
5825 use super::*;
5826 use bubbletea::{KeyMsg, Message};
5827
5828 fn paste_msg(s: &str) -> Message {
5830 let key = KeyMsg::from_runes(s.chars().collect()).with_paste();
5831 Message::new(key)
5832 }
5833
5834 fn type_msg(s: &str) -> Message {
5836 let key = KeyMsg::from_runes(s.chars().collect());
5837 Message::new(key)
5838 }
5839
5840 #[test]
5841 fn test_input_paste_collapses_newlines() {
5842 let mut input = Input::new().key("query");
5843 input.focused = true;
5844
5845 let msg = paste_msg("hello\nworld\nfoo");
5847 input.update(&msg);
5848
5849 assert_eq!(input.get_string_value(), "hello world foo");
5851 }
5852
5853 #[test]
5854 fn test_input_paste_collapses_tabs() {
5855 let mut input = Input::new().key("query");
5856 input.focused = true;
5857
5858 let msg = paste_msg("col1\tcol2\tcol3");
5860 input.update(&msg);
5861
5862 assert_eq!(input.get_string_value(), "col1 col2 col3");
5864 }
5865
5866 #[test]
5867 fn test_input_paste_collapses_multiple_spaces() {
5868 let mut input = Input::new().key("query");
5869 input.focused = true;
5870
5871 let msg = paste_msg("hello\n\n\nworld");
5873 input.update(&msg);
5874
5875 assert_eq!(input.get_string_value(), "hello world");
5877 }
5878
5879 #[test]
5880 fn test_input_paste_respects_char_limit() {
5881 let mut input = Input::new().key("query").char_limit(10);
5882 input.focused = true;
5883
5884 let msg = paste_msg("hello world this is too long");
5886 input.update(&msg);
5887
5888 assert_eq!(input.get_string_value().chars().count(), 10);
5890 assert_eq!(input.get_string_value(), "hello worl");
5891 }
5892
5893 #[test]
5894 fn test_input_paste_partial_fill() {
5895 let mut input = Input::new().key("query").char_limit(15);
5896 input.focused = true;
5897
5898 let msg = type_msg("hi ");
5900 input.update(&msg);
5901
5902 let msg = paste_msg("hello world this is long");
5904 input.update(&msg);
5905
5906 assert_eq!(input.get_string_value().chars().count(), 15);
5907 assert_eq!(input.get_string_value(), "hi hello world ");
5908 }
5909
5910 #[test]
5911 fn test_input_paste_cursor_position() {
5912 let mut input = Input::new().key("query");
5913 input.focused = true;
5914
5915 let msg = paste_msg("hello world");
5917 input.update(&msg);
5918
5919 assert_eq!(input.cursor_pos, 11);
5921 }
5922
5923 #[test]
5924 fn test_input_regular_typing_not_affected() {
5925 let mut input = Input::new().key("query");
5926 input.focused = true;
5927
5928 let msg = type_msg("hello\nworld");
5930 input.update(&msg);
5931
5932 assert_eq!(input.get_string_value(), "hello\nworld");
5934 }
5935
5936 #[test]
5937 fn test_text_paste_preserves_newlines() {
5938 let mut text = Text::new().key("bio");
5939 text.focused = true;
5940
5941 let msg = paste_msg("line 1\nline 2\nline 3");
5943 text.update(&msg);
5944
5945 assert_eq!(text.get_string_value(), "line 1\nline 2\nline 3");
5947 }
5948
5949 #[test]
5950 fn test_text_paste_updates_cursor_row() {
5951 let mut text = Text::new().key("bio");
5952 text.focused = true;
5953
5954 let msg = paste_msg("line 1\nline 2\nline 3");
5956 text.update(&msg);
5957
5958 assert_eq!(text.cursor_row, 2);
5960 assert_eq!(text.cursor_col, 6);
5962 }
5963
5964 #[test]
5965 fn test_text_paste_respects_char_limit() {
5966 let mut text = Text::new().key("bio").char_limit(20);
5967 text.focused = true;
5968
5969 let msg = paste_msg("line 1\nline 2\nline 3 is very long");
5971 text.update(&msg);
5972
5973 assert_eq!(text.get_string_value().chars().count(), 20);
5975 }
5976
5977 #[test]
5978 fn test_input_paste_unicode() {
5979 let mut input = Input::new().key("query");
5980 input.focused = true;
5981
5982 let msg = paste_msg("héllo\nwörld\n日本語");
5984 input.update(&msg);
5985
5986 assert_eq!(input.get_string_value(), "héllo wörld 日本語");
5988 }
5989
5990 #[test]
5991 fn test_text_paste_unicode_cursor() {
5992 let mut text = Text::new().key("bio");
5993 text.focused = true;
5994
5995 let msg = paste_msg("日本語\n한국어");
5997 text.update(&msg);
5998
5999 assert_eq!(text.get_string_value(), "日本語\n한국어");
6000 assert_eq!(text.cursor_row, 1);
6001 assert_eq!(text.cursor_col, 3); }
6003
6004 #[test]
6005 fn test_input_paste_empty() {
6006 let mut input = Input::new().key("query");
6007 input.focused = true;
6008
6009 let msg = paste_msg("");
6011 input.update(&msg);
6012
6013 assert_eq!(input.get_string_value(), "");
6014 assert_eq!(input.cursor_pos, 0);
6015 }
6016
6017 #[test]
6018 fn test_input_paste_crlf_handling() {
6019 let mut input = Input::new().key("query");
6020 input.focused = true;
6021
6022 let msg = paste_msg("hello\r\nworld");
6024 input.update(&msg);
6025
6026 assert_eq!(input.get_string_value(), "hello world");
6028 }
6029
6030 #[test]
6031 fn test_input_not_focused_ignores_paste() {
6032 let mut input = Input::new().key("query");
6033 input.focused = false;
6034
6035 let msg = paste_msg("hello world");
6036 input.update(&msg);
6037
6038 assert_eq!(input.get_string_value(), "");
6040 }
6041
6042 #[test]
6043 fn test_text_not_focused_ignores_paste() {
6044 let mut text = Text::new().key("bio");
6045 text.focused = false;
6046
6047 let msg = paste_msg("hello\nworld");
6048 text.update(&msg);
6049
6050 assert_eq!(text.get_string_value(), "");
6052 }
6053
6054 #[test]
6055 fn test_input_large_paste() {
6056 let mut input = Input::new().key("query");
6057 input.focused = true;
6058
6059 let large_text: String = (0..1000).map(|i| format!("word{} ", i)).collect();
6061 let msg = paste_msg(&large_text);
6062 input.update(&msg);
6063
6064 assert!(input.get_string_value().chars().count() > 100);
6066 }
6067
6068 #[test]
6069 fn test_text_large_paste() {
6070 let mut text = Text::new().key("bio");
6071 text.focused = true;
6072
6073 let large_text: String = (0..100).map(|i| format!("line {}\n", i)).collect();
6075 let msg = paste_msg(&large_text);
6076 text.update(&msg);
6077
6078 assert!(text.get_string_value().contains('\n'));
6080 assert_eq!(text.cursor_row, 100); }
6082 }
6083
6084 #[test]
6085 fn test_multiselect_filter_cursor_stays_on_item() {
6086 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6088 SelectOption::new("Apple", "apple".to_string()),
6089 SelectOption::new("Banana", "banana".to_string()),
6090 SelectOption::new("Cherry", "cherry".to_string()),
6091 SelectOption::new("Blueberry", "blueberry".to_string()),
6092 ]);
6093
6094 multi.focus();
6095
6096 let down_msg = Message::new(KeyMsg {
6098 key_type: KeyType::Down,
6099 runes: vec![],
6100 alt: false,
6101 paste: false,
6102 });
6103 multi.update(&down_msg);
6104 assert_eq!(multi.cursor, 1);
6105
6106 multi.update_filter("b".to_string());
6108
6109 let filtered = multi.filtered_options();
6111 assert_eq!(filtered.len(), 2);
6112 assert_eq!(filtered[multi.cursor].1.key, "Banana");
6113 }
6114
6115 #[test]
6116 fn test_multiselect_filter_cursor_clamps() {
6117 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6119 SelectOption::new("Apple", "apple".to_string()),
6120 SelectOption::new("Banana", "banana".to_string()),
6121 SelectOption::new("Cherry", "cherry".to_string()),
6122 ]);
6123
6124 multi.focus();
6125
6126 let down_msg = Message::new(KeyMsg {
6128 key_type: KeyType::Down,
6129 runes: vec![],
6130 alt: false,
6131 paste: false,
6132 });
6133 multi.update(&down_msg);
6134 multi.update(&down_msg);
6135 assert_eq!(multi.cursor, 2);
6136
6137 multi.update_filter("a".to_string());
6139
6140 let filtered = multi.filtered_options();
6142 assert_eq!(filtered.len(), 2);
6143 assert!(multi.cursor < filtered.len());
6144 }
6145
6146 #[test]
6147 fn test_multiselect_filter_then_toggle() {
6148 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6150 SelectOption::new("Apple", "apple".to_string()),
6151 SelectOption::new("Banana", "banana".to_string()),
6152 SelectOption::new("Cherry", "cherry".to_string()),
6153 SelectOption::new("Blueberry", "blueberry".to_string()),
6154 ]);
6155
6156 multi.focus();
6157
6158 multi.update_filter("b".to_string());
6160
6161 let down_msg = Message::new(KeyMsg {
6163 key_type: KeyType::Down,
6164 runes: vec![],
6165 alt: false,
6166 paste: false,
6167 });
6168 multi.update(&down_msg);
6169
6170 let toggle_msg = Message::new(KeyMsg {
6172 key_type: KeyType::Runes,
6173 runes: vec![' '],
6174 alt: false,
6175 paste: false,
6176 });
6177 multi.update(&toggle_msg);
6178
6179 let selected = multi.get_selected_values();
6181 assert_eq!(selected.len(), 1);
6182 assert!(selected.contains(&&"blueberry".to_string()));
6183
6184 multi.update_filter(String::new());
6186 let selected = multi.get_selected_values();
6187 assert_eq!(selected.len(), 1);
6188 assert!(selected.contains(&&"blueberry".to_string()));
6189 }
6190
6191 #[test]
6192 fn test_multiselect_filter_navigation_bounds() {
6193 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6195 SelectOption::new("Apple", "apple".to_string()),
6196 SelectOption::new("Banana", "banana".to_string()),
6197 SelectOption::new("Cherry", "cherry".to_string()),
6198 SelectOption::new("Date", "date".to_string()),
6199 ]);
6200
6201 multi.focus();
6202
6203 multi.update_filter("a".to_string());
6205 let filtered = multi.filtered_options();
6206 assert_eq!(filtered.len(), 3);
6207
6208 let down_msg = Message::new(KeyMsg {
6210 key_type: KeyType::Down,
6211 runes: vec![],
6212 alt: false,
6213 paste: false,
6214 });
6215 multi.update(&down_msg);
6216 multi.update(&down_msg);
6217 multi.update(&down_msg); multi.update(&down_msg);
6219
6220 assert_eq!(multi.cursor, 2); }
6223
6224 fn filepicker_with_entries(entries: Vec<(&str, bool)>) -> FilePicker {
6231 let mut picker = FilePicker::new();
6232 picker.picking = true;
6233 picker.focused = true;
6234 picker.files = entries
6235 .into_iter()
6236 .map(|(name, is_dir)| FileEntry {
6237 name: name.to_string(),
6238 path: format!("/tmp/{name}"),
6239 is_dir,
6240 size: 0,
6241 mode: String::new(),
6242 })
6243 .collect();
6244 picker
6245 }
6246
6247 fn make_key_msg(key_type: KeyType) -> Message {
6248 Message::new(KeyMsg {
6249 key_type,
6250 runes: vec![],
6251 alt: false,
6252 paste: false,
6253 })
6254 }
6255
6256 #[test]
6257 fn filepicker_single_file_is_selected_by_default() {
6258 let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6259 assert_eq!(picker.selected_index, 0);
6261 assert_eq!(picker.files.len(), 1);
6262 assert_eq!(picker.files[0].name, "only_file.txt");
6263 }
6264
6265 #[test]
6266 fn filepicker_single_file_view_shows_entry() {
6267 let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6268 let view = picker.view();
6269 assert!(view.contains("only_file.txt"));
6270 }
6271
6272 #[test]
6273 fn filepicker_single_file_select_via_enter() {
6274 let mut picker = filepicker_with_entries(vec![("report.pdf", false)]);
6275 let enter_msg = make_key_msg(KeyType::Enter);
6277 let result = picker.update(&enter_msg);
6278 assert_eq!(picker.selected_path, Some("/tmp/report.pdf".to_string()));
6280 assert!(!picker.picking);
6281 assert!(result.is_some()); }
6283
6284 #[test]
6285 fn filepicker_single_file_down_does_not_move() {
6286 let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6287 let down_msg = make_key_msg(KeyType::Down);
6288 picker.update(&down_msg);
6289 assert_eq!(picker.selected_index, 0);
6291 }
6292
6293 #[test]
6294 fn filepicker_single_file_up_does_not_move() {
6295 let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6296 let up_msg = make_key_msg(KeyType::Up);
6297 picker.update(&up_msg);
6298 assert_eq!(picker.selected_index, 0);
6299 }
6300
6301 #[test]
6302 fn filepicker_empty_files_no_panic() {
6303 let mut picker = filepicker_with_entries(vec![]);
6304 let down_msg = make_key_msg(KeyType::Down);
6306 picker.update(&down_msg);
6307 assert_eq!(picker.selected_index, 0);
6308
6309 let up_msg = make_key_msg(KeyType::Up);
6310 picker.update(&up_msg);
6311 assert_eq!(picker.selected_index, 0);
6312 }
6313
6314 #[test]
6315 fn filepicker_empty_files_view_no_panic() {
6316 let picker = filepicker_with_entries(vec![]);
6317 let view = picker.view();
6319 assert_ne!(view, "");
6320 }
6321
6322 #[test]
6323 fn filepicker_empty_goto_top_bottom_no_panic() {
6324 let mut picker = filepicker_with_entries(vec![]);
6325 let home_msg = Message::new(KeyMsg {
6327 key_type: KeyType::Home,
6328 runes: vec![],
6329 alt: false,
6330 paste: false,
6331 });
6332 picker.update(&home_msg);
6333 assert_eq!(picker.selected_index, 0);
6334
6335 let end_msg = Message::new(KeyMsg {
6337 key_type: KeyType::End,
6338 runes: vec![],
6339 alt: false,
6340 paste: false,
6341 });
6342 picker.update(&end_msg);
6343 assert_eq!(picker.selected_index, 0);
6344 }
6345
6346 #[test]
6347 fn filepicker_height_zero_no_panic() {
6348 let mut picker =
6349 filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6350 picker.height = 0;
6351 let down_msg = make_key_msg(KeyType::Down);
6353 picker.update(&down_msg);
6354 picker.update(&down_msg);
6355 assert_eq!(picker.selected_index, 2);
6356 }
6357
6358 #[test]
6359 fn filepicker_height_one_scrolls_correctly() {
6360 let mut picker =
6361 filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6362 picker.height = 1;
6363 assert_eq!(picker.selected_index, 0);
6364 assert_eq!(picker.offset, 0);
6365
6366 let down_msg = make_key_msg(KeyType::Down);
6367 picker.update(&down_msg);
6368 assert_eq!(picker.selected_index, 1);
6369 assert_eq!(picker.offset, 1);
6371
6372 picker.update(&down_msg);
6373 assert_eq!(picker.selected_index, 2);
6374 assert_eq!(picker.offset, 2);
6375 }
6376
6377 #[test]
6378 fn filepicker_navigation_respects_bounds() {
6379 let mut picker = filepicker_with_entries(vec![("a.txt", false), ("b.txt", false)]);
6380 let down_msg = make_key_msg(KeyType::Down);
6381 let up_msg = make_key_msg(KeyType::Up);
6382
6383 picker.update(&down_msg);
6385 assert_eq!(picker.selected_index, 1);
6386 picker.update(&down_msg); assert_eq!(picker.selected_index, 1);
6388
6389 picker.update(&up_msg);
6391 assert_eq!(picker.selected_index, 0);
6392 picker.update(&up_msg); assert_eq!(picker.selected_index, 0);
6394 }
6395
6396 #[test]
6397 fn filepicker_dir_not_selectable_by_default() {
6398 let picker = filepicker_with_entries(vec![("subdir", true)]);
6399 let entry = &picker.files[0];
6400 assert!(!picker.is_selectable(entry));
6402 }
6403
6404 #[test]
6405 fn filepicker_file_selectable_by_default() {
6406 let picker = filepicker_with_entries(vec![("file.rs", false)]);
6407 let entry = &picker.files[0];
6408 assert!(picker.is_selectable(entry));
6409 }
6410
6411 #[test]
6412 fn filepicker_format_size_edge_cases() {
6413 assert_eq!(FilePicker::format_size(0), "0B");
6414 assert_eq!(FilePicker::format_size(1023), "1023B");
6415 assert_eq!(FilePicker::format_size(1024), "1.0K");
6416 assert_eq!(FilePicker::format_size(1024 * 1024), "1.0M");
6417 assert_eq!(FilePicker::format_size(1024 * 1024 * 1024), "1.0G");
6418 }
6419
6420 fn make_select_options() -> Vec<SelectOption<String>> {
6423 vec![
6424 SelectOption::new("Apple", "apple".to_string()),
6425 SelectOption::new("Apricot", "apricot".to_string()),
6426 SelectOption::new("Banana", "banana".to_string()),
6427 SelectOption::new("Cherry", "cherry".to_string()),
6428 SelectOption::new("Date", "date".to_string()),
6429 ]
6430 }
6431
6432 fn make_filterable_select() -> Select<String> {
6433 Select::new()
6434 .options(make_select_options())
6435 .filterable(true)
6436 .height_options(3)
6437 }
6438
6439 #[test]
6440 fn select_filterable_builder() {
6441 let sel = Select::<String>::new().filterable(true);
6442 assert!(sel.filtering);
6443 let sel = Select::<String>::new().filterable(false);
6444 assert!(!sel.filtering);
6445 }
6446
6447 #[test]
6448 fn select_filtered_indices_no_filter() {
6449 let sel = make_filterable_select();
6450 assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6451 }
6452
6453 #[test]
6454 fn select_filtered_indices_with_filter() {
6455 let mut sel = make_filterable_select();
6456 sel.filter_value = "ap".to_string();
6457 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6459 }
6460
6461 #[test]
6462 fn select_filtered_indices_case_insensitive() {
6463 let mut sel = make_filterable_select();
6464 sel.filter_value = "AP".to_string();
6465 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6466 }
6467
6468 #[test]
6469 fn select_filtered_indices_no_match() {
6470 let mut sel = make_filterable_select();
6471 sel.filter_value = "zzz".to_string();
6472 assert_eq!(sel.filtered_indices(), Vec::<usize>::new());
6473 }
6474
6475 #[test]
6476 fn select_update_filter_keeps_selection() {
6477 let mut sel = make_filterable_select();
6478 sel.selected = 2; sel.update_filter("an".to_string());
6480 assert_eq!(sel.selected, 2);
6482 assert_eq!(sel.filter_value, "an");
6483 }
6484
6485 #[test]
6486 fn select_update_filter_clamps_when_item_hidden() {
6487 let mut sel = make_filterable_select();
6488 sel.selected = 2; sel.update_filter("ch".to_string());
6490 assert_eq!(sel.selected, 3);
6493 }
6494
6495 #[test]
6496 fn select_update_filter_clear_restores() {
6497 let mut sel = make_filterable_select();
6498 sel.update_filter("ap".to_string());
6499 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6500 sel.update_filter(String::new());
6501 assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6502 }
6503
6504 #[test]
6505 fn select_filter_display_in_view() {
6506 let mut sel = make_filterable_select();
6507 sel.focused = true;
6508 sel.filter_value = "ap".to_string();
6509 let view = sel.view();
6510 assert!(view.contains("Filter: ap_"));
6511 }
6512
6513 #[test]
6514 fn select_filter_not_displayed_when_empty() {
6515 let mut sel = make_filterable_select();
6516 sel.focused = true;
6517 let view = sel.view();
6518 assert!(!view.contains("Filter:"));
6519 }
6520
6521 #[test]
6522 fn select_filter_not_displayed_when_disabled() {
6523 let mut sel = Select::new()
6524 .options(make_select_options())
6525 .height_options(3);
6526 sel.focused = true;
6527 sel.filter_value = "ap".to_string();
6528 let view = sel.view();
6529 assert!(!view.contains("Filter:"));
6530 }
6531
6532 #[test]
6533 fn select_navigation_respects_filter() {
6534 let mut sel = make_filterable_select();
6535 sel.focused = true;
6536 sel.update_filter("a".to_string());
6537 let indices = sel.filtered_indices();
6539 assert_eq!(indices, vec![0, 1, 2, 4]);
6540
6541 sel.selected = 0;
6543
6544 let down_msg = Message::new(KeyMsg {
6546 key_type: KeyType::Down,
6547 runes: vec![],
6548 alt: false,
6549 paste: false,
6550 });
6551 sel.update(&down_msg);
6552 assert_eq!(sel.selected, 1);
6554
6555 sel.update(&down_msg);
6556 assert_eq!(sel.selected, 2);
6558
6559 sel.update(&down_msg);
6560 assert_eq!(sel.selected, 4);
6562 }
6563
6564 #[test]
6565 fn select_get_selected_value_with_filter() {
6566 let mut sel = make_filterable_select();
6567 sel.update_filter("ch".to_string());
6568 assert_eq!(sel.selected, 3);
6570 assert_eq!(sel.get_selected_value(), Some(&"cherry".to_string()));
6571 }
6572}