bubbletea_widgets/list/
model.rs

1//! Core Model struct and fundamental functionality.
2//!
3//! This module contains the primary Model struct definition and its core methods
4//! including construction, basic state management, and essential accessors.
5
6use super::keys::ListKeyMap;
7use super::style::ListStyles;
8use super::types::{FilterState, FilteredItem, Item, ItemDelegate};
9use crate::{help, paginator, spinner, textinput};
10
11/// A flexible, interactive list component with filtering, pagination, and customizable rendering.
12///
13/// The `Model<I>` is the main list component that can display any items implementing the `Item` trait.
14/// It provides fuzzy filtering, keyboard navigation, viewport scrolling, help integration, and
15/// customizable styling through delegates.
16///
17/// # Features
18///
19/// - **Fuzzy filtering**: Real-time search with character-level highlighting
20/// - **Smooth scrolling**: Viewport-based navigation that maintains context
21/// - **Customizable rendering**: Delegate pattern for complete visual control
22/// - **Keyboard navigation**: Vim-style keys plus standard arrow navigation
23/// - **Contextual help**: Automatic help text generation from key bindings
24/// - **Responsive design**: Adapts to different terminal sizes
25/// - **State management**: Clean separation of filtering, selection, and view states
26///
27/// # Architecture
28///
29/// The list uses a viewport-based scrolling system that maintains smooth navigation
30/// context instead of discrete page jumps. Items are rendered using delegates that
31/// control appearance and behavior, while filtering uses fuzzy matching with
32/// character-level highlighting for search results.
33///
34/// # Navigation
35///
36/// - **Up/Down**: Move cursor through items with smooth viewport scrolling
37/// - **Page Up/Down**: Jump by pages while maintaining cursor visibility
38/// - **Home/End**: Jump to first/last item
39/// - **/** : Start filtering
40/// - **Enter**: Accept filter (while filtering)
41/// - **Escape**: Cancel filter (while filtering)
42/// - **Ctrl+C**: Clear active filter
43///
44/// # Filtering
45///
46/// The list supports fuzzy filtering with real-time preview:
47/// - Type "/" to start filtering
48/// - Type characters to filter items in real-time
49/// - Matched characters are highlighted in the results
50/// - Press Enter to accept the filter or Escape to cancel
51///
52/// # Styling
53///
54/// Visual appearance is controlled through the `ListStyles` struct and item delegates.
55/// The list adapts to light/dark terminal themes automatically and supports
56/// customizable colors, borders, and typography.
57///
58/// # Examples
59///
60/// ```
61/// use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
62///
63/// let items = vec![
64///     DefaultItem::new("Task 1", "Complete documentation"),
65///     DefaultItem::new("Task 2", "Review pull requests"),
66/// ];
67/// let delegate = DefaultDelegate::new();
68/// let list = Model::new(items, delegate, 80, 24);
69/// ```
70///
71/// ## With Custom Items
72///
73/// ```
74/// use bubbletea_widgets::list::{Item, Model, DefaultDelegate};
75/// use std::fmt::Display;
76///
77/// #[derive(Clone)]
78/// struct CustomItem {
79///     title: String,
80///     priority: u8,
81/// }
82///
83/// impl Display for CustomItem {
84///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85///         write!(f, "[{}] {}", self.priority, self.title)
86///     }
87/// }
88///
89/// impl Item for CustomItem {
90///     fn filter_value(&self) -> String {
91///         format!("{} priority:{}", self.title, self.priority)
92///     }
93/// }
94///
95/// let items = vec![
96///     CustomItem { title: "Fix bug".to_string(), priority: 1 },
97///     CustomItem { title: "Add feature".to_string(), priority: 2 },
98/// ];
99/// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
100/// ```
101pub struct Model<I: Item> {
102    pub(super) title: String,
103    pub(super) items: Vec<I>,
104    pub(super) delegate: Box<dyn ItemDelegate<I> + Send + Sync>,
105
106    // Pagination
107    pub(super) paginator: paginator::Model,
108    pub(super) per_page: usize,
109
110    // UI State
111    pub(super) show_title: bool,
112    #[allow(dead_code)]
113    pub(super) spinner: spinner::Model,
114    pub(super) show_spinner: bool,
115    pub(super) width: usize,
116    pub(super) height: usize,
117    pub(super) styles: ListStyles,
118
119    // Status bar
120    pub(super) show_status_bar: bool,
121    #[allow(dead_code)]
122    pub(super) status_message_lifetime: usize,
123    pub(super) status_item_singular: Option<String>,
124    pub(super) status_item_plural: Option<String>,
125
126    // Pagination display
127    pub(super) show_pagination: bool,
128
129    // Help
130    pub(super) help: help::Model,
131    pub(super) show_help: bool,
132    pub(super) keymap: ListKeyMap,
133
134    // State
135    pub(super) filter_state: FilterState,
136    pub(super) filtered_items: Vec<FilteredItem<I>>,
137    pub(super) cursor: usize,
138    /// First visible item index for smooth scrolling.
139    ///
140    /// This field tracks the index of the first item visible in the current viewport.
141    /// It enables smooth, context-preserving scrolling behavior instead of discrete
142    /// page jumps. The viewport scrolls automatically when the cursor moves outside
143    /// the visible area, maintaining visual continuity.
144    pub(super) viewport_start: usize,
145
146    // Filter
147    pub(super) filter_input: textinput::Model,
148}
149
150impl<I: Item + Send + Sync + 'static> Model<I> {
151    /// Creates a new list with the provided items, delegate, and dimensions.
152    ///
153    /// This is the primary constructor for creating a list component. The delegate
154    /// controls how items are rendered and behave, while the dimensions determine
155    /// the initial size for layout calculations.
156    ///
157    /// # Arguments
158    ///
159    /// * `items` - Vector of items to display in the list
160    /// * `delegate` - Item delegate that controls rendering and behavior
161    /// * `width` - Initial width in terminal columns (can be updated later)
162    /// * `height` - Initial height in terminal rows (affects pagination)
163    ///
164    /// # Returns
165    ///
166    /// A new `Model<I>` configured with default settings:
167    /// - Title set to "List"
168    /// - 10 items per page
169    /// - Cursor at position 0
170    /// - All items initially visible (no filtering)
171    /// - Status bar enabled with default item names
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
177    ///
178    /// let items = vec![
179    ///     DefaultItem::new("First", "Description 1"),
180    ///     DefaultItem::new("Second", "Description 2"),
181    /// ];
182    ///
183    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
184    /// assert_eq!(list.len(), 2);
185    /// ```
186    pub fn new<D>(items: Vec<I>, delegate: D, width: usize, height: usize) -> Self
187    where
188        D: ItemDelegate<I> + Send + Sync + 'static,
189    {
190        let styles = ListStyles::default();
191        let mut paginator = paginator::Model::new();
192        let per_page = 10;
193        paginator.set_per_page(per_page);
194        paginator.set_total_items(items.len());
195
196        // Set dots mode by default (like Go version) and apply styled dots
197        paginator.paginator_type = paginator::Type::Dots;
198        paginator.active_dot = styles.active_pagination_dot.render("");
199        paginator.inactive_dot = styles.inactive_pagination_dot.render("");
200
201        let mut list = Self {
202            title: "List".to_string(),
203            items,
204            delegate: Box::new(delegate),
205            paginator,
206            per_page,
207            show_title: true,
208            spinner: spinner::new(&[]),
209            show_spinner: false,
210            width,
211            height,
212            styles,
213            show_status_bar: true,
214            status_message_lifetime: 1,
215            status_item_singular: None,
216            status_item_plural: None,
217            show_pagination: true,
218            help: help::Model::new(),
219            show_help: true,
220            keymap: ListKeyMap::default(),
221            filter_state: FilterState::Unfiltered,
222            filtered_items: vec![],
223            cursor: 0,
224            viewport_start: 0,
225            filter_input: textinput::new(),
226        };
227
228        // Calculate the actual pagination based on the provided height
229        list.update_pagination();
230        list
231    }
232
233    /// Sets the items displayed in the list.
234    ///
235    /// This method replaces all current items with the provided vector.
236    /// The cursor is reset to position 0, and pagination is recalculated
237    /// based on the new item count.
238    ///
239    /// # Arguments
240    ///
241    /// * `items` - Vector of new items to display
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
247    /// let mut list = Model::new(vec![], DefaultDelegate::new(), 80, 24);
248    ///
249    /// let items = vec![
250    ///     DefaultItem::new("Apple", "Red fruit"),
251    ///     DefaultItem::new("Banana", "Yellow fruit"),
252    /// ];
253    /// list.set_items(items);
254    /// assert_eq!(list.len(), 2);
255    /// ```
256    pub fn set_items(&mut self, items: Vec<I>) {
257        self.items = items;
258        self.cursor = 0;
259        self.update_pagination();
260    }
261
262    /// Returns a vector of currently visible items.
263    ///
264    /// The returned items reflect the current filtering state:
265    /// - When unfiltered: returns all items
266    /// - When filtered: returns only items matching the current filter
267    ///
268    /// # Returns
269    ///
270    /// A vector containing clones of all currently visible items.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
276    /// let items = vec![
277    ///     DefaultItem::new("First", "Description 1"),
278    ///     DefaultItem::new("Second", "Description 2"),
279    /// ];
280    ///
281    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
282    /// let visible = list.visible_items();
283    /// assert_eq!(visible.len(), 2);
284    /// ```
285    pub fn visible_items(&self) -> Vec<I> {
286        if self.filter_state == FilterState::Unfiltered {
287            self.items.clone()
288        } else {
289            self.filtered_items
290                .iter()
291                .map(|fi| fi.item.clone())
292                .collect()
293        }
294    }
295
296    /// Sets the filter text without applying the filter.
297    ///
298    /// This method updates the filter input text but does not trigger
299    /// the filtering process. It's primarily used for programmatic
300    /// filter setup or testing.
301    ///
302    /// # Arguments
303    ///
304    /// * `s` - The filter text to set
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
310    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
311    /// list.set_filter_text("search term");
312    /// // Filter text is set but not applied until filtering is activated
313    /// ```
314    pub fn set_filter_text(&mut self, s: &str) {
315        self.filter_input.set_value(s);
316    }
317
318    /// Sets the current filtering state.
319    ///
320    /// This method directly controls the list's filtering state without
321    /// triggering filter application. It's useful for programmatic state
322    /// management or testing specific filter conditions.
323    ///
324    /// # Arguments
325    ///
326    /// * `st` - The new filter state to set
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem, FilterState};
332    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
333    /// list.set_filter_state(FilterState::Filtering);
334    /// // List is now in filtering mode
335    /// ```
336    pub fn set_filter_state(&mut self, st: FilterState) {
337        self.filter_state = st;
338    }
339
340    /// Sets custom singular and plural names for status bar items.
341    ///
342    /// The status bar displays item counts using these names. If not set,
343    /// defaults to "item" and "items".
344    ///
345    /// # Arguments
346    ///
347    /// * `singular` - Name for single item (e.g., "task")
348    /// * `plural` - Name for multiple items (e.g., "tasks")
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
354    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
355    /// list.set_status_bar_item_name("task", "tasks");
356    /// // Status bar will now show "1 task" or "5 tasks"
357    /// ```
358    pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
359        self.status_item_singular = Some(singular.to_string());
360        self.status_item_plural = Some(plural.to_string());
361    }
362
363    /// Updates the list dimensions and recalculates layout.
364    ///
365    /// This method allows dynamic resizing of the list to match terminal
366    /// size changes, similar to the Go bubbles list's `SetSize` method.
367    /// It updates both width and height, then recalculates pagination
368    /// to show the appropriate number of items.
369    ///
370    /// # Arguments
371    ///
372    /// * `width` - New width in terminal columns
373    /// * `height` - New height in terminal rows
374    ///
375    /// # Examples
376    ///
377    /// ```
378    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
379    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
380    ///
381    /// // Resize list to match new terminal size
382    /// list.set_size(100, 30);
383    /// assert_eq!(list.width(), 100);
384    /// assert_eq!(list.height(), 30);
385    /// ```
386    pub fn set_size(&mut self, width: usize, height: usize) {
387        self.width = width;
388        self.height = height;
389        self.update_pagination(); // Recalculate items per page
390    }
391
392    /// Returns the current width of the list.
393    ///
394    /// # Returns
395    ///
396    /// The current width in terminal columns.
397    pub fn width(&self) -> usize {
398        self.width
399    }
400
401    /// Returns the current height of the list.
402    ///
403    /// # Returns
404    ///
405    /// The current height in terminal rows.
406    pub fn height(&self) -> usize {
407        self.height
408    }
409
410    /// Returns the current items per page setting.
411    ///
412    /// # Returns
413    ///
414    /// The number of items displayed per page.
415    pub fn per_page(&self) -> usize {
416        self.per_page
417    }
418
419    /// Returns the total number of pages in the paginator.
420    ///
421    /// # Returns
422    ///
423    /// The total number of pages based on item count and per_page setting.
424    pub fn total_pages(&self) -> usize {
425        self.paginator.total_pages
426    }
427
428    /// Calculates the actual rendered height of UI elements based on their known configurations.
429    ///
430    /// This method mimics Go's `lipgloss.Height()` function by using the known style
431    /// configurations to determine how many terminal lines each element will actually occupy
432    /// when rendered, matching the default padding values set in style.rs.
433    ///
434    /// # Arguments
435    ///
436    /// * `element` - A string identifier for the UI element type
437    ///
438    /// # Returns
439    ///
440    /// The total number of terminal lines this element will occupy.
441    pub fn calculate_element_height(&self, element: &str) -> usize {
442        match element {
443            "title" => {
444                // title: .padding(0, 1, 1, 2) = 1 content + 0 top + 1 bottom = 2 lines
445                2
446            }
447            "status_bar" => {
448                // status_bar: .padding(0, 0, 1, 2) = 1 content + 0 top + 1 bottom = 2 lines
449                2
450            }
451            "pagination" => {
452                // pagination_style: .padding_left(2) = 1 content + 0 top + 0 bottom = 1 line
453                1
454            }
455            "help" => {
456                // help_style: .padding(1, 0, 0, 2) = 1 content + 1 top + 0 bottom = 2 lines
457                2
458            }
459            _ => 1, // Default fallback
460        }
461    }
462
463    /// Updates pagination settings based on current item count and page size.
464    ///
465    /// This method recalculates pagination after changes to item count or
466    /// page size. It's called automatically after operations that affect
467    /// the visible item count.
468    pub(super) fn update_pagination(&mut self) {
469        let total = self.len();
470        self.paginator.set_total_items(total);
471
472        // Calculate how many items can fit in the available height
473        if self.height > 0 {
474            let item_height = self.delegate.height() + self.delegate.spacing();
475
476            // Calculate actual header height based on styles (matching Go's lipgloss.Height() approach)
477            let mut header_height = 0;
478            if self.show_title {
479                header_height += self.calculate_element_height("title");
480            }
481            if self.show_status_bar {
482                header_height += self.calculate_element_height("status_bar");
483            }
484
485            // Calculate actual footer height based on styles
486            let mut footer_height = 0;
487            if self.show_help {
488                footer_height += self.calculate_element_height("help");
489            }
490            if self.show_pagination {
491                footer_height += self.calculate_element_height("pagination");
492            }
493
494            let available_height = self.height.saturating_sub(header_height + footer_height);
495            let items_per_page = if item_height > 0 {
496                (available_height / item_height).max(1)
497            } else {
498                5 // Match Go version default
499            };
500
501            self.per_page = items_per_page;
502            self.paginator.set_per_page(items_per_page);
503            // Recalculate total pages with the new per_page value
504            self.paginator.set_total_items(self.len());
505        }
506    }
507
508    /// Returns the number of currently visible items.
509    ///
510    /// This count reflects the items actually visible to the user:
511    /// - When unfiltered: returns the total number of items
512    /// - When filtering is active: returns only the count of matching items
513    ///
514    /// # Returns
515    ///
516    /// The number of items currently visible in the list.
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
522    /// let items = vec![
523    ///     DefaultItem::new("Apple", "Red"),
524    ///     DefaultItem::new("Banana", "Yellow"),
525    /// ];
526    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
527    /// assert_eq!(list.len(), 2);
528    /// ```
529    pub fn len(&self) -> usize {
530        if self.filter_state == FilterState::Unfiltered {
531            self.items.len()
532        } else {
533            self.filtered_items.len()
534        }
535    }
536
537    /// Returns whether the list has no visible items.
538    ///
539    /// # Returns
540    ///
541    /// `true` if there are no currently visible items, `false` otherwise.
542    ///
543    /// # Examples
544    ///
545    /// ```
546    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
547    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
548    /// assert!(list.is_empty());
549    /// ```
550    pub fn is_empty(&self) -> bool {
551        self.len() == 0
552    }
553
554    /// Returns a reference to the currently selected item.
555    ///
556    /// The selected item is the one at the current cursor position. If the list
557    /// is empty or the cursor is out of bounds, returns `None`.
558    ///
559    /// # Returns
560    ///
561    /// A reference to the selected item, or `None` if no valid selection exists.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
567    /// let items = vec![
568    ///     DefaultItem::new("First", "Description 1"),
569    ///     DefaultItem::new("Second", "Description 2"),
570    /// ];
571    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
572    ///
573    /// if let Some(selected) = list.selected_item() {
574    ///     println!("Selected: {}", selected);
575    /// }
576    /// ```
577    pub fn selected_item(&self) -> Option<&I> {
578        if self.filter_state == FilterState::Unfiltered {
579            self.items.get(self.cursor)
580        } else {
581            self.filtered_items.get(self.cursor).map(|fi| &fi.item)
582        }
583    }
584
585    /// Returns the current cursor position.
586    ///
587    /// The cursor represents the currently selected item index within the
588    /// visible (possibly filtered) list. This is always relative to the
589    /// currently visible items, not the original full list.
590    ///
591    /// # Returns
592    ///
593    /// The zero-based index of the currently selected item.
594    ///
595    /// # Examples
596    ///
597    /// ```
598    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
599    /// let items = vec![
600    ///     DefaultItem::new("First", "Description"),
601    ///     DefaultItem::new("Second", "Description"),
602    /// ];
603    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
604    /// assert_eq!(list.cursor(), 0); // Initially at first item
605    /// ```
606    pub fn cursor(&self) -> usize {
607        self.cursor
608    }
609
610    /// Returns fuzzy match character indices for a given original item index.
611    ///
612    /// This method finds the character positions that matched the current filter
613    /// for a specific item identified by its original index in the full items list.
614    /// These indices can be used for character-level highlighting in custom delegates.
615    ///
616    /// # Arguments
617    ///
618    /// * `original_index` - The original index of the item in the full items list
619    ///
620    /// # Returns
621    ///
622    /// A reference to the vector of character indices that matched the filter,
623    /// or `None` if no matches exist for this item or if filtering is not active.
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
629    /// let items = vec![DefaultItem::new("Apple", "Red fruit")];
630    /// let mut list = Model::new(items, DefaultDelegate::new(), 80, 24);
631    ///
632    /// // Apply a filter first
633    /// list.set_filter_text("app");
634    /// // In a real application, this would be done through user interaction
635    ///
636    /// if let Some(matches) = list.matches_for_original_item(0) {
637    ///     // matches contains the character indices that matched "app" in "Apple"
638    ///     println!("Matched characters at indices: {:?}", matches);
639    /// }
640    /// ```
641    pub fn matches_for_original_item(&self, original_index: usize) -> Option<&Vec<usize>> {
642        self.filtered_items
643            .iter()
644            .find(|fi| fi.index == original_index)
645            .map(|fi| &fi.matches)
646    }
647
648    // === Builder Pattern Methods ===
649
650    /// Sets the list title (builder pattern).
651    ///
652    /// The title is displayed at the top of the list when not filtering.
653    /// During filtering, the title is replaced with the filter input interface.
654    ///
655    /// # Arguments
656    ///
657    /// * `title` - The new title for the list
658    ///
659    /// # Returns
660    ///
661    /// Self, for method chaining.
662    ///
663    /// # Examples
664    ///
665    /// ```
666    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
667    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
668    ///     .with_title("My Tasks");
669    /// ```
670    pub fn with_title(mut self, title: &str) -> Self {
671        self.title = title.to_string();
672        self
673    }
674
675    /// Sets pagination display visibility (builder pattern).
676    ///
677    /// # Arguments
678    ///
679    /// * `show` - `true` to show pagination, `false` to hide it
680    ///
681    /// # Returns
682    ///
683    /// Self, for method chaining.
684    ///
685    /// # Examples
686    ///
687    /// ```
688    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
689    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
690    ///     .with_show_pagination(false);
691    /// assert!(!list.show_pagination());
692    /// ```
693    pub fn with_show_pagination(mut self, show: bool) -> Self {
694        self.show_pagination = show;
695        self
696    }
697
698    /// Sets the pagination type (builder pattern).
699    ///
700    /// # Arguments
701    ///
702    /// * `pagination_type` - The type of pagination to display
703    ///
704    /// # Returns
705    ///
706    /// Self, for method chaining.
707    ///
708    /// # Examples
709    ///
710    /// ```
711    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
712    /// # use bubbletea_widgets::paginator::Type;
713    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
714    ///     .with_pagination_type(Type::Dots);
715    /// assert_eq!(list.pagination_type(), Type::Dots);
716    /// ```
717    pub fn with_pagination_type(mut self, pagination_type: paginator::Type) -> Self {
718        self.paginator.paginator_type = pagination_type;
719        self
720    }
721
722    /// Sets title display visibility (builder pattern).
723    ///
724    /// # Arguments
725    ///
726    /// * `show` - `true` to show title, `false` to hide it
727    ///
728    /// # Returns
729    ///
730    /// Self, for method chaining.
731    ///
732    /// # Examples
733    ///
734    /// ```
735    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
736    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
737    ///     .with_show_title(false);
738    /// assert!(!list.show_title());
739    /// ```
740    pub fn with_show_title(mut self, show: bool) -> Self {
741        self.show_title = show;
742        self
743    }
744
745    /// Sets status bar display visibility (builder pattern).
746    ///
747    /// # Arguments
748    ///
749    /// * `show` - `true` to show status bar, `false` to hide it
750    ///
751    /// # Returns
752    ///
753    /// Self, for method chaining.
754    ///
755    /// # Examples
756    ///
757    /// ```
758    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
759    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
760    ///     .with_show_status_bar(false);
761    /// assert!(!list.show_status_bar());
762    /// ```
763    pub fn with_show_status_bar(mut self, show: bool) -> Self {
764        self.show_status_bar = show;
765        self
766    }
767
768    /// Sets spinner display visibility (builder pattern).
769    ///
770    /// # Arguments
771    ///
772    /// * `show` - `true` to show spinner, `false` to hide it
773    ///
774    /// # Returns
775    ///
776    /// Self, for method chaining.
777    ///
778    /// # Examples
779    ///
780    /// ```
781    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
782    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
783    ///     .with_show_spinner(true);
784    /// assert!(list.show_spinner());
785    /// ```
786    pub fn with_show_spinner(mut self, show: bool) -> Self {
787        self.show_spinner = show;
788        self
789    }
790
791    /// Sets help display visibility (builder pattern).
792    ///
793    /// # Arguments
794    ///
795    /// * `show` - `true` to show help, `false` to hide it
796    ///
797    /// # Returns
798    ///
799    /// Self, for method chaining.
800    ///
801    /// # Examples
802    ///
803    /// ```
804    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
805    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
806    ///     .with_show_help(true);
807    /// assert!(list.show_help());
808    /// ```
809    pub fn with_show_help(mut self, show: bool) -> Self {
810        self.show_help = show;
811        self
812    }
813
814    /// Sets the list's styling configuration (builder pattern).
815    ///
816    /// This replaces all current styles with the provided configuration.
817    ///
818    /// # Arguments
819    ///
820    /// * `styles` - The styling configuration to apply
821    ///
822    /// # Returns
823    ///
824    /// Self, for method chaining.
825    ///
826    /// # Examples
827    ///
828    /// ```
829    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
830    /// # use bubbletea_widgets::list::style::ListStyles;
831    /// let custom_styles = ListStyles::default();
832    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
833    ///     .with_styles(custom_styles);
834    /// ```
835    pub fn with_styles(mut self, styles: ListStyles) -> Self {
836        self.styles = styles;
837        self
838    }
839
840    // === UI Component Toggles and Access ===
841
842    /// Returns whether pagination is currently shown.
843    ///
844    /// # Returns
845    ///
846    /// `true` if pagination is displayed, `false` otherwise.
847    ///
848    /// # Examples
849    ///
850    /// ```
851    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
852    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
853    /// assert!(list.show_pagination()); // pagination is shown by default
854    /// ```
855    pub fn show_pagination(&self) -> bool {
856        self.show_pagination
857    }
858
859    /// Sets whether pagination should be displayed.
860    ///
861    /// When disabled, the pagination section will not be rendered in the list view,
862    /// but pagination state and navigation will continue to work normally.
863    ///
864    /// # Arguments
865    ///
866    /// * `show` - `true` to show pagination, `false` to hide it
867    ///
868    /// # Examples
869    ///
870    /// ```
871    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
872    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
873    /// list.set_show_pagination(false);
874    /// assert!(!list.show_pagination());
875    /// ```
876    pub fn set_show_pagination(&mut self, show: bool) {
877        self.show_pagination = show;
878    }
879
880    /// Toggles pagination display on/off.
881    ///
882    /// This is a convenience method that flips the current pagination display state.
883    ///
884    /// # Returns
885    ///
886    /// The new pagination display state after toggling.
887    ///
888    /// # Examples
889    ///
890    /// ```
891    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
892    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
893    /// let new_state = list.toggle_pagination();
894    /// assert_eq!(new_state, list.show_pagination());
895    /// ```
896    pub fn toggle_pagination(&mut self) -> bool {
897        self.show_pagination = !self.show_pagination;
898        self.show_pagination
899    }
900
901    /// Returns the current pagination type (Arabic or Dots).
902    ///
903    /// # Returns
904    ///
905    /// The pagination type currently configured for this list.
906    ///
907    /// # Examples
908    ///
909    /// ```
910    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
911    /// # use bubbletea_widgets::paginator::Type;
912    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
913    /// let pagination_type = list.pagination_type();
914    /// ```
915    pub fn pagination_type(&self) -> paginator::Type {
916        self.paginator.paginator_type
917    }
918
919    /// Sets the pagination display type.
920    ///
921    /// This controls how pagination is rendered:
922    /// - `paginator::Type::Arabic`: Shows "1/5" style numbering
923    /// - `paginator::Type::Dots`: Shows "• ○ • ○ •" style dots
924    ///
925    /// # Arguments
926    ///
927    /// * `pagination_type` - The type of pagination to display
928    ///
929    /// # Examples
930    ///
931    /// ```
932    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
933    /// # use bubbletea_widgets::paginator::Type;
934    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
935    /// list.set_pagination_type(Type::Dots);
936    /// assert_eq!(list.pagination_type(), Type::Dots);
937    /// ```
938    pub fn set_pagination_type(&mut self, pagination_type: paginator::Type) {
939        self.paginator.paginator_type = pagination_type;
940    }
941
942    // === Item Manipulation Methods ===
943
944    /// Inserts an item at the specified index.
945    ///
946    /// All items at and after the specified index are shifted to the right.
947    /// The cursor and pagination are updated appropriately.
948    ///
949    /// # Arguments
950    ///
951    /// * `index` - The position to insert the item at
952    /// * `item` - The item to insert
953    ///
954    /// # Panics
955    ///
956    /// Panics if `index > len()`.
957    ///
958    /// # Examples
959    ///
960    /// ```
961    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
962    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
963    /// list.insert_item(0, DefaultItem::new("First", "Description"));
964    /// assert_eq!(list.len(), 1);
965    /// ```
966    pub fn insert_item(&mut self, index: usize, item: I) {
967        self.items.insert(index, item);
968        // Clear any active filter since item indices have changed
969        if self.filter_state != FilterState::Unfiltered {
970            self.filter_state = FilterState::Unfiltered;
971            self.filtered_items.clear();
972        }
973        // Update cursor if needed to maintain current selection
974        if index <= self.cursor {
975            self.cursor = self
976                .cursor
977                .saturating_add(1)
978                .min(self.items.len().saturating_sub(1));
979        }
980        self.update_pagination();
981    }
982
983    /// Removes and returns the item at the specified index.
984    ///
985    /// All items after the specified index are shifted to the left.
986    /// The cursor and pagination are updated appropriately.
987    ///
988    /// # Arguments
989    ///
990    /// * `index` - The position to remove the item from
991    ///
992    /// # Returns
993    ///
994    /// The removed item.
995    ///
996    /// # Panics
997    ///
998    /// Panics if `index >= len()`.
999    ///
1000    /// # Examples
1001    ///
1002    /// ```
1003    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1004    /// let mut list = Model::new(
1005    ///     vec![DefaultItem::new("First", "Desc")],
1006    ///     DefaultDelegate::new(), 80, 24
1007    /// );
1008    /// let removed = list.remove_item(0);
1009    /// assert_eq!(list.len(), 0);
1010    /// ```
1011    pub fn remove_item(&mut self, index: usize) -> I {
1012        if index >= self.items.len() {
1013            panic!("Index out of bounds");
1014        }
1015
1016        // Check if the item can be removed
1017        if !self.delegate.can_remove(index, &self.items[index]) {
1018            panic!("Item cannot be removed");
1019        }
1020
1021        // Call the on_remove callback before removal
1022        let item_ref = &self.items[index];
1023        let _ = self.delegate.on_remove(index, item_ref);
1024
1025        let item = self.items.remove(index);
1026        // Clear any active filter since item indices have changed
1027        if self.filter_state != FilterState::Unfiltered {
1028            self.filter_state = FilterState::Unfiltered;
1029            self.filtered_items.clear();
1030        }
1031        // Update cursor to maintain valid position
1032        if !self.items.is_empty() {
1033            if index < self.cursor {
1034                self.cursor = self.cursor.saturating_sub(1);
1035            } else if self.cursor >= self.items.len() {
1036                self.cursor = self.items.len().saturating_sub(1);
1037            }
1038        } else {
1039            self.cursor = 0;
1040        }
1041        self.update_pagination();
1042        item
1043    }
1044
1045    /// Moves an item from one position to another.
1046    ///
1047    /// The item at `from_index` is removed and inserted at `to_index`.
1048    /// The cursor is updated to follow the moved item if it was selected.
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `from_index` - The current position of the item to move
1053    /// * `to_index` - The target position to move the item to
1054    ///
1055    /// # Panics
1056    ///
1057    /// Panics if either index is out of bounds.
1058    ///
1059    /// # Examples
1060    ///
1061    /// ```
1062    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1063    /// let mut list = Model::new(
1064    ///     vec![
1065    ///         DefaultItem::new("First", "1"),
1066    ///         DefaultItem::new("Second", "2"),
1067    ///     ],
1068    ///     DefaultDelegate::new(), 80, 24
1069    /// );
1070    /// list.move_item(0, 1); // Move "First" to position 1
1071    /// ```
1072    pub fn move_item(&mut self, from_index: usize, to_index: usize) {
1073        if from_index >= self.items.len() || to_index >= self.items.len() {
1074            panic!("Index out of bounds");
1075        }
1076        if from_index == to_index {
1077            return; // No movement needed
1078        }
1079
1080        let item = self.items.remove(from_index);
1081        self.items.insert(to_index, item);
1082
1083        // Clear any active filter since item indices have changed
1084        if self.filter_state != FilterState::Unfiltered {
1085            self.filter_state = FilterState::Unfiltered;
1086            self.filtered_items.clear();
1087        }
1088
1089        // Update cursor to follow the moved item if it was selected
1090        if self.cursor == from_index {
1091            self.cursor = to_index;
1092        } else if from_index < self.cursor && to_index >= self.cursor {
1093            self.cursor = self.cursor.saturating_sub(1);
1094        } else if from_index > self.cursor && to_index <= self.cursor {
1095            self.cursor = self.cursor.saturating_add(1);
1096        }
1097
1098        self.update_pagination();
1099    }
1100
1101    /// Adds an item to the end of the list.
1102    ///
1103    /// This is equivalent to `insert_item(len(), item)`.
1104    ///
1105    /// # Arguments
1106    ///
1107    /// * `item` - The item to add
1108    ///
1109    /// # Examples
1110    ///
1111    /// ```
1112    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1113    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1114    /// list.push_item(DefaultItem::new("New Item", "Description"));
1115    /// assert_eq!(list.len(), 1);
1116    /// ```
1117    pub fn push_item(&mut self, item: I) {
1118        self.items.push(item);
1119        // Clear any active filter since item indices have changed
1120        if self.filter_state != FilterState::Unfiltered {
1121            self.filter_state = FilterState::Unfiltered;
1122            self.filtered_items.clear();
1123        }
1124        self.update_pagination();
1125    }
1126
1127    /// Removes and returns the last item from the list.
1128    ///
1129    /// # Returns
1130    ///
1131    /// The last item, or `None` if the list is empty.
1132    ///
1133    /// # Examples
1134    ///
1135    /// ```
1136    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1137    /// let mut list = Model::new(
1138    ///     vec![DefaultItem::new("Item", "Desc")],
1139    ///     DefaultDelegate::new(), 80, 24
1140    /// );
1141    /// let popped = list.pop_item();
1142    /// assert!(popped.is_some());
1143    /// assert_eq!(list.len(), 0);
1144    /// ```
1145    pub fn pop_item(&mut self) -> Option<I> {
1146        if self.items.is_empty() {
1147            return None;
1148        }
1149
1150        let item = self.items.pop();
1151        // Clear any active filter since item indices have changed
1152        if self.filter_state != FilterState::Unfiltered {
1153            self.filter_state = FilterState::Unfiltered;
1154            self.filtered_items.clear();
1155        }
1156        // Update cursor if it's now out of bounds
1157        if self.cursor >= self.items.len() && !self.items.is_empty() {
1158            self.cursor = self.items.len() - 1;
1159        } else if self.items.is_empty() {
1160            self.cursor = 0;
1161        }
1162        self.update_pagination();
1163        item
1164    }
1165
1166    /// Returns a reference to the underlying items collection.
1167    ///
1168    /// This provides read-only access to all items in the list,
1169    /// regardless of the current filtering state.
1170    ///
1171    /// # Returns
1172    ///
1173    /// A slice containing all items in the list.
1174    ///
1175    /// # Examples
1176    ///
1177    /// ```
1178    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1179    /// let items = vec![DefaultItem::new("First", "1"), DefaultItem::new("Second", "2")];
1180    /// let list = Model::new(items.clone(), DefaultDelegate::new(), 80, 24);
1181    /// assert_eq!(list.items().len(), 2);
1182    /// assert_eq!(list.items()[0].to_string(), items[0].to_string());
1183    /// ```
1184    pub fn items(&self) -> &[I] {
1185        &self.items
1186    }
1187
1188    /// Returns a mutable reference to the underlying items collection.
1189    ///
1190    /// This provides direct mutable access to the items. Note that after
1191    /// modifying items through this method, you should call `update_pagination()`
1192    /// to ensure pagination state remains consistent.
1193    ///
1194    /// **Warning**: Direct modification may invalidate the current filter state.
1195    /// Consider using the specific item manipulation methods instead.
1196    ///
1197    /// # Returns
1198    ///
1199    /// A mutable slice containing all items in the list.
1200    ///
1201    /// # Examples
1202    ///
1203    /// ```
1204    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1205    /// let mut list = Model::new(
1206    ///     vec![DefaultItem::new("First", "1")],
1207    ///     DefaultDelegate::new(), 80, 24
1208    /// );
1209    /// list.items_mut()[0] = DefaultItem::new("Modified", "Updated");
1210    /// assert_eq!(list.items()[0].to_string(), "Modified");
1211    /// ```
1212    pub fn items_mut(&mut self) -> &mut Vec<I> {
1213        // Clear filter state since items may be modified
1214        if self.filter_state != FilterState::Unfiltered {
1215            self.filter_state = FilterState::Unfiltered;
1216            self.filtered_items.clear();
1217        }
1218        &mut self.items
1219    }
1220
1221    /// Returns the total number of items in the list.
1222    ///
1223    /// This returns the count of all items, not just visible items.
1224    /// For visible items count, use `len()`.
1225    ///
1226    /// # Returns
1227    ///
1228    /// The total number of items in the underlying collection.
1229    ///
1230    /// # Examples
1231    ///
1232    /// ```
1233    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1234    /// let list = Model::new(
1235    ///     vec![DefaultItem::new("1", ""), DefaultItem::new("2", "")],
1236    ///     DefaultDelegate::new(), 80, 24
1237    /// );
1238    /// assert_eq!(list.items_len(), 2);
1239    /// ```
1240    pub fn items_len(&self) -> usize {
1241        self.items.len()
1242    }
1243
1244    /// Returns whether the underlying items collection is empty.
1245    ///
1246    /// This checks the total items count, not just visible items.
1247    /// For visible items check, use `is_empty()`.
1248    ///
1249    /// # Returns
1250    ///
1251    /// `true` if there are no items in the underlying collection, `false` otherwise.
1252    ///
1253    /// # Examples
1254    ///
1255    /// ```
1256    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1257    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1258    /// assert!(list.items_empty());
1259    /// ```
1260    pub fn items_empty(&self) -> bool {
1261        self.items.is_empty()
1262    }
1263
1264    // === UI Component Access and Styling ===
1265
1266    /// Returns whether the title is currently shown.
1267    ///
1268    /// # Returns
1269    ///
1270    /// `true` if the title is displayed, `false` otherwise.
1271    ///
1272    /// # Examples
1273    ///
1274    /// ```
1275    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1276    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1277    /// assert!(list.show_title()); // title is shown by default
1278    /// ```
1279    pub fn show_title(&self) -> bool {
1280        self.show_title
1281    }
1282
1283    /// Sets whether the title should be displayed.
1284    ///
1285    /// When disabled, the title section will not be rendered in the list view.
1286    ///
1287    /// # Arguments
1288    ///
1289    /// * `show` - `true` to show the title, `false` to hide it
1290    ///
1291    /// # Examples
1292    ///
1293    /// ```
1294    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1295    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1296    /// list.set_show_title(false);
1297    /// assert!(!list.show_title());
1298    /// ```
1299    pub fn set_show_title(&mut self, show: bool) {
1300        self.show_title = show;
1301    }
1302
1303    /// Toggles title display on/off.
1304    ///
1305    /// This is a convenience method that flips the current title display state.
1306    ///
1307    /// # Returns
1308    ///
1309    /// The new title display state after toggling.
1310    ///
1311    /// # Examples
1312    ///
1313    /// ```
1314    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1315    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1316    /// let new_state = list.toggle_title();
1317    /// assert_eq!(new_state, list.show_title());
1318    /// ```
1319    pub fn toggle_title(&mut self) -> bool {
1320        self.show_title = !self.show_title;
1321        self.show_title
1322    }
1323
1324    /// Returns whether the status bar is currently shown.
1325    ///
1326    /// # Returns
1327    ///
1328    /// `true` if the status bar is displayed, `false` otherwise.
1329    ///
1330    /// # Examples
1331    ///
1332    /// ```
1333    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1334    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1335    /// assert!(list.show_status_bar()); // status bar is shown by default
1336    /// ```
1337    pub fn show_status_bar(&self) -> bool {
1338        self.show_status_bar
1339    }
1340
1341    /// Sets whether the status bar should be displayed.
1342    ///
1343    /// When disabled, the status bar section will not be rendered in the list view.
1344    ///
1345    /// # Arguments
1346    ///
1347    /// * `show` - `true` to show the status bar, `false` to hide it
1348    ///
1349    /// # Examples
1350    ///
1351    /// ```
1352    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1353    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1354    /// list.set_show_status_bar(false);
1355    /// assert!(!list.show_status_bar());
1356    /// ```
1357    pub fn set_show_status_bar(&mut self, show: bool) {
1358        self.show_status_bar = show;
1359    }
1360
1361    /// Toggles status bar display on/off.
1362    ///
1363    /// This is a convenience method that flips the current status bar display state.
1364    ///
1365    /// # Returns
1366    ///
1367    /// The new status bar display state after toggling.
1368    ///
1369    /// # Examples
1370    ///
1371    /// ```
1372    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1373    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1374    /// let new_state = list.toggle_status_bar();
1375    /// assert_eq!(new_state, list.show_status_bar());
1376    /// ```
1377    pub fn toggle_status_bar(&mut self) -> bool {
1378        self.show_status_bar = !self.show_status_bar;
1379        self.show_status_bar
1380    }
1381
1382    /// Returns whether the spinner is currently shown.
1383    ///
1384    /// # Returns
1385    ///
1386    /// `true` if the spinner is displayed, `false` otherwise.
1387    ///
1388    /// # Examples
1389    ///
1390    /// ```
1391    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1392    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1393    /// assert!(!list.show_spinner()); // spinner is hidden by default
1394    /// ```
1395    pub fn show_spinner(&self) -> bool {
1396        self.show_spinner
1397    }
1398
1399    /// Sets whether the spinner should be displayed.
1400    ///
1401    /// When enabled, the spinner will be rendered as part of the list view,
1402    /// typically to indicate loading state.
1403    ///
1404    /// # Arguments
1405    ///
1406    /// * `show` - `true` to show the spinner, `false` to hide it
1407    ///
1408    /// # Examples
1409    ///
1410    /// ```
1411    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1412    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1413    /// list.set_show_spinner(true);
1414    /// assert!(list.show_spinner());
1415    /// ```
1416    pub fn set_show_spinner(&mut self, show: bool) {
1417        self.show_spinner = show;
1418    }
1419
1420    /// Toggles spinner display on/off.
1421    ///
1422    /// This is a convenience method that flips the current spinner display state.
1423    ///
1424    /// # Returns
1425    ///
1426    /// The new spinner display state after toggling.
1427    ///
1428    /// # Examples
1429    ///
1430    /// ```
1431    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1432    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1433    /// let new_state = list.toggle_spinner();
1434    /// assert_eq!(new_state, list.show_spinner());
1435    /// ```
1436    pub fn toggle_spinner(&mut self) -> bool {
1437        self.show_spinner = !self.show_spinner;
1438        self.show_spinner
1439    }
1440
1441    /// Returns a reference to the spinner model.
1442    ///
1443    /// This allows access to the underlying spinner for customization
1444    /// and state management.
1445    ///
1446    /// # Returns
1447    ///
1448    /// A reference to the spinner model.
1449    ///
1450    /// # Examples
1451    ///
1452    /// ```
1453    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1454    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1455    /// let spinner = list.spinner();
1456    /// ```
1457    pub fn spinner(&self) -> &spinner::Model {
1458        &self.spinner
1459    }
1460
1461    /// Returns a mutable reference to the spinner model.
1462    ///
1463    /// This allows modification of the underlying spinner for customization
1464    /// and state management.
1465    ///
1466    /// # Returns
1467    ///
1468    /// A mutable reference to the spinner model.
1469    ///
1470    /// # Examples
1471    ///
1472    /// ```
1473    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1474    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1475    /// let spinner = list.spinner_mut();
1476    /// ```
1477    pub fn spinner_mut(&mut self) -> &mut spinner::Model {
1478        &mut self.spinner
1479    }
1480
1481    /// Returns whether the help is currently shown.
1482    ///
1483    /// # Returns
1484    ///
1485    /// `true` if help is displayed, `false` otherwise.
1486    ///
1487    /// # Examples
1488    ///
1489    /// ```
1490    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1491    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1492    /// assert!(list.show_help()); // help is shown by default
1493    /// ```
1494    pub fn show_help(&self) -> bool {
1495        self.show_help
1496    }
1497
1498    /// Sets whether help should be displayed.
1499    ///
1500    /// When enabled, help text will be rendered as part of the list view,
1501    /// showing available key bindings and controls.
1502    ///
1503    /// # Arguments
1504    ///
1505    /// * `show` - `true` to show help, `false` to hide it
1506    ///
1507    /// # Examples
1508    ///
1509    /// ```
1510    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1511    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1512    /// list.set_show_help(true);
1513    /// assert!(list.show_help());
1514    /// ```
1515    pub fn set_show_help(&mut self, show: bool) {
1516        self.show_help = show;
1517    }
1518
1519    /// Toggles help display on/off.
1520    ///
1521    /// This is a convenience method that flips the current help display state.
1522    ///
1523    /// # Returns
1524    ///
1525    /// The new help display state after toggling.
1526    ///
1527    /// # Examples
1528    ///
1529    /// ```
1530    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1531    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1532    /// let new_state = list.toggle_help();
1533    /// assert_eq!(new_state, list.show_help());
1534    /// ```
1535    pub fn toggle_help(&mut self) -> bool {
1536        self.show_help = !self.show_help;
1537        self.show_help
1538    }
1539
1540    /// Returns a reference to the help model.
1541    ///
1542    /// This allows access to the underlying help system for customization
1543    /// and state management.
1544    ///
1545    /// # Returns
1546    ///
1547    /// A reference to the help model.
1548    ///
1549    /// # Examples
1550    ///
1551    /// ```
1552    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1553    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1554    /// let help = list.help();
1555    /// ```
1556    pub fn help(&self) -> &help::Model {
1557        &self.help
1558    }
1559
1560    /// Returns a mutable reference to the help model.
1561    ///
1562    /// This allows modification of the underlying help system for customization
1563    /// and state management.
1564    ///
1565    /// # Returns
1566    ///
1567    /// A mutable reference to the help model.
1568    ///
1569    /// # Examples
1570    ///
1571    /// ```
1572    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1573    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1574    /// let help = list.help_mut();
1575    /// ```
1576    pub fn help_mut(&mut self) -> &mut help::Model {
1577        &mut self.help
1578    }
1579
1580    /// Returns a reference to the list's styling configuration.
1581    ///
1582    /// This provides read-only access to all visual styles used by the list,
1583    /// including title, item, status bar, pagination, and help styles.
1584    ///
1585    /// # Returns
1586    ///
1587    /// A reference to the `ListStyles` configuration.
1588    ///
1589    /// # Examples
1590    ///
1591    /// ```
1592    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1593    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1594    /// let styles = list.styles();
1595    /// // Access specific styles, e.g., styles.title, styles.pagination_style
1596    /// ```
1597    pub fn styles(&self) -> &ListStyles {
1598        &self.styles
1599    }
1600
1601    /// Returns a mutable reference to the list's styling configuration.
1602    ///
1603    /// This provides direct mutable access to all visual styles used by the list.
1604    /// Changes to styles take effect immediately on the next render.
1605    ///
1606    /// # Returns
1607    ///
1608    /// A mutable reference to the `ListStyles` configuration.
1609    ///
1610    /// # Examples
1611    ///
1612    /// ```
1613    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1614    /// # use lipgloss_extras::prelude::*;
1615    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1616    /// let styles = list.styles_mut();
1617    /// styles.title = Style::new().foreground("#FF0000"); // Red title
1618    /// ```
1619    pub fn styles_mut(&mut self) -> &mut ListStyles {
1620        &mut self.styles
1621    }
1622
1623    /// Sets the list's styling configuration.
1624    ///
1625    /// This replaces all current styles with the provided configuration.
1626    /// Changes take effect immediately on the next render.
1627    ///
1628    /// # Arguments
1629    ///
1630    /// * `styles` - The new styling configuration to apply
1631    ///
1632    /// # Examples
1633    ///
1634    /// ```
1635    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1636    /// # use bubbletea_widgets::list::style::ListStyles;
1637    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1638    /// let custom_styles = ListStyles::default();
1639    /// list.set_styles(custom_styles);
1640    /// ```
1641    pub fn set_styles(&mut self, styles: ListStyles) {
1642        // Update paginator dots from styled strings
1643        self.paginator.active_dot = styles.active_pagination_dot.render("");
1644        self.paginator.inactive_dot = styles.inactive_pagination_dot.render("");
1645        self.styles = styles;
1646    }
1647
1648    /// Renders the status bar as a formatted string.
1649    ///
1650    /// The status bar shows the current selection position and total item count,
1651    /// using the custom item names if set. The format is "X/Y items" where X is
1652    /// the current cursor position + 1, and Y is the total item count.
1653    ///
1654    /// # Returns
1655    ///
1656    /// A formatted status string, or empty string if status bar is disabled.
1657    ///
1658    /// # Examples
1659    ///
1660    /// ```
1661    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1662    /// let items = vec![
1663    ///     DefaultItem::new("First", ""),
1664    ///     DefaultItem::new("Second", ""),
1665    /// ];
1666    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
1667    /// let status = list.status_view();
1668    /// assert!(status.contains("1/2"));
1669    /// ```
1670    pub fn status_view(&self) -> String {
1671        if !self.show_status_bar {
1672            return String::new();
1673        }
1674
1675        let mut footer = String::new();
1676        if !self.is_empty() {
1677            let singular = self.status_item_singular.as_deref().unwrap_or("item");
1678            let plural = self.status_item_plural.as_deref().unwrap_or("items");
1679            let noun = if self.len() == 1 { singular } else { plural };
1680            footer.push_str(&format!("{}/{} {}", self.cursor + 1, self.len(), noun));
1681        }
1682        let help_view = self.help.view(self);
1683        if !help_view.is_empty() {
1684            footer.push('\n');
1685            footer.push_str(&help_view);
1686        }
1687        footer
1688    }
1689
1690    // === Advanced Filtering API ===
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695    use super::*;
1696    use crate::list::{DefaultDelegate, DefaultItem};
1697
1698    #[test]
1699    fn test_pagination_calculation_fix() {
1700        // Test that our pagination calculation fix works correctly
1701        let items: Vec<DefaultItem> = (0..23)
1702            .map(|i| DefaultItem::new(&format!("Item {}", i), "Description"))
1703            .collect();
1704        let delegate = DefaultDelegate::new();
1705
1706        // Test different terminal heights like the user mentioned
1707        for terminal_height in [24, 30, 34] {
1708            let doc_margin = 2; // doc_style margin from the example
1709            let list_height = terminal_height - doc_margin;
1710
1711            let list = Model::new(items.clone(), delegate.clone(), 80, list_height)
1712                .with_title("Test List");
1713
1714            // Calculate expected values
1715            let title_height = list.calculate_element_height("title"); // 2
1716            let status_height = list.calculate_element_height("status_bar"); // 2
1717            let pagination_height = list.calculate_element_height("pagination"); // 1
1718            let help_height = list.calculate_element_height("help"); // 2
1719
1720            let header_height = title_height + status_height; // 4
1721            let footer_height = pagination_height + help_height; // 3
1722            let total_reserved = header_height + footer_height; // 7
1723            let available_height = list_height - total_reserved;
1724
1725            // DefaultDelegate uses 3 lines per item (2 content + 1 spacing)
1726            let delegate_item_height = 3;
1727            let expected_per_page = available_height / delegate_item_height;
1728            let expected_total_pages =
1729                (items.len() as f32 / expected_per_page as f32).ceil() as usize;
1730
1731            println!("Terminal height {}: list_height={}, available={}, expected_per_page={}, expected_pages={}, actual_per_page={}, actual_pages={}", 
1732                terminal_height, list_height, available_height, expected_per_page, expected_total_pages, list.per_page(), list.total_pages());
1733
1734            assert_eq!(
1735                list.per_page(),
1736                expected_per_page,
1737                "Items per page mismatch for terminal height {}",
1738                terminal_height
1739            );
1740            assert_eq!(
1741                list.total_pages(),
1742                expected_total_pages,
1743                "Total pages mismatch for terminal height {}",
1744                terminal_height
1745            );
1746        }
1747    }
1748}