Skip to main content

rusty_bubbles/
list.rs

1//! Cleanroom Rust port of upstream Go source file: `list/list.go`
2//! Cleanroom Rust port of upstream Go source file: `list/defaultitem.go`
3//! Cleanroom Rust port of upstream Go source file: `list/keys.go`
4//! Cleanroom Rust port of upstream Go source file: `list/style.go`
5//! Upstream Target Tag / Version: `v2.1.0`
6//!
7//! <public-docs>
8//! # List
9//!
10//! A feature-rich Bubble Tea component for browsing a general purpose list
11//! of items. It features optional filtering, pagination, help, status
12//! messages, and a spinner to indicate activity.
13//!
14//! The fuzzy filter is an inline port of `github.com/sahilm/fuzzy`.
15//! </public-docs>
16
17use 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
30/// The update callback type for [DefaultDelegate] items (upstream
31/// `func(m Msg, l *Model) Cmd`).
32type 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
44/// Item is an item that appears in the list.
45///
46/// Note: `box_clone` and `as_any` are Rust-side requirements (trait objects
47/// cannot structurally clone or downcast), mirroring the upstream Go
48/// interface which is implemented by any struct.
49pub trait Item: Send + Sync + std::fmt::Debug {
50    /// FilterValue is the value we use when filtering against this item when
51    /// we're filtering the list.
52    fn filter_value(&self) -> String;
53
54    /// Clones this item into a boxed trait object (Rust-side adaptation).
55    fn box_clone(&self) -> Box<dyn Item + Send + Sync>;
56
57    /// Downcasts to `Any` (Rust-side adaptation for delegate type checks).
58    fn as_any(&self) -> &dyn std::any::Any;
59
60    /// Returns the item as a [`DefaultItem`] view, if it implements it.
61    fn as_default_item(&self) -> Option<&dyn DefaultItem> {
62        None
63    }
64}
65
66/// ItemDelegate encapsulates the general functionality for all list items.
67/// The benefit to separating this logic from the item itself is that you can
68/// change the functionality of items without changing the actual items
69/// themselves.
70///
71/// Note that if the delegate also implements help.KeyMap delegate-related
72/// help items will be added to the help view.
73pub trait ItemDelegate {
74    /// Render renders the item's view.
75    fn render(&self, m: &Model, index: usize, item: &dyn Item) -> String;
76
77    /// Height is the height of the list item.
78    fn height(&self) -> usize;
79
80    /// Spacing is the size of the horizontal gap between list items in
81    /// cells.
82    fn spacing(&self) -> usize;
83
84    /// Update is the update loop for items. All messages in the list's
85    /// update loop will pass through here except when the user is setting a
86    /// filter. Use this method to perform item-level updates appropriate to
87    /// this delegate.
88    ///
89    /// Note: `self` and `m` are passed by shared reference (Rust-side
90    /// adaptation; the upstream signature takes a mutable model pointer).
91    fn update(&self, msg: &dyn Msg, m: &Model) -> Cmd;
92
93    /// ShortHelp returns bindings for the short help view, if the delegate
94    /// implements the help keymap interface.
95    fn short_help(&self) -> Vec<Binding> {
96        vec![]
97    }
98
99    /// FullHelp returns bindings for the full help view, if the delegate
100    /// implements the help keymap interface.
101    fn full_help(&self) -> Vec<Vec<Binding>> {
102        vec![]
103    }
104}
105
106/// FilterMatchesMsg contains data about items matched during filtering. The
107/// message should be routed to Update for processing.
108#[derive(Debug)]
109pub struct FilterMatchesMsg(pub Vec<FilteredItem>);
110
111impl FilteredItem {
112    /// Clones this filtered item (Rust-side adaptation).
113    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    /// Clones the filtered items (Rust-side adaptation).
124    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/// FilteredItem holds an item matched by the filter, its index in the
137/// unfiltered list, and the rune indices of matched characters.
138#[derive(Debug)]
139pub struct FilteredItem {
140    /// index in the unfiltered list
141    pub index: usize,
142    /// item matched
143    pub item: Box<dyn Item + Send + Sync>,
144    /// rune indices of matched items
145    pub matches: Vec<usize>,
146}
147
148/// Rank defines a rank for a given item.
149#[derive(Debug, Clone)]
150pub struct Rank {
151    /// The index of the item in the original input.
152    pub index: usize,
153    /// Indices of the actual word that were matched against the filter term.
154    pub matched_indexes: Vec<usize>,
155}
156
157/// FilterFunc takes a term and a list of strings to search through
158/// (defined by `Item::filter_value`). It should return a sorted list of
159/// ranks.
160pub type FilterFunc = Box<dyn Fn(&str, &[String]) -> Vec<Rank> + Send + Sync>;
161
162/// DefaultFilter uses the fuzzy matcher to filter through the list. This is
163/// set by default.
164pub 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
175/// UnsortedFilter uses the fuzzy matcher to filter through the list. It does
176/// not sort the results.
177pub 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/// FilterState describes the current filtering state on the model.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193pub enum FilterState {
194    /// no filter set
195    #[default]
196    Unfiltered,
197    /// user is actively setting a filter
198    Filtering,
199    /// a filter is applied and user is not editing filter
200    FilterApplied,
201}
202
203impl FilterState {
204    /// String returns a human-readable string of the current filter state.
205    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
214/// Model contains the state of this component.
215pub 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    /// The title of the list.
227    pub title: String,
228    /// The styles for the list.
229    pub styles: Styles,
230    /// Whether scrolling is infinite.
231    pub infinite_scrolling: bool,
232
233    /// Key mappings for navigating the list.
234    pub key_map: KeyMap,
235
236    /// Filter is used to filter the list.
237    pub filter: FilterFunc,
238
239    pub disable_quit_keybindings: bool,
240
241    /// Additional key mappings for the short and full help views.
242    pub additional_short_help_keys: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
243    /// Additional key mappings for the short and full help views.
244    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    /// The paginator of the list.
251    pub paginator: paginator::Model,
252    cursor: usize,
253    /// The help view of the list.
254    pub help: help::Model,
255    /// The filter input of the list.
256    pub filter_input: textinput::Model,
257    pub filter_state: FilterState,
258
259    /// How long status messages should stay visible. By default this is
260    /// 1 second.
261    pub status_message_lifetime: Duration,
262
263    status_message: String,
264    status_message_timer: Option<std::thread::JoinHandle<()>>,
265
266    /// The master set of items we're working with.
267    pub items: Vec<Box<dyn Item + Send + Sync>>,
268
269    /// Filtered items we're currently displaying. Filtering, toggles and so
270    /// on will alter this slice so we can show what is relevant.
271    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
297/// New returns a new model with sensible defaults.
298pub 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    /// SetFilteringEnabled enables or disables filtering. Note that this is
369    /// different from ShowFilter, which merely hides or shows the input view.
370    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    /// FilteringEnabled returns whether or not filtering is enabled.
379    pub fn filtering_enabled(&self) -> bool {
380        self.filtering_enabled
381    }
382
383    /// SetShowTitle shows or hides the title bar.
384    pub fn set_show_title(&mut self, v: bool) {
385        self.show_title = v;
386        self.update_pagination();
387    }
388
389    /// SetFilterText explicitly sets the filter text without relying on user
390    /// input. It also sets the filterState to a sane default of
391    /// FilterApplied.
392    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    /// SetFilterState allows setting the filtering state manually.
405    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    /// ShowTitle returns whether or not the title bar is set to be rendered.
414    pub fn show_title(&self) -> bool {
415        self.show_title
416    }
417
418    /// SetShowFilter shows or hides the filter bar. Note that this does not
419    /// disable filtering, it simply hides the built-in filter view.
420    pub fn set_show_filter(&mut self, v: bool) {
421        self.show_filter = v;
422        self.update_pagination();
423    }
424
425    /// ShowFilter returns whether or not the filter is set to be rendered.
426    pub fn show_filter(&self) -> bool {
427        self.show_filter
428    }
429
430    /// SetShowStatusBar shows or hides the view that displays metadata about
431    /// the list, such as item counts.
432    pub fn set_show_status_bar(&mut self, v: bool) {
433        self.show_status_bar = v;
434        self.update_pagination();
435    }
436
437    /// ShowStatusBar returns whether or not the status bar is set to be
438    /// rendered.
439    pub fn show_status_bar(&self) -> bool {
440        self.show_status_bar
441    }
442
443    /// SetStatusBarItemName defines a replacement for the item's identifier.
444    /// Defaults to item/items.
445    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    /// StatusBarItemName returns singular and plural status bar item names.
451    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    /// SetShowPagination hides or shows the paginator. Note that pagination
459    /// will still be active, it simply won't be displayed.
460    pub fn set_show_pagination(&mut self, v: bool) {
461        self.show_pagination = v;
462        self.update_pagination();
463    }
464
465    /// ShowPagination returns whether the pagination is visible.
466    pub fn show_pagination(&mut self) -> bool {
467        self.show_pagination
468    }
469
470    /// SetShowHelp shows or hides the help view.
471    pub fn set_show_help(&mut self, v: bool) {
472        self.show_help = v;
473        self.update_pagination();
474    }
475
476    /// ShowHelp returns whether or not the help is set to be rendered.
477    pub fn show_help(&self) -> bool {
478        self.show_help
479    }
480
481    /// Items returns the items in the list.
482    pub fn items(&self) -> &[Box<dyn Item + Send + Sync>] {
483        &self.items
484    }
485
486    /// SetItems sets the items available in the list. This returns a
487    /// command.
488    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    /// Select selects the given index of the list and goes to its respective
503    /// page.
504    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    /// ResetSelected resets the selected item to the first item in the first
510    /// page of the list.
511    pub fn reset_selected(&mut self) {
512        self.select(0);
513    }
514
515    /// ResetFilter resets the current filtering state.
516    pub fn reset_filter(&mut self) {
517        self.reset_filtering();
518    }
519
520    /// SetItem replaces an item at the given index. This returns a command.
521    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    /// InsertItem inserts an item at the given index. If the index is out of
534    /// the upper bound, the item will be appended. This returns a command.
535    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    /// RemoveItem removes an item at the given index. If the index is out of
549    /// bounds this will be a no-op. O(n) complexity, which probably won't
550    /// matter in the case of a TUI.
551    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    /// SetDelegate sets the item delegate.
563    pub fn set_delegate(&mut self, d: Box<dyn ItemDelegate + Send + Sync>) {
564        self.delegate = d;
565        self.update_pagination();
566    }
567
568    /// VisibleItems returns the total items available to be shown.
569    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    /// SelectedItem returns the current selected item in the list.
581    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    /// MatchesForItem returns rune positions matched by the current filter,
592    /// if any. Use this to style runes matched by the active filter.
593    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    /// Index returns the index of the currently selected item as it is
601    /// stored in the filtered list of items.
602    pub fn index(&self) -> usize {
603        self.paginator.page * self.paginator.per_page + self.cursor
604    }
605
606    /// GlobalIndex returns the index of the currently selected item as it is
607    /// stored in the unfiltered list of items. This value can be used with
608    /// SetItem.
609    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    /// Cursor returns the index of the cursor on the current page.
620    pub fn cursor(&self) -> usize {
621        self.cursor
622    }
623
624    /// CursorUp moves the cursor up. This can also move the state to the
625    /// previous page.
626    pub fn cursor_up(&mut self) {
627        if self.cursor == 0 && self.paginator.on_first_page() {
628            // if infinite scrolling is enabled, go to the last item
629            if self.infinite_scrolling {
630                self.go_to_end();
631                return;
632            }
633            return;
634        }
635
636        // Move the cursor as normal
637        if self.cursor > 0 {
638            self.cursor -= 1;
639            return;
640        }
641
642        // Go to the previous page
643        self.paginator.prev_page();
644        self.cursor = self.max_cursor_index();
645    }
646
647    /// CursorDown moves the cursor down. This can also advance the state to
648    /// the next page.
649    pub fn cursor_down(&mut self) {
650        let max_cursor_index = self.max_cursor_index();
651
652        self.cursor += 1;
653
654        // We're still within bounds of the current page, so no need to do
655        // anything.
656        if self.cursor <= max_cursor_index {
657            return;
658        }
659
660        // Go to the next page
661        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 infinite scrolling is enabled, go to the first item.
670        if self.infinite_scrolling {
671            self.go_to_start();
672        }
673    }
674
675    /// GoToStart moves to the first page, and first item on the first page.
676    pub fn go_to_start(&mut self) {
677        self.paginator.page = 0;
678        self.cursor = 0;
679    }
680
681    /// GoToEnd moves to the last page, and last item on the last page.
682    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    /// PrevPage moves to the previous page, if available.
688    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    /// NextPage moves to the next page, if available.
694    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    /// FilterState returns the current filter state.
706    pub fn filter_state(&self) -> FilterState {
707        self.filter_state
708    }
709
710    /// FilterValue returns the current value of the filter.
711    pub fn filter_value(&self) -> String {
712        self.filter_input.value()
713    }
714
715    /// SettingFilter returns whether or not the user is currently editing
716    /// the filter value. It's purely a convenience method for the following:
717    ///
718    /// ```text
719    /// m.FilterState() == Filtering
720    /// ```
721    pub fn setting_filter(&self) -> bool {
722        self.filter_state == FilterState::Filtering
723    }
724
725    /// IsFiltered returns whether or not the list is currently filtered.
726    pub fn is_filtered(&self) -> bool {
727        self.filter_state == FilterState::FilterApplied
728    }
729
730    /// Width returns the current width setting.
731    pub fn width(&self) -> usize {
732        self.width
733    }
734
735    /// Height returns the current height setting.
736    pub fn height(&self) -> usize {
737        self.height
738    }
739
740    /// SetSpinner allows to set the spinner style.
741    pub fn set_spinner(&mut self, spinner: spinner::Spinner) {
742        self.spinner.spinner = spinner;
743    }
744
745    /// ToggleSpinner toggles the spinner. Note that this also returns a
746    /// command.
747    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    /// StartSpinner starts the spinner. Note that this returns a command.
756    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    /// StopSpinner stops the spinner.
763    pub fn stop_spinner(&mut self) {
764        self.show_spinner = false;
765    }
766
767    /// DisableQuitKeybindings is a helper for disabling the keybindings used
768    /// for quitting, in case you want to handle this elsewhere in your
769    /// application.
770    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    /// NewStatusMessage sets a new status message, which will show for a
777    /// limited amount of time. Note that this also returns a command.
778    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    /// SetWidth sets the width of this component.
791    pub fn set_width(&mut self, v: usize) {
792        self.set_size(v, self.height);
793    }
794
795    /// SetHeight sets the height of this component.
796    pub fn set_height(&mut self, v: usize) {
797        self.set_size(self.width, v);
798    }
799
800    /// SetSize sets the width and height of this component.
801    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    /// Set keybindings according to the filter state.
842    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    /// Update pagination according to the amount of items for the current
898    /// state.
899    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        // Restore index
933        self.paginator.page = index / self.paginator.per_page.max(1);
934        self.cursor = index % self.paginator.per_page.max(1);
935
936        // Make sure the page stays in bounds
937        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    /// Update is the Bubble Tea update loop.
951    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    /// Updates for when a user is browsing the list.
992    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            // Note: we match clear filter before quit because, by default,
996            // they're both mapped to escape.
997            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                    // Populate filter with all items only if the filter is
1017                    // empty.
1018                    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    /// Updates for when a user is in the filter editing interface.
1041    fn handle_filtering(&mut self, msg: &dyn Msg) -> Cmd {
1042        let mut cmds: Vec<Cmd> = Vec::new();
1043
1044        // Handle keys
1045        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                    // fallthrough
1062                } else {
1063                    let h = self.visible_items();
1064
1065                    // If we've filtered down to nothing, clear the filter
1066                    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        // Update the filter text input component
1082        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 the filtering input has changed, request updated filtering
1088        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        // Update pagination
1096        self.update_pagination();
1097
1098        commands::batch(cmds)
1099    }
1100
1101    /// ShortHelp returns bindings to show in the abbreviated help view. It's
1102    /// part of the help.KeyMap interface.
1103    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 the delegate implements the help.KeyMap interface add the short
1112        // help items to the short help after the cursor movement keys.
1113        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    /// FullHelp returns bindings to show the full help view. It's part of
1136    /// the help.KeyMap interface.
1137    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 the delegate implements the help.KeyMap interface add full help
1150        // keybindings to a special section of the full help.
1151        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    /// View renders the component.
1180    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        // We need to account for the size of the spinner, even if we don't
1229        // render it, to reserve some space for it should we turn it on later.
1230        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 the filter's showing, draw that. Otherwise draw the title.
1240        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                // titleBarStyle.PaddingLeft(gap - spinnerWidth - width(spinnerLeftGap))
1249            }
1250
1251            view += &self.styles.title.clone().render(&self.title);
1252
1253            // Status message
1254            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        // Spinner
1266        if self.show_spinner && !spinner_on_left {
1267            // Place spinner on the right
1268            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            // Filter results
1298            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            // Not filtering: no items.
1305            status = self
1306                .styles
1307                .status_empty
1308                .clone()
1309                .render(&format!("No {}", self.item_name_plural));
1310        } else {
1311            // Normal
1312            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 the dot pagination is wider than the width of the window use
1344        // the arabic paginator.
1345        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        // Mirrors upstream mutating the paginator's Type in-place. The
1364        // paginator is owned by the model; this helper is only invoked from
1365        // &self contexts via interior mutation below.
1366        let _ = self;
1367    }
1368
1369    fn populated_view(&self) -> String {
1370        let items = self.visible_items();
1371
1372        // Empty states
1373        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        // If there aren't enough items to fill up this page (always the
1399        // last page) then we need to add some newlines to fill up the space
1400        // where items would have been.
1401        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
1474/// Remove an item from a slice of items at the given index. This runs in
1475/// O(n).
1476fn 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(); // noop
1482    }
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(); // noop
1491    }
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/// KeyMap defines keybindings. It satisfies to the help.KeyMap interface,
1510/// which is used to render the menu.
1511#[derive(Debug, Clone)]
1512pub struct KeyMap {
1513    /// Keybindings used when browsing the list.
1514    /// CursorUp binding.
1515    pub cursor_up: Binding,
1516    /// CursorDown binding.
1517    pub cursor_down: Binding,
1518    /// NextPage binding.
1519    pub next_page: Binding,
1520    /// PrevPage binding.
1521    pub prev_page: Binding,
1522    /// GoToStart binding.
1523    pub go_to_start: Binding,
1524    /// GoToEnd binding.
1525    pub go_to_end: Binding,
1526    /// Filter binding.
1527    pub filter: Binding,
1528    /// ClearFilter binding.
1529    pub clear_filter: Binding,
1530
1531    /// Keybindings used when setting a filter.
1532    /// CancelWhileFiltering binding.
1533    pub cancel_while_filtering: Binding,
1534    /// AcceptWhileFiltering binding.
1535    pub accept_while_filtering: Binding,
1536
1537    /// Help toggle keybindings.
1538    /// ShowFullHelp binding.
1539    pub show_full_help: Binding,
1540    /// CloseFullHelp binding.
1541    pub close_full_help: Binding,
1542
1543    /// The quit keybinding. This won't be caught when filtering.
1544    pub quit: Binding,
1545
1546    /// The quit-no-matter-what keybinding. This will be caught when
1547    /// filtering.
1548    pub force_quit: Binding,
1549}
1550
1551/// DefaultKeyMap returns a default set of keybindings.
1552pub fn default_key_map() -> KeyMap {
1553    KeyMap {
1554        // Browsing.
1555        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        // Filtering.
1586        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        // Toggle help.
1604        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        // Quitting.
1611        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/// DefaultItemStyles defines styling for a default list item.
1620/// See `DefaultItemView` for when these come into play.
1621#[derive(Debug, Clone)]
1622pub struct DefaultItemStyles {
1623    /// The Normal state.
1624    pub normal_title: Style,
1625    /// The Normal state.
1626    pub normal_desc: Style,
1627
1628    /// The selected item state.
1629    pub selected_title: Style,
1630    /// The selected item state.
1631    pub selected_desc: Style,
1632
1633    /// The dimmed state, for when the filter input is initially activated.
1634    pub dimmed_title: Style,
1635    /// The dimmed state, for when the filter input is initially activated.
1636    pub dimmed_desc: Style,
1637
1638    /// Characters matching the current filter, if any.
1639    pub filter_match: Style,
1640}
1641
1642/// NewDefaultItemStyles returns style definitions for a default item. See
1643/// `DefaultItemView` for when these come into play.
1644pub 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
1684/// DefaultItem describes an item designed to work with DefaultDelegate.
1685pub trait DefaultItem: Item {
1686    /// Title returns the item's title.
1687    fn title(&self) -> String;
1688    /// Description returns the item's description.
1689    fn description(&self) -> String;
1690
1691    /// Returns `Some(self)` so the default delegate can view the item.
1692    fn as_default_item(&self) -> Option<&dyn DefaultItem>
1693    where
1694        Self: Sized,
1695    {
1696        Some(self)
1697    }
1698}
1699
1700/// DefaultDelegate is a standard delegate designed to work in lists. It's
1701/// styled by [`DefaultItemStyles`], which can be customized as you like.
1702///
1703/// The description line can be hidden by setting `show_description` to
1704/// false, which renders the list as single-line-items. The spacing between
1705/// items can be set with the `set_spacing` method.
1706pub struct DefaultDelegate {
1707    /// Whether to show the description line.
1708    pub show_description: bool,
1709    /// The styles for the delegate.
1710    pub styles: DefaultItemStyles,
1711    /// An optional update function called on item updates.
1712    pub update_func: Option<ItemUpdateFunc>,
1713    /// An optional short help function.
1714    pub short_help_func: Option<Box<dyn Fn() -> Vec<Binding> + Send + Sync>>,
1715    /// An optional full help function.
1716    pub full_help_func: Option<Box<dyn Fn() -> Vec<Vec<Binding>> + Send + Sync>>,
1717    height: usize,
1718    spacing: usize,
1719}
1720
1721/// NewDefaultDelegate creates a new delegate with default styles.
1722pub fn new_default_delegate() -> DefaultDelegate {
1723    const DEFAULT_HEIGHT: usize = 2;
1724    const DEFAULT_SPACING: usize = 1;
1725    DefaultDelegate {
1726        show_description: true,
1727        // XXX: Let the user choose between light and dark colors. We've
1728        // temporarily hardcoded the dark colors here.
1729        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    /// SetHeight sets delegate's preferred height.
1740    pub fn set_height(&mut self, i: usize) {
1741        self.height = i;
1742    }
1743
1744    /// Height returns the delegate's preferred height.
1745    /// This has effect only if ShowDescription is true, otherwise height is
1746    /// always 1.
1747    pub fn height(&self) -> usize {
1748        if self.show_description {
1749            self.height
1750        } else {
1751            1
1752        }
1753    }
1754
1755    /// SetSpacing sets the delegate's spacing.
1756    pub fn set_spacing(&mut self, i: usize) {
1757        self.spacing = i;
1758    }
1759
1760    /// Spacing returns the delegate's spacing.
1761    pub fn spacing(&self) -> usize {
1762        self.spacing
1763    }
1764
1765    /// Render prints an item.
1766    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            // short-circuit
1780            return String::new();
1781        }
1782
1783        let s = &self.styles;
1784
1785        // Prevent text from exceeding list width
1786        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        // Conditions
1801        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            // Get indices of matched characters
1810            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                // Highlight matches
1819                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                // Highlight matches
1833                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/// Styles contains style definitions for this list component. By default,
1889/// these values are generated by [`default_styles`].
1890#[derive(Debug, Clone)]
1891pub struct Styles {
1892    /// Style for the title bar.
1893    pub title_bar: Style,
1894    /// Style for the title.
1895    pub title: Style,
1896    /// Style for the spinner.
1897    pub spinner: Style,
1898    /// Styles for the filter input.
1899    pub filter: textinput::Styles,
1900
1901    /// Default styling for matched characters in a filter. This can be
1902    /// overridden by delegates.
1903    pub default_filter_character_match: Style,
1904
1905    /// Style for the status bar.
1906    pub status_bar: Style,
1907    /// Style for the empty status.
1908    pub status_empty: Style,
1909    /// Style for the active filter status.
1910    pub status_bar_active_filter: Style,
1911    /// Style for the filter count status.
1912    pub status_bar_filter_count: Style,
1913
1914    /// Style for the "no items" view.
1915    pub no_items: Style,
1916
1917    /// Style for the pagination.
1918    pub pagination_style: Style,
1919    /// Style for the help.
1920    pub help_style: Style,
1921
1922    /// Styled characters.
1923    /// Style for the active pagination dot.
1924    pub active_pagination_dot: Style,
1925    /// Style for the inactive pagination dot.
1926    pub inactive_pagination_dot: Style,
1927    /// Style for the arabic pagination.
1928    pub arabic_pagination: Style,
1929    /// Style for the divider dot.
1930    pub divider_dot: Style,
1931}
1932
1933/// DefaultStyles returns a set of default style definitions for this list
1934/// component.
1935pub 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}