1#![forbid(unsafe_code)]
2#![allow(clippy::nursery)]
4#![allow(clippy::pedantic)]
5
6use std::any::Any;
62use std::sync::atomic::{AtomicUsize, Ordering};
63
64use thiserror::Error;
65
66use bubbles::key::Binding;
67use bubbletea::{Cmd, KeyMsg, KeyType, Message, Model};
68use lipgloss::{Border, Style};
69
70static LAST_ID: AtomicUsize = AtomicUsize::new(0);
75
76fn next_id() -> usize {
77 LAST_ID.fetch_add(1, Ordering::SeqCst)
78}
79
80#[derive(Error, Debug, Clone, PartialEq, Eq)]
135pub enum FormError {
136 #[error("user aborted")]
154 UserAborted,
155
156 #[error("timeout")]
167 Timeout,
168
169 #[error("validation error: {0}")]
193 Validation(String),
194
195 #[error("io error: {0}")]
209 Io(String),
210}
211
212impl FormError {
213 pub fn validation(message: impl Into<String>) -> Self {
215 Self::Validation(message.into())
216 }
217
218 pub fn io(message: impl Into<String>) -> Self {
220 Self::Io(message.into())
221 }
222
223 pub fn is_user_abort(&self) -> bool {
225 matches!(self, Self::UserAborted)
226 }
227
228 pub fn is_timeout(&self) -> bool {
230 matches!(self, Self::Timeout)
231 }
232
233 pub fn is_recoverable(&self) -> bool {
235 matches!(self, Self::Validation(_))
236 }
237}
238
239pub type Result<T> = std::result::Result<T, FormError>;
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265pub enum FormState {
266 #[default]
268 Normal,
269 Completed,
271 Aborted,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct SelectOption<T: Clone + PartialEq> {
282 pub key: String,
284 pub value: T,
286 pub selected: bool,
288}
289
290impl<T: Clone + PartialEq> SelectOption<T> {
291 pub fn new(key: impl Into<String>, value: T) -> Self {
293 Self {
294 key: key.into(),
295 value,
296 selected: false,
297 }
298 }
299
300 pub fn selected(mut self, selected: bool) -> Self {
302 self.selected = selected;
303 self
304 }
305}
306
307impl<T: Clone + PartialEq + std::fmt::Display> SelectOption<T> {
308 pub fn from_values(values: impl IntoIterator<Item = T>) -> Vec<Self> {
310 values
311 .into_iter()
312 .map(|v| Self::new(v.to_string(), v))
313 .collect()
314 }
315}
316
317pub fn new_options<S: Into<String> + Clone>(
319 values: impl IntoIterator<Item = S>,
320) -> Vec<SelectOption<String>> {
321 values
322 .into_iter()
323 .map(|v| {
324 let s: String = v.clone().into();
325 SelectOption::new(s.clone(), s)
326 })
327 .collect()
328}
329
330#[derive(Debug, Clone)]
336pub struct Theme {
337 pub form: FormStyles,
339 pub group: GroupStyles,
341 pub field_separator: Style,
343 pub blurred: FieldStyles,
345 pub focused: FieldStyles,
347 pub help: Style,
349}
350
351impl Default for Theme {
352 fn default() -> Self {
353 theme_charm()
354 }
355}
356
357#[derive(Debug, Clone, Default)]
359pub struct FormStyles {
360 pub base: Style,
362}
363
364#[derive(Debug, Clone, Default)]
366pub struct GroupStyles {
367 pub base: Style,
369 pub title: Style,
371 pub description: Style,
373}
374
375#[derive(Debug, Clone, Default)]
377pub struct FieldStyles {
378 pub base: Style,
380 pub title: Style,
382 pub description: Style,
384 pub error_indicator: Style,
386 pub error_message: Style,
388
389 pub select_selector: Style,
392 pub option: Style,
394 pub next_indicator: Style,
396 pub prev_indicator: Style,
398
399 pub multi_select_selector: Style,
402 pub selected_option: Style,
404 pub selected_prefix: Style,
406 pub unselected_option: Style,
408 pub unselected_prefix: Style,
410
411 pub text_input: TextInputStyles,
414
415 pub focused_button: Style,
418 pub blurred_button: Style,
420
421 pub note_title: Style,
424}
425
426#[derive(Debug, Clone, Default)]
428pub struct TextInputStyles {
429 pub cursor: Style,
431 pub cursor_text: Style,
433 pub placeholder: Style,
435 pub prompt: Style,
437 pub text: Style,
439}
440
441#[allow(clippy::field_reassign_with_default)]
443pub fn theme_base() -> Theme {
444 let button = Style::new().padding((0, 2)).margin_right(1);
445
446 let mut focused = FieldStyles::default();
447 focused.base = Style::new()
448 .padding_left(1)
449 .border(Border::thick())
450 .border_left(true);
451 focused.error_indicator = Style::new().set_string(" *");
452 focused.error_message = Style::new().set_string(" *");
453 focused.select_selector = Style::new().set_string("> ");
454 focused.next_indicator = Style::new().margin_left(1).set_string("→");
455 focused.prev_indicator = Style::new().margin_right(1).set_string("←");
456 focused.multi_select_selector = Style::new().set_string("> ");
457 focused.selected_prefix = Style::new().set_string("[•] ");
458 focused.unselected_prefix = Style::new().set_string("[ ] ");
459 focused.focused_button = button.clone().foreground("0").background("7");
460 focused.blurred_button = button.foreground("7").background("0");
461 focused.text_input.placeholder = Style::new().foreground("8");
462
463 let mut blurred = focused.clone();
464 blurred.base = blurred.base.border(Border::hidden());
465 blurred.multi_select_selector = Style::new().set_string(" ");
466 blurred.next_indicator = Style::new();
467 blurred.prev_indicator = Style::new();
468
469 Theme {
470 form: FormStyles { base: Style::new() },
471 group: GroupStyles::default(),
472 field_separator: Style::new().set_string("\n\n"),
473 focused,
474 blurred,
475 help: Style::new().foreground("241").margin_top(1),
476 }
477}
478
479pub fn theme_charm() -> Theme {
481 let mut t = theme_base();
482
483 let indigo = "#7571F9";
484 let fuchsia = "#F780E2";
485 let green = "#02BF87";
486 let red = "#ED567A";
487 let normal_fg = "252";
488
489 t.focused.base = t.focused.base.border_foreground("238");
490 t.focused.title = t.focused.title.foreground(indigo).bold();
491 t.focused.note_title = t
492 .focused
493 .note_title
494 .foreground(indigo)
495 .bold()
496 .margin_bottom(1);
497 t.focused.description = t.focused.description.foreground("243");
498 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
499 t.focused.error_message = t.focused.error_message.foreground(red);
500 t.focused.select_selector = t.focused.select_selector.foreground(fuchsia);
501 t.focused.next_indicator = t.focused.next_indicator.foreground(fuchsia);
502 t.focused.prev_indicator = t.focused.prev_indicator.foreground(fuchsia);
503 t.focused.option = t.focused.option.foreground(normal_fg);
504 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(fuchsia);
505 t.focused.selected_option = t.focused.selected_option.foreground(green);
506 t.focused.selected_prefix = Style::new().foreground("#02A877").set_string("✓ ");
507 t.focused.unselected_prefix = Style::new().foreground("243").set_string("• ");
508 t.focused.unselected_option = t.focused.unselected_option.foreground(normal_fg);
509 t.focused.focused_button = t
510 .focused
511 .focused_button
512 .foreground("#FFFDF5")
513 .background(fuchsia);
514 t.focused.blurred_button = t
515 .focused
516 .blurred_button
517 .foreground(normal_fg)
518 .background("237");
519 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(green);
520 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground("238");
521 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(fuchsia);
522
523 t.blurred = t.focused.clone();
524 t.blurred.base = t.focused.base.clone().border(Border::hidden());
525 t.blurred.next_indicator = Style::new();
526 t.blurred.prev_indicator = Style::new();
527
528 t.group.title = t.focused.title.clone();
529 t.group.description = t.focused.description.clone();
530 t.help = Style::new().foreground("241").margin_top(1);
531
532 t
533}
534
535pub fn theme_dracula() -> Theme {
537 let mut t = theme_base();
538
539 let selection = "#44475a";
540 let foreground = "#f8f8f2";
541 let comment = "#6272a4";
542 let green = "#50fa7b";
543 let purple = "#bd93f9";
544 let red = "#ff5555";
545 let yellow = "#f1fa8c";
546
547 t.focused.base = t.focused.base.border_foreground(selection);
548 t.focused.title = t.focused.title.foreground(purple);
549 t.focused.note_title = t.focused.note_title.foreground(purple);
550 t.focused.description = t.focused.description.foreground(comment);
551 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
552 t.focused.error_message = t.focused.error_message.foreground(red);
553 t.focused.select_selector = t.focused.select_selector.foreground(yellow);
554 t.focused.next_indicator = t.focused.next_indicator.foreground(yellow);
555 t.focused.prev_indicator = t.focused.prev_indicator.foreground(yellow);
556 t.focused.option = t.focused.option.foreground(foreground);
557 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(yellow);
558 t.focused.selected_option = t.focused.selected_option.foreground(green);
559 t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
560 t.focused.unselected_option = t.focused.unselected_option.foreground(foreground);
561 t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(comment);
562 t.focused.focused_button = t
563 .focused
564 .focused_button
565 .foreground(yellow)
566 .background(purple)
567 .bold();
568 t.focused.blurred_button = t
569 .focused
570 .blurred_button
571 .foreground(foreground)
572 .background("#282a36");
573 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(yellow);
574 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(comment);
575 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(yellow);
576
577 t.blurred = t.focused.clone();
578 t.blurred.base = t.blurred.base.border(Border::hidden());
579 t.blurred.next_indicator = Style::new();
580 t.blurred.prev_indicator = Style::new();
581
582 t.group.title = t.focused.title.clone();
583 t.group.description = t.focused.description.clone();
584 t.help = Style::new().foreground(comment).margin_top(1);
585
586 t
587}
588
589pub fn theme_base16() -> Theme {
591 let mut t = theme_base();
592
593 t.focused.base = t.focused.base.border_foreground("8");
594 t.focused.title = t.focused.title.foreground("6");
595 t.focused.note_title = t.focused.note_title.foreground("6");
596 t.focused.description = t.focused.description.foreground("8");
597 t.focused.error_indicator = t.focused.error_indicator.foreground("9");
598 t.focused.error_message = t.focused.error_message.foreground("9");
599 t.focused.select_selector = t.focused.select_selector.foreground("3");
600 t.focused.next_indicator = t.focused.next_indicator.foreground("3");
601 t.focused.prev_indicator = t.focused.prev_indicator.foreground("3");
602 t.focused.option = t.focused.option.foreground("7");
603 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground("3");
604 t.focused.selected_option = t.focused.selected_option.foreground("2");
605 t.focused.selected_prefix = t.focused.selected_prefix.foreground("2");
606 t.focused.unselected_option = t.focused.unselected_option.foreground("7");
607 t.focused.focused_button = t.focused.focused_button.foreground("7").background("5");
608 t.focused.blurred_button = t.focused.blurred_button.foreground("7").background("0");
609
610 t.blurred = t.focused.clone();
611 t.blurred.base = t.blurred.base.border(Border::hidden());
612 t.blurred.note_title = t.blurred.note_title.foreground("8");
613 t.blurred.title = t.blurred.title.foreground("8");
614 t.blurred.text_input.prompt = t.blurred.text_input.prompt.foreground("8");
615 t.blurred.text_input.text = t.blurred.text_input.text.foreground("7");
616 t.blurred.next_indicator = Style::new();
617 t.blurred.prev_indicator = Style::new();
618
619 t.group.title = t.focused.title.clone();
620 t.group.description = t.focused.description.clone();
621 t.help = Style::new().foreground("8").margin_top(1);
622
623 t
624}
625
626pub fn theme_catppuccin() -> Theme {
631 let mut t = theme_base();
632
633 let base = "#1e1e2e";
635 let text = "#cdd6f4";
636 let subtext1 = "#bac2de";
637 let subtext0 = "#a6adc8";
638 let _overlay1 = "#7f849c";
639 let overlay0 = "#6c7086";
640 let green = "#a6e3a1";
641 let red = "#f38ba8";
642 let pink = "#f5c2e7";
643 let mauve = "#cba6f7";
644 let rosewater = "#f5e0dc";
645
646 t.focused.base = t.focused.base.border_foreground(subtext1);
647 t.focused.title = t.focused.title.foreground(mauve);
648 t.focused.note_title = t.focused.note_title.foreground(mauve);
649 t.focused.description = t.focused.description.foreground(subtext0);
650 t.focused.error_indicator = t.focused.error_indicator.foreground(red);
651 t.focused.error_message = t.focused.error_message.foreground(red);
652 t.focused.select_selector = t.focused.select_selector.foreground(pink);
653 t.focused.next_indicator = t.focused.next_indicator.foreground(pink);
654 t.focused.prev_indicator = t.focused.prev_indicator.foreground(pink);
655 t.focused.option = t.focused.option.foreground(text);
656 t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(pink);
657 t.focused.selected_option = t.focused.selected_option.foreground(green);
658 t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
659 t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(text);
660 t.focused.unselected_option = t.focused.unselected_option.foreground(text);
661 t.focused.focused_button = t.focused.focused_button.foreground(base).background(pink);
662 t.focused.blurred_button = t.focused.blurred_button.foreground(text).background(base);
663
664 t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(rosewater);
665 t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(overlay0);
666 t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(pink);
667
668 t.blurred = t.focused.clone();
669 t.blurred.base = t.blurred.base.border(Border::hidden());
670 t.blurred.next_indicator = Style::new();
671 t.blurred.prev_indicator = Style::new();
672
673 t.group.title = t.focused.title.clone();
674 t.group.description = t.focused.description.clone();
675 t.help = Style::new().foreground(subtext0).margin_top(1);
676
677 t
678}
679
680#[derive(Debug, Clone)]
686pub struct KeyMap {
687 pub quit: Binding,
689 pub input: InputKeyMap,
691 pub select: SelectKeyMap,
693 pub multi_select: MultiSelectKeyMap,
695 pub confirm: ConfirmKeyMap,
697 pub note: NoteKeyMap,
699 pub text: TextKeyMap,
701 pub file_picker: FilePickerKeyMap,
703}
704
705impl Default for KeyMap {
706 fn default() -> Self {
707 Self::new()
708 }
709}
710
711impl KeyMap {
712 pub fn new() -> Self {
714 Self {
715 quit: Binding::new().keys(&["ctrl+c"]),
716 input: InputKeyMap::default(),
717 select: SelectKeyMap::default(),
718 multi_select: MultiSelectKeyMap::default(),
719 confirm: ConfirmKeyMap::default(),
720 note: NoteKeyMap::default(),
721 text: TextKeyMap::default(),
722 file_picker: FilePickerKeyMap::default(),
723 }
724 }
725}
726
727#[derive(Debug, Clone)]
729pub struct InputKeyMap {
730 pub accept_suggestion: Binding,
732 pub next: Binding,
734 pub prev: Binding,
736 pub submit: Binding,
738}
739
740impl Default for InputKeyMap {
741 fn default() -> Self {
742 Self {
743 accept_suggestion: Binding::new().keys(&["ctrl+e"]).help("ctrl+e", "complete"),
744 prev: Binding::new()
745 .keys(&["shift+tab"])
746 .help("shift+tab", "back"),
747 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
748 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
749 }
750 }
751}
752
753#[derive(Debug, Clone)]
755pub struct SelectKeyMap {
756 pub next: Binding,
758 pub prev: Binding,
760 pub up: Binding,
762 pub down: Binding,
764 pub left: Binding,
766 pub right: Binding,
768 pub filter: Binding,
770 pub set_filter: Binding,
772 pub clear_filter: Binding,
774 pub half_page_up: Binding,
776 pub half_page_down: Binding,
778 pub goto_top: Binding,
780 pub goto_bottom: Binding,
782 pub submit: Binding,
784}
785
786impl Default for SelectKeyMap {
787 fn default() -> Self {
788 Self {
789 prev: Binding::new()
790 .keys(&["shift+tab"])
791 .help("shift+tab", "back"),
792 next: Binding::new()
793 .keys(&["enter", "tab"])
794 .help("enter", "select"),
795 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
796 up: Binding::new()
797 .keys(&["up", "k", "ctrl+k", "ctrl+p"])
798 .help("↑", "up"),
799 down: Binding::new()
800 .keys(&["down", "j", "ctrl+j", "ctrl+n"])
801 .help("↓", "down"),
802 left: Binding::new()
803 .keys(&["h", "left"])
804 .help("←", "left")
805 .set_enabled(false),
806 right: Binding::new()
807 .keys(&["l", "right"])
808 .help("→", "right")
809 .set_enabled(false),
810 filter: Binding::new().keys(&["/"]).help("/", "filter"),
811 set_filter: Binding::new()
812 .keys(&["escape"])
813 .help("esc", "set filter")
814 .set_enabled(false),
815 clear_filter: Binding::new()
816 .keys(&["escape"])
817 .help("esc", "clear filter")
818 .set_enabled(false),
819 half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
820 half_page_down: Binding::new()
821 .keys(&["ctrl+d"])
822 .help("ctrl+d", "½ page down"),
823 goto_top: Binding::new()
824 .keys(&["home", "g"])
825 .help("g/home", "go to start"),
826 goto_bottom: Binding::new()
827 .keys(&["end", "G"])
828 .help("G/end", "go to end"),
829 }
830 }
831}
832
833#[derive(Debug, Clone)]
835pub struct MultiSelectKeyMap {
836 pub next: Binding,
838 pub prev: Binding,
840 pub up: Binding,
842 pub down: Binding,
844 pub toggle: Binding,
846 pub filter: Binding,
848 pub set_filter: Binding,
850 pub clear_filter: Binding,
852 pub half_page_up: Binding,
854 pub half_page_down: Binding,
856 pub goto_top: Binding,
858 pub goto_bottom: Binding,
860 pub select_all: Binding,
862 pub select_none: Binding,
864 pub submit: Binding,
866}
867
868impl Default for MultiSelectKeyMap {
869 fn default() -> Self {
870 Self {
871 prev: Binding::new()
872 .keys(&["shift+tab"])
873 .help("shift+tab", "back"),
874 next: Binding::new()
875 .keys(&["enter", "tab"])
876 .help("enter", "confirm"),
877 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
878 toggle: Binding::new().keys(&[" ", "x"]).help("x", "toggle"),
879 up: Binding::new().keys(&["up", "k", "ctrl+p"]).help("↑", "up"),
880 down: Binding::new()
881 .keys(&["down", "j", "ctrl+n"])
882 .help("↓", "down"),
883 filter: Binding::new().keys(&["/"]).help("/", "filter"),
884 set_filter: Binding::new()
885 .keys(&["enter", "escape"])
886 .help("esc", "set filter")
887 .set_enabled(false),
888 clear_filter: Binding::new()
889 .keys(&["escape"])
890 .help("esc", "clear filter")
891 .set_enabled(false),
892 half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
893 half_page_down: Binding::new()
894 .keys(&["ctrl+d"])
895 .help("ctrl+d", "½ page down"),
896 goto_top: Binding::new()
897 .keys(&["home", "g"])
898 .help("g/home", "go to start"),
899 goto_bottom: Binding::new()
900 .keys(&["end", "G"])
901 .help("G/end", "go to end"),
902 select_all: Binding::new()
903 .keys(&["ctrl+a"])
904 .help("ctrl+a", "select all"),
905 select_none: Binding::new()
906 .keys(&["ctrl+a"])
907 .help("ctrl+a", "select none")
908 .set_enabled(false),
909 }
910 }
911}
912
913#[derive(Debug, Clone)]
915pub struct ConfirmKeyMap {
916 pub next: Binding,
918 pub prev: Binding,
920 pub toggle: Binding,
922 pub submit: Binding,
924 pub accept: Binding,
926 pub reject: Binding,
928}
929
930impl Default for ConfirmKeyMap {
931 fn default() -> Self {
932 Self {
933 prev: Binding::new()
934 .keys(&["shift+tab"])
935 .help("shift+tab", "back"),
936 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
937 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
938 toggle: Binding::new()
939 .keys(&["h", "l", "right", "left"])
940 .help("←/→", "toggle"),
941 accept: Binding::new().keys(&["y", "Y"]).help("y", "Yes"),
942 reject: Binding::new().keys(&["n", "N"]).help("n", "No"),
943 }
944 }
945}
946
947#[derive(Debug, Clone)]
949pub struct NoteKeyMap {
950 pub next: Binding,
952 pub prev: Binding,
954 pub submit: Binding,
956}
957
958impl Default for NoteKeyMap {
959 fn default() -> Self {
960 Self {
961 prev: Binding::new()
962 .keys(&["shift+tab"])
963 .help("shift+tab", "back"),
964 next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
965 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
966 }
967 }
968}
969
970#[derive(Debug, Clone)]
972pub struct TextKeyMap {
973 pub next: Binding,
975 pub prev: Binding,
977 pub new_line: Binding,
979 pub editor: Binding,
981 pub submit: Binding,
983 pub uppercase_word_forward: Binding,
985 pub lowercase_word_forward: Binding,
987 pub capitalize_word_forward: Binding,
989 pub transpose_character_backward: Binding,
991}
992
993impl Default for TextKeyMap {
994 fn default() -> Self {
995 Self {
996 prev: Binding::new()
997 .keys(&["shift+tab"])
998 .help("shift+tab", "back"),
999 next: Binding::new().keys(&["tab", "enter"]).help("enter", "next"),
1000 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1001 new_line: Binding::new()
1002 .keys(&["alt+enter", "ctrl+j"])
1003 .help("alt+enter / ctrl+j", "new line"),
1004 editor: Binding::new()
1005 .keys(&["ctrl+e"])
1006 .help("ctrl+e", "open editor"),
1007 uppercase_word_forward: Binding::new()
1008 .keys(&["alt+u"])
1009 .help("alt+u", "uppercase word"),
1010 lowercase_word_forward: Binding::new()
1011 .keys(&["alt+l"])
1012 .help("alt+l", "lowercase word"),
1013 capitalize_word_forward: Binding::new()
1014 .keys(&["alt+c"])
1015 .help("alt+c", "capitalize word"),
1016 transpose_character_backward: Binding::new()
1017 .keys(&["ctrl+t"])
1018 .help("ctrl+t", "transpose"),
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone)]
1025pub struct FilePickerKeyMap {
1026 pub next: Binding,
1028 pub prev: Binding,
1030 pub submit: Binding,
1032 pub up: Binding,
1034 pub down: Binding,
1036 pub open: Binding,
1038 pub close: Binding,
1040 pub back: Binding,
1042 pub select: Binding,
1044 pub goto_top: Binding,
1046 pub goto_bottom: Binding,
1048 pub page_up: Binding,
1050 pub page_down: Binding,
1052}
1053
1054impl Default for FilePickerKeyMap {
1055 fn default() -> Self {
1056 Self {
1057 prev: Binding::new()
1058 .keys(&["shift+tab"])
1059 .help("shift+tab", "back"),
1060 next: Binding::new().keys(&["tab"]).help("tab", "next"),
1061 submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1062 up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
1063 down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
1064 open: Binding::new().keys(&["enter", "l"]).help("enter", "open"),
1065 close: Binding::new().keys(&["esc", "q"]).help("esc", "close"),
1066 back: Binding::new().keys(&["backspace", "h"]).help("h", "back"),
1067 select: Binding::new().keys(&["enter"]).help("enter", "select"),
1068 goto_top: Binding::new().keys(&["g"]).help("g", "first"),
1069 goto_bottom: Binding::new().keys(&["G"]).help("G", "last"),
1070 page_up: Binding::new().keys(&["pgup", "K"]).help("pgup", "page up"),
1071 page_down: Binding::new()
1072 .keys(&["pgdown", "J"])
1073 .help("pgdown", "page down"),
1074 }
1075 }
1076}
1077
1078#[derive(Debug, Clone, Copy, Default)]
1084pub struct FieldPosition {
1085 pub group: usize,
1087 pub field: usize,
1089 pub first_field: usize,
1091 pub last_field: usize,
1093 pub group_count: usize,
1095 pub first_group: usize,
1097 pub last_group: usize,
1099}
1100
1101impl FieldPosition {
1102 pub fn is_first(&self) -> bool {
1104 self.field == self.first_field && self.group == self.first_group
1105 }
1106
1107 pub fn is_last(&self) -> bool {
1109 self.field == self.last_field && self.group == self.last_group
1110 }
1111}
1112
1113fn binding_matches(binding: &Binding, key: &KeyMsg) -> bool {
1119 if !binding.enabled() {
1120 return false;
1121 }
1122 let key_str = key.to_string();
1123 binding.get_keys().iter().any(|k| k == &key_str)
1124}
1125
1126pub trait Field: Send + Sync {
1132 fn get_key(&self) -> &str;
1134
1135 fn get_value(&self) -> Box<dyn Any>;
1137
1138 fn skip(&self) -> bool {
1140 false
1141 }
1142
1143 fn zoom(&self) -> bool {
1145 false
1146 }
1147
1148 fn error(&self) -> Option<&str>;
1150
1151 fn init(&mut self) -> Option<Cmd>;
1153
1154 fn update(&mut self, msg: &Message) -> Option<Cmd>;
1156
1157 fn view(&self) -> String;
1159
1160 fn focus(&mut self) -> Option<Cmd>;
1162
1163 fn blur(&mut self) -> Option<Cmd>;
1165
1166 fn key_binds(&self) -> Vec<Binding>;
1168
1169 fn with_theme(&mut self, theme: &Theme);
1171
1172 fn with_keymap(&mut self, keymap: &KeyMap);
1174
1175 fn with_width(&mut self, width: usize);
1177
1178 fn with_height(&mut self, height: usize);
1180
1181 fn with_position(&mut self, position: FieldPosition);
1183}
1184
1185#[derive(Debug, Clone)]
1191pub struct NextFieldMsg;
1192
1193#[derive(Debug, Clone)]
1195pub struct PrevFieldMsg;
1196
1197#[derive(Debug, Clone)]
1199pub struct NextGroupMsg;
1200
1201#[derive(Debug, Clone)]
1203pub struct PrevGroupMsg;
1204
1205#[derive(Debug, Clone)]
1207pub struct UpdateFieldMsg;
1208
1209pub struct Input {
1215 id: usize,
1216 key: String,
1217 value: String,
1218 title: String,
1219 description: String,
1220 placeholder: String,
1221 prompt: String,
1222 char_limit: usize,
1223 echo_mode: EchoMode,
1224 inline: bool,
1225 focused: bool,
1226 error: Option<String>,
1227 validate: Option<fn(&str) -> Option<String>>,
1228 width: usize,
1229 _height: usize,
1230 theme: Option<Theme>,
1231 keymap: InputKeyMap,
1232 _position: FieldPosition,
1233 cursor_pos: usize,
1234 suggestions: Vec<String>,
1235 show_suggestions: bool,
1236}
1237
1238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1240pub enum EchoMode {
1241 #[default]
1243 Normal,
1244 Password,
1246 None,
1248}
1249
1250impl Default for Input {
1251 fn default() -> Self {
1252 Self::new()
1253 }
1254}
1255
1256impl Input {
1257 pub fn new() -> Self {
1259 Self {
1260 id: next_id(),
1261 key: String::new(),
1262 value: String::new(),
1263 title: String::new(),
1264 description: String::new(),
1265 placeholder: String::new(),
1266 prompt: "> ".to_string(),
1267 char_limit: 0,
1268 echo_mode: EchoMode::Normal,
1269 inline: false,
1270 focused: false,
1271 error: None,
1272 validate: None,
1273 width: 80,
1274 _height: 0,
1275 theme: None,
1276 keymap: InputKeyMap::default(),
1277 _position: FieldPosition::default(),
1278 cursor_pos: 0,
1279 suggestions: Vec::new(),
1280 show_suggestions: false,
1281 }
1282 }
1283
1284 pub fn key(mut self, key: impl Into<String>) -> Self {
1286 self.key = key.into();
1287 self
1288 }
1289
1290 pub fn value(mut self, value: impl Into<String>) -> Self {
1292 self.value = value.into();
1293 self.cursor_pos = self.value.chars().count();
1294 self
1295 }
1296
1297 pub fn title(mut self, title: impl Into<String>) -> Self {
1299 self.title = title.into();
1300 self
1301 }
1302
1303 pub fn description(mut self, description: impl Into<String>) -> Self {
1305 self.description = description.into();
1306 self
1307 }
1308
1309 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1311 self.placeholder = placeholder.into();
1312 self
1313 }
1314
1315 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
1317 self.prompt = prompt.into();
1318 self
1319 }
1320
1321 pub fn char_limit(mut self, limit: usize) -> Self {
1323 self.char_limit = limit;
1324 self
1325 }
1326
1327 pub fn echo_mode(mut self, mode: EchoMode) -> Self {
1329 self.echo_mode = mode;
1330 self
1331 }
1332
1333 pub fn password(self, password: bool) -> Self {
1335 if password {
1336 self.echo_mode(EchoMode::Password)
1337 } else {
1338 self.echo_mode(EchoMode::Normal)
1339 }
1340 }
1341
1342 pub fn inline(mut self, inline: bool) -> Self {
1344 self.inline = inline;
1345 self
1346 }
1347
1348 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
1350 self.validate = Some(validate);
1351 self
1352 }
1353
1354 pub fn suggestions(mut self, suggestions: Vec<String>) -> Self {
1356 self.suggestions = suggestions;
1357 self.show_suggestions = !self.suggestions.is_empty();
1358 self
1359 }
1360
1361 fn get_theme(&self) -> Theme {
1362 self.theme.clone().unwrap_or_else(theme_charm)
1363 }
1364
1365 fn active_styles(&self) -> FieldStyles {
1366 let theme = self.get_theme();
1367 if self.focused {
1368 theme.focused
1369 } else {
1370 theme.blurred
1371 }
1372 }
1373
1374 fn run_validation(&mut self) {
1375 if let Some(validate) = self.validate {
1376 self.error = validate(&self.value);
1377 }
1378 }
1379
1380 fn display_value(&self) -> String {
1381 match self.echo_mode {
1382 EchoMode::Normal => self.value.clone(),
1383 EchoMode::Password => "•".repeat(self.value.chars().count()),
1384 EchoMode::None => String::new(),
1385 }
1386 }
1387
1388 pub fn get_string_value(&self) -> &str {
1390 &self.value
1391 }
1392
1393 pub fn id(&self) -> usize {
1395 self.id
1396 }
1397}
1398
1399impl Field for Input {
1400 fn get_key(&self) -> &str {
1401 &self.key
1402 }
1403
1404 fn get_value(&self) -> Box<dyn Any> {
1405 Box::new(self.value.clone())
1406 }
1407
1408 fn error(&self) -> Option<&str> {
1409 self.error.as_deref()
1410 }
1411
1412 fn init(&mut self) -> Option<Cmd> {
1413 None
1414 }
1415
1416 fn update(&mut self, msg: &Message) -> Option<Cmd> {
1417 if !self.focused {
1418 return None;
1419 }
1420
1421 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1422 self.error = None;
1423
1424 if binding_matches(&self.keymap.prev, key_msg) {
1426 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1427 }
1428
1429 if binding_matches(&self.keymap.next, key_msg)
1431 || binding_matches(&self.keymap.submit, key_msg)
1432 {
1433 self.run_validation();
1434 if self.error.is_some() {
1435 return None;
1436 }
1437 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1438 }
1439
1440 match key_msg.key_type {
1443 KeyType::Runes => {
1444 let chars_to_insert: Vec<char> = if key_msg.paste {
1446 key_msg
1447 .runes
1448 .iter()
1449 .map(|&c| {
1450 if c == '\n' || c == '\r' || c == '\t' {
1451 ' '
1452 } else {
1453 c
1454 }
1455 })
1456 .fold(Vec::new(), |mut acc, c| {
1458 if c == ' ' && acc.last() == Some(&' ') {
1459 } else {
1461 acc.push(c);
1462 }
1463 acc
1464 })
1465 } else {
1466 key_msg.runes.clone()
1467 };
1468
1469 let current_count = self.value.chars().count();
1471 let available = if self.char_limit == 0 {
1472 usize::MAX
1473 } else {
1474 self.char_limit.saturating_sub(current_count)
1475 };
1476 let chars_to_add: Vec<char> =
1477 chars_to_insert.into_iter().take(available).collect();
1478
1479 if !chars_to_add.is_empty() {
1480 let byte_pos = self
1482 .value
1483 .char_indices()
1484 .nth(self.cursor_pos)
1485 .map(|(i, _)| i)
1486 .unwrap_or(self.value.len());
1487
1488 let insert_str: String = chars_to_add.iter().collect();
1490 self.value.insert_str(byte_pos, &insert_str);
1491 self.cursor_pos += chars_to_add.len();
1492 }
1493 }
1494 KeyType::Backspace => {
1495 if self.cursor_pos > 0 {
1496 self.cursor_pos -= 1;
1497 if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1499 {
1500 self.value.remove(byte_pos);
1501 }
1502 }
1503 }
1504 KeyType::Delete => {
1505 let char_count = self.value.chars().count();
1506 if self.cursor_pos < char_count {
1507 if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1509 {
1510 self.value.remove(byte_pos);
1511 }
1512 }
1513 }
1514 KeyType::Left => {
1515 if self.cursor_pos > 0 {
1516 self.cursor_pos -= 1;
1517 }
1518 }
1519 KeyType::Right => {
1520 let char_count = self.value.chars().count();
1521 if self.cursor_pos < char_count {
1522 self.cursor_pos += 1;
1523 }
1524 }
1525 KeyType::Home => {
1526 self.cursor_pos = 0;
1527 }
1528 KeyType::End => {
1529 self.cursor_pos = self.value.chars().count();
1530 }
1531 _ => {}
1532 }
1533 }
1534
1535 None
1536 }
1537
1538 fn view(&self) -> String {
1539 let styles = self.active_styles();
1540 let mut output = String::new();
1541
1542 if !self.title.is_empty() {
1544 output.push_str(&styles.title.render(&self.title));
1545 if !self.inline {
1546 output.push('\n');
1547 }
1548 }
1549
1550 if !self.description.is_empty() {
1552 output.push_str(&styles.description.render(&self.description));
1553 if !self.inline {
1554 output.push('\n');
1555 }
1556 }
1557
1558 output.push_str(&styles.text_input.prompt.render(&self.prompt));
1560
1561 let display = self.display_value();
1562 if display.is_empty() && !self.placeholder.is_empty() {
1563 output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
1564 } else {
1565 output.push_str(&styles.text_input.text.render(&display));
1566 }
1567
1568 if self.error.is_some() {
1570 output.push_str(&styles.error_indicator.render(""));
1571 }
1572
1573 styles
1574 .base
1575 .width(self.width.try_into().unwrap_or(u16::MAX))
1576 .render(&output)
1577 }
1578
1579 fn focus(&mut self) -> Option<Cmd> {
1580 self.focused = true;
1581 None
1582 }
1583
1584 fn blur(&mut self) -> Option<Cmd> {
1585 self.focused = false;
1586 self.run_validation();
1587 None
1588 }
1589
1590 fn key_binds(&self) -> Vec<Binding> {
1591 if self.show_suggestions {
1592 vec![
1593 self.keymap.accept_suggestion.clone(),
1594 self.keymap.prev.clone(),
1595 self.keymap.submit.clone(),
1596 self.keymap.next.clone(),
1597 ]
1598 } else {
1599 vec![
1600 self.keymap.prev.clone(),
1601 self.keymap.submit.clone(),
1602 self.keymap.next.clone(),
1603 ]
1604 }
1605 }
1606
1607 fn with_theme(&mut self, theme: &Theme) {
1608 if self.theme.is_none() {
1609 self.theme = Some(theme.clone());
1610 }
1611 }
1612
1613 fn with_keymap(&mut self, keymap: &KeyMap) {
1614 self.keymap = keymap.input.clone();
1615 }
1616
1617 fn with_width(&mut self, width: usize) {
1618 self.width = width;
1619 }
1620
1621 fn with_height(&mut self, height: usize) {
1622 self._height = height;
1623 }
1624
1625 fn with_position(&mut self, position: FieldPosition) {
1626 self._position = position;
1627 }
1628}
1629
1630pub struct Select<T: Clone + PartialEq + Send + Sync + 'static> {
1636 id: usize,
1637 key: String,
1638 options: Vec<SelectOption<T>>,
1639 selected: usize,
1640 title: String,
1641 description: String,
1642 inline: bool,
1643 focused: bool,
1644 error: Option<String>,
1645 validate: Option<fn(&T) -> Option<String>>,
1646 width: usize,
1647 height: usize,
1648 theme: Option<Theme>,
1649 keymap: SelectKeyMap,
1650 _position: FieldPosition,
1651 filtering: bool,
1652 filter_value: String,
1653 offset: usize,
1654}
1655
1656impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for Select<T> {
1657 fn default() -> Self {
1658 Self::new()
1659 }
1660}
1661
1662impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Select<T> {
1663 pub fn new() -> Self {
1665 Self {
1666 id: next_id(),
1667 key: String::new(),
1668 options: Vec::new(),
1669 selected: 0,
1670 title: String::new(),
1671 description: String::new(),
1672 inline: false,
1673 focused: false,
1674 error: None,
1675 validate: None,
1676 width: 80,
1677 height: 5,
1678 theme: None,
1679 keymap: SelectKeyMap::default(),
1680 _position: FieldPosition::default(),
1681 filtering: false,
1682 filter_value: String::new(),
1683 offset: 0,
1684 }
1685 }
1686
1687 pub fn key(mut self, key: impl Into<String>) -> Self {
1689 self.key = key.into();
1690 self
1691 }
1692
1693 pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
1695 self.options = options;
1696 for (i, opt) in self.options.iter().enumerate() {
1698 if opt.selected {
1699 self.selected = i;
1700 break;
1701 }
1702 }
1703 self
1704 }
1705
1706 pub fn title(mut self, title: impl Into<String>) -> Self {
1708 self.title = title.into();
1709 self
1710 }
1711
1712 pub fn description(mut self, description: impl Into<String>) -> Self {
1714 self.description = description.into();
1715 self
1716 }
1717
1718 pub fn inline(mut self, inline: bool) -> Self {
1720 self.inline = inline;
1721 self
1722 }
1723
1724 pub fn validate(mut self, validate: fn(&T) -> Option<String>) -> Self {
1726 self.validate = Some(validate);
1727 self
1728 }
1729
1730 pub fn height_options(mut self, height: usize) -> Self {
1732 self.height = height;
1733 self
1734 }
1735
1736 pub fn filterable(mut self, enabled: bool) -> Self {
1742 self.filtering = enabled;
1743 self
1744 }
1745
1746 fn update_filter(&mut self, new_value: String) {
1750 let current_item_idx = self.selected;
1752
1753 self.filter_value = new_value;
1755
1756 let filtered_indices: Vec<usize> = self.filtered_indices();
1758
1759 if filtered_indices.contains(¤t_item_idx) {
1761 self.adjust_offset_from_indices(&filtered_indices);
1763 return;
1764 }
1765
1766 if let Some(&first_idx) = filtered_indices.first() {
1768 self.selected = first_idx;
1769 }
1770 self.adjust_offset_from_indices(&filtered_indices);
1771 }
1772
1773 fn filtered_indices(&self) -> Vec<usize> {
1776 if self.filter_value.is_empty() {
1777 (0..self.options.len()).collect()
1778 } else {
1779 let filter_lower = self.filter_value.to_lowercase();
1780 self.options
1781 .iter()
1782 .enumerate()
1783 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1784 .map(|(i, _)| i)
1785 .collect()
1786 }
1787 }
1788
1789 fn adjust_offset_from_indices(&mut self, filtered_indices: &[usize]) {
1792 let pos = filtered_indices
1793 .iter()
1794 .position(|&idx| idx == self.selected)
1795 .unwrap_or(0);
1796 if pos < self.offset {
1797 self.offset = pos;
1798 } else if pos >= self.offset + self.height {
1799 self.offset = pos.saturating_sub(self.height.saturating_sub(1));
1800 }
1801 }
1802
1803 fn get_theme(&self) -> Theme {
1804 self.theme.clone().unwrap_or_else(theme_charm)
1805 }
1806
1807 fn active_styles(&self) -> FieldStyles {
1808 let theme = self.get_theme();
1809 if self.focused {
1810 theme.focused
1811 } else {
1812 theme.blurred
1813 }
1814 }
1815
1816 fn run_validation(&mut self) {
1817 if let Some(validate) = self.validate
1818 && let Some(opt) = self.options.get(self.selected)
1819 {
1820 self.error = validate(&opt.value);
1821 }
1822 }
1823
1824 fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
1825 if self.filter_value.is_empty() {
1826 self.options.iter().enumerate().collect()
1827 } else {
1828 let filter_lower = self.filter_value.to_lowercase();
1829 self.options
1830 .iter()
1831 .enumerate()
1832 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1833 .collect()
1834 }
1835 }
1836
1837 pub fn get_selected_value(&self) -> Option<&T> {
1839 self.options.get(self.selected).map(|o| &o.value)
1840 }
1841
1842 pub fn id(&self) -> usize {
1844 self.id
1845 }
1846}
1847
1848impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for Select<T> {
1849 fn get_key(&self) -> &str {
1850 &self.key
1851 }
1852
1853 fn get_value(&self) -> Box<dyn Any> {
1854 if let Some(opt) = self.options.get(self.selected) {
1855 Box::new(opt.value.clone())
1856 } else {
1857 Box::new(T::default())
1858 }
1859 }
1860
1861 fn error(&self) -> Option<&str> {
1862 self.error.as_deref()
1863 }
1864
1865 fn init(&mut self) -> Option<Cmd> {
1866 None
1867 }
1868
1869 fn update(&mut self, msg: &Message) -> Option<Cmd> {
1870 if !self.focused {
1871 return None;
1872 }
1873
1874 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1875 self.error = None;
1876
1877 if self.filtering {
1879 if key_msg.key_type == KeyType::Esc {
1881 self.update_filter(String::new());
1882 return None;
1883 }
1884
1885 if key_msg.key_type == KeyType::Backspace {
1887 if !self.filter_value.is_empty() {
1888 let mut new_filter = self.filter_value.clone();
1889 new_filter.pop();
1890 self.update_filter(new_filter);
1891 }
1892 return None;
1893 }
1894
1895 if key_msg.key_type == KeyType::Runes {
1897 let mut new_filter = self.filter_value.clone();
1898 for c in &key_msg.runes {
1899 match c {
1901 'j' | 'k' | 'g' | 'G' | '/' => continue,
1902 _ => {}
1903 }
1904 if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
1905 new_filter.push(*c);
1906 }
1907 }
1908 if new_filter != self.filter_value {
1909 self.update_filter(new_filter);
1910 return None;
1911 }
1912 }
1913 }
1914
1915 if binding_matches(&self.keymap.prev, key_msg) {
1917 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1918 }
1919
1920 if binding_matches(&self.keymap.next, key_msg)
1922 || binding_matches(&self.keymap.submit, key_msg)
1923 {
1924 self.run_validation();
1925 if self.error.is_some() {
1926 return None;
1927 }
1928 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1929 }
1930
1931 let filtered_indices = self.filtered_indices();
1934 let current_pos = filtered_indices
1935 .iter()
1936 .position(|&idx| idx == self.selected);
1937
1938 if binding_matches(&self.keymap.up, key_msg)
1939 && let Some(pos) = current_pos
1940 && pos > 0
1941 {
1942 self.selected = filtered_indices[pos - 1];
1943 self.adjust_offset_from_indices(&filtered_indices);
1944 } else if binding_matches(&self.keymap.down, key_msg)
1945 && let Some(pos) = current_pos
1946 && pos < filtered_indices.len().saturating_sub(1)
1947 {
1948 self.selected = filtered_indices[pos + 1];
1949 self.adjust_offset_from_indices(&filtered_indices);
1950 } else if binding_matches(&self.keymap.goto_top, key_msg)
1951 && let Some(&idx) = filtered_indices.first()
1952 {
1953 self.selected = idx;
1954 self.offset = 0;
1955 } else if binding_matches(&self.keymap.goto_bottom, key_msg)
1956 && let Some(&idx) = filtered_indices.last()
1957 {
1958 self.selected = idx;
1959 let last_pos = filtered_indices.len().saturating_sub(1);
1960 self.offset = last_pos.saturating_sub(self.height.saturating_sub(1));
1961 }
1962 }
1963
1964 None
1965 }
1966
1967 fn view(&self) -> String {
1968 let styles = self.active_styles();
1969 let mut output = String::new();
1970
1971 if !self.title.is_empty() {
1973 output.push_str(&styles.title.render(&self.title));
1974 output.push('\n');
1975 }
1976
1977 if !self.description.is_empty() {
1979 output.push_str(&styles.description.render(&self.description));
1980 output.push('\n');
1981 }
1982
1983 if self.filtering && !self.filter_value.is_empty() {
1985 let filter_display = format!("Filter: {}_", self.filter_value);
1986 output.push_str(&styles.description.render(&filter_display));
1987 output.push('\n');
1988 }
1989
1990 let filtered = self.filtered_options();
1992 let visible: Vec<_> = filtered
1993 .iter()
1994 .skip(self.offset)
1995 .take(self.height)
1996 .collect();
1997
1998 if self.inline {
1999 let mut inline_output = String::new();
2001 inline_output.push_str(&styles.prev_indicator.render(""));
2002 for (i, (idx, opt)) in visible.iter().enumerate() {
2003 if *idx == self.selected {
2004 inline_output.push_str(&styles.selected_option.render(&opt.key));
2005 } else {
2006 inline_output.push_str(&styles.option.render(&opt.key));
2007 }
2008 if i < visible.len() - 1 {
2009 inline_output.push_str(" ");
2010 }
2011 }
2012 inline_output.push_str(&styles.next_indicator.render(""));
2013 output.push_str(&inline_output);
2014 } else {
2015 let has_visible = !visible.is_empty();
2017 for (idx, opt) in &visible {
2018 if *idx == self.selected {
2019 output.push_str(&styles.select_selector.render(""));
2020 output.push_str(&styles.selected_option.render(&opt.key));
2021 } else {
2022 output.push_str(" ");
2023 output.push_str(&styles.option.render(&opt.key));
2024 }
2025 output.push('\n');
2026 }
2027 if has_visible {
2029 output.pop();
2030 }
2031 }
2032
2033 if self.error.is_some() {
2035 output.push_str(&styles.error_indicator.render(""));
2036 }
2037
2038 styles
2039 .base
2040 .width(self.width.try_into().unwrap_or(u16::MAX))
2041 .render(&output)
2042 }
2043
2044 fn focus(&mut self) -> Option<Cmd> {
2045 self.focused = true;
2046 None
2047 }
2048
2049 fn blur(&mut self) -> Option<Cmd> {
2050 self.focused = false;
2051 self.run_validation();
2052 None
2053 }
2054
2055 fn key_binds(&self) -> Vec<Binding> {
2056 vec![
2057 self.keymap.up.clone(),
2058 self.keymap.down.clone(),
2059 self.keymap.prev.clone(),
2060 self.keymap.submit.clone(),
2061 self.keymap.next.clone(),
2062 ]
2063 }
2064
2065 fn with_theme(&mut self, theme: &Theme) {
2066 if self.theme.is_none() {
2067 self.theme = Some(theme.clone());
2068 }
2069 }
2070
2071 fn with_keymap(&mut self, keymap: &KeyMap) {
2072 self.keymap = keymap.select.clone();
2073 }
2074
2075 fn with_width(&mut self, width: usize) {
2076 self.width = width;
2077 }
2078
2079 fn with_height(&mut self, height: usize) {
2080 self.height = height;
2081 }
2082
2083 fn with_position(&mut self, position: FieldPosition) {
2084 self._position = position;
2085 }
2086}
2087
2088pub struct MultiSelect<T: Clone + PartialEq + Send + Sync + 'static> {
2094 id: usize,
2095 key: String,
2096 options: Vec<SelectOption<T>>,
2097 selected: Vec<usize>,
2098 cursor: usize,
2099 title: String,
2100 description: String,
2101 focused: bool,
2102 error: Option<String>,
2103 #[allow(clippy::type_complexity)]
2104 validate: Option<fn(&[T]) -> Option<String>>,
2105 width: usize,
2106 height: usize,
2107 limit: Option<usize>,
2108 theme: Option<Theme>,
2109 keymap: MultiSelectKeyMap,
2110 _position: FieldPosition,
2111 filtering: bool,
2112 filter_value: String,
2113 offset: usize,
2114}
2115
2116impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for MultiSelect<T> {
2117 fn default() -> Self {
2118 Self::new()
2119 }
2120}
2121
2122impl<T: Clone + PartialEq + Send + Sync + Default + 'static> MultiSelect<T> {
2123 pub fn new() -> Self {
2125 Self {
2126 id: next_id(),
2127 key: String::new(),
2128 options: Vec::new(),
2129 selected: Vec::new(),
2130 cursor: 0,
2131 title: String::new(),
2132 description: String::new(),
2133 focused: false,
2134 error: None,
2135 validate: None,
2136 width: 80,
2137 height: 5,
2138 limit: None,
2139 theme: None,
2140 keymap: MultiSelectKeyMap::default(),
2141 _position: FieldPosition::default(),
2142 filtering: false,
2143 filter_value: String::new(),
2144 offset: 0,
2145 }
2146 }
2147
2148 pub fn key(mut self, key: impl Into<String>) -> Self {
2150 self.key = key.into();
2151 self
2152 }
2153
2154 pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
2156 self.options = options;
2157 self.selected = self
2159 .options
2160 .iter()
2161 .enumerate()
2162 .filter(|(_, opt)| opt.selected)
2163 .map(|(i, _)| i)
2164 .collect();
2165 self
2166 }
2167
2168 pub fn title(mut self, title: impl Into<String>) -> Self {
2170 self.title = title.into();
2171 self
2172 }
2173
2174 pub fn description(mut self, description: impl Into<String>) -> Self {
2176 self.description = description.into();
2177 self
2178 }
2179
2180 pub fn validate(mut self, validate: fn(&[T]) -> Option<String>) -> Self {
2182 self.validate = Some(validate);
2183 self
2184 }
2185
2186 pub fn height_options(mut self, height: usize) -> Self {
2188 self.height = height;
2189 self
2190 }
2191
2192 pub fn limit(mut self, limit: usize) -> Self {
2194 self.limit = Some(limit);
2195 self
2196 }
2197
2198 pub fn filterable(mut self, enabled: bool) -> Self {
2202 self.filtering = enabled;
2203 self
2204 }
2205
2206 fn update_filter(&mut self, new_value: String) {
2211 let old_filtered = self.filtered_options();
2213 let current_item_idx = old_filtered.get(self.cursor).map(|(idx, _)| *idx);
2214
2215 self.filter_value = new_value;
2217
2218 let new_filtered = self.filtered_options();
2220
2221 if let Some(item_idx) = current_item_idx
2223 && let Some(new_pos) = new_filtered.iter().position(|(idx, _)| *idx == item_idx)
2224 {
2225 self.cursor = new_pos;
2226 self.adjust_offset();
2227 return;
2228 }
2229
2230 self.cursor = self.cursor.min(new_filtered.len().saturating_sub(1));
2232 self.adjust_offset();
2233 }
2234
2235 fn adjust_offset(&mut self) {
2237 if self.cursor < self.offset {
2239 self.offset = self.cursor;
2240 } else if self.cursor >= self.offset + self.height {
2241 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2242 }
2243 }
2244
2245 fn get_theme(&self) -> Theme {
2246 self.theme.clone().unwrap_or_else(theme_charm)
2247 }
2248
2249 fn active_styles(&self) -> FieldStyles {
2250 let theme = self.get_theme();
2251 if self.focused {
2252 theme.focused
2253 } else {
2254 theme.blurred
2255 }
2256 }
2257
2258 fn run_validation(&mut self) {
2259 if let Some(validate) = self.validate {
2260 let values: Vec<T> = self
2261 .selected
2262 .iter()
2263 .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2264 .collect();
2265 self.error = validate(&values);
2266 }
2267 }
2268
2269 fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
2270 if self.filter_value.is_empty() {
2271 self.options.iter().enumerate().collect()
2272 } else {
2273 let filter_lower = self.filter_value.to_lowercase();
2274 self.options
2275 .iter()
2276 .enumerate()
2277 .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
2278 .collect()
2279 }
2280 }
2281
2282 fn toggle_current(&mut self) {
2283 let filtered = self.filtered_options();
2284 if let Some((idx, _)) = filtered.get(self.cursor) {
2285 if let Some(pos) = self.selected.iter().position(|&i| i == *idx) {
2286 self.selected.remove(pos);
2288 } else if self.limit.is_none_or(|l| self.selected.len() < l) {
2289 self.selected.push(*idx);
2291 }
2292 }
2293 }
2294
2295 fn select_all(&mut self) {
2296 if let Some(limit) = self.limit {
2297 self.selected = self
2299 .options
2300 .iter()
2301 .enumerate()
2302 .take(limit)
2303 .map(|(i, _)| i)
2304 .collect();
2305 } else {
2306 self.selected = (0..self.options.len()).collect();
2307 }
2308 }
2309
2310 fn select_none(&mut self) {
2311 self.selected.clear();
2312 }
2313
2314 pub fn get_selected_values(&self) -> Vec<&T> {
2316 self.selected
2317 .iter()
2318 .filter_map(|&i| self.options.get(i).map(|o| &o.value))
2319 .collect()
2320 }
2321
2322 pub fn id(&self) -> usize {
2324 self.id
2325 }
2326}
2327
2328impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for MultiSelect<T> {
2329 fn get_key(&self) -> &str {
2330 &self.key
2331 }
2332
2333 fn get_value(&self) -> Box<dyn Any> {
2334 let values: Vec<T> = self
2335 .selected
2336 .iter()
2337 .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2338 .collect();
2339 Box::new(values)
2340 }
2341
2342 fn error(&self) -> Option<&str> {
2343 self.error.as_deref()
2344 }
2345
2346 fn init(&mut self) -> Option<Cmd> {
2347 None
2348 }
2349
2350 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2351 if !self.focused {
2352 return None;
2353 }
2354
2355 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2356 self.error = None;
2357
2358 if self.filtering {
2360 if key_msg.key_type == KeyType::Esc {
2362 self.update_filter(String::new());
2363 return None;
2364 }
2365
2366 if key_msg.key_type == KeyType::Backspace {
2368 if !self.filter_value.is_empty() {
2369 let mut new_filter = self.filter_value.clone();
2370 new_filter.pop();
2371 self.update_filter(new_filter);
2372 }
2373 return None;
2374 }
2375
2376 if key_msg.key_type == KeyType::Runes {
2378 let mut new_filter = self.filter_value.clone();
2379 for c in &key_msg.runes {
2380 match c {
2383 'j' | 'k' | 'g' | 'G' | ' ' | 'x' | '/' => continue,
2384 _ => {}
2385 }
2386 if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
2387 new_filter.push(*c);
2388 }
2389 }
2390 if new_filter != self.filter_value {
2391 self.update_filter(new_filter);
2392 return None;
2393 }
2394 }
2395 }
2396
2397 if binding_matches(&self.keymap.prev, key_msg) {
2399 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2400 }
2401
2402 if binding_matches(&self.keymap.next, key_msg)
2404 || binding_matches(&self.keymap.submit, key_msg)
2405 {
2406 self.run_validation();
2407 if self.error.is_some() {
2408 return None;
2409 }
2410 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2411 }
2412
2413 if binding_matches(&self.keymap.toggle, key_msg) {
2415 self.toggle_current();
2416 }
2417
2418 if binding_matches(&self.keymap.select_all, key_msg) {
2420 if self.selected.len() == self.options.len() {
2421 self.select_none();
2422 } else {
2423 self.select_all();
2424 }
2425 }
2426
2427 if binding_matches(&self.keymap.up, key_msg) {
2429 if self.cursor > 0 {
2430 self.cursor -= 1;
2431 if self.cursor < self.offset {
2432 self.offset = self.cursor;
2433 }
2434 }
2435 } else if binding_matches(&self.keymap.down, key_msg) {
2436 let filtered = self.filtered_options();
2437 if self.cursor < filtered.len().saturating_sub(1) {
2438 self.cursor += 1;
2439 if self.cursor >= self.offset + self.height {
2440 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2441 }
2442 }
2443 } else if binding_matches(&self.keymap.goto_top, key_msg) {
2444 self.cursor = 0;
2445 self.offset = 0;
2446 } else if binding_matches(&self.keymap.goto_bottom, key_msg) {
2447 let filtered = self.filtered_options();
2448 self.cursor = filtered.len().saturating_sub(1);
2449 self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2450 }
2451 }
2452
2453 None
2454 }
2455
2456 fn view(&self) -> String {
2457 let styles = self.active_styles();
2458 let mut output = String::new();
2459
2460 if !self.title.is_empty() {
2462 output.push_str(&styles.title.render(&self.title));
2463 output.push('\n');
2464 }
2465
2466 if !self.description.is_empty() {
2468 output.push_str(&styles.description.render(&self.description));
2469 output.push('\n');
2470 }
2471
2472 if self.filtering && !self.filter_value.is_empty() {
2474 let filter_display = format!("Filter: {}_", self.filter_value);
2475 output.push_str(&styles.description.render(&filter_display));
2476 output.push('\n');
2477 }
2478
2479 let filtered = self.filtered_options();
2481 let visible: Vec<_> = filtered
2482 .iter()
2483 .skip(self.offset)
2484 .take(self.height)
2485 .collect();
2486
2487 for (i, (idx, opt)) in visible.iter().enumerate() {
2489 let is_cursor = self.offset + i == self.cursor;
2490 let is_selected = self.selected.contains(idx);
2491
2492 if is_cursor {
2494 output.push_str(&styles.select_selector.render(""));
2495 } else {
2496 output.push_str(" ");
2497 }
2498
2499 let checkbox = if is_selected { "[x] " } else { "[ ] " };
2501 output.push_str(checkbox);
2502
2503 if is_cursor {
2505 output.push_str(&styles.selected_option.render(&opt.key));
2506 } else {
2507 output.push_str(&styles.option.render(&opt.key));
2508 }
2509
2510 output.push('\n');
2511 }
2512
2513 if !visible.is_empty() {
2515 output.pop();
2516 }
2517
2518 if self.error.is_some() {
2520 output.push_str(&styles.error_indicator.render(""));
2521 }
2522
2523 styles
2524 .base
2525 .width(self.width.try_into().unwrap_or(u16::MAX))
2526 .render(&output)
2527 }
2528
2529 fn focus(&mut self) -> Option<Cmd> {
2530 self.focused = true;
2531 None
2532 }
2533
2534 fn blur(&mut self) -> Option<Cmd> {
2535 self.focused = false;
2536 self.run_validation();
2537 None
2538 }
2539
2540 fn key_binds(&self) -> Vec<Binding> {
2541 vec![
2542 self.keymap.up.clone(),
2543 self.keymap.down.clone(),
2544 self.keymap.toggle.clone(),
2545 self.keymap.prev.clone(),
2546 self.keymap.submit.clone(),
2547 self.keymap.next.clone(),
2548 ]
2549 }
2550
2551 fn with_theme(&mut self, theme: &Theme) {
2552 if self.theme.is_none() {
2553 self.theme = Some(theme.clone());
2554 }
2555 }
2556
2557 fn with_keymap(&mut self, keymap: &KeyMap) {
2558 self.keymap = keymap.multi_select.clone();
2559 }
2560
2561 fn with_width(&mut self, width: usize) {
2562 self.width = width;
2563 }
2564
2565 fn with_height(&mut self, height: usize) {
2566 self.height = height;
2567 }
2568
2569 fn with_position(&mut self, position: FieldPosition) {
2570 self._position = position;
2571 }
2572}
2573
2574pub struct Confirm {
2580 id: usize,
2581 key: String,
2582 value: bool,
2583 title: String,
2584 description: String,
2585 affirmative: String,
2586 negative: String,
2587 focused: bool,
2588 width: usize,
2589 theme: Option<Theme>,
2590 keymap: ConfirmKeyMap,
2591 _position: FieldPosition,
2592}
2593
2594impl Default for Confirm {
2595 fn default() -> Self {
2596 Self::new()
2597 }
2598}
2599
2600impl Confirm {
2601 pub fn new() -> Self {
2603 Self {
2604 id: next_id(),
2605 key: String::new(),
2606 value: false,
2607 title: String::new(),
2608 description: String::new(),
2609 affirmative: "Yes".to_string(),
2610 negative: "No".to_string(),
2611 focused: false,
2612 width: 80,
2613 theme: None,
2614 keymap: ConfirmKeyMap::default(),
2615 _position: FieldPosition::default(),
2616 }
2617 }
2618
2619 pub fn key(mut self, key: impl Into<String>) -> Self {
2621 self.key = key.into();
2622 self
2623 }
2624
2625 pub fn value(mut self, value: bool) -> Self {
2627 self.value = value;
2628 self
2629 }
2630
2631 pub fn title(mut self, title: impl Into<String>) -> Self {
2633 self.title = title.into();
2634 self
2635 }
2636
2637 pub fn description(mut self, description: impl Into<String>) -> Self {
2639 self.description = description.into();
2640 self
2641 }
2642
2643 pub fn affirmative(mut self, text: impl Into<String>) -> Self {
2645 self.affirmative = text.into();
2646 self
2647 }
2648
2649 pub fn negative(mut self, text: impl Into<String>) -> Self {
2651 self.negative = text.into();
2652 self
2653 }
2654
2655 fn get_theme(&self) -> Theme {
2656 self.theme.clone().unwrap_or_else(theme_charm)
2657 }
2658
2659 fn active_styles(&self) -> FieldStyles {
2660 let theme = self.get_theme();
2661 if self.focused {
2662 theme.focused
2663 } else {
2664 theme.blurred
2665 }
2666 }
2667
2668 pub fn get_bool_value(&self) -> bool {
2670 self.value
2671 }
2672
2673 pub fn id(&self) -> usize {
2675 self.id
2676 }
2677}
2678
2679impl Field for Confirm {
2680 fn get_key(&self) -> &str {
2681 &self.key
2682 }
2683
2684 fn get_value(&self) -> Box<dyn Any> {
2685 Box::new(self.value)
2686 }
2687
2688 fn error(&self) -> Option<&str> {
2689 None
2690 }
2691
2692 fn init(&mut self) -> Option<Cmd> {
2693 None
2694 }
2695
2696 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2697 if !self.focused {
2698 return None;
2699 }
2700
2701 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2702 if binding_matches(&self.keymap.prev, key_msg) {
2704 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2705 }
2706
2707 if binding_matches(&self.keymap.next, key_msg)
2709 || binding_matches(&self.keymap.submit, key_msg)
2710 {
2711 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2712 }
2713
2714 if binding_matches(&self.keymap.toggle, key_msg) {
2716 self.value = !self.value;
2717 }
2718
2719 if binding_matches(&self.keymap.accept, key_msg) {
2721 self.value = true;
2722 }
2723 if binding_matches(&self.keymap.reject, key_msg) {
2724 self.value = false;
2725 }
2726 }
2727
2728 None
2729 }
2730
2731 fn view(&self) -> String {
2732 let styles = self.active_styles();
2733 let mut output = String::new();
2734
2735 if !self.title.is_empty() {
2737 output.push_str(&styles.title.render(&self.title));
2738 output.push('\n');
2739 }
2740
2741 if !self.description.is_empty() {
2743 output.push_str(&styles.description.render(&self.description));
2744 output.push('\n');
2745 }
2746
2747 if self.value {
2749 output.push_str(&styles.focused_button.render(&self.affirmative));
2750 output.push_str(&styles.blurred_button.render(&self.negative));
2751 } else {
2752 output.push_str(&styles.blurred_button.render(&self.affirmative));
2753 output.push_str(&styles.focused_button.render(&self.negative));
2754 }
2755
2756 styles
2757 .base
2758 .width(self.width.try_into().unwrap_or(u16::MAX))
2759 .render(&output)
2760 }
2761
2762 fn focus(&mut self) -> Option<Cmd> {
2763 self.focused = true;
2764 None
2765 }
2766
2767 fn blur(&mut self) -> Option<Cmd> {
2768 self.focused = false;
2769 None
2770 }
2771
2772 fn key_binds(&self) -> Vec<Binding> {
2773 vec![
2774 self.keymap.toggle.clone(),
2775 self.keymap.accept.clone(),
2776 self.keymap.reject.clone(),
2777 self.keymap.prev.clone(),
2778 self.keymap.submit.clone(),
2779 self.keymap.next.clone(),
2780 ]
2781 }
2782
2783 fn with_theme(&mut self, theme: &Theme) {
2784 if self.theme.is_none() {
2785 self.theme = Some(theme.clone());
2786 }
2787 }
2788
2789 fn with_keymap(&mut self, keymap: &KeyMap) {
2790 self.keymap = keymap.confirm.clone();
2791 }
2792
2793 fn with_width(&mut self, width: usize) {
2794 self.width = width;
2795 }
2796
2797 fn with_height(&mut self, _height: usize) {
2798 }
2800
2801 fn with_position(&mut self, position: FieldPosition) {
2802 self._position = position;
2803 }
2804}
2805
2806pub struct Note {
2812 id: usize,
2813 key: String,
2814 title: String,
2815 description: String,
2816 focused: bool,
2817 width: usize,
2818 theme: Option<Theme>,
2819 keymap: NoteKeyMap,
2820 _position: FieldPosition,
2821 next_label: String,
2822}
2823
2824impl Default for Note {
2825 fn default() -> Self {
2826 Self::new()
2827 }
2828}
2829
2830impl Note {
2831 pub fn new() -> Self {
2833 Self {
2834 id: next_id(),
2835 key: String::new(),
2836 title: String::new(),
2837 description: String::new(),
2838 focused: false,
2839 width: 80,
2840 theme: None,
2841 keymap: NoteKeyMap::default(),
2842 _position: FieldPosition::default(),
2843 next_label: "Next".to_string(),
2844 }
2845 }
2846
2847 pub fn key(mut self, key: impl Into<String>) -> Self {
2849 self.key = key.into();
2850 self
2851 }
2852
2853 pub fn title(mut self, title: impl Into<String>) -> Self {
2855 self.title = title.into();
2856 self
2857 }
2858
2859 pub fn description(mut self, description: impl Into<String>) -> Self {
2861 self.description = description.into();
2862 self
2863 }
2864
2865 pub fn next_label(mut self, label: impl Into<String>) -> Self {
2867 self.next_label = label.into();
2868 self
2869 }
2870
2871 pub fn next(self, label: impl Into<String>) -> Self {
2875 self.next_label(label)
2876 }
2877
2878 fn get_theme(&self) -> Theme {
2879 self.theme.clone().unwrap_or_else(theme_charm)
2880 }
2881
2882 fn active_styles(&self) -> FieldStyles {
2883 let theme = self.get_theme();
2884 if self.focused {
2885 theme.focused
2886 } else {
2887 theme.blurred
2888 }
2889 }
2890
2891 pub fn id(&self) -> usize {
2893 self.id
2894 }
2895}
2896
2897impl Field for Note {
2898 fn get_key(&self) -> &str {
2899 &self.key
2900 }
2901
2902 fn get_value(&self) -> Box<dyn Any> {
2903 Box::new(())
2904 }
2905
2906 fn error(&self) -> Option<&str> {
2907 None
2908 }
2909
2910 fn init(&mut self) -> Option<Cmd> {
2911 None
2912 }
2913
2914 fn update(&mut self, msg: &Message) -> Option<Cmd> {
2915 if !self.focused {
2916 return None;
2917 }
2918
2919 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2920 if binding_matches(&self.keymap.prev, key_msg) {
2922 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2923 }
2924
2925 if binding_matches(&self.keymap.next, key_msg)
2927 || binding_matches(&self.keymap.submit, key_msg)
2928 {
2929 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2930 }
2931 }
2932
2933 None
2934 }
2935
2936 fn view(&self) -> String {
2937 let styles = self.active_styles();
2938 let mut output = String::new();
2939
2940 if !self.title.is_empty() {
2942 output.push_str(&styles.note_title.render(&self.title));
2943 output.push('\n');
2944 }
2945
2946 if !self.description.is_empty() {
2948 output.push_str(&styles.description.render(&self.description));
2949 }
2950
2951 styles
2952 .base
2953 .width(self.width.try_into().unwrap_or(u16::MAX))
2954 .render(&output)
2955 }
2956
2957 fn focus(&mut self) -> Option<Cmd> {
2958 self.focused = true;
2959 None
2960 }
2961
2962 fn blur(&mut self) -> Option<Cmd> {
2963 self.focused = false;
2964 None
2965 }
2966
2967 fn key_binds(&self) -> Vec<Binding> {
2968 vec![
2969 self.keymap.prev.clone(),
2970 self.keymap.submit.clone(),
2971 self.keymap.next.clone(),
2972 ]
2973 }
2974
2975 fn with_theme(&mut self, theme: &Theme) {
2976 if self.theme.is_none() {
2977 self.theme = Some(theme.clone());
2978 }
2979 }
2980
2981 fn with_keymap(&mut self, keymap: &KeyMap) {
2982 self.keymap = keymap.note.clone();
2983 }
2984
2985 fn with_width(&mut self, width: usize) {
2986 self.width = width;
2987 }
2988
2989 fn with_height(&mut self, _height: usize) {
2990 }
2992
2993 fn with_position(&mut self, position: FieldPosition) {
2994 self._position = position;
2995 }
2996}
2997
2998pub struct Text {
3020 id: usize,
3021 key: String,
3022 value: String,
3023 title: String,
3024 description: String,
3025 placeholder: String,
3026 lines: usize,
3027 char_limit: usize,
3028 show_line_numbers: bool,
3029 focused: bool,
3030 error: Option<String>,
3031 validate: Option<fn(&str) -> Option<String>>,
3032 width: usize,
3033 height: usize,
3034 theme: Option<Theme>,
3035 keymap: TextKeyMap,
3036 _position: FieldPosition,
3037 cursor_row: usize,
3038 cursor_col: usize,
3039}
3040
3041impl Default for Text {
3042 fn default() -> Self {
3043 Self::new()
3044 }
3045}
3046
3047impl Text {
3048 pub fn new() -> Self {
3050 Self {
3051 id: next_id(),
3052 key: String::new(),
3053 value: String::new(),
3054 title: String::new(),
3055 description: String::new(),
3056 placeholder: String::new(),
3057 lines: 5,
3058 char_limit: 0,
3059 show_line_numbers: false,
3060 focused: false,
3061 error: None,
3062 validate: None,
3063 width: 80,
3064 height: 0,
3065 theme: None,
3066 keymap: TextKeyMap::default(),
3067 _position: FieldPosition::default(),
3068 cursor_row: 0,
3069 cursor_col: 0,
3070 }
3071 }
3072
3073 pub fn key(mut self, key: impl Into<String>) -> Self {
3075 self.key = key.into();
3076 self
3077 }
3078
3079 pub fn value(mut self, value: impl Into<String>) -> Self {
3081 self.value = value.into();
3082 self
3083 }
3084
3085 pub fn title(mut self, title: impl Into<String>) -> Self {
3087 self.title = title.into();
3088 self
3089 }
3090
3091 pub fn description(mut self, description: impl Into<String>) -> Self {
3093 self.description = description.into();
3094 self
3095 }
3096
3097 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
3099 self.placeholder = placeholder.into();
3100 self
3101 }
3102
3103 pub fn lines(mut self, lines: usize) -> Self {
3105 self.lines = lines;
3106 self
3107 }
3108
3109 pub fn char_limit(mut self, limit: usize) -> Self {
3111 self.char_limit = limit;
3112 self
3113 }
3114
3115 pub fn show_line_numbers(mut self, show: bool) -> Self {
3117 self.show_line_numbers = show;
3118 self
3119 }
3120
3121 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3123 self.validate = Some(validate);
3124 self
3125 }
3126
3127 fn get_theme(&self) -> Theme {
3128 self.theme.clone().unwrap_or_else(theme_charm)
3129 }
3130
3131 fn active_styles(&self) -> FieldStyles {
3132 let theme = self.get_theme();
3133 if self.focused {
3134 theme.focused
3135 } else {
3136 theme.blurred
3137 }
3138 }
3139
3140 fn run_validation(&mut self) {
3141 if let Some(validate) = self.validate {
3142 self.error = validate(&self.value);
3143 }
3144 }
3145
3146 pub fn get_string_value(&self) -> &str {
3148 &self.value
3149 }
3150
3151 pub fn id(&self) -> usize {
3153 self.id
3154 }
3155
3156 fn visible_lines(&self) -> Vec<&str> {
3157 let lines: Vec<&str> = self.value.lines().collect();
3158 if lines.is_empty() { vec![""] } else { lines }
3159 }
3160
3161 fn transpose_left(&mut self) {
3167 let lines: Vec<String> = self.value.lines().map(String::from).collect();
3168 if self.cursor_row >= lines.len() {
3169 return;
3170 }
3171
3172 let line_chars: Vec<char> = lines[self.cursor_row].chars().collect();
3173
3174 if self.cursor_col == 0 || line_chars.len() < 2 {
3176 return;
3177 }
3178
3179 let mut col = self.cursor_col;
3180
3181 if col >= line_chars.len() {
3183 col = line_chars.len() - 1;
3184 self.cursor_col = col;
3185 }
3186
3187 let mut new_chars = line_chars;
3189 new_chars.swap(col - 1, col);
3190
3191 let mut new_lines = lines;
3193 new_lines[self.cursor_row] = new_chars.into_iter().collect();
3194 self.value = new_lines.join("\n");
3195
3196 let new_line_len = self
3198 .value
3199 .lines()
3200 .nth(self.cursor_row)
3201 .map(|l| l.chars().count())
3202 .unwrap_or(0);
3203 if self.cursor_col < new_line_len {
3204 self.cursor_col += 1;
3205 }
3206 }
3207
3208 fn do_word_right<F>(&mut self, mut f: F)
3213 where
3214 F: FnMut(usize, char) -> char,
3215 {
3216 let lines: Vec<String> = self.value.lines().map(String::from).collect();
3217 if self.cursor_row >= lines.len() {
3218 return;
3219 }
3220
3221 let mut chars: Vec<char> = lines[self.cursor_row].chars().collect();
3222 let len = chars.len();
3223
3224 while self.cursor_col < len && chars[self.cursor_col].is_whitespace() {
3226 self.cursor_col += 1;
3227 }
3228
3229 let mut char_idx = 0;
3231 while self.cursor_col < len && !chars[self.cursor_col].is_whitespace() {
3232 chars[self.cursor_col] = f(char_idx, chars[self.cursor_col]);
3233 self.cursor_col += 1;
3234 char_idx += 1;
3235 }
3236
3237 let mut new_lines = lines;
3239 new_lines[self.cursor_row] = chars.into_iter().collect();
3240 self.value = new_lines.join("\n");
3241 }
3242
3243 fn uppercase_right(&mut self) {
3245 self.do_word_right(|_, c| c.to_uppercase().next().unwrap_or(c));
3246 }
3247
3248 fn lowercase_right(&mut self) {
3250 self.do_word_right(|_, c| c.to_lowercase().next().unwrap_or(c));
3251 }
3252
3253 fn capitalize_right(&mut self) {
3255 self.do_word_right(|idx, c| {
3256 if idx == 0 {
3257 c.to_uppercase().next().unwrap_or(c)
3258 } else {
3259 c
3260 }
3261 });
3262 }
3263}
3264
3265impl Field for Text {
3266 fn get_key(&self) -> &str {
3267 &self.key
3268 }
3269
3270 fn get_value(&self) -> Box<dyn Any> {
3271 Box::new(self.value.clone())
3272 }
3273
3274 fn error(&self) -> Option<&str> {
3275 self.error.as_deref()
3276 }
3277
3278 fn init(&mut self) -> Option<Cmd> {
3279 None
3280 }
3281
3282 fn update(&mut self, msg: &Message) -> Option<Cmd> {
3283 if !self.focused {
3284 return None;
3285 }
3286
3287 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3288 self.error = None;
3289
3290 if binding_matches(&self.keymap.prev, key_msg) {
3292 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3293 }
3294
3295 if binding_matches(&self.keymap.next, key_msg)
3297 || binding_matches(&self.keymap.submit, key_msg)
3298 {
3299 self.run_validation();
3300 if self.error.is_some() {
3301 return None;
3302 }
3303 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3304 }
3305
3306 if binding_matches(&self.keymap.new_line, key_msg) {
3308 if self.char_limit == 0 || self.value.len() < self.char_limit {
3309 self.value.push('\n');
3310 self.cursor_row += 1;
3311 self.cursor_col = 0;
3312 }
3313 return None;
3314 }
3315
3316 if binding_matches(&self.keymap.uppercase_word_forward, key_msg) {
3318 self.uppercase_right();
3319 return None;
3320 }
3321 if binding_matches(&self.keymap.lowercase_word_forward, key_msg) {
3322 self.lowercase_right();
3323 return None;
3324 }
3325 if binding_matches(&self.keymap.capitalize_word_forward, key_msg) {
3326 self.capitalize_right();
3327 return None;
3328 }
3329 if binding_matches(&self.keymap.transpose_character_backward, key_msg) {
3330 self.transpose_left();
3331 return None;
3332 }
3333
3334 match key_msg.key_type {
3336 KeyType::Runes => {
3337 let current_count = self.value.chars().count();
3339 let available = if self.char_limit == 0 {
3340 usize::MAX
3341 } else {
3342 self.char_limit.saturating_sub(current_count)
3343 };
3344
3345 let chars_to_add: Vec<char> =
3348 key_msg.runes.iter().copied().take(available).collect();
3349
3350 for c in chars_to_add {
3351 self.value.push(c);
3352 if c == '\n' {
3353 self.cursor_row += 1;
3354 self.cursor_col = 0;
3355 } else {
3356 self.cursor_col += 1;
3357 }
3358 }
3359 }
3360 KeyType::Backspace => {
3361 if !self.value.is_empty() {
3362 let removed = self.value.pop();
3363 if removed == Some('\n') {
3364 self.cursor_row = self.cursor_row.saturating_sub(1);
3365 let lines = self.visible_lines();
3366 self.cursor_col =
3367 lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3368 } else {
3369 self.cursor_col = self.cursor_col.saturating_sub(1);
3370 }
3371 }
3372 }
3373 KeyType::Enter => {
3374 if self.char_limit == 0 || self.value.len() < self.char_limit {
3376 self.value.push('\n');
3377 self.cursor_row += 1;
3378 self.cursor_col = 0;
3379 }
3380 }
3381 KeyType::Up => {
3382 self.cursor_row = self.cursor_row.saturating_sub(1);
3383 }
3384 KeyType::Down => {
3385 let line_count = self.visible_lines().len();
3386 if self.cursor_row < line_count.saturating_sub(1) {
3387 self.cursor_row += 1;
3388 }
3389 }
3390 KeyType::Left => {
3391 if self.cursor_col > 0 {
3392 self.cursor_col -= 1;
3393 }
3394 }
3395 KeyType::Right => {
3396 let lines = self.visible_lines();
3397 let current_line_len = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3398 if self.cursor_col < current_line_len {
3399 self.cursor_col += 1;
3400 }
3401 }
3402 KeyType::Home => {
3403 self.cursor_col = 0;
3404 }
3405 KeyType::End => {
3406 let lines = self.visible_lines();
3407 self.cursor_col = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3408 }
3409 _ => {}
3410 }
3411 }
3412
3413 None
3414 }
3415
3416 fn view(&self) -> String {
3417 let styles = self.active_styles();
3418 let mut output = String::new();
3419
3420 if !self.title.is_empty() {
3422 output.push_str(&styles.title.render(&self.title));
3423 if self.error.is_some() {
3424 output.push_str(&styles.error_indicator.render(""));
3425 }
3426 output.push('\n');
3427 }
3428
3429 if !self.description.is_empty() {
3431 output.push_str(&styles.description.render(&self.description));
3432 output.push('\n');
3433 }
3434
3435 let lines = self.visible_lines();
3437 let visible_lines = self.lines.min(lines.len().max(1));
3438
3439 for (i, line) in lines.iter().take(visible_lines).enumerate() {
3440 if self.show_line_numbers {
3441 let line_num = format!("{:3} ", i + 1);
3442 output.push_str(&styles.description.render(&line_num));
3443 }
3444
3445 if line.is_empty() && i == 0 && self.value.is_empty() && !self.placeholder.is_empty() {
3446 output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
3447 } else {
3448 output.push_str(&styles.text_input.text.render(line));
3449 }
3450
3451 if i < visible_lines - 1 {
3452 output.push('\n');
3453 }
3454 }
3455
3456 for i in lines.len()..visible_lines {
3458 output.push('\n');
3459 if self.show_line_numbers {
3460 let line_num = format!("{:3} ", i + 1);
3461 output.push_str(&styles.description.render(&line_num));
3462 }
3463 }
3464
3465 if let Some(ref err) = self.error {
3467 output.push('\n');
3468 output.push_str(&styles.error_message.render(err));
3469 }
3470
3471 styles
3472 .base
3473 .width(self.width.try_into().unwrap_or(u16::MAX))
3474 .render(&output)
3475 }
3476
3477 fn focus(&mut self) -> Option<Cmd> {
3478 self.focused = true;
3479 None
3480 }
3481
3482 fn blur(&mut self) -> Option<Cmd> {
3483 self.focused = false;
3484 self.run_validation();
3485 None
3486 }
3487
3488 fn key_binds(&self) -> Vec<Binding> {
3489 vec![
3490 self.keymap.new_line.clone(),
3491 self.keymap.prev.clone(),
3492 self.keymap.submit.clone(),
3493 self.keymap.next.clone(),
3494 self.keymap.uppercase_word_forward.clone(),
3495 self.keymap.lowercase_word_forward.clone(),
3496 self.keymap.capitalize_word_forward.clone(),
3497 self.keymap.transpose_character_backward.clone(),
3498 ]
3499 }
3500
3501 fn with_theme(&mut self, theme: &Theme) {
3502 if self.theme.is_none() {
3503 self.theme = Some(theme.clone());
3504 }
3505 }
3506
3507 fn with_keymap(&mut self, keymap: &KeyMap) {
3508 self.keymap = keymap.text.clone();
3509 }
3510
3511 fn with_width(&mut self, width: usize) {
3512 self.width = width;
3513 }
3514
3515 fn with_height(&mut self, height: usize) {
3516 self.height = height;
3517 let adjust = if self.title.is_empty() { 0 } else { 1 }
3519 + if self.description.is_empty() { 0 } else { 1 };
3520 if height > adjust {
3521 self.lines = height - adjust;
3522 }
3523 }
3524
3525 fn with_position(&mut self, position: FieldPosition) {
3526 self._position = position;
3527 }
3528}
3529
3530pub struct FilePicker {
3553 id: usize,
3554 key: String,
3555 selected_path: Option<String>,
3556 title: String,
3557 description: String,
3558 current_directory: String,
3559 allowed_types: Vec<String>,
3560 show_hidden: bool,
3561 show_size: bool,
3562 show_permissions: bool,
3563 file_allowed: bool,
3564 dir_allowed: bool,
3565 picking: bool,
3566 focused: bool,
3567 error: Option<String>,
3568 validate: Option<fn(&str) -> Option<String>>,
3569 width: usize,
3570 height: usize,
3571 theme: Option<Theme>,
3572 keymap: FilePickerKeyMap,
3573 _position: FieldPosition,
3574 files: Vec<FileEntry>,
3576 selected_index: usize,
3577 offset: usize,
3578}
3579
3580#[derive(Debug, Clone)]
3582struct FileEntry {
3583 name: String,
3584 path: String,
3585 is_dir: bool,
3586 size: u64,
3587 #[allow(dead_code)]
3588 mode: String,
3589}
3590
3591impl Default for FilePicker {
3592 fn default() -> Self {
3593 Self::new()
3594 }
3595}
3596
3597impl FilePicker {
3598 pub fn new() -> Self {
3600 Self {
3601 id: next_id(),
3602 key: String::new(),
3603 selected_path: None,
3604 title: String::new(),
3605 description: String::new(),
3606 current_directory: ".".to_string(),
3607 allowed_types: Vec::new(),
3608 show_hidden: false,
3609 show_size: false,
3610 show_permissions: false,
3611 file_allowed: true,
3612 dir_allowed: false,
3613 picking: false,
3614 focused: false,
3615 error: None,
3616 validate: None,
3617 width: 80,
3618 height: 10,
3619 theme: None,
3620 keymap: FilePickerKeyMap::default(),
3621 _position: FieldPosition::default(),
3622 files: Vec::new(),
3623 selected_index: 0,
3624 offset: 0,
3625 }
3626 }
3627
3628 pub fn key(mut self, key: impl Into<String>) -> Self {
3630 self.key = key.into();
3631 self
3632 }
3633
3634 pub fn title(mut self, title: impl Into<String>) -> Self {
3636 self.title = title.into();
3637 self
3638 }
3639
3640 pub fn description(mut self, description: impl Into<String>) -> Self {
3642 self.description = description.into();
3643 self
3644 }
3645
3646 pub fn current_directory(mut self, dir: impl Into<String>) -> Self {
3648 self.current_directory = dir.into();
3649 self
3650 }
3651
3652 pub fn allowed_types(mut self, types: Vec<String>) -> Self {
3654 self.allowed_types = types;
3655 self
3656 }
3657
3658 pub fn show_hidden(mut self, show: bool) -> Self {
3660 self.show_hidden = show;
3661 self
3662 }
3663
3664 pub fn show_size(mut self, show: bool) -> Self {
3666 self.show_size = show;
3667 self
3668 }
3669
3670 pub fn show_permissions(mut self, show: bool) -> Self {
3672 self.show_permissions = show;
3673 self
3674 }
3675
3676 pub fn file_allowed(mut self, allowed: bool) -> Self {
3678 self.file_allowed = allowed;
3679 self
3680 }
3681
3682 pub fn dir_allowed(mut self, allowed: bool) -> Self {
3684 self.dir_allowed = allowed;
3685 self
3686 }
3687
3688 pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3690 self.validate = Some(validate);
3691 self
3692 }
3693
3694 pub fn height_entries(mut self, height: usize) -> Self {
3696 self.height = height;
3697 self
3698 }
3699
3700 fn get_theme(&self) -> Theme {
3701 self.theme.clone().unwrap_or_else(theme_charm)
3702 }
3703
3704 fn active_styles(&self) -> FieldStyles {
3705 let theme = self.get_theme();
3706 if self.focused {
3707 theme.focused
3708 } else {
3709 theme.blurred
3710 }
3711 }
3712
3713 fn run_validation(&mut self) {
3714 if let Some(validate) = self.validate
3715 && let Some(ref path) = self.selected_path
3716 {
3717 self.error = validate(path);
3718 }
3719 }
3720
3721 fn read_directory(&mut self) {
3722 self.files.clear();
3723 self.selected_index = 0;
3724 self.offset = 0;
3725
3726 if self.current_directory != "/" {
3728 self.files.push(FileEntry {
3729 name: "..".to_string(),
3730 path: "..".to_string(),
3731 is_dir: true,
3732 size: 0,
3733 mode: String::new(),
3734 });
3735 }
3736
3737 if let Ok(entries) = std::fs::read_dir(&self.current_directory) {
3739 let mut entries: Vec<_> = entries
3740 .filter_map(|e| e.ok())
3741 .filter_map(|entry| {
3742 let name = entry.file_name().to_string_lossy().to_string();
3743
3744 if !self.show_hidden && name.starts_with('.') {
3746 return None;
3747 }
3748
3749 let metadata = entry.metadata().ok()?;
3750 let is_dir = metadata.is_dir();
3751 let size = metadata.len();
3752
3753 if !is_dir && !self.allowed_types.is_empty() {
3755 let matches = self.allowed_types.iter().any(|ext| {
3756 name.ends_with(ext)
3757 || name.ends_with(&ext.trim_start_matches('.').to_string())
3758 });
3759 if !matches {
3760 return None;
3761 }
3762 }
3763
3764 let path = entry.path().to_string_lossy().to_string();
3765
3766 Some(FileEntry {
3767 name,
3768 path,
3769 is_dir,
3770 size,
3771 mode: String::new(),
3772 })
3773 })
3774 .collect();
3775
3776 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
3778 (true, false) => std::cmp::Ordering::Less,
3779 (false, true) => std::cmp::Ordering::Greater,
3780 _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
3781 });
3782
3783 self.files.extend(entries);
3784 }
3785 }
3786
3787 fn is_selectable(&self, entry: &FileEntry) -> bool {
3788 if entry.is_dir {
3789 self.dir_allowed
3790 } else {
3791 self.file_allowed
3792 }
3793 }
3794
3795 fn format_size(size: u64) -> String {
3796 const KB: u64 = 1024;
3797 const MB: u64 = KB * 1024;
3798 const GB: u64 = MB * 1024;
3799
3800 if size >= GB {
3801 format!("{:.1}G", size as f64 / GB as f64)
3802 } else if size >= MB {
3803 format!("{:.1}M", size as f64 / MB as f64)
3804 } else if size >= KB {
3805 format!("{:.1}K", size as f64 / KB as f64)
3806 } else {
3807 format!("{}B", size)
3808 }
3809 }
3810
3811 pub fn get_selected_path(&self) -> Option<&str> {
3813 self.selected_path.as_deref()
3814 }
3815
3816 pub fn id(&self) -> usize {
3818 self.id
3819 }
3820}
3821
3822impl Field for FilePicker {
3823 fn get_key(&self) -> &str {
3824 &self.key
3825 }
3826
3827 fn get_value(&self) -> Box<dyn Any> {
3828 Box::new(self.selected_path.clone().unwrap_or_default())
3829 }
3830
3831 fn error(&self) -> Option<&str> {
3832 self.error.as_deref()
3833 }
3834
3835 fn init(&mut self) -> Option<Cmd> {
3836 self.read_directory();
3837 None
3838 }
3839
3840 fn update(&mut self, msg: &Message) -> Option<Cmd> {
3841 if !self.focused {
3842 return None;
3843 }
3844
3845 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3846 self.error = None;
3847
3848 if binding_matches(&self.keymap.prev, key_msg) {
3850 self.picking = false;
3851 return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3852 }
3853
3854 if binding_matches(&self.keymap.next, key_msg) {
3856 self.picking = false;
3857 self.run_validation();
3858 if self.error.is_some() {
3859 return None;
3860 }
3861 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3862 }
3863
3864 if binding_matches(&self.keymap.close, key_msg) {
3866 if self.picking {
3867 self.picking = false;
3868 } else {
3869 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3870 }
3871 return None;
3872 }
3873
3874 if binding_matches(&self.keymap.open, key_msg) {
3876 if !self.picking {
3877 self.picking = true;
3878 self.read_directory();
3879 return None;
3880 }
3881
3882 if let Some(entry) = self.files.get(self.selected_index) {
3884 if entry.name == ".." {
3885 if let Some(parent) = std::path::Path::new(&self.current_directory).parent()
3887 {
3888 self.current_directory = parent.to_string_lossy().to_string();
3889 if self.current_directory.is_empty() {
3890 self.current_directory = "/".to_string();
3891 }
3892 self.read_directory();
3893 }
3894 } else if entry.is_dir {
3895 self.current_directory = entry.path.clone();
3897 self.read_directory();
3898 } else if self.is_selectable(entry) {
3899 self.selected_path = Some(entry.path.clone());
3901 self.picking = false;
3902 self.run_validation();
3903 if self.error.is_some() {
3904 return None;
3905 }
3906 return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3907 }
3908 }
3909 return None;
3910 }
3911
3912 if self.picking && binding_matches(&self.keymap.back, key_msg) {
3914 if let Some(parent) = std::path::Path::new(&self.current_directory).parent() {
3915 self.current_directory = parent.to_string_lossy().to_string();
3916 if self.current_directory.is_empty() {
3917 self.current_directory = "/".to_string();
3918 }
3919 self.read_directory();
3920 }
3921 return None;
3922 }
3923
3924 if self.picking {
3926 if binding_matches(&self.keymap.up, key_msg) {
3927 if self.selected_index > 0 {
3928 self.selected_index -= 1;
3929 if self.selected_index < self.offset {
3930 self.offset = self.selected_index;
3931 }
3932 }
3933 } else if binding_matches(&self.keymap.down, key_msg) {
3934 if !self.files.is_empty()
3935 && self.selected_index < self.files.len().saturating_sub(1)
3936 {
3937 self.selected_index += 1;
3938 if self.height > 0 && self.selected_index >= self.offset + self.height {
3939 self.offset = self
3940 .selected_index
3941 .saturating_sub(self.height.saturating_sub(1));
3942 }
3943 }
3944 } else if binding_matches(&self.keymap.goto_top, key_msg) {
3945 self.selected_index = 0;
3946 self.offset = 0;
3947 } else if binding_matches(&self.keymap.goto_bottom, key_msg)
3948 && !self.files.is_empty()
3949 {
3950 self.selected_index = self.files.len().saturating_sub(1);
3951 self.offset = self
3952 .selected_index
3953 .saturating_sub(self.height.saturating_sub(1));
3954 }
3955 }
3956 }
3957
3958 None
3959 }
3960
3961 fn view(&self) -> String {
3962 let styles = self.active_styles();
3963 let mut output = String::new();
3964
3965 if !self.title.is_empty() {
3967 output.push_str(&styles.title.render(&self.title));
3968 if self.error.is_some() {
3969 output.push_str(&styles.error_indicator.render(""));
3970 }
3971 output.push('\n');
3972 }
3973
3974 if !self.description.is_empty() {
3976 output.push_str(&styles.description.render(&self.description));
3977 output.push('\n');
3978 }
3979
3980 if self.picking {
3981 let visible: Vec<_> = self
3983 .files
3984 .iter()
3985 .skip(self.offset)
3986 .take(self.height)
3987 .collect();
3988
3989 for (i, entry) in visible.iter().enumerate() {
3990 let idx = self.offset + i;
3991 let is_selected = idx == self.selected_index;
3992 let is_selectable = self.is_selectable(entry);
3993
3994 if is_selected {
3996 output.push_str(&styles.select_selector.render(""));
3997 } else {
3998 output.push_str(" ");
3999 }
4000
4001 let mut entry_str = String::new();
4003
4004 if entry.is_dir {
4006 entry_str.push_str("📁 ");
4007 } else {
4008 entry_str.push_str(" ");
4009 }
4010
4011 entry_str.push_str(&entry.name);
4012
4013 if self.show_size && !entry.is_dir {
4015 entry_str.push_str(&format!(" ({})", Self::format_size(entry.size)));
4016 }
4017
4018 if is_selected && is_selectable {
4019 output.push_str(&styles.selected_option.render(&entry_str));
4020 } else if !is_selectable && !entry.is_dir && entry.name != ".." {
4021 output.push_str(&styles.text_input.placeholder.render(&entry_str));
4022 } else {
4023 output.push_str(&styles.option.render(&entry_str));
4024 }
4025
4026 output.push('\n');
4027 }
4028
4029 if !visible.is_empty() {
4031 output.pop();
4032 }
4033
4034 output.push('\n');
4036 output.push_str(
4037 &styles
4038 .description
4039 .render(&format!("📂 {}", self.current_directory)),
4040 );
4041 } else {
4042 if let Some(ref path) = self.selected_path {
4044 output.push_str(&styles.selected_option.render(path));
4045 } else {
4046 output.push_str(
4047 &styles
4048 .text_input
4049 .placeholder
4050 .render("No file selected. Press Enter to browse."),
4051 );
4052 }
4053 }
4054
4055 if let Some(ref err) = self.error {
4057 output.push('\n');
4058 output.push_str(&styles.error_message.render(err));
4059 }
4060
4061 styles
4062 .base
4063 .width(self.width.try_into().unwrap_or(u16::MAX))
4064 .render(&output)
4065 }
4066
4067 fn focus(&mut self) -> Option<Cmd> {
4068 self.focused = true;
4069 None
4070 }
4071
4072 fn blur(&mut self) -> Option<Cmd> {
4073 self.focused = false;
4074 self.picking = false;
4075 self.run_validation();
4076 None
4077 }
4078
4079 fn key_binds(&self) -> Vec<Binding> {
4080 if self.picking {
4081 vec![
4082 self.keymap.up.clone(),
4083 self.keymap.down.clone(),
4084 self.keymap.open.clone(),
4085 self.keymap.back.clone(),
4086 self.keymap.close.clone(),
4087 ]
4088 } else {
4089 vec![
4090 self.keymap.open.clone(),
4091 self.keymap.prev.clone(),
4092 self.keymap.next.clone(),
4093 ]
4094 }
4095 }
4096
4097 fn with_theme(&mut self, theme: &Theme) {
4098 if self.theme.is_none() {
4099 self.theme = Some(theme.clone());
4100 }
4101 }
4102
4103 fn with_keymap(&mut self, keymap: &KeyMap) {
4104 self.keymap = keymap.file_picker.clone();
4105 }
4106
4107 fn with_width(&mut self, width: usize) {
4108 self.width = width;
4109 }
4110
4111 fn with_height(&mut self, height: usize) {
4112 self.height = height;
4113 }
4114
4115 fn with_position(&mut self, position: FieldPosition) {
4116 self._position = position;
4117 }
4118}
4119
4120pub struct Group {
4126 fields: Vec<Box<dyn Field>>,
4127 current: usize,
4128 title: String,
4129 description: String,
4130 width: usize,
4131 #[allow(dead_code)]
4132 height: usize,
4133 theme: Option<Theme>,
4134 keymap: Option<KeyMap>,
4135 hide: Option<Box<dyn Fn() -> bool + Send + Sync>>,
4136}
4137
4138impl Default for Group {
4139 fn default() -> Self {
4140 Self::new(Vec::new())
4141 }
4142}
4143
4144impl Group {
4145 pub fn new(fields: Vec<Box<dyn Field>>) -> Self {
4147 Self {
4148 fields,
4149 current: 0,
4150 title: String::new(),
4151 description: String::new(),
4152 width: 80,
4153 height: 0,
4154 theme: None,
4155 keymap: None,
4156 hide: None,
4157 }
4158 }
4159
4160 pub fn title(mut self, title: impl Into<String>) -> Self {
4162 self.title = title.into();
4163 self
4164 }
4165
4166 pub fn description(mut self, description: impl Into<String>) -> Self {
4168 self.description = description.into();
4169 self
4170 }
4171
4172 pub fn hide(mut self, hide: bool) -> Self {
4174 self.hide = Some(Box::new(move || hide));
4175 self
4176 }
4177
4178 pub fn hide_func<F: Fn() -> bool + Send + Sync + 'static>(mut self, f: F) -> Self {
4180 self.hide = Some(Box::new(f));
4181 self
4182 }
4183
4184 pub fn is_hidden(&self) -> bool {
4186 self.hide.as_ref().map(|f| f()).unwrap_or(false)
4187 }
4188
4189 pub fn current(&self) -> usize {
4191 self.current
4192 }
4193
4194 pub fn len(&self) -> usize {
4196 self.fields.len()
4197 }
4198
4199 pub fn is_empty(&self) -> bool {
4201 self.fields.is_empty()
4202 }
4203
4204 pub fn current_field(&self) -> Option<&dyn Field> {
4206 self.fields.get(self.current).map(|f| f.as_ref())
4207 }
4208
4209 pub fn current_field_mut(&mut self) -> Option<&mut Box<dyn Field>> {
4211 self.fields.get_mut(self.current)
4212 }
4213
4214 pub fn errors(&self) -> Vec<&str> {
4216 self.fields.iter().filter_map(|f| f.error()).collect()
4217 }
4218
4219 fn get_theme(&self) -> Theme {
4220 self.theme.clone().unwrap_or_else(theme_charm)
4221 }
4222
4223 pub fn header(&self) -> String {
4228 let theme = self.get_theme();
4229 let mut output = String::new();
4230
4231 if !self.title.is_empty() {
4232 output.push_str(&theme.group.title.render(&self.title));
4233 output.push('\n');
4234 }
4235
4236 if !self.description.is_empty() {
4237 output.push_str(&theme.group.description.render(&self.description));
4238 output.push('\n');
4239 }
4240
4241 output
4242 }
4243
4244 pub fn content(&self) -> String {
4249 let theme = self.get_theme();
4250 let mut output = String::new();
4251
4252 for (i, field) in self.fields.iter().enumerate() {
4253 output.push_str(&field.view());
4254 if i < self.fields.len() - 1 {
4255 output.push_str(&theme.field_separator.render(""));
4256 }
4257 }
4258
4259 output
4260 }
4261
4262 pub fn footer(&self) -> String {
4267 let theme = self.get_theme();
4268 let errors = self.errors();
4269
4270 if errors.is_empty() {
4271 return String::new();
4272 }
4273
4274 let error_text = errors.join(", ");
4275 theme.focused.error_message.render(&error_text)
4276 }
4277}
4278
4279impl Model for Group {
4280 fn init(&self) -> Option<Cmd> {
4281 None
4282 }
4283
4284 fn update(&mut self, msg: Message) -> Option<Cmd> {
4285 if msg.is::<NextFieldMsg>() {
4287 if self.current < self.fields.len().saturating_sub(1) {
4288 if let Some(field) = self.fields.get_mut(self.current) {
4289 field.blur();
4290 }
4291 self.current += 1;
4292 if let Some(field) = self.fields.get_mut(self.current) {
4293 return field.focus();
4294 }
4295 } else {
4296 return Some(Cmd::new(|| Message::new(NextGroupMsg)));
4297 }
4298 } else if msg.is::<PrevFieldMsg>() {
4299 if self.current > 0 {
4300 if let Some(field) = self.fields.get_mut(self.current) {
4301 field.blur();
4302 }
4303 self.current -= 1;
4304 if let Some(field) = self.fields.get_mut(self.current) {
4305 return field.focus();
4306 }
4307 } else {
4308 return Some(Cmd::new(|| Message::new(PrevGroupMsg)));
4309 }
4310 }
4311
4312 if let Some(field) = self.fields.get_mut(self.current) {
4314 return field.update(&msg);
4315 }
4316
4317 None
4318 }
4319
4320 fn view(&self) -> String {
4321 let theme = self.get_theme();
4322 let mut output = String::new();
4323
4324 if !self.title.is_empty() {
4326 output.push_str(&theme.group.title.render(&self.title));
4327 output.push('\n');
4328 }
4329
4330 if !self.description.is_empty() {
4332 output.push_str(&theme.group.description.render(&self.description));
4333 output.push('\n');
4334 }
4335
4336 for (i, field) in self.fields.iter().enumerate() {
4338 output.push_str(&field.view());
4339 if i < self.fields.len() - 1 {
4340 output.push_str(&theme.field_separator.render(""));
4341 }
4342 }
4343
4344 theme
4345 .group
4346 .base
4347 .width(self.width.try_into().unwrap_or(u16::MAX))
4348 .render(&output)
4349 }
4350}
4351
4352pub trait Layout: Send + Sync {
4364 fn view(&self, form: &Form) -> String;
4366
4367 fn group_width(&self, form: &Form, group_index: usize, total_width: usize) -> usize;
4369}
4370
4371#[derive(Debug, Clone, Default)]
4376pub struct LayoutDefault;
4377
4378impl Layout for LayoutDefault {
4379 fn view(&self, form: &Form) -> String {
4380 if let Some(group) = form.groups.get(form.current_group) {
4381 if group.is_hidden() {
4382 return String::new();
4383 }
4384 form.theme
4385 .form
4386 .base
4387 .clone()
4388 .width(form.width.try_into().unwrap_or(u16::MAX))
4389 .render(&group.view())
4390 } else {
4391 String::new()
4392 }
4393 }
4394
4395 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4396 form.width
4397 }
4398}
4399
4400#[derive(Debug, Clone, Default)]
4405pub struct LayoutStack;
4406
4407impl Layout for LayoutStack {
4408 fn view(&self, form: &Form) -> String {
4409 let mut output = String::new();
4410 let visible_groups: Vec<_> = form
4411 .groups
4412 .iter()
4413 .enumerate()
4414 .filter(|(_, g)| !g.is_hidden())
4415 .collect();
4416
4417 for (i, (_, group)) in visible_groups.iter().enumerate() {
4418 output.push_str(&group.view());
4419 if i < visible_groups.len() - 1 {
4420 output.push('\n');
4421 }
4422 }
4423
4424 form.theme
4425 .form
4426 .base
4427 .clone()
4428 .width(form.width.try_into().unwrap_or(u16::MAX))
4429 .render(&output)
4430 }
4431
4432 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4433 form.width
4434 }
4435}
4436
4437#[derive(Debug, Clone)]
4441pub struct LayoutColumns {
4442 columns: usize,
4443}
4444
4445impl LayoutColumns {
4446 pub fn new(columns: usize) -> Self {
4448 Self {
4449 columns: columns.max(1),
4450 }
4451 }
4452}
4453
4454impl Default for LayoutColumns {
4455 fn default() -> Self {
4456 Self::new(2)
4457 }
4458}
4459
4460impl Layout for LayoutColumns {
4461 fn view(&self, form: &Form) -> String {
4462 let visible_groups: Vec<_> = form
4463 .groups
4464 .iter()
4465 .enumerate()
4466 .filter(|(_, g)| !g.is_hidden())
4467 .collect();
4468
4469 if visible_groups.is_empty() {
4470 return String::new();
4471 }
4472
4473 let column_width = form.width / self.columns;
4474 let mut rows: Vec<String> = Vec::new();
4475
4476 for chunk in visible_groups.chunks(self.columns) {
4477 let mut row_parts: Vec<String> = Vec::new();
4478 for (_, group) in chunk {
4479 let group_view = group.view();
4481 let lines: Vec<&str> = group_view.lines().collect();
4483 let padded: Vec<String> = lines
4484 .iter()
4485 .map(|line| {
4486 let visual_width = lipgloss::width(line);
4487 if visual_width < column_width {
4488 format!("{}{}", line, " ".repeat(column_width - visual_width))
4489 } else {
4490 line.to_string()
4491 }
4492 })
4493 .collect();
4494 row_parts.push(padded.join("\n"));
4495 }
4496
4497 if row_parts.len() == 1 {
4499 rows.push(row_parts.into_iter().next().unwrap_or_default());
4501 } else {
4502 let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4503 rows.push(lipgloss::join_horizontal(
4504 lipgloss::Position::Top,
4505 &row_refs,
4506 ));
4507 }
4508 }
4509
4510 let output = rows.join("\n");
4511 form.theme
4512 .form
4513 .base
4514 .clone()
4515 .width(form.width.try_into().unwrap_or(u16::MAX))
4516 .render(&output)
4517 }
4518
4519 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4520 form.width / self.columns
4521 }
4522}
4523
4524#[derive(Debug, Clone)]
4529pub struct LayoutGrid {
4530 rows: usize,
4531 columns: usize,
4532}
4533
4534impl LayoutGrid {
4535 pub fn new(rows: usize, columns: usize) -> Self {
4537 Self {
4538 rows: rows.max(1),
4539 columns: columns.max(1),
4540 }
4541 }
4542}
4543
4544impl Default for LayoutGrid {
4545 fn default() -> Self {
4546 Self::new(2, 2)
4547 }
4548}
4549
4550impl Layout for LayoutGrid {
4551 fn view(&self, form: &Form) -> String {
4552 let visible_groups: Vec<_> = form
4553 .groups
4554 .iter()
4555 .enumerate()
4556 .filter(|(_, g)| !g.is_hidden())
4557 .collect();
4558
4559 if visible_groups.is_empty() {
4560 return String::new();
4561 }
4562
4563 let column_width = form.width / self.columns;
4564 let max_cells = self.rows * self.columns;
4565 let mut rows: Vec<String> = Vec::new();
4566
4567 for row_idx in 0..self.rows {
4568 let start = row_idx * self.columns;
4569 if start >= visible_groups.len() || start >= max_cells {
4570 break;
4571 }
4572 let end = (start + self.columns)
4573 .min(visible_groups.len())
4574 .min(max_cells);
4575
4576 let mut row_parts: Vec<String> = Vec::new();
4577 for (_, group) in &visible_groups[start..end] {
4578 let group_view = group.view();
4579 let lines: Vec<&str> = group_view.lines().collect();
4580 let padded: Vec<String> = lines
4581 .iter()
4582 .map(|line| {
4583 let visual_width = lipgloss::width(line);
4584 if visual_width < column_width {
4585 format!("{}{}", line, " ".repeat(column_width - visual_width))
4586 } else {
4587 line.to_string()
4588 }
4589 })
4590 .collect();
4591 row_parts.push(padded.join("\n"));
4592 }
4593
4594 if row_parts.len() == 1 {
4595 rows.push(row_parts.into_iter().next().unwrap_or_default());
4597 } else {
4598 let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4599 rows.push(lipgloss::join_horizontal(
4600 lipgloss::Position::Top,
4601 &row_refs,
4602 ));
4603 }
4604 }
4605
4606 let output = rows.join("\n");
4607 form.theme
4608 .form
4609 .base
4610 .clone()
4611 .width(form.width.try_into().unwrap_or(u16::MAX))
4612 .render(&output)
4613 }
4614
4615 fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4616 form.width / self.columns
4617 }
4618}
4619
4620pub struct Form {
4626 groups: Vec<Group>,
4627 current_group: usize,
4628 state: FormState,
4629 width: usize,
4630 theme: Theme,
4631 keymap: KeyMap,
4632 layout: Box<dyn Layout>,
4633 show_help: bool,
4634 show_errors: bool,
4635 accessible: bool,
4636}
4637
4638impl Default for Form {
4639 fn default() -> Self {
4640 Self::new(Vec::new())
4641 }
4642}
4643
4644impl Form {
4645 pub fn new(groups: Vec<Group>) -> Self {
4647 Self {
4648 groups,
4649 current_group: 0,
4650 state: FormState::Normal,
4651 width: 80,
4652 theme: theme_charm(),
4653 keymap: KeyMap::default(),
4654 layout: Box::new(LayoutDefault),
4655 show_help: true,
4656 show_errors: true,
4657 accessible: false,
4658 }
4659 }
4660
4661 pub fn width(mut self, width: usize) -> Self {
4663 self.width = width;
4664 self
4665 }
4666
4667 pub fn theme(mut self, theme: Theme) -> Self {
4669 self.theme = theme;
4670 self
4671 }
4672
4673 pub fn keymap(mut self, keymap: KeyMap) -> Self {
4675 self.keymap = keymap;
4676 self
4677 }
4678
4679 pub fn layout<L: Layout + 'static>(mut self, layout: L) -> Self {
4690 self.layout = Box::new(layout);
4691 self
4692 }
4693
4694 pub fn show_help(mut self, show: bool) -> Self {
4696 self.show_help = show;
4697 self
4698 }
4699
4700 pub fn show_errors(mut self, show: bool) -> Self {
4702 self.show_errors = show;
4703 self
4704 }
4705
4706 pub fn with_accessible(mut self, accessible: bool) -> Self {
4722 self.accessible = accessible;
4723 self
4724 }
4725
4726 pub fn is_accessible(&self) -> bool {
4728 self.accessible
4729 }
4730
4731 pub fn state(&self) -> FormState {
4733 self.state
4734 }
4735
4736 pub fn current_group(&self) -> usize {
4738 self.current_group
4739 }
4740
4741 pub fn len(&self) -> usize {
4743 self.groups.len()
4744 }
4745
4746 pub fn is_empty(&self) -> bool {
4748 self.groups.is_empty()
4749 }
4750
4751 fn init_fields(&mut self) {
4753 for group in &mut self.groups {
4754 group.theme = Some(self.theme.clone());
4755 group.keymap = Some(self.keymap.clone());
4756 group.width = self.width;
4757 for field in &mut group.fields {
4758 field.with_theme(&self.theme);
4759 field.with_keymap(&self.keymap);
4760 field.with_width(self.width);
4761 }
4762 }
4763 }
4764
4765 fn next_group(&mut self) -> Option<Cmd> {
4766 loop {
4768 if self.current_group >= self.groups.len().saturating_sub(1) {
4769 self.state = FormState::Completed;
4770 return Some(bubbletea::quit());
4771 }
4772 self.current_group += 1;
4773 if !self.groups[self.current_group].is_hidden() {
4774 break;
4775 }
4776 }
4777 if let Some(group) = self.groups.get_mut(self.current_group) {
4779 group.current = 0;
4780 if let Some(field) = group.fields.get_mut(0) {
4781 return field.focus();
4782 }
4783 }
4784 None
4785 }
4786
4787 fn prev_group(&mut self) -> Option<Cmd> {
4788 loop {
4790 if self.current_group == 0 {
4791 return None;
4792 }
4793 self.current_group -= 1;
4794 if !self.groups[self.current_group].is_hidden() {
4795 break;
4796 }
4797 }
4798 if let Some(group) = self.groups.get_mut(self.current_group) {
4800 group.current = group.fields.len().saturating_sub(1);
4801 if let Some(field) = group.fields.last_mut() {
4802 return field.focus();
4803 }
4804 }
4805 None
4806 }
4807
4808 pub fn get_value(&self, key: &str) -> Option<Box<dyn Any>> {
4810 for group in &self.groups {
4811 for field in &group.fields {
4812 if field.get_key() == key {
4813 return Some(field.get_value());
4814 }
4815 }
4816 }
4817 None
4818 }
4819
4820 pub fn get_string(&self, key: &str) -> Option<String> {
4822 self.get_value(key)
4823 .and_then(|v| v.downcast::<String>().ok())
4824 .map(|v| *v)
4825 }
4826
4827 pub fn get_bool(&self, key: &str) -> Option<bool> {
4829 self.get_value(key)
4830 .and_then(|v| v.downcast::<bool>().ok())
4831 .map(|v| *v)
4832 }
4833
4834 pub fn all_errors(&self) -> Vec<String> {
4836 self.groups
4837 .iter()
4838 .flat_map(|g| g.errors())
4839 .map(|s| s.to_string())
4840 .collect()
4841 }
4842
4843 fn errors_view(&self) -> String {
4845 let errors = self.all_errors();
4846 if errors.is_empty() {
4847 return String::new();
4848 }
4849
4850 let error_text = errors.join(", ");
4851 self.theme.focused.error_message.render(&error_text)
4852 }
4853
4854 fn help_view(&self) -> String {
4856 let mut help_parts = Vec::new();
4858
4859 if let Some(group) = self.groups.get(self.current_group)
4861 && let Some(field) = group.fields.get(group.current)
4862 {
4863 for binding in field.key_binds() {
4864 let help = binding.get_help();
4865 if binding.enabled() && !help.desc.is_empty() {
4866 let keys = binding.get_keys();
4867 if !keys.is_empty() {
4868 help_parts.push(format!("{}: {}", keys.join("/"), help.desc));
4869 }
4870 }
4871 }
4872 }
4873
4874 let quit_help = self.keymap.quit.get_help();
4876 if self.keymap.quit.enabled() && !quit_help.desc.is_empty() {
4877 let keys = self.keymap.quit.get_keys();
4878 if !keys.is_empty() {
4879 help_parts.push(format!("{}: {}", keys.join("/"), quit_help.desc));
4880 }
4881 }
4882
4883 if help_parts.is_empty() {
4884 return String::new();
4885 }
4886
4887 let help_text = help_parts.join(" • ");
4889 self.theme.help.render(&help_text)
4890 }
4891
4892 pub fn group_width(&self, group_index: usize) -> usize {
4894 self.layout.group_width(self, group_index, self.width)
4895 }
4896}
4897
4898impl Model for Form {
4899 fn init(&self) -> Option<Cmd> {
4900 None
4901 }
4902
4903 fn update(&mut self, msg: Message) -> Option<Cmd> {
4904 if self.state == FormState::Normal && self.current_group == 0 {
4906 self.init_fields();
4907 if let Some(group) = self.groups.get_mut(0)
4909 && let Some(field) = group.fields.get_mut(0)
4910 {
4911 field.focus();
4912 }
4913 }
4914
4915 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>()
4917 && binding_matches(&self.keymap.quit, key_msg)
4918 {
4919 self.state = FormState::Aborted;
4920 return Some(bubbletea::quit());
4921 }
4922
4923 if msg.is::<NextGroupMsg>() {
4925 return self.next_group();
4926 } else if msg.is::<PrevGroupMsg>() {
4927 return self.prev_group();
4928 }
4929
4930 if let Some(group) = self.groups.get_mut(self.current_group) {
4932 return group.update(msg);
4933 }
4934
4935 None
4936 }
4937
4938 fn view(&self) -> String {
4939 let mut output = self.layout.view(self);
4940
4941 if self.show_help {
4943 let help_text = self.help_view();
4944 if !help_text.is_empty() {
4945 output.push('\n');
4946 output.push_str(&help_text);
4947 }
4948 }
4949
4950 if self.show_errors {
4952 let errors = self.errors_view();
4953 if !errors.is_empty() {
4954 output.push('\n');
4955 output.push_str(&errors);
4956 }
4957 }
4958
4959 output
4960 }
4961}
4962
4963pub fn validate_required(_field_name: &'static str) -> fn(&str) -> Option<String> {
4991 |s| {
4992 if s.trim().is_empty() {
4993 Some("field is required".to_string())
4994 } else {
4995 None
4996 }
4997 }
4998}
4999
5000pub fn validate_required_name() -> fn(&str) -> Option<String> {
5002 |s| {
5003 if s.trim().is_empty() {
5004 Some("name is required".to_string())
5005 } else {
5006 None
5007 }
5008 }
5009}
5010
5011pub fn validate_min_length_8() -> fn(&str) -> Option<String> {
5015 |s| {
5016 if s.chars().count() < 8 {
5017 Some("password must be at least 8 characters".to_string())
5018 } else {
5019 None
5020 }
5021 }
5022}
5023
5024pub fn validate_email() -> fn(&str) -> Option<String> {
5027 |s| {
5028 if s.is_empty() {
5029 return Some("email is required".to_string());
5030 }
5031 let parts: Vec<&str> = s.split('@').collect();
5034 if parts.len() != 2 {
5035 return Some("invalid email address".to_string());
5036 }
5037 let (local, domain) = (parts[0], parts[1]);
5038 if local.is_empty() || domain.is_empty() || !domain.contains('.') {
5039 return Some("invalid email address".to_string());
5040 }
5041 let domain_parts: Vec<&str> = domain.split('.').collect();
5043 if domain_parts.len() < 2 || domain_parts.iter().any(|p| p.is_empty()) {
5044 return Some("invalid email address".to_string());
5045 }
5046 None
5047 }
5048}
5049
5050#[cfg(test)]
5055mod tests {
5056 use super::*;
5057
5058 #[test]
5059 fn test_form_error_display() {
5060 let err = FormError::UserAborted;
5061 assert_eq!(format!("{}", err), "user aborted");
5062
5063 let err = FormError::Validation("invalid input".to_string());
5064 assert_eq!(format!("{}", err), "validation error: invalid input");
5065 }
5066
5067 #[test]
5068 fn test_form_state_default() {
5069 let state = FormState::default();
5070 assert_eq!(state, FormState::Normal);
5071 }
5072
5073 #[test]
5074 fn test_select_option() {
5075 let opt = SelectOption::new("Red", "red".to_string());
5076 assert_eq!(opt.key, "Red");
5077 assert_eq!(opt.value, "red");
5078 assert!(!opt.selected);
5079
5080 let opt = opt.selected(true);
5081 assert!(opt.selected);
5082 }
5083
5084 #[test]
5085 fn test_new_options() {
5086 let opts = new_options(["apple", "banana", "cherry"]);
5087 assert_eq!(opts.len(), 3);
5088 assert_eq!(opts[0].key, "apple");
5089 assert_eq!(opts[0].value, "apple");
5090 }
5091
5092 #[test]
5093 fn test_input_builder() {
5094 let input = Input::new()
5095 .key("name")
5096 .title("Name")
5097 .description("Enter your name")
5098 .placeholder("John Doe")
5099 .value("Jane");
5100
5101 assert_eq!(input.get_key(), "name");
5102 assert_eq!(input.get_string_value(), "Jane");
5103 }
5104
5105 #[test]
5106 fn test_confirm_builder() {
5107 let confirm = Confirm::new()
5108 .key("agree")
5109 .title("Terms")
5110 .affirmative("I Agree")
5111 .negative("I Disagree")
5112 .value(true);
5113
5114 assert_eq!(confirm.get_key(), "agree");
5115 assert!(confirm.get_bool_value());
5116 }
5117
5118 #[test]
5119 fn test_note_builder() {
5120 let note = Note::new()
5121 .key("info")
5122 .title("Information")
5123 .description("This is an informational note.");
5124
5125 assert_eq!(note.get_key(), "info");
5126 }
5127
5128 #[test]
5129 fn test_text_builder() {
5130 let text = Text::new()
5131 .key("bio")
5132 .title("Biography")
5133 .description("Tell us about yourself")
5134 .placeholder("Enter your bio...")
5135 .lines(10)
5136 .value("Hello world");
5137
5138 assert_eq!(text.get_key(), "bio");
5139 assert_eq!(text.get_string_value(), "Hello world");
5140 }
5141
5142 #[test]
5143 fn test_text_char_limit() {
5144 let text = Text::new().char_limit(50).show_line_numbers(true);
5145
5146 assert_eq!(text.char_limit, 50);
5147 assert!(text.show_line_numbers);
5148 }
5149
5150 #[test]
5151 fn test_filepicker_builder() {
5152 let picker = FilePicker::new()
5153 .key("config_file")
5154 .title("Select Configuration")
5155 .description("Choose a file")
5156 .current_directory("/tmp")
5157 .show_hidden(true)
5158 .file_allowed(true)
5159 .dir_allowed(false);
5160
5161 assert_eq!(picker.get_key(), "config_file");
5162 assert!(picker.file_allowed);
5163 assert!(!picker.dir_allowed);
5164 assert!(picker.show_hidden);
5165 }
5166
5167 #[test]
5168 fn test_filepicker_allowed_types() {
5169 let picker = FilePicker::new()
5170 .allowed_types(vec![".toml".to_string(), ".json".to_string()])
5171 .show_size(true);
5172
5173 assert_eq!(picker.allowed_types.len(), 2);
5174 assert!(picker.show_size);
5175 }
5176
5177 #[test]
5178 fn test_select_builder() {
5179 let select: Select<String> =
5180 Select::new()
5181 .key("color")
5182 .title("Favorite Color")
5183 .options(vec![
5184 SelectOption::new("Red", "red".to_string()),
5185 SelectOption::new("Green", "green".to_string()).selected(true),
5186 SelectOption::new("Blue", "blue".to_string()),
5187 ]);
5188
5189 assert_eq!(select.get_key(), "color");
5190 assert_eq!(select.get_selected_value(), Some(&"green".to_string()));
5191 }
5192
5193 #[test]
5194 fn test_theme_base() {
5195 let theme = theme_base();
5196 assert!(!theme.focused.title.value().is_empty() || theme.focused.title.value().is_empty());
5197 }
5198
5199 #[test]
5200 fn test_theme_charm() {
5201 let theme = theme_charm();
5202 let _ = theme.focused.title.render("Test");
5204 }
5205
5206 #[test]
5207 fn test_theme_dracula() {
5208 let theme = theme_dracula();
5209 let _ = theme.focused.title.render("Test");
5210 }
5211
5212 #[test]
5213 fn test_theme_base16() {
5214 let theme = theme_base16();
5215 let _ = theme.focused.title.render("Test");
5216 }
5217
5218 #[test]
5219 fn test_theme_catppuccin() {
5220 let theme = theme_catppuccin();
5221 let _ = theme.focused.title.render("Test");
5223 let _ = theme.focused.selected_option.render("Selected");
5224 let _ = theme.focused.focused_button.render("OK");
5225 let _ = theme.blurred.title.render("Blurred");
5226 }
5227
5228 #[test]
5229 fn test_keymap_default() {
5230 let keymap = KeyMap::default();
5231 assert!(keymap.quit.enabled());
5232 assert!(keymap.input.next.enabled());
5233 }
5234
5235 #[test]
5236 fn test_field_position() {
5237 let pos = FieldPosition {
5238 group: 0,
5239 field: 0,
5240 first_field: 0,
5241 last_field: 2,
5242 group_count: 2,
5243 first_group: 0,
5244 last_group: 1,
5245 };
5246 assert!(pos.is_first());
5247 assert!(!pos.is_last());
5248 }
5249
5250 #[test]
5251 fn test_group_basic() {
5252 let group = Group::new(vec![
5253 Box::new(Input::new().key("name").title("Name")),
5254 Box::new(Input::new().key("email").title("Email")),
5255 ]);
5256
5257 assert_eq!(group.len(), 2);
5258 assert!(!group.is_empty());
5259 assert_eq!(group.current(), 0);
5260 }
5261
5262 #[test]
5263 fn test_group_hide() {
5264 let group = Group::new(Vec::new()).hide(true);
5265 assert!(group.is_hidden());
5266
5267 let group = Group::new(Vec::new()).hide(false);
5268 assert!(!group.is_hidden());
5269 }
5270
5271 #[test]
5272 fn test_form_basic() {
5273 let form = Form::new(vec![Group::new(vec![Box::new(Input::new().key("name"))])]);
5274
5275 assert_eq!(form.len(), 1);
5276 assert!(!form.is_empty());
5277 assert_eq!(form.state(), FormState::Normal);
5278 }
5279
5280 #[test]
5281 fn test_input_echo_mode() {
5282 let input = Input::new().password(true);
5283 assert_eq!(input.echo_mode, EchoMode::Password);
5284
5285 let input = Input::new().echo_mode(EchoMode::None);
5286 assert_eq!(input.echo_mode, EchoMode::None);
5287 }
5288
5289 #[test]
5290 fn test_key_to_string() {
5291 let key = KeyMsg {
5292 key_type: KeyType::Enter,
5293 runes: vec![],
5294 alt: false,
5295 paste: false,
5296 };
5297 assert_eq!(key.to_string(), "enter");
5298
5299 let key = KeyMsg {
5300 key_type: KeyType::Runes,
5301 runes: vec!['a'],
5302 alt: false,
5303 paste: false,
5304 };
5305 assert_eq!(key.to_string(), "a");
5306
5307 let key = KeyMsg {
5308 key_type: KeyType::CtrlC,
5309 runes: vec![],
5310 alt: false,
5311 paste: false,
5312 };
5313 assert_eq!(key.to_string(), "ctrl+c");
5314 }
5315
5316 #[test]
5317 fn test_input_view() {
5318 let input = Input::new()
5319 .title("Name")
5320 .placeholder("Enter name")
5321 .value("");
5322
5323 let view = input.view();
5324 assert!(view.contains("Name"));
5325 }
5326
5327 #[test]
5328 fn test_confirm_view() {
5329 let confirm = Confirm::new()
5330 .title("Proceed?")
5331 .affirmative("Yes")
5332 .negative("No");
5333
5334 let view = confirm.view();
5335 assert!(view.contains("Proceed"));
5336 }
5337
5338 #[test]
5339 fn test_select_view() {
5340 let select: Select<String> = Select::new().title("Choose").options(vec![
5341 SelectOption::new("A", "a".to_string()),
5342 SelectOption::new("B", "b".to_string()),
5343 ]);
5344
5345 let view = select.view();
5346 assert!(view.contains("Choose"));
5347 }
5348
5349 #[test]
5350 fn test_note_view() {
5351 let note = Note::new().title("Info").description("Some information");
5352
5353 let view = note.view();
5354 assert!(view.contains("Info"));
5355 }
5356
5357 #[test]
5358 fn test_multiselect_view() {
5359 let multi: MultiSelect<String> = MultiSelect::new().title("Select items").options(vec![
5360 SelectOption::new("A", "a".to_string()),
5361 SelectOption::new("B", "b".to_string()).selected(true),
5362 SelectOption::new("C", "c".to_string()),
5363 ]);
5364
5365 let view = multi.view();
5366 assert!(view.contains("Select items"));
5367 }
5368
5369 #[test]
5370 fn test_multiselect_initial_selection() {
5371 let multi: MultiSelect<String> = MultiSelect::new().options(vec![
5372 SelectOption::new("A", "a".to_string()),
5373 SelectOption::new("B", "b".to_string()).selected(true),
5374 SelectOption::new("C", "c".to_string()).selected(true),
5375 ]);
5376
5377 let selected = multi.get_selected_values();
5378 assert_eq!(selected.len(), 2);
5379 assert!(selected.contains(&&"b".to_string()));
5380 assert!(selected.contains(&&"c".to_string()));
5381 }
5382
5383 #[test]
5384 fn test_multiselect_limit() {
5385 let mut multi: MultiSelect<String> = MultiSelect::new().limit(2).options(vec![
5386 SelectOption::new("A", "a".to_string()),
5387 SelectOption::new("B", "b".to_string()),
5388 SelectOption::new("C", "c".to_string()),
5389 ]);
5390
5391 multi.focus();
5393
5394 let toggle_msg = Message::new(KeyMsg {
5396 key_type: KeyType::Runes,
5397 runes: vec![' '],
5398 alt: false,
5399 paste: false,
5400 });
5401 multi.update(&toggle_msg);
5402 assert_eq!(multi.get_selected_values().len(), 1);
5403
5404 let down_msg = Message::new(KeyMsg {
5406 key_type: KeyType::Down,
5407 runes: vec![],
5408 alt: false,
5409 paste: false,
5410 });
5411 multi.update(&down_msg);
5412 multi.update(&toggle_msg);
5413 assert_eq!(multi.get_selected_values().len(), 2);
5414
5415 multi.update(&down_msg);
5417 multi.update(&toggle_msg);
5418 assert_eq!(multi.get_selected_values().len(), 2);
5420 }
5421
5422 #[test]
5423 fn test_input_unicode_cursor_handling() {
5424 let mut input = Input::new().value("café"); input.focus();
5429
5430 assert_eq!(input.cursor_pos, 4);
5432 assert_eq!(input.value.chars().count(), 4);
5433
5434 let end_msg = Message::new(KeyMsg {
5436 key_type: KeyType::End,
5437 runes: vec![],
5438 alt: false,
5439 paste: false,
5440 });
5441 input.update(&end_msg);
5442 assert_eq!(input.cursor_pos, 4);
5443
5444 let left_msg = Message::new(KeyMsg {
5446 key_type: KeyType::Left,
5447 runes: vec![],
5448 alt: false,
5449 paste: false,
5450 });
5451 input.update(&left_msg);
5452 assert_eq!(input.cursor_pos, 3);
5453
5454 let backspace_msg = Message::new(KeyMsg {
5456 key_type: KeyType::Backspace,
5457 runes: vec![],
5458 alt: false,
5459 paste: false,
5460 });
5461 input.update(&backspace_msg);
5462 assert_eq!(input.get_string_value(), "caé");
5463 assert_eq!(input.cursor_pos, 2);
5464
5465 let insert_msg = Message::new(KeyMsg {
5467 key_type: KeyType::Runes,
5468 runes: vec!['ñ'], alt: false,
5470 paste: false,
5471 });
5472 input.update(&insert_msg);
5473 assert_eq!(input.get_string_value(), "cañé");
5474 assert_eq!(input.cursor_pos, 3);
5475
5476 let delete_msg = Message::new(KeyMsg {
5478 key_type: KeyType::Delete,
5479 runes: vec![],
5480 alt: false,
5481 paste: false,
5482 });
5483 input.update(&delete_msg);
5484 assert_eq!(input.get_string_value(), "cañ");
5485
5486 let home_msg = Message::new(KeyMsg {
5488 key_type: KeyType::Home,
5489 runes: vec![],
5490 alt: false,
5491 paste: false,
5492 });
5493 input.update(&home_msg);
5494 assert_eq!(input.cursor_pos, 0);
5495 }
5496
5497 #[test]
5498 fn test_input_char_limit_with_unicode() {
5499 let mut input = Input::new().char_limit(5);
5501 input.focus();
5502
5503 let chars = ['日', '本', '語', '文', '字']; for c in chars {
5506 let msg = Message::new(KeyMsg {
5507 key_type: KeyType::Runes,
5508 runes: vec![c],
5509 alt: false,
5510 paste: false,
5511 });
5512 input.update(&msg);
5513 }
5514
5515 assert_eq!(input.value.chars().count(), 5);
5517 assert_eq!(input.get_string_value(), "日本語文字");
5518
5519 let msg = Message::new(KeyMsg {
5521 key_type: KeyType::Runes,
5522 runes: vec!['!'],
5523 alt: false,
5524 paste: false,
5525 });
5526 input.update(&msg);
5527
5528 assert_eq!(input.value.chars().count(), 5);
5530 }
5531
5532 #[test]
5533 fn test_layout_default() {
5534 let _layout = LayoutDefault;
5535 }
5537
5538 #[test]
5539 fn test_layout_stack() {
5540 let _layout = LayoutStack;
5541 }
5543
5544 #[test]
5545 fn test_layout_columns() {
5546 let layout = LayoutColumns::new(3);
5547 assert_eq!(layout.columns, 3);
5548
5549 let layout = LayoutColumns::new(0);
5551 assert_eq!(layout.columns, 1);
5552 }
5553
5554 #[test]
5555 fn test_layout_grid() {
5556 let layout = LayoutGrid::new(2, 3);
5557 assert_eq!(layout.rows, 2);
5558 assert_eq!(layout.columns, 3);
5559
5560 let layout = LayoutGrid::new(0, 0);
5562 assert_eq!(layout.rows, 1);
5563 assert_eq!(layout.columns, 1);
5564 }
5565
5566 #[test]
5567 fn test_layout_columns_view_single_empty_group_no_panic() {
5568 let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutColumns::new(1));
5569 let _ = form.view();
5570 }
5571
5572 #[test]
5573 fn test_layout_grid_view_single_empty_group_no_panic() {
5574 let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutGrid::new(1, 1));
5575 let _ = form.view();
5576 }
5577
5578 #[test]
5579 fn test_form_with_layout() {
5580 let form = Form::new(vec![
5581 Group::new(vec![Box::new(Input::new().key("a"))]),
5582 Group::new(vec![Box::new(Input::new().key("b"))]),
5583 ])
5584 .layout(LayoutColumns::new(2));
5585
5586 assert_eq!(form.len(), 2);
5588 }
5589
5590 #[test]
5591 fn test_form_show_help() {
5592 let form = Form::new(Vec::new()).show_help(false).show_errors(false);
5593
5594 assert!(!form.show_help);
5596 assert!(!form.show_errors);
5597 }
5598
5599 #[test]
5600 fn test_group_header_footer_content() {
5601 let group = Group::new(vec![Box::new(Input::new().key("test").title("Test Input"))])
5602 .title("Group Title")
5603 .description("Group Description");
5604
5605 let header = group.header();
5606 assert!(header.contains("Group Title"));
5607 assert!(header.contains("Group Description"));
5608
5609 let content = group.content();
5610 assert!(content.contains("Test Input"));
5611
5612 let footer = group.footer();
5613 assert!(footer.is_empty());
5615 }
5616
5617 #[test]
5618 fn test_form_all_errors() {
5619 let form = Form::new(vec![Group::new(Vec::new())]);
5620
5621 let errors = form.all_errors();
5623 assert!(errors.is_empty());
5624 }
5625
5626 #[test]
5629 fn test_text_transpose_left() {
5630 let mut text = Text::new().value("hello");
5631 text.cursor_row = 0;
5632 text.cursor_col = 5; text.transpose_left();
5635
5636 assert_eq!(text.get_string_value(), "helol");
5638 assert_eq!(text.cursor_col, 5); }
5640
5641 #[test]
5642 fn test_text_transpose_left_middle() {
5643 let mut text = Text::new().value("hello");
5644 text.cursor_row = 0;
5645 text.cursor_col = 2; text.transpose_left();
5648
5649 assert_eq!(text.get_string_value(), "hlelo");
5651 assert_eq!(text.cursor_col, 3); }
5653
5654 #[test]
5655 fn test_text_transpose_left_at_beginning() {
5656 let mut text = Text::new().value("hello");
5657 text.cursor_row = 0;
5658 text.cursor_col = 0; text.transpose_left();
5661
5662 assert_eq!(text.get_string_value(), "hello");
5664 assert_eq!(text.cursor_col, 0);
5665 }
5666
5667 #[test]
5668 fn test_text_uppercase_right() {
5669 let mut text = Text::new().value("hello world");
5670 text.cursor_row = 0;
5671 text.cursor_col = 0; text.uppercase_right();
5674
5675 assert_eq!(text.get_string_value(), "HELLO world");
5676 assert_eq!(text.cursor_col, 5); }
5678
5679 #[test]
5680 fn test_text_uppercase_right_with_spaces() {
5681 let mut text = Text::new().value(" hello world");
5682 text.cursor_row = 0;
5683 text.cursor_col = 0; text.uppercase_right();
5686
5687 assert_eq!(text.get_string_value(), " HELLO world");
5689 assert_eq!(text.cursor_col, 7); }
5691
5692 #[test]
5693 fn test_text_lowercase_right() {
5694 let mut text = Text::new().value("HELLO WORLD");
5695 text.cursor_row = 0;
5696 text.cursor_col = 0;
5697
5698 text.lowercase_right();
5699
5700 assert_eq!(text.get_string_value(), "hello WORLD");
5701 assert_eq!(text.cursor_col, 5);
5702 }
5703
5704 #[test]
5705 fn test_text_capitalize_right() {
5706 let mut text = Text::new().value("hello world");
5707 text.cursor_row = 0;
5708 text.cursor_col = 0;
5709
5710 text.capitalize_right();
5711
5712 assert_eq!(text.get_string_value(), "Hello world");
5714 assert_eq!(text.cursor_col, 5);
5715 }
5716
5717 #[test]
5718 fn test_text_capitalize_right_already_upper() {
5719 let mut text = Text::new().value("HELLO WORLD");
5720 text.cursor_row = 0;
5721 text.cursor_col = 0;
5722
5723 text.capitalize_right();
5724
5725 assert_eq!(text.get_string_value(), "HELLO WORLD");
5727 assert_eq!(text.cursor_col, 5);
5728 }
5729
5730 #[test]
5731 fn test_text_word_ops_multiline() {
5732 let mut text = Text::new().value("hello\nworld");
5733 text.cursor_row = 1;
5734 text.cursor_col = 0;
5735
5736 text.uppercase_right();
5737
5738 assert_eq!(text.get_string_value(), "hello\nWORLD");
5740 assert_eq!(text.cursor_row, 1);
5741 assert_eq!(text.cursor_col, 5);
5742 }
5743
5744 #[test]
5745 fn test_text_transpose_multiline() {
5746 let mut text = Text::new().value("ab\ncd");
5747 text.cursor_row = 1;
5748 text.cursor_col = 2; text.transpose_left();
5751
5752 assert_eq!(text.get_string_value(), "ab\ndc");
5754 }
5755
5756 #[test]
5757 fn test_text_word_ops_unicode() {
5758 let mut text = Text::new().value("café résumé");
5759 text.cursor_row = 0;
5760 text.cursor_col = 0;
5761
5762 text.uppercase_right();
5763
5764 assert_eq!(text.get_string_value(), "CAFÉ résumé");
5765 assert_eq!(text.cursor_col, 4);
5766 }
5767
5768 #[test]
5769 fn test_text_keymap_has_word_ops() {
5770 let keymap = TextKeyMap::default();
5771
5772 assert!(keymap.uppercase_word_forward.enabled());
5774 assert!(keymap.lowercase_word_forward.enabled());
5775 assert!(keymap.capitalize_word_forward.enabled());
5776 assert!(keymap.transpose_character_backward.enabled());
5777
5778 assert!(
5780 keymap
5781 .uppercase_word_forward
5782 .get_keys()
5783 .contains(&"alt+u".to_string())
5784 );
5785 assert!(
5786 keymap
5787 .lowercase_word_forward
5788 .get_keys()
5789 .contains(&"alt+l".to_string())
5790 );
5791 assert!(
5792 keymap
5793 .capitalize_word_forward
5794 .get_keys()
5795 .contains(&"alt+c".to_string())
5796 );
5797 assert!(
5798 keymap
5799 .transpose_character_backward
5800 .get_keys()
5801 .contains(&"ctrl+t".to_string())
5802 );
5803 }
5804
5805 mod paste_tests {
5810 use super::*;
5811 use bubbletea::{KeyMsg, Message};
5812
5813 fn paste_msg(s: &str) -> Message {
5815 let key = KeyMsg::from_runes(s.chars().collect()).with_paste();
5816 Message::new(key)
5817 }
5818
5819 fn type_msg(s: &str) -> Message {
5821 let key = KeyMsg::from_runes(s.chars().collect());
5822 Message::new(key)
5823 }
5824
5825 #[test]
5826 fn test_input_paste_collapses_newlines() {
5827 let mut input = Input::new().key("query");
5828 input.focused = true;
5829
5830 let msg = paste_msg("hello\nworld\nfoo");
5832 input.update(&msg);
5833
5834 assert_eq!(input.get_string_value(), "hello world foo");
5836 }
5837
5838 #[test]
5839 fn test_input_paste_collapses_tabs() {
5840 let mut input = Input::new().key("query");
5841 input.focused = true;
5842
5843 let msg = paste_msg("col1\tcol2\tcol3");
5845 input.update(&msg);
5846
5847 assert_eq!(input.get_string_value(), "col1 col2 col3");
5849 }
5850
5851 #[test]
5852 fn test_input_paste_collapses_multiple_spaces() {
5853 let mut input = Input::new().key("query");
5854 input.focused = true;
5855
5856 let msg = paste_msg("hello\n\n\nworld");
5858 input.update(&msg);
5859
5860 assert_eq!(input.get_string_value(), "hello world");
5862 }
5863
5864 #[test]
5865 fn test_input_paste_respects_char_limit() {
5866 let mut input = Input::new().key("query").char_limit(10);
5867 input.focused = true;
5868
5869 let msg = paste_msg("hello world this is too long");
5871 input.update(&msg);
5872
5873 assert_eq!(input.get_string_value().chars().count(), 10);
5875 assert_eq!(input.get_string_value(), "hello worl");
5876 }
5877
5878 #[test]
5879 fn test_input_paste_partial_fill() {
5880 let mut input = Input::new().key("query").char_limit(15);
5881 input.focused = true;
5882
5883 let msg = type_msg("hi ");
5885 input.update(&msg);
5886
5887 let msg = paste_msg("hello world this is long");
5889 input.update(&msg);
5890
5891 assert_eq!(input.get_string_value().chars().count(), 15);
5892 assert_eq!(input.get_string_value(), "hi hello world ");
5893 }
5894
5895 #[test]
5896 fn test_input_paste_cursor_position() {
5897 let mut input = Input::new().key("query");
5898 input.focused = true;
5899
5900 let msg = paste_msg("hello world");
5902 input.update(&msg);
5903
5904 assert_eq!(input.cursor_pos, 11);
5906 }
5907
5908 #[test]
5909 fn test_input_regular_typing_not_affected() {
5910 let mut input = Input::new().key("query");
5911 input.focused = true;
5912
5913 let msg = type_msg("hello\nworld");
5915 input.update(&msg);
5916
5917 assert_eq!(input.get_string_value(), "hello\nworld");
5919 }
5920
5921 #[test]
5922 fn test_text_paste_preserves_newlines() {
5923 let mut text = Text::new().key("bio");
5924 text.focused = true;
5925
5926 let msg = paste_msg("line 1\nline 2\nline 3");
5928 text.update(&msg);
5929
5930 assert_eq!(text.get_string_value(), "line 1\nline 2\nline 3");
5932 }
5933
5934 #[test]
5935 fn test_text_paste_updates_cursor_row() {
5936 let mut text = Text::new().key("bio");
5937 text.focused = true;
5938
5939 let msg = paste_msg("line 1\nline 2\nline 3");
5941 text.update(&msg);
5942
5943 assert_eq!(text.cursor_row, 2);
5945 assert_eq!(text.cursor_col, 6);
5947 }
5948
5949 #[test]
5950 fn test_text_paste_respects_char_limit() {
5951 let mut text = Text::new().key("bio").char_limit(20);
5952 text.focused = true;
5953
5954 let msg = paste_msg("line 1\nline 2\nline 3 is very long");
5956 text.update(&msg);
5957
5958 assert_eq!(text.get_string_value().chars().count(), 20);
5960 }
5961
5962 #[test]
5963 fn test_input_paste_unicode() {
5964 let mut input = Input::new().key("query");
5965 input.focused = true;
5966
5967 let msg = paste_msg("héllo\nwörld\n日本語");
5969 input.update(&msg);
5970
5971 assert_eq!(input.get_string_value(), "héllo wörld 日本語");
5973 }
5974
5975 #[test]
5976 fn test_text_paste_unicode_cursor() {
5977 let mut text = Text::new().key("bio");
5978 text.focused = true;
5979
5980 let msg = paste_msg("日本語\n한국어");
5982 text.update(&msg);
5983
5984 assert_eq!(text.get_string_value(), "日本語\n한국어");
5985 assert_eq!(text.cursor_row, 1);
5986 assert_eq!(text.cursor_col, 3); }
5988
5989 #[test]
5990 fn test_input_paste_empty() {
5991 let mut input = Input::new().key("query");
5992 input.focused = true;
5993
5994 let msg = paste_msg("");
5996 input.update(&msg);
5997
5998 assert_eq!(input.get_string_value(), "");
5999 assert_eq!(input.cursor_pos, 0);
6000 }
6001
6002 #[test]
6003 fn test_input_paste_crlf_handling() {
6004 let mut input = Input::new().key("query");
6005 input.focused = true;
6006
6007 let msg = paste_msg("hello\r\nworld");
6009 input.update(&msg);
6010
6011 assert_eq!(input.get_string_value(), "hello world");
6013 }
6014
6015 #[test]
6016 fn test_input_not_focused_ignores_paste() {
6017 let mut input = Input::new().key("query");
6018 input.focused = false;
6019
6020 let msg = paste_msg("hello world");
6021 input.update(&msg);
6022
6023 assert_eq!(input.get_string_value(), "");
6025 }
6026
6027 #[test]
6028 fn test_text_not_focused_ignores_paste() {
6029 let mut text = Text::new().key("bio");
6030 text.focused = false;
6031
6032 let msg = paste_msg("hello\nworld");
6033 text.update(&msg);
6034
6035 assert_eq!(text.get_string_value(), "");
6037 }
6038
6039 #[test]
6040 fn test_input_large_paste() {
6041 let mut input = Input::new().key("query");
6042 input.focused = true;
6043
6044 let large_text: String = (0..1000).map(|i| format!("word{} ", i)).collect();
6046 let msg = paste_msg(&large_text);
6047 input.update(&msg);
6048
6049 assert!(input.get_string_value().chars().count() > 100);
6051 }
6052
6053 #[test]
6054 fn test_text_large_paste() {
6055 let mut text = Text::new().key("bio");
6056 text.focused = true;
6057
6058 let large_text: String = (0..100).map(|i| format!("line {}\n", i)).collect();
6060 let msg = paste_msg(&large_text);
6061 text.update(&msg);
6062
6063 assert!(text.get_string_value().contains('\n'));
6065 assert_eq!(text.cursor_row, 100); }
6067 }
6068
6069 #[test]
6070 fn test_multiselect_filter_cursor_stays_on_item() {
6071 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6073 SelectOption::new("Apple", "apple".to_string()),
6074 SelectOption::new("Banana", "banana".to_string()),
6075 SelectOption::new("Cherry", "cherry".to_string()),
6076 SelectOption::new("Blueberry", "blueberry".to_string()),
6077 ]);
6078
6079 multi.focus();
6080
6081 let down_msg = Message::new(KeyMsg {
6083 key_type: KeyType::Down,
6084 runes: vec![],
6085 alt: false,
6086 paste: false,
6087 });
6088 multi.update(&down_msg);
6089 assert_eq!(multi.cursor, 1);
6090
6091 multi.update_filter("b".to_string());
6093
6094 let filtered = multi.filtered_options();
6096 assert_eq!(filtered.len(), 2);
6097 assert_eq!(filtered[multi.cursor].1.key, "Banana");
6098 }
6099
6100 #[test]
6101 fn test_multiselect_filter_cursor_clamps() {
6102 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6104 SelectOption::new("Apple", "apple".to_string()),
6105 SelectOption::new("Banana", "banana".to_string()),
6106 SelectOption::new("Cherry", "cherry".to_string()),
6107 ]);
6108
6109 multi.focus();
6110
6111 let down_msg = Message::new(KeyMsg {
6113 key_type: KeyType::Down,
6114 runes: vec![],
6115 alt: false,
6116 paste: false,
6117 });
6118 multi.update(&down_msg);
6119 multi.update(&down_msg);
6120 assert_eq!(multi.cursor, 2);
6121
6122 multi.update_filter("a".to_string());
6124
6125 let filtered = multi.filtered_options();
6127 assert_eq!(filtered.len(), 2);
6128 assert!(multi.cursor < filtered.len());
6129 }
6130
6131 #[test]
6132 fn test_multiselect_filter_then_toggle() {
6133 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6135 SelectOption::new("Apple", "apple".to_string()),
6136 SelectOption::new("Banana", "banana".to_string()),
6137 SelectOption::new("Cherry", "cherry".to_string()),
6138 SelectOption::new("Blueberry", "blueberry".to_string()),
6139 ]);
6140
6141 multi.focus();
6142
6143 multi.update_filter("b".to_string());
6145
6146 let down_msg = Message::new(KeyMsg {
6148 key_type: KeyType::Down,
6149 runes: vec![],
6150 alt: false,
6151 paste: false,
6152 });
6153 multi.update(&down_msg);
6154
6155 let toggle_msg = Message::new(KeyMsg {
6157 key_type: KeyType::Runes,
6158 runes: vec![' '],
6159 alt: false,
6160 paste: false,
6161 });
6162 multi.update(&toggle_msg);
6163
6164 let selected = multi.get_selected_values();
6166 assert_eq!(selected.len(), 1);
6167 assert!(selected.contains(&&"blueberry".to_string()));
6168
6169 multi.update_filter(String::new());
6171 let selected = multi.get_selected_values();
6172 assert_eq!(selected.len(), 1);
6173 assert!(selected.contains(&&"blueberry".to_string()));
6174 }
6175
6176 #[test]
6177 fn test_multiselect_filter_navigation_bounds() {
6178 let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6180 SelectOption::new("Apple", "apple".to_string()),
6181 SelectOption::new("Banana", "banana".to_string()),
6182 SelectOption::new("Cherry", "cherry".to_string()),
6183 SelectOption::new("Date", "date".to_string()),
6184 ]);
6185
6186 multi.focus();
6187
6188 multi.update_filter("a".to_string());
6190 let filtered = multi.filtered_options();
6191 assert_eq!(filtered.len(), 3);
6192
6193 let down_msg = Message::new(KeyMsg {
6195 key_type: KeyType::Down,
6196 runes: vec![],
6197 alt: false,
6198 paste: false,
6199 });
6200 multi.update(&down_msg);
6201 multi.update(&down_msg);
6202 multi.update(&down_msg); multi.update(&down_msg);
6204
6205 assert_eq!(multi.cursor, 2); }
6208
6209 fn filepicker_with_entries(entries: Vec<(&str, bool)>) -> FilePicker {
6216 let mut picker = FilePicker::new();
6217 picker.picking = true;
6218 picker.focused = true;
6219 picker.files = entries
6220 .into_iter()
6221 .map(|(name, is_dir)| FileEntry {
6222 name: name.to_string(),
6223 path: format!("/tmp/{name}"),
6224 is_dir,
6225 size: 0,
6226 mode: String::new(),
6227 })
6228 .collect();
6229 picker
6230 }
6231
6232 fn make_key_msg(key_type: KeyType) -> Message {
6233 Message::new(KeyMsg {
6234 key_type,
6235 runes: vec![],
6236 alt: false,
6237 paste: false,
6238 })
6239 }
6240
6241 #[test]
6242 fn filepicker_single_file_is_selected_by_default() {
6243 let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6244 assert_eq!(picker.selected_index, 0);
6246 assert_eq!(picker.files.len(), 1);
6247 assert_eq!(picker.files[0].name, "only_file.txt");
6248 }
6249
6250 #[test]
6251 fn filepicker_single_file_view_shows_entry() {
6252 let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6253 let view = picker.view();
6254 assert!(view.contains("only_file.txt"));
6255 }
6256
6257 #[test]
6258 fn filepicker_single_file_select_via_enter() {
6259 let mut picker = filepicker_with_entries(vec![("report.pdf", false)]);
6260 let enter_msg = make_key_msg(KeyType::Enter);
6262 let result = picker.update(&enter_msg);
6263 assert_eq!(picker.selected_path, Some("/tmp/report.pdf".to_string()));
6265 assert!(!picker.picking);
6266 assert!(result.is_some()); }
6268
6269 #[test]
6270 fn filepicker_single_file_down_does_not_move() {
6271 let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6272 let down_msg = make_key_msg(KeyType::Down);
6273 picker.update(&down_msg);
6274 assert_eq!(picker.selected_index, 0);
6276 }
6277
6278 #[test]
6279 fn filepicker_single_file_up_does_not_move() {
6280 let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6281 let up_msg = make_key_msg(KeyType::Up);
6282 picker.update(&up_msg);
6283 assert_eq!(picker.selected_index, 0);
6284 }
6285
6286 #[test]
6287 fn filepicker_empty_files_no_panic() {
6288 let mut picker = filepicker_with_entries(vec![]);
6289 let down_msg = make_key_msg(KeyType::Down);
6291 picker.update(&down_msg);
6292 assert_eq!(picker.selected_index, 0);
6293
6294 let up_msg = make_key_msg(KeyType::Up);
6295 picker.update(&up_msg);
6296 assert_eq!(picker.selected_index, 0);
6297 }
6298
6299 #[test]
6300 fn filepicker_empty_files_view_no_panic() {
6301 let picker = filepicker_with_entries(vec![]);
6302 let view = picker.view();
6304 assert!(!view.is_empty());
6305 }
6306
6307 #[test]
6308 fn filepicker_empty_goto_top_bottom_no_panic() {
6309 let mut picker = filepicker_with_entries(vec![]);
6310 let home_msg = Message::new(KeyMsg {
6312 key_type: KeyType::Home,
6313 runes: vec![],
6314 alt: false,
6315 paste: false,
6316 });
6317 picker.update(&home_msg);
6318 assert_eq!(picker.selected_index, 0);
6319
6320 let end_msg = Message::new(KeyMsg {
6322 key_type: KeyType::End,
6323 runes: vec![],
6324 alt: false,
6325 paste: false,
6326 });
6327 picker.update(&end_msg);
6328 assert_eq!(picker.selected_index, 0);
6329 }
6330
6331 #[test]
6332 fn filepicker_height_zero_no_panic() {
6333 let mut picker =
6334 filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6335 picker.height = 0;
6336 let down_msg = make_key_msg(KeyType::Down);
6338 picker.update(&down_msg);
6339 picker.update(&down_msg);
6340 assert_eq!(picker.selected_index, 2);
6341 }
6342
6343 #[test]
6344 fn filepicker_height_one_scrolls_correctly() {
6345 let mut picker =
6346 filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6347 picker.height = 1;
6348 assert_eq!(picker.selected_index, 0);
6349 assert_eq!(picker.offset, 0);
6350
6351 let down_msg = make_key_msg(KeyType::Down);
6352 picker.update(&down_msg);
6353 assert_eq!(picker.selected_index, 1);
6354 assert_eq!(picker.offset, 1);
6356
6357 picker.update(&down_msg);
6358 assert_eq!(picker.selected_index, 2);
6359 assert_eq!(picker.offset, 2);
6360 }
6361
6362 #[test]
6363 fn filepicker_navigation_respects_bounds() {
6364 let mut picker = filepicker_with_entries(vec![("a.txt", false), ("b.txt", false)]);
6365 let down_msg = make_key_msg(KeyType::Down);
6366 let up_msg = make_key_msg(KeyType::Up);
6367
6368 picker.update(&down_msg);
6370 assert_eq!(picker.selected_index, 1);
6371 picker.update(&down_msg); assert_eq!(picker.selected_index, 1);
6373
6374 picker.update(&up_msg);
6376 assert_eq!(picker.selected_index, 0);
6377 picker.update(&up_msg); assert_eq!(picker.selected_index, 0);
6379 }
6380
6381 #[test]
6382 fn filepicker_dir_not_selectable_by_default() {
6383 let picker = filepicker_with_entries(vec![("subdir", true)]);
6384 let entry = &picker.files[0];
6385 assert!(!picker.is_selectable(entry));
6387 }
6388
6389 #[test]
6390 fn filepicker_file_selectable_by_default() {
6391 let picker = filepicker_with_entries(vec![("file.rs", false)]);
6392 let entry = &picker.files[0];
6393 assert!(picker.is_selectable(entry));
6394 }
6395
6396 #[test]
6397 fn filepicker_format_size_edge_cases() {
6398 assert_eq!(FilePicker::format_size(0), "0B");
6399 assert_eq!(FilePicker::format_size(1023), "1023B");
6400 assert_eq!(FilePicker::format_size(1024), "1.0K");
6401 assert_eq!(FilePicker::format_size(1024 * 1024), "1.0M");
6402 assert_eq!(FilePicker::format_size(1024 * 1024 * 1024), "1.0G");
6403 }
6404
6405 fn make_select_options() -> Vec<SelectOption<String>> {
6408 vec![
6409 SelectOption::new("Apple", "apple".to_string()),
6410 SelectOption::new("Apricot", "apricot".to_string()),
6411 SelectOption::new("Banana", "banana".to_string()),
6412 SelectOption::new("Cherry", "cherry".to_string()),
6413 SelectOption::new("Date", "date".to_string()),
6414 ]
6415 }
6416
6417 fn make_filterable_select() -> Select<String> {
6418 Select::new()
6419 .options(make_select_options())
6420 .filterable(true)
6421 .height_options(3)
6422 }
6423
6424 #[test]
6425 fn select_filterable_builder() {
6426 let sel = Select::<String>::new().filterable(true);
6427 assert!(sel.filtering);
6428 let sel = Select::<String>::new().filterable(false);
6429 assert!(!sel.filtering);
6430 }
6431
6432 #[test]
6433 fn select_filtered_indices_no_filter() {
6434 let sel = make_filterable_select();
6435 assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6436 }
6437
6438 #[test]
6439 fn select_filtered_indices_with_filter() {
6440 let mut sel = make_filterable_select();
6441 sel.filter_value = "ap".to_string();
6442 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6444 }
6445
6446 #[test]
6447 fn select_filtered_indices_case_insensitive() {
6448 let mut sel = make_filterable_select();
6449 sel.filter_value = "AP".to_string();
6450 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6451 }
6452
6453 #[test]
6454 fn select_filtered_indices_no_match() {
6455 let mut sel = make_filterable_select();
6456 sel.filter_value = "zzz".to_string();
6457 assert!(sel.filtered_indices().is_empty());
6458 }
6459
6460 #[test]
6461 fn select_update_filter_keeps_selection() {
6462 let mut sel = make_filterable_select();
6463 sel.selected = 2; sel.update_filter("an".to_string());
6465 assert_eq!(sel.selected, 2);
6467 assert_eq!(sel.filter_value, "an");
6468 }
6469
6470 #[test]
6471 fn select_update_filter_clamps_when_item_hidden() {
6472 let mut sel = make_filterable_select();
6473 sel.selected = 2; sel.update_filter("ch".to_string());
6475 assert_eq!(sel.selected, 3);
6478 }
6479
6480 #[test]
6481 fn select_update_filter_clear_restores() {
6482 let mut sel = make_filterable_select();
6483 sel.update_filter("ap".to_string());
6484 assert_eq!(sel.filtered_indices(), vec![0, 1]);
6485 sel.update_filter(String::new());
6486 assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6487 }
6488
6489 #[test]
6490 fn select_filter_display_in_view() {
6491 let mut sel = make_filterable_select();
6492 sel.focused = true;
6493 sel.filter_value = "ap".to_string();
6494 let view = sel.view();
6495 assert!(view.contains("Filter: ap_"));
6496 }
6497
6498 #[test]
6499 fn select_filter_not_displayed_when_empty() {
6500 let mut sel = make_filterable_select();
6501 sel.focused = true;
6502 let view = sel.view();
6503 assert!(!view.contains("Filter:"));
6504 }
6505
6506 #[test]
6507 fn select_filter_not_displayed_when_disabled() {
6508 let mut sel = Select::new()
6509 .options(make_select_options())
6510 .height_options(3);
6511 sel.focused = true;
6512 sel.filter_value = "ap".to_string();
6513 let view = sel.view();
6514 assert!(!view.contains("Filter:"));
6515 }
6516
6517 #[test]
6518 fn select_navigation_respects_filter() {
6519 let mut sel = make_filterable_select();
6520 sel.focused = true;
6521 sel.update_filter("a".to_string());
6522 let indices = sel.filtered_indices();
6524 assert_eq!(indices, vec![0, 1, 2, 4]);
6525
6526 sel.selected = 0;
6528
6529 let down_msg = Message::new(KeyMsg {
6531 key_type: KeyType::Down,
6532 runes: vec![],
6533 alt: false,
6534 paste: false,
6535 });
6536 sel.update(&down_msg);
6537 assert_eq!(sel.selected, 1);
6539
6540 sel.update(&down_msg);
6541 assert_eq!(sel.selected, 2);
6543
6544 sel.update(&down_msg);
6545 assert_eq!(sel.selected, 4);
6547 }
6548
6549 #[test]
6550 fn select_get_selected_value_with_filter() {
6551 let mut sel = make_filterable_select();
6552 sel.update_filter("ch".to_string());
6553 assert_eq!(sel.selected, 3);
6555 assert_eq!(sel.get_selected_value(), Some(&"cherry".to_string()));
6556 }
6557}