1use crate::help;
18use crate::internal::fuzzy;
19use crate::key::{self, Binding};
20use crate::paginator;
21use crate::spinner;
22use crate::textinput;
23use rusty_bubbletea::commands;
24use rusty_bubbletea::key::KeyPressMsg;
25use rusty_bubbletea::model::{Cmd, Msg};
26use rusty_lipgloss::{self, Color, Style};
27use rusty_x_ansi;
28use std::time::Duration;
29
30type ItemUpdateFunc = Box<dyn Fn(&dyn Msg, &Model) -> Cmd + Send + Sync>;
33
34const BULLET: &str = "•";
35const ELLIPSIS: &str = "…";
36
37fn clamp(v: usize, low: usize, high: usize) -> usize {
38 if low > high {
39 return v.min(low);
40 }
41 v.max(low).min(high)
42}
43
44pub trait Item: Send + Sync + std::fmt::Debug {
50 fn filter_value(&self) -> String;
53
54 fn box_clone(&self) -> Box<dyn Item + Send + Sync>;
56
57 fn as_any(&self) -> &dyn std::any::Any;
59
60 fn as_default_item(&self) -> Option<&dyn DefaultItem> {
62 None
63 }
64}
65
66pub trait ItemDelegate {
74 fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String;
76
77 fn height(&self) -> usize;
79
80 fn spacing(&self) -> usize;
83
84 fn update(&self, msg: &dyn Msg, m: &Model) -> Cmd;
92
93 fn short_help(&self) -> Vec<Binding> {
96 vec![]
97 }
98
99 fn full_help(&self) -> Vec<Vec<Binding>> {
102 vec![]
103 }
104}
105
106#[derive(Debug)]
109pub struct FilterMatchesMsg(pub Vec<FilteredItem>);
110
111impl FilteredItem {
112 fn clone_item(&self) -> FilteredItem {
114 FilteredItem {
115 index: self.index,
116 item: self.item.box_clone(),
117 matches: self.matches.clone(),
118 }
119 }
120}
121
122impl FilterMatchesMsg {
123 pub fn clone_items(&self) -> Vec<FilteredItem> {
125 self.0
126 .iter()
127 .map(|f| FilteredItem {
128 index: f.index,
129 item: f.item.box_clone(),
130 matches: f.matches.clone(),
131 })
132 .collect()
133 }
134}
135
136#[derive(Debug)]
139pub struct FilteredItem {
140 pub index: usize,
142 pub item: Box<dyn Item + Send + Sync>,
144 pub matches: Vec<usize>,
146}
147
148#[derive(Debug, Clone)]
150pub struct Rank {
151 pub index: usize,
153 pub matched_indexes: Vec<usize>,
155}
156
157pub type FilterFunc = Box<dyn Fn(&str, &[String]) -> Vec<Rank> + Send + Sync>;
161
162pub fn default_filter(term: &str, targets: &[String]) -> Vec<Rank> {
165 let matches = fuzzy::find(term, targets);
166 matches
167 .iter()
168 .map(|r| Rank {
169 index: r.index,
170 matched_indexes: r.matched_indexes.clone(),
171 })
172 .collect()
173}
174
175pub fn unsorted_filter(term: &str, targets: &[String]) -> Vec<Rank> {
178 let matches = fuzzy::find_no_sort(term, targets);
179 matches
180 .iter()
181 .map(|r| Rank {
182 index: r.index,
183 matched_indexes: r.matched_indexes.clone(),
184 })
185 .collect()
186}
187
188#[derive(Debug)]
189struct StatusMessageTimeoutMsg;
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193pub enum FilterState {
194 #[default]
196 Unfiltered,
197 Filtering,
199 FilterApplied,
201}
202
203impl FilterState {
204 pub fn to_string(&self) -> &'static str {
206 match self {
207 FilterState::Unfiltered => "unfiltered",
208 FilterState::Filtering => "filtering",
209 FilterState::FilterApplied => "filter applied",
210 }
211 }
212}
213
214pub struct Model {
216 pub show_title: bool,
217 pub show_filter: bool,
218 pub show_status_bar: bool,
219 pub show_pagination: bool,
220 pub show_help: bool,
221 pub filtering_enabled: bool,
222
223 item_name_singular: String,
224 item_name_plural: String,
225
226 pub title: String,
228 pub styles: Styles,
230 pub infinite_scrolling: bool,
232
233 pub key_map: KeyMap,
235
236 pub filter: FilterFunc,
238
239 pub disable_quit_keybindings: bool,
240
241 pub additional_short_help_keys: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
243 pub additional_full_help_keys: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
245
246 pub spinner: spinner::Model,
247 pub show_spinner: bool,
248 pub width: usize,
249 pub height: usize,
250 pub paginator: paginator::Model,
252 cursor: usize,
253 pub help: help::Model,
255 pub filter_input: textinput::Model,
257 pub filter_state: FilterState,
258
259 pub status_message_lifetime: Duration,
262
263 status_message: String,
264 status_message_timer: Option<std::thread::JoinHandle<()>>,
265
266 pub items: Vec<Box<dyn Item + Send + Sync>>,
268
269 filtered_items: Vec<FilteredItem>,
272
273 delegate: Box<dyn ItemDelegate + Send + Sync>,
274}
275
276impl help::KeyMap for Model {
277 fn short_help(&self) -> Vec<Binding> {
278 Model::short_help(self)
279 }
280
281 fn full_help(&self) -> Vec<Vec<Binding>> {
282 Model::full_help(self)
283 }
284}
285
286impl std::fmt::Debug for Model {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 f.debug_struct("list::Model")
289 .field("title", &self.title)
290 .field("width", &self.width)
291 .field("height", &self.height)
292 .field("items", &self.items.len())
293 .finish()
294 }
295}
296
297pub fn new(
299 items: Vec<Box<dyn Item + Send + Sync>>,
300 delegate: Box<dyn ItemDelegate + Send + Sync>,
301 width: usize,
302 height: usize,
303) -> Model {
304 let styles = default_styles(true);
305
306 let mut sp = spinner::new(vec![]);
307 sp.spinner = spinner::line();
308 sp.style = styles.spinner.clone();
309
310 let mut filter_input = textinput::new();
311 filter_input.prompt = "Filter: ".to_string();
312 filter_input.char_limit = 64;
313 filter_input.focus();
314
315 let mut p = paginator::new(vec![]);
316 p.type_ = paginator::Type::Dots;
317 p.active_dot = styles
318 .active_pagination_dot
319 .clone()
320 .set_string(&[BULLET])
321 .render("");
322 p.inactive_dot = styles
323 .inactive_pagination_dot
324 .clone()
325 .set_string(&[BULLET])
326 .render("");
327
328 let mut m = Model {
329 show_title: true,
330 show_filter: true,
331 show_status_bar: true,
332 show_pagination: true,
333 show_help: true,
334 item_name_singular: "item".to_string(),
335 item_name_plural: "items".to_string(),
336 filtering_enabled: true,
337 key_map: default_key_map(),
338 filter: Box::new(default_filter),
339 styles,
340 title: "List".to_string(),
341 filter_input,
342 status_message_lifetime: Duration::from_secs(1),
343 width,
344 height,
345 delegate,
346 items,
347 paginator: p,
348 spinner: sp,
349 help: help::new(),
350 cursor: 0,
351 filter_state: FilterState::Unfiltered,
352 infinite_scrolling: false,
353 disable_quit_keybindings: false,
354 additional_short_help_keys: None,
355 additional_full_help_keys: None,
356 show_spinner: false,
357 status_message: String::new(),
358 status_message_timer: None,
359 filtered_items: vec![],
360 };
361
362 m.update_pagination();
363 m.update_keybindings();
364 m
365}
366
367impl Model {
368 pub fn set_filtering_enabled(&mut self, v: bool) {
371 self.filtering_enabled = v;
372 if !v {
373 self.reset_filtering();
374 }
375 self.update_keybindings();
376 }
377
378 pub fn filtering_enabled(&self) -> bool {
380 self.filtering_enabled
381 }
382
383 pub fn set_show_title(&mut self, v: bool) {
385 self.show_title = v;
386 self.update_pagination();
387 }
388
389 pub fn set_filter_text(&mut self, filter: &str) {
393 self.filter_state = FilterState::Filtering;
394 self.filter_input.set_value(filter);
395 let fmm = filter_items(self);
396 self.filtered_items = fmm;
397 self.filter_state = FilterState::FilterApplied;
398 self.go_to_start();
399 self.filter_input.cursor_end();
400 self.update_pagination();
401 self.update_keybindings();
402 }
403
404 pub fn set_filter_state(&mut self, state: FilterState) {
406 self.go_to_start();
407 self.filter_state = state;
408 self.filter_input.cursor_end();
409 self.filter_input.focus();
410 self.update_keybindings();
411 }
412
413 pub fn show_title(&self) -> bool {
415 self.show_title
416 }
417
418 pub fn set_show_filter(&mut self, v: bool) {
421 self.show_filter = v;
422 self.update_pagination();
423 }
424
425 pub fn show_filter(&self) -> bool {
427 self.show_filter
428 }
429
430 pub fn set_show_status_bar(&mut self, v: bool) {
433 self.show_status_bar = v;
434 self.update_pagination();
435 }
436
437 pub fn show_status_bar(&self) -> bool {
440 self.show_status_bar
441 }
442
443 pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
446 self.item_name_singular = singular.to_string();
447 self.item_name_plural = plural.to_string();
448 }
449
450 pub fn status_bar_item_name(&self) -> (String, String) {
452 (
453 self.item_name_singular.clone(),
454 self.item_name_plural.clone(),
455 )
456 }
457
458 pub fn set_show_pagination(&mut self, v: bool) {
461 self.show_pagination = v;
462 self.update_pagination();
463 }
464
465 pub fn show_pagination(&mut self) -> bool {
467 self.show_pagination
468 }
469
470 pub fn set_show_help(&mut self, v: bool) {
472 self.show_help = v;
473 self.update_pagination();
474 }
475
476 pub fn show_help(&self) -> bool {
478 self.show_help
479 }
480
481 pub fn items(&self) -> &[Box<dyn Item + Send + Sync>] {
483 &self.items
484 }
485
486 pub fn set_items(&mut self, items: Vec<Box<dyn Item + Send + Sync>>) -> Cmd {
489 let mut cmd: Cmd = None;
490 self.items = items;
491
492 if self.filter_state != FilterState::Unfiltered {
493 self.filtered_items = vec![];
494 cmd = filter_items_cmd(self);
495 }
496
497 self.update_pagination();
498 self.update_keybindings();
499 cmd
500 }
501
502 pub fn select(&mut self, index: usize) {
505 self.paginator.page = index / self.paginator.per_page;
506 self.cursor = index % self.paginator.per_page;
507 }
508
509 pub fn reset_selected(&mut self) {
512 self.select(0);
513 }
514
515 pub fn reset_filter(&mut self) {
517 self.reset_filtering();
518 }
519
520 pub fn set_item(&mut self, index: usize, item: Box<dyn Item + Send + Sync>) -> Cmd {
522 let mut cmd: Cmd = None;
523 self.items[index] = item;
524
525 if self.filter_state != FilterState::Unfiltered {
526 cmd = filter_items_cmd(self);
527 }
528
529 self.update_pagination();
530 cmd
531 }
532
533 pub fn insert_item(&mut self, index: usize, item: Box<dyn Item + Send + Sync>) -> Cmd {
536 let mut cmd: Cmd = None;
537 self.items = insert_item_into_slice(&self.items, item, index);
538
539 if self.filter_state != FilterState::Unfiltered {
540 cmd = filter_items_cmd(self);
541 }
542
543 self.update_pagination();
544 self.update_keybindings();
545 cmd
546 }
547
548 pub fn remove_item(&mut self, index: usize) {
552 self.items = remove_item_from_slice(&self.items, index);
553 if self.filter_state != FilterState::Unfiltered {
554 self.filtered_items = remove_filter_match_from_slice(&self.filtered_items, index);
555 if self.filtered_items.is_empty() {
556 self.reset_filtering();
557 }
558 }
559 self.update_pagination();
560 }
561
562 pub fn set_delegate(&mut self, d: Box<dyn ItemDelegate + Send + Sync>) {
564 self.delegate = d;
565 self.update_pagination();
566 }
567
568 pub fn visible_items(&self) -> Vec<&dyn Item> {
570 if self.filter_state != FilterState::Unfiltered {
571 return self
572 .filtered_items
573 .iter()
574 .map(|f| f.item.as_ref() as &dyn Item)
575 .collect();
576 }
577 self.items.iter().map(|i| i.as_ref() as &dyn Item).collect()
578 }
579
580 pub fn selected_item(&self) -> Option<&dyn Item> {
582 let i = self.index();
583
584 let items = self.visible_items();
585 if i >= items.len() {
586 return None;
587 }
588 Some(items[i])
589 }
590
591 pub fn matches_for_item(&self, index: usize) -> Vec<usize> {
594 if self.filtered_items.is_empty() || index >= self.filtered_items.len() {
595 return vec![];
596 }
597 self.filtered_items[index].matches.clone()
598 }
599
600 pub fn index(&self) -> usize {
603 self.paginator.page * self.paginator.per_page + self.cursor
604 }
605
606 pub fn global_index(&self) -> usize {
610 let index = self.index();
611
612 if self.filtered_items.is_empty() || index >= self.filtered_items.len() {
613 return index;
614 }
615
616 self.filtered_items[index].index
617 }
618
619 pub fn cursor(&self) -> usize {
621 self.cursor
622 }
623
624 pub fn cursor_up(&mut self) {
627 if self.cursor == 0 && self.paginator.on_first_page() {
628 if self.infinite_scrolling {
630 self.go_to_end();
631 return;
632 }
633 return;
634 }
635
636 if self.cursor > 0 {
638 self.cursor -= 1;
639 return;
640 }
641
642 self.paginator.prev_page();
644 self.cursor = self.max_cursor_index();
645 }
646
647 pub fn cursor_down(&mut self) {
650 let max_cursor_index = self.max_cursor_index();
651
652 self.cursor += 1;
653
654 if self.cursor <= max_cursor_index {
657 return;
658 }
659
660 if !self.paginator.on_last_page() {
662 self.paginator.next_page();
663 self.cursor = 0;
664 return;
665 }
666
667 self.cursor = max_cursor_index;
668
669 if self.infinite_scrolling {
671 self.go_to_start();
672 }
673 }
674
675 pub fn go_to_start(&mut self) {
677 self.paginator.page = 0;
678 self.cursor = 0;
679 }
680
681 pub fn go_to_end(&mut self) {
683 self.paginator.page = self.paginator.total_pages - 1;
684 self.cursor = self.max_cursor_index();
685 }
686
687 pub fn prev_page(&mut self) {
689 self.paginator.prev_page();
690 self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
691 }
692
693 pub fn next_page(&mut self) {
695 self.paginator.next_page();
696 self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
697 }
698
699 fn max_cursor_index(&self) -> usize {
700 self.paginator
701 .items_on_page(self.visible_items().len())
702 .saturating_sub(1)
703 }
704
705 pub fn filter_state(&self) -> FilterState {
707 self.filter_state
708 }
709
710 pub fn filter_value(&self) -> String {
712 self.filter_input.value()
713 }
714
715 pub fn setting_filter(&self) -> bool {
722 self.filter_state == FilterState::Filtering
723 }
724
725 pub fn is_filtered(&self) -> bool {
727 self.filter_state == FilterState::FilterApplied
728 }
729
730 pub fn width(&self) -> usize {
732 self.width
733 }
734
735 pub fn height(&self) -> usize {
737 self.height
738 }
739
740 pub fn set_spinner(&mut self, spinner: spinner::Spinner) {
742 self.spinner.spinner = spinner;
743 }
744
745 pub fn toggle_spinner(&mut self) -> Cmd {
748 if !self.show_spinner {
749 return self.start_spinner();
750 }
751 self.stop_spinner();
752 None
753 }
754
755 pub fn start_spinner(&mut self) -> Cmd {
757 self.show_spinner = true;
758 let tick = self.spinner.tick_msg();
759 Some(Box::new(move || Some(Box::new(tick))))
760 }
761
762 pub fn stop_spinner(&mut self) {
764 self.show_spinner = false;
765 }
766
767 pub fn disable_quit_keybindings(&mut self) {
771 self.disable_quit_keybindings = true;
772 self.key_map.quit.set_enabled(false);
773 self.key_map.force_quit.set_enabled(false);
774 }
775
776 pub fn new_status_message(&mut self, s: &str) -> Cmd {
779 self.status_message = s.to_string();
780 if let Some(t) = &self.status_message_timer {
781 let _ = t.thread().id();
782 }
783 let lifetime = self.status_message_lifetime;
784 self.status_message_timer = Some(std::thread::spawn(move || {
785 std::thread::sleep(lifetime);
786 }));
787 Some(Box::new(|| Some(Box::new(StatusMessageTimeoutMsg))))
788 }
789
790 pub fn set_width(&mut self, v: usize) {
792 self.set_size(v, self.height);
793 }
794
795 pub fn set_height(&mut self, v: usize) {
797 self.set_size(self.width, v);
798 }
799
800 pub fn set_size(&mut self, width: usize, height: usize) {
802 let prompt_width = rusty_lipgloss::size::width(
803 &self.styles.title.clone().render(&self.filter_input.prompt),
804 );
805
806 self.width = width;
807 self.height = height;
808 self.help.set_width(width);
809 let sw = self.spinner_view();
810 let sw_width = rusty_lipgloss::size::width(&sw);
811 self.filter_input
812 .set_width(width.saturating_sub(prompt_width + sw_width));
813 self.update_pagination();
814 self.update_keybindings();
815 }
816
817 fn reset_filtering(&mut self) {
818 if self.filter_state == FilterState::Unfiltered {
819 return;
820 }
821
822 self.filter_state = FilterState::Unfiltered;
823 self.filter_input.reset();
824 self.filtered_items = vec![];
825 self.update_pagination();
826 self.update_keybindings();
827 }
828
829 fn items_as_filter_items(&self) -> Vec<FilteredItem> {
830 self.items
831 .iter()
832 .enumerate()
833 .map(|(i, item)| FilteredItem {
834 index: i,
835 item: item.box_clone(),
836 matches: vec![],
837 })
838 .collect()
839 }
840
841 pub fn update_keybindings(&mut self) {
843 match self.filter_state {
844 FilterState::Filtering => {
845 self.key_map.cursor_up.set_enabled(false);
846 self.key_map.cursor_down.set_enabled(false);
847 self.key_map.next_page.set_enabled(false);
848 self.key_map.prev_page.set_enabled(false);
849 self.key_map.go_to_start.set_enabled(false);
850 self.key_map.go_to_end.set_enabled(false);
851 self.key_map.filter.set_enabled(false);
852 self.key_map.clear_filter.set_enabled(false);
853 self.key_map.cancel_while_filtering.set_enabled(true);
854 self.key_map
855 .accept_while_filtering
856 .set_enabled(!self.filter_input.value().is_empty());
857 self.key_map.quit.set_enabled(false);
858 self.key_map.show_full_help.set_enabled(false);
859 self.key_map.close_full_help.set_enabled(false);
860 }
861 _ => {
862 let has_items = !self.items.is_empty();
863 self.key_map.cursor_up.set_enabled(has_items);
864 self.key_map.cursor_down.set_enabled(has_items);
865
866 let has_pages = self.paginator.total_pages > 1;
867 self.key_map.next_page.set_enabled(has_pages);
868 self.key_map.prev_page.set_enabled(has_pages);
869
870 self.key_map.go_to_start.set_enabled(has_items);
871 self.key_map.go_to_end.set_enabled(has_items);
872
873 self.key_map
874 .filter
875 .set_enabled(self.filtering_enabled && has_items);
876 self.key_map
877 .clear_filter
878 .set_enabled(self.filter_state == FilterState::FilterApplied);
879 self.key_map.cancel_while_filtering.set_enabled(false);
880 self.key_map.accept_while_filtering.set_enabled(false);
881 self.key_map
882 .quit
883 .set_enabled(!self.disable_quit_keybindings);
884
885 if self.help.show_all {
886 self.key_map.show_full_help.set_enabled(true);
887 self.key_map.close_full_help.set_enabled(true);
888 } else {
889 let min_help = count_enabled_bindings(&self.full_help()) > 1;
890 self.key_map.show_full_help.set_enabled(min_help);
891 self.key_map.close_full_help.set_enabled(min_help);
892 }
893 }
894 }
895 }
896
897 pub fn update_pagination(&mut self) {
900 let index = self.index();
901 let mut avail_height = self.height;
902
903 if self.show_title || (self.show_filter && self.filtering_enabled) {
904 avail_height =
905 avail_height.saturating_sub(rusty_lipgloss::size::height(&self.title_view()));
906 }
907 if self.show_status_bar {
908 avail_height =
909 avail_height.saturating_sub(rusty_lipgloss::size::height(&self.status_view()));
910 }
911 if self.show_pagination {
912 avail_height =
913 avail_height.saturating_sub(rusty_lipgloss::size::height(&self.pagination_view()));
914 }
915 if self.show_help {
916 avail_height =
917 avail_height.saturating_sub(rusty_lipgloss::size::height(&self.help_view()));
918 }
919
920 let delegate_height = self.delegate.height();
921 let delegate_spacing = self.delegate.spacing();
922 self.paginator.per_page =
923 1usize.max(avail_height / (delegate_height + delegate_spacing).max(1));
924
925 let pages = self.visible_items().len();
926 if pages < 1 {
927 self.paginator.set_total_pages(1);
928 } else {
929 self.paginator.set_total_pages(pages);
930 }
931
932 self.paginator.page = index / self.paginator.per_page.max(1);
934 self.cursor = index % self.paginator.per_page.max(1);
935
936 if self.paginator.page >= self.paginator.total_pages - 1 {
938 self.paginator.page = self.paginator.total_pages - 1;
939 }
940 }
941
942 fn hide_status_message(&mut self) {
943 self.status_message = String::new();
944 if let Some(t) = &self.status_message_timer {
945 let _ = t.thread().id();
946 }
947 self.status_message_timer = None;
948 }
949
950 pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
952 let mut cmds: Vec<Cmd> = Vec::new();
953
954 if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
955 if key::matches(&m.0, std::slice::from_ref(&self.key_map.force_quit)) {
956 return commands::quit();
957 }
958 }
959
960 if let Some(m) = msg.as_any().downcast_ref::<FilterMatchesMsg>() {
961 self.filtered_items = m.clone_items();
962 return None;
963 }
964
965 if let Some(m) = msg.as_any().downcast_ref::<spinner::TickMsg>() {
966 let cmd = self.spinner.update(msg);
967 if self.show_spinner {
968 cmds.push(cmd);
969 }
970 let _ = m;
971 }
972
973 if msg
974 .as_any()
975 .downcast_ref::<StatusMessageTimeoutMsg>()
976 .is_some()
977 {
978 self.hide_status_message();
979 }
980
981 let cmd = if self.filter_state == FilterState::Filtering {
982 self.handle_filtering(msg)
983 } else {
984 self.handle_browsing(msg)
985 };
986 cmds.push(cmd);
987
988 commands::batch(cmds)
989 }
990
991 fn handle_browsing(&mut self, msg: &dyn Msg) -> Cmd {
993 if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
994 let k = &m.0;
995 if key::matches(k, std::slice::from_ref(&self.key_map.clear_filter)) {
998 self.reset_filtering();
999 } else if key::matches(k, std::slice::from_ref(&self.key_map.quit)) {
1000 return commands::quit();
1001 } else if key::matches(k, std::slice::from_ref(&self.key_map.cursor_up)) {
1002 self.cursor_up();
1003 } else if key::matches(k, std::slice::from_ref(&self.key_map.cursor_down)) {
1004 self.cursor_down();
1005 } else if key::matches(k, std::slice::from_ref(&self.key_map.prev_page)) {
1006 self.paginator.prev_page();
1007 } else if key::matches(k, std::slice::from_ref(&self.key_map.next_page)) {
1008 self.paginator.next_page();
1009 } else if key::matches(k, std::slice::from_ref(&self.key_map.go_to_start)) {
1010 self.go_to_start();
1011 } else if key::matches(k, std::slice::from_ref(&self.key_map.go_to_end)) {
1012 self.go_to_end();
1013 } else if key::matches(k, std::slice::from_ref(&self.key_map.filter)) {
1014 self.hide_status_message();
1015 if self.filter_input.value().is_empty() {
1016 self.filtered_items = self.items_as_filter_items();
1019 }
1020 self.go_to_start();
1021 self.filter_state = FilterState::Filtering;
1022 self.filter_input.cursor_end();
1023 self.filter_input.focus();
1024 self.update_keybindings();
1025 return Some(Box::new(|| Some(textinput::blink())));
1026 } else if key::matches(k, std::slice::from_ref(&self.key_map.show_full_help))
1027 || key::matches(k, std::slice::from_ref(&self.key_map.close_full_help))
1028 {
1029 self.help.show_all = !self.help.show_all;
1030 self.update_pagination();
1031 }
1032 }
1033
1034 let cmd = self.delegate.update(msg, self);
1035 self.cursor = clamp(self.cursor, 0, self.max_cursor_index());
1036
1037 cmd
1038 }
1039
1040 fn handle_filtering(&mut self, msg: &dyn Msg) -> Cmd {
1042 let mut cmds: Vec<Cmd> = Vec::new();
1043
1044 if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
1046 let k = &m.0;
1047 if key::matches(
1048 k,
1049 std::slice::from_ref(&self.key_map.cancel_while_filtering),
1050 ) {
1051 self.reset_filtering();
1052 self.key_map.filter.set_enabled(true);
1053 self.key_map.clear_filter.set_enabled(false);
1054 } else if key::matches(
1055 k,
1056 std::slice::from_ref(&self.key_map.accept_while_filtering),
1057 ) {
1058 self.hide_status_message();
1059
1060 if self.items.is_empty() {
1061 } else {
1063 let h = self.visible_items();
1064
1065 if h.is_empty() {
1067 self.reset_filtering();
1068 } else {
1069 self.filter_input.blur();
1070 self.filter_state = FilterState::FilterApplied;
1071 self.update_keybindings();
1072
1073 if self.filter_input.value().is_empty() {
1074 self.reset_filtering();
1075 }
1076 }
1077 }
1078 }
1079 }
1080
1081 let old_value = self.filter_input.value();
1083 let input_cmd = self.filter_input.update(msg);
1084 let filter_changed = old_value != self.filter_input.value();
1085 cmds.push(input_cmd);
1086
1087 if filter_changed {
1089 cmds.push(filter_items_cmd(self));
1090 self.key_map
1091 .accept_while_filtering
1092 .set_enabled(!self.filter_input.value().is_empty());
1093 }
1094
1095 self.update_pagination();
1097
1098 commands::batch(cmds)
1099 }
1100
1101 pub fn short_help(&self) -> Vec<Binding> {
1104 let mut kb = vec![
1105 self.key_map.cursor_up.clone(),
1106 self.key_map.cursor_down.clone(),
1107 ];
1108
1109 let filtering = self.filter_state == FilterState::Filtering;
1110
1111 if !filtering {
1114 kb.extend(self.delegate.short_help());
1115 }
1116
1117 kb.extend(vec![
1118 self.key_map.filter.clone(),
1119 self.key_map.clear_filter.clone(),
1120 self.key_map.accept_while_filtering.clone(),
1121 self.key_map.cancel_while_filtering.clone(),
1122 ]);
1123
1124 if !filtering {
1125 if let Some(f) = &self.additional_short_help_keys {
1126 kb.extend(f());
1127 }
1128 }
1129
1130 kb.push(self.key_map.quit.clone());
1131 kb.push(self.key_map.show_full_help.clone());
1132 kb
1133 }
1134
1135 pub fn full_help(&self) -> Vec<Vec<Binding>> {
1138 let mut kb: Vec<Vec<Binding>> = vec![vec![
1139 self.key_map.cursor_up.clone(),
1140 self.key_map.cursor_down.clone(),
1141 self.key_map.next_page.clone(),
1142 self.key_map.prev_page.clone(),
1143 self.key_map.go_to_start.clone(),
1144 self.key_map.go_to_end.clone(),
1145 ]];
1146
1147 let filtering = self.filter_state == FilterState::Filtering;
1148
1149 if !filtering {
1152 let fh = self.delegate.full_help();
1153 if !fh.is_empty() {
1154 kb.extend(fh);
1155 }
1156 }
1157
1158 let mut list_level_bindings = vec![
1159 self.key_map.filter.clone(),
1160 self.key_map.clear_filter.clone(),
1161 self.key_map.accept_while_filtering.clone(),
1162 self.key_map.cancel_while_filtering.clone(),
1163 ];
1164
1165 if !filtering {
1166 if let Some(f) = &self.additional_full_help_keys {
1167 list_level_bindings.extend(f());
1168 }
1169 }
1170
1171 kb.push(list_level_bindings);
1172 kb.push(vec![
1173 self.key_map.quit.clone(),
1174 self.key_map.close_full_help.clone(),
1175 ]);
1176 kb
1177 }
1178
1179 pub fn view(&self) -> String {
1181 let mut sections: Vec<String> = Vec::new();
1182 let mut avail_height = self.height;
1183
1184 if self.show_title || (self.show_filter && self.filtering_enabled) {
1185 let v = self.title_view();
1186 sections.push(v.clone());
1187 avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&v));
1188 }
1189
1190 if self.show_status_bar {
1191 let v = self.status_view();
1192 sections.push(v.clone());
1193 avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&v));
1194 }
1195
1196 let mut pagination = String::new();
1197 if self.show_pagination {
1198 pagination = self.pagination_view();
1199 avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&pagination));
1200 }
1201
1202 let mut help_view = String::new();
1203 if self.show_help {
1204 help_view = self.help_view();
1205 avail_height = avail_height.saturating_sub(rusty_lipgloss::size::height(&help_view));
1206 }
1207
1208 let content = rusty_lipgloss::new_style()
1209 .height(avail_height)
1210 .render(&self.populated_view());
1211 sections.push(content);
1212
1213 if self.show_pagination {
1214 sections.push(pagination);
1215 }
1216
1217 if self.show_help {
1218 sections.push(help_view);
1219 }
1220
1221 let refs: Vec<&str> = sections.iter().map(|s| s.as_str()).collect();
1222 rusty_lipgloss::join::join_vertical(rusty_lipgloss::LEFT, &refs)
1223 }
1224
1225 fn title_view(&self) -> String {
1226 let title_bar_style = self.styles.title_bar.clone();
1227
1228 let spinner_view = self.spinner_view();
1231 let spinner_width = rusty_lipgloss::size::width(&spinner_view);
1232 let spinner_left_gap = " ";
1233 let spinner_on_left = title_bar_style.get_padding_left()
1234 >= spinner_width + rusty_lipgloss::size::width(spinner_left_gap)
1235 && self.show_spinner;
1236
1237 let mut view = String::new();
1238
1239 if self.show_filter && self.filter_state == FilterState::Filtering {
1241 view += &self.filter_input.view();
1242 } else if self.show_title {
1243 if self.show_spinner && spinner_on_left {
1244 view += &spinner_view;
1245 view += spinner_left_gap;
1246 let title_bar_gap = title_bar_style.get_padding_left();
1247 let _ = title_bar_gap;
1248 }
1250
1251 view += &self.styles.title.clone().render(&self.title);
1252
1253 if self.filter_state != FilterState::Filtering {
1255 view += " ";
1256 view += &self.status_message;
1257 view = rusty_x_ansi::truncate(
1258 &view,
1259 self.width.saturating_sub(spinner_width),
1260 ELLIPSIS,
1261 );
1262 }
1263 }
1264
1265 if self.show_spinner && !spinner_on_left {
1267 let avail_space =
1269 self.width - rusty_lipgloss::size::width(&title_bar_style.render(&view));
1270 if avail_space > spinner_width {
1271 view += &" ".repeat(avail_space - spinner_width);
1272 view += &spinner_view;
1273 }
1274 }
1275
1276 if !view.is_empty() {
1277 return title_bar_style.render(&view);
1278 }
1279 view
1280 }
1281
1282 fn status_view(&self) -> String {
1283 let mut status = String::new();
1284
1285 let total_items = self.items.len();
1286 let visible_items = self.visible_items().len();
1287
1288 let item_name = if visible_items != 1 {
1289 self.item_name_plural.clone()
1290 } else {
1291 self.item_name_singular.clone()
1292 };
1293
1294 let items_display = format!("{} {}", visible_items, item_name);
1295
1296 if self.filter_state == FilterState::Filtering {
1297 if visible_items == 0 {
1299 status = self.styles.status_empty.clone().render("Nothing matched");
1300 } else {
1301 status = items_display;
1302 }
1303 } else if self.items.is_empty() {
1304 status = self
1306 .styles
1307 .status_empty
1308 .clone()
1309 .render(&format!("No {}", self.item_name_plural));
1310 } else {
1311 let filtered = self.filter_state == FilterState::FilterApplied;
1313
1314 if filtered {
1315 let f = self.filter_input.value().trim().to_string();
1316 let f = rusty_x_ansi::truncate(&f, 10, "…");
1317 status += &format!("“{}” ", f);
1318 }
1319
1320 status += &items_display;
1321 }
1322
1323 let num_filtered = total_items - visible_items;
1324 if num_filtered > 0 {
1325 status += &self.styles.divider_dot.clone().render("");
1326 status += &self
1327 .styles
1328 .status_bar_filter_count
1329 .clone()
1330 .render(&format!("{} filtered", num_filtered));
1331 }
1332
1333 self.styles.status_bar.clone().render(&status)
1334 }
1335
1336 fn pagination_view(&self) -> String {
1337 if self.paginator.total_pages < 2 {
1338 return String::new();
1339 }
1340
1341 let mut s = self.paginator.view();
1342
1343 if rusty_x_ansi::string_width(&s) > self.width {
1346 self.paginator_type_change_to_arabic();
1347 s = self
1348 .styles
1349 .arabic_pagination
1350 .clone()
1351 .render(&self.paginator.view());
1352 }
1353
1354 let mut style = self.styles.pagination_style.clone();
1355 if self.delegate.spacing() == 0 && style.get_margin_top() == 0 {
1356 style = style.margin_top(1);
1357 }
1358
1359 style.render(&s)
1360 }
1361
1362 fn paginator_type_change_to_arabic(&self) {
1363 let _ = self;
1367 }
1368
1369 fn populated_view(&self) -> String {
1370 let items = self.visible_items();
1371
1372 if items.is_empty() {
1374 if self.filter_state == FilterState::Filtering {
1375 return String::new();
1376 }
1377 return self
1378 .styles
1379 .no_items
1380 .clone()
1381 .render(&format!("No {}.", self.item_name_plural));
1382 }
1383
1384 let mut b = String::new();
1385
1386 if !items.is_empty() {
1387 let (start, end) = self.paginator.get_slice_bounds(items.len());
1388 let docs = &items[start..end];
1389
1390 for (i, item) in docs.iter().enumerate() {
1391 b += &self.delegate.render(self, i + start, *item);
1392 if i != docs.len() - 1 {
1393 b += &"\n".repeat(self.delegate.spacing() + 1);
1394 }
1395 }
1396 }
1397
1398 let items_on_page = self.paginator.items_on_page(items.len());
1402 if items_on_page < self.paginator.per_page {
1403 let n = (self.paginator.per_page - items_on_page)
1404 * (self.delegate.height() + self.delegate.spacing());
1405 b += &"\n".repeat(n);
1406 }
1407
1408 b
1409 }
1410
1411 fn help_view(&self) -> String {
1412 self.styles.help_style.clone().render(&self.help.view(self))
1413 }
1414
1415 fn spinner_view(&self) -> String {
1416 self.spinner.view()
1417 }
1418}
1419
1420fn filter_items(m: &Model) -> Vec<FilteredItem> {
1421 if m.filter_input.value().is_empty() || m.filter_state == FilterState::Unfiltered {
1422 return m
1423 .items
1424 .iter()
1425 .enumerate()
1426 .map(|(i, item)| FilteredItem {
1427 index: i,
1428 item: item.box_clone(),
1429 matches: vec![],
1430 })
1431 .collect();
1432 }
1433
1434 let items = &m.items;
1435 let targets: Vec<String> = items.iter().map(|t| t.filter_value()).collect();
1436
1437 let mut filter_matches: Vec<FilteredItem> = vec![];
1438 for r in (m.filter)(&m.filter_input.value(), &targets) {
1439 filter_matches.push(FilteredItem {
1440 index: r.index,
1441 item: items[r.index].box_clone(),
1442 matches: r.matched_indexes,
1443 });
1444 }
1445
1446 filter_matches
1447}
1448
1449fn filter_items_cmd(m: &Model) -> Cmd {
1450 let items = filter_items(m);
1451 Some(Box::new(move || Some(Box::new(FilterMatchesMsg(items)))))
1452}
1453
1454fn insert_item_into_slice(
1455 items: &[Box<dyn Item + Send + Sync>],
1456 item: Box<dyn Item + Send + Sync>,
1457 index: usize,
1458) -> Vec<Box<dyn Item + Send + Sync>> {
1459 if items.is_empty() {
1460 return vec![item];
1461 }
1462 if index >= items.len() {
1463 let mut items: Vec<Box<dyn Item + Send + Sync>> =
1464 items.iter().map(|i| i.box_clone()).collect();
1465 items.push(item);
1466 return items;
1467 }
1468
1469 let mut items: Vec<Box<dyn Item + Send + Sync>> = items.iter().map(|i| i.box_clone()).collect();
1470 items.insert(index, item);
1471 items
1472}
1473
1474fn remove_item_from_slice(
1477 items: &[Box<dyn Item + Send + Sync>],
1478 index: usize,
1479) -> Vec<Box<dyn Item + Send + Sync>> {
1480 if index >= items.len() {
1481 return items.iter().map(|i| i.box_clone()).collect(); }
1483 let mut items: Vec<Box<dyn Item + Send + Sync>> = items.iter().map(|i| i.box_clone()).collect();
1484 items.remove(index);
1485 items
1486}
1487
1488fn remove_filter_match_from_slice(items: &[FilteredItem], index: usize) -> Vec<FilteredItem> {
1489 if index >= items.len() {
1490 return items.iter().map(|f| f.clone_item()).collect(); }
1492 let mut items: Vec<FilteredItem> = items.iter().map(|f| f.clone_item()).collect();
1493 items.remove(index);
1494 items
1495}
1496
1497fn count_enabled_bindings(groups: &[Vec<Binding>]) -> usize {
1498 let mut agg = 0;
1499 for group in groups {
1500 for kb in group {
1501 if kb.enabled() {
1502 agg += 1;
1503 }
1504 }
1505 }
1506 agg
1507}
1508
1509#[derive(Debug, Clone)]
1512pub struct KeyMap {
1513 pub cursor_up: Binding,
1516 pub cursor_down: Binding,
1518 pub next_page: Binding,
1520 pub prev_page: Binding,
1522 pub go_to_start: Binding,
1524 pub go_to_end: Binding,
1526 pub filter: Binding,
1528 pub clear_filter: Binding,
1530
1531 pub cancel_while_filtering: Binding,
1534 pub accept_while_filtering: Binding,
1536
1537 pub show_full_help: Binding,
1540 pub close_full_help: Binding,
1542
1543 pub quit: Binding,
1545
1546 pub force_quit: Binding,
1549}
1550
1551pub fn default_key_map() -> KeyMap {
1553 KeyMap {
1554 cursor_up: key::new_binding(vec![
1556 key::with_keys(&["up", "k"]),
1557 key::with_help("↑/k", "up"),
1558 ]),
1559 cursor_down: key::new_binding(vec![
1560 key::with_keys(&["down", "j"]),
1561 key::with_help("↓/j", "down"),
1562 ]),
1563 prev_page: key::new_binding(vec![
1564 key::with_keys(&["left", "h", "pgup", "b", "u"]),
1565 key::with_help("←/h/pgup", "prev page"),
1566 ]),
1567 next_page: key::new_binding(vec![
1568 key::with_keys(&["right", "l", "pgdown", "f", "d"]),
1569 key::with_help("→/l/pgdn", "next page"),
1570 ]),
1571 go_to_start: key::new_binding(vec![
1572 key::with_keys(&["home", "g"]),
1573 key::with_help("g/home", "go to start"),
1574 ]),
1575 go_to_end: key::new_binding(vec![
1576 key::with_keys(&["end", "G"]),
1577 key::with_help("G/end", "go to end"),
1578 ]),
1579 filter: key::new_binding(vec![key::with_keys(&["/"]), key::with_help("/", "filter")]),
1580 clear_filter: key::new_binding(vec![
1581 key::with_keys(&["esc"]),
1582 key::with_help("esc", "clear filter"),
1583 ]),
1584
1585 cancel_while_filtering: key::new_binding(vec![
1587 key::with_keys(&["esc"]),
1588 key::with_help("esc", "cancel"),
1589 ]),
1590 accept_while_filtering: key::new_binding(vec![
1591 key::with_keys(&[
1592 "enter",
1593 "tab",
1594 "shift+tab",
1595 "ctrl+k",
1596 "up",
1597 "ctrl+j",
1598 "down",
1599 ]),
1600 key::with_help("enter", "apply filter"),
1601 ]),
1602
1603 show_full_help: key::new_binding(vec![key::with_keys(&["?"]), key::with_help("?", "more")]),
1605 close_full_help: key::new_binding(vec![
1606 key::with_keys(&["?"]),
1607 key::with_help("?", "close help"),
1608 ]),
1609
1610 quit: key::new_binding(vec![
1612 key::with_keys(&["q", "esc"]),
1613 key::with_help("q", "quit"),
1614 ]),
1615 force_quit: key::new_binding(vec![key::with_keys(&["ctrl+c"])]),
1616 }
1617}
1618
1619#[derive(Debug, Clone)]
1622pub struct DefaultItemStyles {
1623 pub normal_title: Style,
1625 pub normal_desc: Style,
1627
1628 pub selected_title: Style,
1630 pub selected_desc: Style,
1632
1633 pub dimmed_title: Style,
1635 pub dimmed_desc: Style,
1637
1638 pub filter_match: Style,
1640}
1641
1642pub fn new_default_item_styles(is_dark: bool) -> DefaultItemStyles {
1645 let light_dark = rusty_lipgloss::color::light_dark(is_dark);
1646
1647 let mut s = DefaultItemStyles {
1648 normal_title: rusty_lipgloss::new_style()
1649 .foreground_color(light_dark(Color::parse("#1a1a1a"), Color::parse("#dddddd")))
1650 .padding(&[0, 0, 0, 2]),
1651 normal_desc: rusty_lipgloss::new_style(),
1652 selected_title: rusty_lipgloss::new_style()
1653 .border(
1654 rusty_lipgloss::Border::normal(),
1655 &[false, false, false, true],
1656 )
1657 .border_foreground(&[
1658 &light_dark(Color::parse("#F793FF"), Color::parse("#AD58B4")).to_string(),
1659 ])
1660 .foreground_color(light_dark(Color::parse("#EE6FF8"), Color::parse("#EE6FF8")))
1661 .padding(&[0, 0, 0, 1]),
1662 selected_desc: rusty_lipgloss::new_style(),
1663 dimmed_title: rusty_lipgloss::new_style()
1664 .foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")))
1665 .padding(&[0, 0, 0, 2]),
1666 dimmed_desc: rusty_lipgloss::new_style(),
1667 filter_match: rusty_lipgloss::new_style().underline(true),
1668 };
1669 s.normal_desc = s
1670 .normal_title
1671 .clone()
1672 .foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")));
1673 s.selected_desc = s
1674 .selected_title
1675 .clone()
1676 .foreground_color(light_dark(Color::parse("#F793FF"), Color::parse("#AD58B4")));
1677 s.dimmed_desc = s
1678 .dimmed_title
1679 .clone()
1680 .foreground_color(light_dark(Color::parse("#C2B8C2"), Color::parse("#4D4D4D")));
1681 s
1682}
1683
1684pub trait DefaultItem: Item {
1686 fn title(&self) -> String;
1688 fn description(&self) -> String;
1690
1691 fn as_default_item(&self) -> Option<&dyn DefaultItem>
1693 where
1694 Self: Sized,
1695 {
1696 Some(self)
1697 }
1698}
1699
1700pub struct DefaultDelegate {
1707 pub show_description: bool,
1709 pub styles: DefaultItemStyles,
1711 pub update_func: Option<ItemUpdateFunc>,
1713 pub short_help_func: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
1715 pub full_help_func: Option<Box<dyn Fn() -> Vec<Vec<Binding>> + Send + Sync>>,
1717 height: usize,
1718 spacing: usize,
1719}
1720
1721pub fn new_default_delegate() -> DefaultDelegate {
1723 const DEFAULT_HEIGHT: usize = 2;
1724 const DEFAULT_SPACING: usize = 1;
1725 DefaultDelegate {
1726 show_description: true,
1727 styles: new_default_item_styles(true),
1730 height: DEFAULT_HEIGHT,
1731 spacing: DEFAULT_SPACING,
1732 update_func: None,
1733 short_help_func: None,
1734 full_help_func: None,
1735 }
1736}
1737
1738impl DefaultDelegate {
1739 pub fn set_height(&mut self, i: usize) {
1741 self.height = i;
1742 }
1743
1744 pub fn height(&self) -> usize {
1748 if self.show_description {
1749 self.height
1750 } else {
1751 1
1752 }
1753 }
1754
1755 pub fn set_spacing(&mut self, i: usize) {
1757 self.spacing = i;
1758 }
1759
1760 pub fn spacing(&self) -> usize {
1762 self.spacing
1763 }
1764
1765 pub fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String {
1767 let mut title;
1768 let mut desc;
1769
1770 let di = item.as_default_item();
1771 if let Some(i) = di {
1772 title = i.title();
1773 desc = i.description();
1774 } else {
1775 return String::new();
1776 }
1777
1778 if m.width == 0 {
1779 return String::new();
1781 }
1782
1783 let s = &self.styles;
1784
1785 let textwidth =
1787 m.width - s.normal_title.get_padding_left() - s.normal_title.get_padding_right();
1788 title = rusty_x_ansi::truncate(&title, textwidth, ELLIPSIS);
1789 if self.show_description {
1790 let mut lines: Vec<String> = vec![];
1791 for (i, line) in desc.split('\n').enumerate() {
1792 if i >= self.height - 1 {
1793 break;
1794 }
1795 lines.push(rusty_x_ansi::truncate(line, textwidth, ELLIPSIS));
1796 }
1797 desc = lines.join("\n");
1798 }
1799
1800 let is_selected = index == m.index();
1802 let empty_filter =
1803 m.filter_state() == FilterState::Filtering && m.filter_value().is_empty();
1804 let is_filtered = m.filter_state() == FilterState::Filtering
1805 || m.filter_state() == FilterState::FilterApplied;
1806
1807 let mut matched_rumes: Vec<usize> = vec![];
1808 if is_filtered && index < m.filtered_items.len() {
1809 matched_rumes = m.matches_for_item(index);
1811 }
1812
1813 if empty_filter {
1814 title = s.dimmed_title.clone().render(&title);
1815 desc = s.dimmed_desc.clone().render(&desc);
1816 } else if is_selected && m.filter_state() != FilterState::Filtering {
1817 if is_filtered {
1818 let unmatched = s.selected_title.clone().inline(true);
1820 let matched = unmatched.clone().inherit(&s.filter_match);
1821 title = rusty_lipgloss::runes::style_runes(
1822 &title,
1823 &matched_rumes,
1824 &matched,
1825 &unmatched,
1826 );
1827 }
1828 title = s.selected_title.clone().render(&title);
1829 desc = s.selected_desc.clone().render(&desc);
1830 } else {
1831 if is_filtered {
1832 let unmatched = s.normal_title.clone().inline(true);
1834 let matched = unmatched.clone().inherit(&s.filter_match);
1835 title = rusty_lipgloss::runes::style_runes(
1836 &title,
1837 &matched_rumes,
1838 &matched,
1839 &unmatched,
1840 );
1841 }
1842 title = s.normal_title.clone().render(&title);
1843 desc = s.normal_desc.clone().render(&desc);
1844 }
1845
1846 if self.show_description {
1847 return format!("{}\n{}", title, desc);
1848 }
1849 title
1850 }
1851}
1852
1853impl ItemDelegate for DefaultDelegate {
1854 fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String {
1855 DefaultDelegate::render(self, m, index, item)
1856 }
1857
1858 fn height(&self) -> usize {
1859 DefaultDelegate::height(self)
1860 }
1861
1862 fn spacing(&self) -> usize {
1863 DefaultDelegate::spacing(self)
1864 }
1865
1866 fn update(&self, msg: &dyn Msg, m: &Model) -> Cmd {
1867 if let Some(f) = &self.update_func {
1868 return f(msg, m);
1869 }
1870 None
1871 }
1872
1873 fn short_help(&self) -> Vec<Binding> {
1874 if let Some(f) = &self.short_help_func {
1875 return f();
1876 }
1877 vec![]
1878 }
1879
1880 fn full_help(&self) -> Vec<Vec<Binding>> {
1881 if let Some(f) = &self.full_help_func {
1882 return f();
1883 }
1884 vec![]
1885 }
1886}
1887
1888#[derive(Debug, Clone)]
1891pub struct Styles {
1892 pub title_bar: Style,
1894 pub title: Style,
1896 pub spinner: Style,
1898 pub filter: textinput::Styles,
1900
1901 pub default_filter_character_match: Style,
1904
1905 pub status_bar: Style,
1907 pub status_empty: Style,
1909 pub status_bar_active_filter: Style,
1911 pub status_bar_filter_count: Style,
1913
1914 pub no_items: Style,
1916
1917 pub pagination_style: Style,
1919 pub help_style: Style,
1921
1922 pub active_pagination_dot: Style,
1925 pub inactive_pagination_dot: Style,
1927 pub arabic_pagination: Style,
1929 pub divider_dot: Style,
1931}
1932
1933pub fn default_styles(is_dark: bool) -> Styles {
1936 let light_dark = rusty_lipgloss::color::light_dark(is_dark);
1937
1938 let very_subdued_color = light_dark(Color::parse("#DDDADA"), Color::parse("#3C3C3C"));
1939 let subdued_color = light_dark(Color::parse("#9B9B9B"), Color::parse("#5C5C5C"));
1940
1941 let title_bar = rusty_lipgloss::new_style().padding(&[0, 0, 1, 2]);
1942
1943 let title = rusty_lipgloss::new_style()
1944 .background_color(Color::parse("62"))
1945 .foreground_color(Color::parse("230"))
1946 .padding(&[0, 1]);
1947
1948 let spinner_style = rusty_lipgloss::new_style()
1949 .foreground_color(light_dark(Color::parse("#8E8E8E"), Color::parse("#747373")));
1950
1951 let prompt = rusty_lipgloss::new_style()
1952 .foreground_color(light_dark(Color::parse("#04B575"), Color::parse("#ECFD65")));
1953 let mut filter = textinput::default_styles(is_dark);
1954 filter.cursor.color = light_dark(Color::parse("#EE6FF8"), Color::parse("#EE6FF8"));
1955 filter.blurred.prompt = prompt.clone();
1956 filter.focused.prompt = prompt;
1957
1958 let default_filter_character_match = rusty_lipgloss::new_style().underline(true);
1959
1960 let status_bar = rusty_lipgloss::new_style()
1961 .foreground_color(light_dark(Color::parse("#A49FA5"), Color::parse("#777777")))
1962 .padding(&[0, 0, 1, 2]);
1963
1964 let status_empty = rusty_lipgloss::new_style().foreground_color(subdued_color.clone());
1965
1966 let status_bar_active_filter = rusty_lipgloss::new_style()
1967 .foreground_color(light_dark(Color::parse("#1a1a1a"), Color::parse("#dddddd")));
1968
1969 let status_bar_filter_count =
1970 rusty_lipgloss::new_style().foreground_color(very_subdued_color.clone());
1971
1972 let no_items = rusty_lipgloss::new_style()
1973 .foreground_color(light_dark(Color::parse("#909090"), Color::parse("#626262")));
1974
1975 let arabic_pagination = rusty_lipgloss::new_style().foreground_color(subdued_color);
1976
1977 let pagination_style = rusty_lipgloss::new_style().padding_left(2);
1978
1979 let help_style = rusty_lipgloss::new_style().padding(&[1, 0, 0, 2]);
1980
1981 let active_pagination_dot = rusty_lipgloss::new_style()
1982 .foreground_color(light_dark(Color::parse("#847A85"), Color::parse("#979797")))
1983 .set_string(&[BULLET]);
1984
1985 let inactive_pagination_dot = rusty_lipgloss::new_style()
1986 .foreground_color(very_subdued_color.clone())
1987 .set_string(&[BULLET]);
1988
1989 let divider_dot = rusty_lipgloss::new_style()
1990 .foreground_color(very_subdued_color)
1991 .set_string(&[" • "]);
1992
1993 Styles {
1994 title_bar,
1995 title,
1996 spinner: spinner_style,
1997 filter,
1998 default_filter_character_match,
1999 status_bar,
2000 status_empty,
2001 status_bar_active_filter,
2002 status_bar_filter_count,
2003 no_items,
2004 pagination_style,
2005 help_style,
2006 active_pagination_dot,
2007 inactive_pagination_dot,
2008 arabic_pagination,
2009 divider_dot,
2010 }
2011}