1use crate::help::Help;
32use crate::key::{Binding, matches};
33use crate::paginator::Paginator;
34use crate::spinner::{SpinnerModel, TickMsg};
35use crate::textinput::TextInput;
36use bubbletea::{Cmd, KeyMsg, Message, Model, MouseAction, MouseButton, MouseMsg};
37use lipgloss::{Color, Style};
38use std::time::Duration;
39
40pub trait Item: Clone + Send + 'static {
42 fn filter_value(&self) -> &str;
44}
45
46pub trait ItemDelegate<I: Item>: Clone + Send + 'static {
48 fn height(&self) -> usize;
50
51 fn spacing(&self) -> usize;
53
54 fn render(&self, item: &I, index: usize, selected: bool, width: usize) -> String;
56
57 fn update(&mut self, _msg: &Message, _item: &mut I) -> Option<Cmd> {
59 None
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct DefaultDelegate {
66 pub normal_style: Style,
68 pub selected_style: Style,
70 pub item_height: usize,
72 pub item_spacing: usize,
74}
75
76impl Default for DefaultDelegate {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl DefaultDelegate {
83 #[must_use]
85 pub fn new() -> Self {
86 Self {
87 normal_style: Style::new(),
88 selected_style: Style::new().foreground_color(Color::from("212")).bold(),
89 item_height: 1,
90 item_spacing: 0,
91 }
92 }
93
94 #[must_use]
96 pub fn with_height(mut self, h: usize) -> Self {
97 self.item_height = h;
98 self
99 }
100
101 #[must_use]
103 pub fn with_spacing(mut self, s: usize) -> Self {
104 self.item_spacing = s;
105 self
106 }
107}
108
109impl<I: Item> ItemDelegate<I> for DefaultDelegate {
110 fn height(&self) -> usize {
111 self.item_height
112 }
113
114 fn spacing(&self) -> usize {
115 self.item_spacing
116 }
117
118 fn render(&self, item: &I, _index: usize, selected: bool, width: usize) -> String {
119 let value = item.filter_value();
120
121 let truncated = {
123 use unicode_width::UnicodeWidthStr;
124 if UnicodeWidthStr::width(value) <= width {
125 value.to_string()
126 } else if width == 0 {
127 String::new()
128 } else {
129 let target_width = width.saturating_sub(1);
130 let mut current_width = 0;
131 let mut result = String::new();
132
133 for c in value.chars() {
134 let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
135 if current_width + w > target_width {
136 break;
137 }
138 result.push(c);
139 current_width += w;
140 }
141 format!("{}…", result)
142 }
143 };
144
145 if selected {
146 self.selected_style.render(&truncated)
147 } else {
148 self.normal_style.render(&truncated)
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct Rank {
156 pub index: usize,
158 pub matched_indices: Vec<usize>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum FilterState {
165 Unfiltered,
167 Filtering,
169 FilterApplied,
171}
172
173impl std::fmt::Display for FilterState {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 match self {
176 Self::Unfiltered => write!(f, "unfiltered"),
177 Self::Filtering => write!(f, "filtering"),
178 Self::FilterApplied => write!(f, "filter applied"),
179 }
180 }
181}
182
183pub type FilterFn = Box<dyn Fn(&str, &[String]) -> Vec<Rank> + Send + Sync>;
185
186pub fn default_filter(term: &str, targets: &[String]) -> Vec<Rank> {
188 let term_lower = term.to_lowercase();
189 targets
190 .iter()
191 .enumerate()
192 .filter(|(_, target)| target.to_lowercase().contains(&term_lower))
193 .map(|(index, target)| {
194 let target_lower = target.to_lowercase();
196 let start = target_lower.find(&term_lower).unwrap_or(0);
197 let matched_indices: Vec<usize> = (start..start + term.len()).collect();
198 Rank {
199 index,
200 matched_indices,
201 }
202 })
203 .collect()
204}
205
206#[derive(Debug, Clone)]
208pub struct KeyMap {
209 pub cursor_up: Binding,
211 pub cursor_down: Binding,
213 pub next_page: Binding,
215 pub prev_page: Binding,
217 pub goto_start: Binding,
219 pub goto_end: Binding,
221 pub filter: Binding,
223 pub clear_filter: Binding,
225 pub cancel_while_filtering: Binding,
227 pub accept_while_filtering: Binding,
229 pub show_full_help: Binding,
231 pub close_full_help: Binding,
233 pub quit: Binding,
235 pub force_quit: Binding,
237}
238
239impl Default for KeyMap {
240 fn default() -> Self {
241 Self {
242 cursor_up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
243 cursor_down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
244 next_page: Binding::new()
245 .keys(&["right", "l", "pgdown"])
246 .help("→/l", "next page"),
247 prev_page: Binding::new()
248 .keys(&["left", "h", "pgup"])
249 .help("←/h", "prev page"),
250 goto_start: Binding::new().keys(&["home", "g"]).help("g/home", "start"),
251 goto_end: Binding::new().keys(&["end", "G"]).help("G/end", "end"),
252 filter: Binding::new().keys(&["/"]).help("/", "filter"),
253 clear_filter: Binding::new().keys(&["esc"]).help("esc", "clear filter"),
254 cancel_while_filtering: Binding::new().keys(&["esc"]).help("esc", "cancel"),
255 accept_while_filtering: Binding::new()
256 .keys(&["enter"])
257 .help("enter", "apply filter"),
258 show_full_help: Binding::new().keys(&["?"]).help("?", "help"),
259 close_full_help: Binding::new()
260 .keys(&["esc", "?"])
261 .help("?/esc", "close help"),
262 quit: Binding::new().keys(&["q"]).help("q", "quit"),
263 force_quit: Binding::new()
264 .keys(&["ctrl+c"])
265 .help("ctrl+c", "force quit"),
266 }
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct Styles {
273 pub title: Style,
275 pub title_bar: Style,
277 pub filter_prompt: Style,
279 pub filter_cursor: Style,
281 pub status_bar: Style,
283 pub status_empty: Style,
285 pub no_items: Style,
287 pub pagination: Style,
289 pub help: Style,
291 pub active_pagination_dot: Style,
293 pub inactive_pagination_dot: Style,
295 pub divider_dot: Style,
297}
298
299impl Default for Styles {
300 fn default() -> Self {
301 Self {
302 title: Style::new().bold(),
303 title_bar: Style::new(),
304 filter_prompt: Style::new(),
305 filter_cursor: Style::new(),
306 status_bar: Style::new().foreground_color(Color::from("240")),
307 status_empty: Style::new().foreground_color(Color::from("240")),
308 no_items: Style::new().foreground_color(Color::from("240")),
309 pagination: Style::new(),
310 help: Style::new().foreground_color(Color::from("240")),
311 active_pagination_dot: Style::new().foreground_color(Color::from("212")),
312 inactive_pagination_dot: Style::new().foreground_color(Color::from("240")),
313 divider_dot: Style::new().foreground_color(Color::from("240")),
314 }
315 }
316}
317
318#[derive(Debug, Clone)]
320pub struct FilterMatchesMsg(pub Vec<Rank>);
321
322#[derive(Debug, Clone, Copy)]
324pub struct StatusMessageTimeoutMsg;
325
326#[derive(Clone)]
328pub struct List<I: Item, D: ItemDelegate<I>> {
329 pub title: String,
331 pub show_title: bool,
333 pub show_filter: bool,
335 pub show_status_bar: bool,
337 pub show_pagination: bool,
339 pub show_help: bool,
341 pub filtering_enabled: bool,
343 pub infinite_scrolling: bool,
345 pub item_name_singular: String,
347 pub item_name_plural: String,
349 pub key_map: KeyMap,
351 pub styles: Styles,
353 pub status_message_lifetime: Duration,
355 pub mouse_wheel_enabled: bool,
357 pub mouse_wheel_delta: usize,
359 pub mouse_click_enabled: bool,
361
362 spinner: SpinnerModel,
365 paginator: Paginator,
367 help: Help,
369 filter_input: TextInput,
371
372 items: Vec<I>,
374 filtered_indices: Vec<usize>,
375 delegate: D,
376 width: usize,
377 height: usize,
378 cursor: usize,
379 filter_state: FilterState,
380 show_spinner: bool,
381 status_message: Option<String>,
382}
383
384impl<I: Item, D: ItemDelegate<I>> List<I, D> {
385 #[must_use]
387 pub fn new(items: Vec<I>, delegate: D, width: usize, height: usize) -> Self {
388 let items_len = items.len();
389 let filtered_indices: Vec<usize> = (0..items_len).collect();
390
391 let item_height = delegate.height() + delegate.spacing();
393 let available = height.saturating_sub(4); let per_page = available / item_height.max(1);
395
396 let mut paginator = Paginator::new().per_page(per_page.max(1));
397 paginator.set_total_pages_from_items(items_len);
398
399 let mut filter_input = TextInput::new();
400 filter_input.prompt = "Filter: ".to_string();
401
402 Self {
403 title: String::new(),
404 show_title: true,
405 show_filter: true,
406 show_status_bar: true,
407 show_pagination: true,
408 show_help: true,
409 filtering_enabled: true,
410 infinite_scrolling: false,
411 item_name_singular: "item".to_string(),
412 item_name_plural: "items".to_string(),
413 key_map: KeyMap::default(),
414 styles: Styles::default(),
415 status_message_lifetime: Duration::from_secs(1),
416 mouse_wheel_enabled: true,
417 mouse_wheel_delta: 1,
418 mouse_click_enabled: true,
419 spinner: SpinnerModel::new(),
420 paginator,
421 help: Help::new(),
422 filter_input,
423 items,
424 filtered_indices,
425 delegate,
426 width,
427 height,
428 cursor: 0,
429 filter_state: FilterState::Unfiltered,
430 show_spinner: false,
431 status_message: None,
432 }
433 }
434
435 #[must_use]
437 pub fn title(mut self, title: impl Into<String>) -> Self {
438 self.title = title.into();
439 self
440 }
441
442 #[must_use]
444 pub fn mouse_wheel(mut self, enabled: bool) -> Self {
445 self.mouse_wheel_enabled = enabled;
446 self
447 }
448
449 #[must_use]
451 pub fn mouse_wheel_delta(mut self, delta: usize) -> Self {
452 self.mouse_wheel_delta = delta;
453 self
454 }
455
456 #[must_use]
458 pub fn mouse_click(mut self, enabled: bool) -> Self {
459 self.mouse_click_enabled = enabled;
460 self
461 }
462
463 pub fn set_items(&mut self, items: Vec<I>) {
465 let len = items.len();
466 self.items = items;
467 self.filtered_indices = (0..len).collect();
468 self.paginator.set_total_pages_from_items(len);
469 self.paginator.set_page(0);
470 self.cursor = 0;
471 }
472
473 #[must_use]
475 pub fn items(&self) -> &[I] {
476 &self.items
477 }
478
479 #[must_use]
481 pub fn visible_items(&self) -> Vec<&I> {
482 self.filtered_indices
483 .iter()
484 .filter_map(|&i| self.items.get(i))
485 .collect()
486 }
487
488 #[must_use]
490 pub fn index(&self) -> usize {
491 self.cursor
492 }
493
494 #[must_use]
496 pub fn selected_item(&self) -> Option<&I> {
497 self.filtered_indices
498 .get(self.cursor)
499 .and_then(|&i| self.items.get(i))
500 }
501
502 pub fn select(&mut self, index: usize) {
504 self.cursor = index.min(self.filtered_indices.len().saturating_sub(1));
505 }
506
507 pub fn cursor_up(&mut self) {
509 if self.filtered_indices.is_empty() {
510 return;
511 }
512 if self.cursor == 0 {
513 if self.infinite_scrolling {
514 self.cursor = self.filtered_indices.len() - 1;
515 }
516 } else {
517 self.cursor -= 1;
518 }
519 }
520
521 pub fn cursor_down(&mut self) {
523 if self.filtered_indices.is_empty() {
524 return;
525 }
526 if self.cursor >= self.filtered_indices.len() - 1 {
527 if self.infinite_scrolling {
528 self.cursor = 0;
529 }
530 } else {
531 self.cursor += 1;
532 }
533 }
534
535 #[must_use]
537 pub fn filter_state(&self) -> FilterState {
538 self.filter_state
539 }
540
541 #[must_use]
543 pub fn filter_value(&self) -> String {
544 self.filter_input.value()
545 }
546
547 pub fn set_filter_value(&mut self, value: &str) {
549 self.filter_input.set_value(value);
550 self.apply_filter();
551 }
552
553 pub fn reset_filter(&mut self) {
555 self.filter_input.reset();
556 self.filter_state = FilterState::Unfiltered;
557 self.filtered_indices = (0..self.items.len()).collect();
558 self.paginator.set_total_pages_from_items(self.items.len());
559 self.paginator.set_page(0);
560 self.cursor = 0;
561 }
562
563 fn apply_filter(&mut self) {
565 let term = self.filter_input.value();
566 if term.is_empty() {
567 self.reset_filter();
568 return;
569 }
570
571 let targets: Vec<String> = self
572 .items
573 .iter()
574 .map(|i| i.filter_value().to_string())
575 .collect();
576 let ranks = default_filter(&term, &targets);
577
578 self.filtered_indices = ranks.iter().map(|r| r.index).collect();
579 self.paginator
580 .set_total_pages_from_items(self.filtered_indices.len());
581 self.paginator.set_page(0);
582 self.cursor = 0;
583 self.filter_state = FilterState::FilterApplied;
584 }
585
586 pub fn start_spinner(&mut self) -> Option<Message> {
589 self.show_spinner = true;
590 Some(self.spinner.tick())
591 }
592
593 pub fn stop_spinner(&mut self) {
595 self.show_spinner = false;
596 }
597
598 #[must_use]
600 pub fn spinner_visible(&self) -> bool {
601 self.show_spinner
602 }
603
604 pub fn new_status_message(&mut self, msg: impl Into<String>) -> Option<Cmd> {
606 self.status_message = Some(msg.into());
607 let lifetime = self.status_message_lifetime;
608 Some(Cmd::new(move || {
609 std::thread::sleep(lifetime);
610 Message::new(StatusMessageTimeoutMsg)
611 }))
612 }
613
614 #[must_use]
616 pub fn status_message(&self) -> Option<&str> {
617 self.status_message.as_deref()
618 }
619
620 pub fn set_width(&mut self, w: usize) {
622 self.width = w;
623 self.help.width = w;
624 }
625
626 pub fn set_height(&mut self, h: usize) {
628 self.height = h;
629 self.update_pagination();
630 }
631
632 #[must_use]
634 pub fn width(&self) -> usize {
635 self.width
636 }
637
638 #[must_use]
640 pub fn height(&self) -> usize {
641 self.height
642 }
643
644 #[must_use]
646 pub fn paginator(&self) -> &Paginator {
647 &self.paginator
648 }
649
650 fn update_pagination(&mut self) {
652 let item_height = self.delegate.height() + self.delegate.spacing();
653 let available = self.height.saturating_sub(4); let per_page = available / item_height.max(1);
655 self.paginator = Paginator::new().per_page(per_page);
657 self.paginator
658 .set_total_pages_from_items(self.filtered_indices.len());
659 }
660
661 pub fn update(&mut self, msg: Message) -> Option<Cmd> {
663 if msg.is::<StatusMessageTimeoutMsg>() {
665 self.status_message = None;
666 return None;
667 }
668
669 if self.show_spinner && msg.is::<TickMsg>() {
671 return self.spinner.update(msg);
672 }
673
674 if let Some(key) = msg.downcast_ref::<KeyMsg>() {
676 let key_str = key.to_string();
677
678 if self.filter_state == FilterState::Filtering {
680 if matches(&key_str, &[&self.key_map.cancel_while_filtering]) {
681 self.reset_filter();
682 return None;
683 }
684 if matches(&key_str, &[&self.key_map.accept_while_filtering]) {
685 self.apply_filter();
686 self.filter_state = FilterState::FilterApplied;
687 self.filter_input.blur();
688 return None;
689 }
690
691 return self.filter_input.update(msg);
693 }
694
695 if matches(&key_str, &[&self.key_map.cursor_up]) {
697 self.cursor_up();
698 } else if matches(&key_str, &[&self.key_map.cursor_down]) {
699 self.cursor_down();
700 } else if matches(&key_str, &[&self.key_map.next_page]) {
701 self.paginator.next_page();
702 let start = self.paginator.page() * self.paginator.get_per_page();
704 self.cursor = if self.filtered_indices.is_empty() {
705 0
706 } else {
707 start.min(self.filtered_indices.len() - 1)
708 };
709 } else if matches(&key_str, &[&self.key_map.prev_page]) {
710 self.paginator.prev_page();
711 let start = self.paginator.page() * self.paginator.get_per_page();
712 self.cursor = if self.filtered_indices.is_empty() {
713 0
714 } else {
715 start.min(self.filtered_indices.len() - 1)
716 };
717 } else if matches(&key_str, &[&self.key_map.goto_start]) {
718 self.cursor = 0;
719 self.paginator.set_page(0);
720 } else if matches(&key_str, &[&self.key_map.goto_end]) {
721 self.cursor = self.filtered_indices.len().saturating_sub(1);
722 self.paginator
723 .set_page(self.paginator.get_total_pages().saturating_sub(1));
724 } else if matches(&key_str, &[&self.key_map.filter]) && self.filtering_enabled {
725 self.filter_state = FilterState::Filtering;
726 self.filter_input.focus();
727 } else if matches(&key_str, &[&self.key_map.clear_filter]) {
728 self.reset_filter();
729 } else if matches(&key_str, &[&self.key_map.show_full_help]) {
730 self.help.show_all = true;
731 } else if matches(&key_str, &[&self.key_map.close_full_help]) {
732 self.help.show_all = false;
733 }
734 }
735
736 if let Some(mouse) = msg.downcast_ref::<MouseMsg>() {
738 if mouse.action != MouseAction::Press {
740 return None;
741 }
742
743 match mouse.button {
744 MouseButton::WheelUp if self.mouse_wheel_enabled => {
746 for _ in 0..self.mouse_wheel_delta {
747 self.cursor_up();
748 }
749 }
750 MouseButton::WheelDown if self.mouse_wheel_enabled => {
751 for _ in 0..self.mouse_wheel_delta {
752 self.cursor_down();
753 }
754 }
755 MouseButton::Left if self.mouse_click_enabled => {
757 let mut content_start_y = 0usize;
761 if self.show_title && !self.title.is_empty() {
762 content_start_y += 1;
763 }
764 if self.show_filter && self.filter_state == FilterState::Filtering {
765 content_start_y += 1;
766 }
767
768 let click_y = mouse.y as usize;
769 if click_y >= content_start_y {
770 let item_height = self.delegate.height() + self.delegate.spacing();
771 let relative_y = click_y - content_start_y;
772 let item_index_in_view = relative_y / item_height.max(1);
773
774 let per_page = self.paginator.get_per_page();
776 let page_start = self.paginator.page() * per_page;
777 let target_cursor = page_start + item_index_in_view;
778
779 if target_cursor < self.filtered_indices.len() {
781 self.cursor = target_cursor;
782 }
783 }
784 }
785 _ => {}
786 }
787 }
788
789 None
790 }
791
792 #[must_use]
794 pub fn view(&self) -> String {
795 let mut sections = Vec::new();
796
797 if self.show_title && !self.title.is_empty() {
799 sections.push(self.styles.title.render(&self.title));
800 }
801
802 if self.show_filter && self.filter_state == FilterState::Filtering {
804 sections.push(self.filter_input.view());
805 }
806
807 if self.filtered_indices.is_empty() {
809 sections.push(self.styles.no_items.render("No items."));
810 } else {
811 let per_page = self.paginator.get_per_page();
812 let start = self.paginator.page() * per_page;
813 let end = (start + per_page).min(self.filtered_indices.len());
814
815 for (view_idx, &item_idx) in self.filtered_indices[start..end].iter().enumerate() {
816 let global_idx = start + view_idx;
817 let selected = global_idx == self.cursor;
818
819 if let Some(item) = self.items.get(item_idx) {
820 let rendered = self.delegate.render(item, global_idx, selected, self.width);
821 sections.push(rendered);
822 }
823 }
824 }
825
826 if self.show_spinner {
828 sections.push(self.spinner.view());
829 }
830
831 if self.show_status_bar {
833 let status = if let Some(status) = self.status_message.as_deref() {
834 status.to_string()
835 } else {
836 let count = self.filtered_indices.len();
837 if count == 1 {
838 format!("1 {}", self.item_name_singular)
839 } else {
840 format!("{} {}", count, self.item_name_plural)
841 }
842 };
843 sections.push(self.styles.status_bar.render(&status));
844 }
845
846 if self.show_pagination && self.paginator.get_total_pages() > 1 {
848 sections.push(self.paginator.view());
849 }
850
851 if self.show_help {
853 let bindings: Vec<&Binding> = vec![
854 &self.key_map.cursor_up,
855 &self.key_map.cursor_down,
856 &self.key_map.filter,
857 &self.key_map.quit,
858 ];
859 sections.push(
860 self.styles
861 .help
862 .render(&self.help.short_help_view(&bindings)),
863 );
864 }
865
866 sections.join("\n")
867 }
868
869 #[must_use]
874 pub fn init(&self) -> Option<Cmd> {
875 None
876 }
877}
878
879impl<I: Item, D: ItemDelegate<I>> Model for List<I, D> {
881 fn init(&self) -> Option<Cmd> {
882 List::init(self)
883 }
884
885 fn update(&mut self, msg: Message) -> Option<Cmd> {
886 List::update(self, msg)
887 }
888
889 fn view(&self) -> String {
890 List::view(self)
891 }
892}
893
894impl<I: Item + std::fmt::Debug, D: ItemDelegate<I> + std::fmt::Debug> std::fmt::Debug
896 for List<I, D>
897{
898 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
899 f.debug_struct("List")
900 .field("title", &self.title)
901 .field("items_count", &self.items.len())
902 .field("cursor", &self.cursor)
903 .field("filter_state", &self.filter_state)
904 .finish()
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[derive(Debug, Clone)]
913 struct TestItem {
914 name: String,
915 }
916
917 impl Item for TestItem {
918 fn filter_value(&self) -> &str {
919 &self.name
920 }
921 }
922
923 fn test_items() -> Vec<TestItem> {
924 vec![
925 TestItem {
926 name: "Apple".into(),
927 },
928 TestItem {
929 name: "Banana".into(),
930 },
931 TestItem {
932 name: "Cherry".into(),
933 },
934 TestItem {
935 name: "Date".into(),
936 },
937 ]
938 }
939
940 #[test]
941 fn test_list_new() {
942 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
943 assert_eq!(list.items().len(), 4);
944 assert_eq!(list.index(), 0);
945 }
946
947 #[test]
948 fn test_list_navigation() {
949 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
950
951 assert_eq!(list.index(), 0);
952
953 list.cursor_down();
954 assert_eq!(list.index(), 1);
955
956 list.cursor_down();
957 assert_eq!(list.index(), 2);
958
959 list.cursor_up();
960 assert_eq!(list.index(), 1);
961 }
962
963 #[test]
964 fn test_list_selected_item() {
965 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
966
967 assert_eq!(list.selected_item().map(|i| i.name.as_str()), Some("Apple"));
968
969 list.cursor_down();
970 assert_eq!(
971 list.selected_item().map(|i| i.name.as_str()),
972 Some("Banana")
973 );
974 }
975
976 #[test]
977 fn test_list_filter() {
978 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
979
980 list.set_filter_value("an");
981
982 assert_eq!(list.visible_items().len(), 1);
984 assert_eq!(list.visible_items()[0].name, "Banana");
985 }
986
987 #[test]
988 fn test_list_reset_filter() {
989 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
990
991 list.set_filter_value("an");
992 assert_eq!(list.visible_items().len(), 1);
993
994 list.reset_filter();
995 assert_eq!(list.visible_items().len(), 4);
996 }
997
998 #[test]
999 fn test_cancel_filter_resets_pagination() {
1000 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 6);
1001
1002 list.filter_state = FilterState::Filtering;
1003 list.filtered_indices = vec![0];
1004 list.paginator
1005 .set_total_pages_from_items(list.filtered_indices.len());
1006
1007 let key_msg = Message::new(KeyMsg::from_type(bubbletea::KeyType::Esc));
1008 let _ = list.update(key_msg);
1009
1010 assert_eq!(list.filter_state, FilterState::Unfiltered);
1011 assert_eq!(list.filtered_indices.len(), list.items.len());
1012 assert_eq!(list.paginator.get_total_pages(), 2);
1013 assert_eq!(list.cursor, 0);
1014 }
1015
1016 #[test]
1017 fn test_list_filter_state() {
1018 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1019 assert_eq!(list.filter_state(), FilterState::Unfiltered);
1020 }
1021
1022 #[test]
1023 fn test_list_infinite_scroll() {
1024 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1025 list.infinite_scrolling = true;
1026
1027 list.cursor_up();
1029 assert_eq!(list.index(), 3);
1030
1031 list.cursor_down();
1033 assert_eq!(list.index(), 0);
1034 }
1035
1036 #[test]
1037 fn test_list_status_message() {
1038 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1039
1040 assert!(list.status_message().is_none());
1041
1042 list.new_status_message("Test message");
1043 assert_eq!(list.status_message(), Some("Test message"));
1044 }
1045
1046 #[test]
1047 fn test_list_status_message_uses_singular_name() {
1048 let items = vec![TestItem {
1049 name: "Apple".into(),
1050 }];
1051 let mut list = List::new(items, DefaultDelegate::new(), 80, 6);
1052 list.item_name_singular = "fruit".to_string();
1053 list.item_name_plural = "fruits".to_string();
1054
1055 let view = list.view();
1056 assert!(view.contains("1 fruit"));
1057 }
1058
1059 #[test]
1060 fn test_list_apply_filter_resets_page() {
1061 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 5);
1062 list.paginator.set_page(2);
1063
1064 list.set_filter_value("a");
1065
1066 assert_eq!(list.paginator.page(), 0);
1067 }
1068
1069 #[test]
1070 fn test_list_reset_filter_resets_page() {
1071 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 5);
1072 list.paginator.set_page(3);
1073
1074 list.reset_filter();
1075
1076 assert_eq!(list.paginator.page(), 0);
1077 }
1078
1079 #[test]
1080 fn test_list_spinner() {
1081 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1082
1083 assert!(!list.spinner_visible());
1084
1085 list.start_spinner();
1086 assert!(list.spinner_visible());
1087
1088 list.stop_spinner();
1089 assert!(!list.spinner_visible());
1090 }
1091
1092 #[test]
1093 fn test_list_view() {
1094 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24).title("Fruits");
1095
1096 let view = list.view();
1097 assert!(view.contains("Fruits"));
1098 assert!(view.contains("Apple"));
1099 }
1100
1101 #[test]
1102 fn test_default_filter() {
1103 let targets = vec![
1104 "Apple".to_string(),
1105 "Banana".to_string(),
1106 "Cherry".to_string(),
1107 ];
1108
1109 let ranks = default_filter("an", &targets);
1110 assert_eq!(ranks.len(), 1);
1111 assert_eq!(ranks[0].index, 1); }
1113
1114 #[test]
1115 fn test_default_delegate() {
1116 let delegate = DefaultDelegate::new().with_height(2).with_spacing(1);
1117 assert_eq!(delegate.item_height, 2);
1118 assert_eq!(delegate.item_spacing, 1);
1119 }
1120
1121 #[test]
1122 fn test_keymap_default() {
1123 let km = KeyMap::default();
1124 assert!(!km.cursor_up.get_keys().is_empty());
1125 assert!(!km.filter.get_keys().is_empty());
1126 }
1127
1128 #[test]
1129 fn test_filter_state_display() {
1130 assert_eq!(FilterState::Unfiltered.to_string(), "unfiltered");
1131 assert_eq!(FilterState::Filtering.to_string(), "filtering");
1132 assert_eq!(FilterState::FilterApplied.to_string(), "filter applied");
1133 }
1134
1135 #[test]
1138 fn test_model_trait_init_returns_none() {
1139 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1140 let cmd = Model::init(&list);
1142 assert!(cmd.is_none(), "Model::init should return None for List");
1143 }
1144
1145 #[test]
1146 fn test_model_trait_view_returns_content() {
1147 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24).title("Test List");
1148 let view = Model::view(&list);
1150 assert!(view.contains("Test List"), "View should contain the title");
1151 assert!(view.contains("Apple"), "View should contain first item");
1152 }
1153
1154 #[test]
1155 fn test_model_trait_update_handles_messages() {
1156 let mut list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1157 assert_eq!(list.index(), 0);
1158
1159 let key_msg = Message::new(KeyMsg {
1161 key_type: bubbletea::KeyType::Runes,
1162 runes: vec!['j'], alt: false,
1164 paste: false,
1165 });
1166
1167 let _ = Model::update(&mut list, key_msg);
1169 assert_eq!(list.index(), 1, "Cursor should have moved down");
1170 }
1171
1172 #[test]
1173 fn test_list_satisfies_model_bounds() {
1174 fn accepts_model<M: Model + Send + 'static>(_model: M) {}
1176 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1177 accepts_model(list);
1178 }
1179
1180 #[test]
1181 fn test_list_pagination_calculation() {
1182 let list = List::new(test_items(), DefaultDelegate::new(), 80, 10);
1189 assert_eq!(list.paginator().get_per_page(), 6);
1191 }
1192
1193 #[test]
1194 fn test_list_paginator_accessor() {
1195 let list = List::new(test_items(), DefaultDelegate::new(), 80, 24);
1196 assert!(list.paginator().get_per_page() > 0);
1198 assert_eq!(list.paginator().page(), 0);
1199 }
1200
1201 #[test]
1202 fn test_list_pagination_with_many_items() {
1203 let items: Vec<TestItem> = (1..=50)
1204 .map(|i| TestItem {
1205 name: format!("Item {}", i),
1206 })
1207 .collect();
1208 let list = List::new(items, DefaultDelegate::new(), 80, 10);
1209 assert_eq!(list.paginator().get_total_pages(), 9);
1211 }
1212}