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        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: false,
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
229    /// Sets the items displayed in the list.
230    ///
231    /// This method replaces all current items with the provided vector.
232    /// The cursor is reset to position 0, and pagination is recalculated
233    /// based on the new item count.
234    ///
235    /// # Arguments
236    ///
237    /// * `items` - Vector of new items to display
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
243    /// let mut list = Model::new(vec![], DefaultDelegate::new(), 80, 24);
244    ///
245    /// let items = vec![
246    ///     DefaultItem::new("Apple", "Red fruit"),
247    ///     DefaultItem::new("Banana", "Yellow fruit"),
248    /// ];
249    /// list.set_items(items);
250    /// assert_eq!(list.len(), 2);
251    /// ```
252    pub fn set_items(&mut self, items: Vec<I>) {
253        self.items = items;
254        self.cursor = 0;
255        self.update_pagination();
256    }
257
258    /// Returns a vector of currently visible items.
259    ///
260    /// The returned items reflect the current filtering state:
261    /// - When unfiltered: returns all items
262    /// - When filtered: returns only items matching the current filter
263    ///
264    /// # Returns
265    ///
266    /// A vector containing clones of all currently visible items.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
272    /// let items = vec![
273    ///     DefaultItem::new("First", "Description 1"),
274    ///     DefaultItem::new("Second", "Description 2"),
275    /// ];
276    ///
277    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
278    /// let visible = list.visible_items();
279    /// assert_eq!(visible.len(), 2);
280    /// ```
281    pub fn visible_items(&self) -> Vec<I> {
282        if self.filter_state == FilterState::Unfiltered {
283            self.items.clone()
284        } else {
285            self.filtered_items
286                .iter()
287                .map(|fi| fi.item.clone())
288                .collect()
289        }
290    }
291
292    /// Sets the filter text without applying the filter.
293    ///
294    /// This method updates the filter input text but does not trigger
295    /// the filtering process. It's primarily used for programmatic
296    /// filter setup or testing.
297    ///
298    /// # Arguments
299    ///
300    /// * `s` - The filter text to set
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
306    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
307    /// list.set_filter_text("search term");
308    /// // Filter text is set but not applied until filtering is activated
309    /// ```
310    pub fn set_filter_text(&mut self, s: &str) {
311        self.filter_input.set_value(s);
312    }
313
314    /// Sets the current filtering state.
315    ///
316    /// This method directly controls the list's filtering state without
317    /// triggering filter application. It's useful for programmatic state
318    /// management or testing specific filter conditions.
319    ///
320    /// # Arguments
321    ///
322    /// * `st` - The new filter state to set
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem, FilterState};
328    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
329    /// list.set_filter_state(FilterState::Filtering);
330    /// // List is now in filtering mode
331    /// ```
332    pub fn set_filter_state(&mut self, st: FilterState) {
333        self.filter_state = st;
334    }
335
336    /// Sets custom singular and plural names for status bar items.
337    ///
338    /// The status bar displays item counts using these names. If not set,
339    /// defaults to "item" and "items".
340    ///
341    /// # Arguments
342    ///
343    /// * `singular` - Name for single item (e.g., "task")
344    /// * `plural` - Name for multiple items (e.g., "tasks")
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
350    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
351    /// list.set_status_bar_item_name("task", "tasks");
352    /// // Status bar will now show "1 task" or "5 tasks"
353    /// ```
354    pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
355        self.status_item_singular = Some(singular.to_string());
356        self.status_item_plural = Some(plural.to_string());
357    }
358
359    /// Updates pagination settings based on current item count and page size.
360    ///
361    /// This method recalculates pagination after changes to item count or
362    /// page size. It's called automatically after operations that affect
363    /// the visible item count.
364    pub(super) fn update_pagination(&mut self) {
365        let total = self.len();
366        self.paginator.set_total_items(total);
367
368        // Calculate how many items can fit in the available height
369        if self.height > 0 {
370            let item_height = self.delegate.height() + self.delegate.spacing();
371            let header_height = 1; // Title or filter input
372            let footer_height = if self.show_status_bar { 2 } else { 0 }; // Status + help
373
374            let available_height = self.height.saturating_sub(header_height + footer_height);
375            let items_per_page = if item_height > 0 {
376                (available_height / item_height).max(1)
377            } else {
378                10 // Fallback to default value
379            };
380
381            self.per_page = items_per_page;
382            self.paginator.set_per_page(items_per_page);
383        }
384    }
385
386    /// Returns the number of currently visible items.
387    ///
388    /// This count reflects the items actually visible to the user:
389    /// - When unfiltered: returns the total number of items
390    /// - When filtering is active: returns only the count of matching items
391    ///
392    /// # Returns
393    ///
394    /// The number of items currently visible in the list.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
400    /// let items = vec![
401    ///     DefaultItem::new("Apple", "Red"),
402    ///     DefaultItem::new("Banana", "Yellow"),
403    /// ];
404    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
405    /// assert_eq!(list.len(), 2);
406    /// ```
407    pub fn len(&self) -> usize {
408        if self.filter_state == FilterState::Unfiltered {
409            self.items.len()
410        } else {
411            self.filtered_items.len()
412        }
413    }
414
415    /// Returns whether the list has no visible items.
416    ///
417    /// # Returns
418    ///
419    /// `true` if there are no currently visible items, `false` otherwise.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
425    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
426    /// assert!(list.is_empty());
427    /// ```
428    pub fn is_empty(&self) -> bool {
429        self.len() == 0
430    }
431
432    /// Returns a reference to the currently selected item.
433    ///
434    /// The selected item is the one at the current cursor position. If the list
435    /// is empty or the cursor is out of bounds, returns `None`.
436    ///
437    /// # Returns
438    ///
439    /// A reference to the selected item, or `None` if no valid selection exists.
440    ///
441    /// # Examples
442    ///
443    /// ```
444    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
445    /// let items = vec![
446    ///     DefaultItem::new("First", "Description 1"),
447    ///     DefaultItem::new("Second", "Description 2"),
448    /// ];
449    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
450    ///
451    /// if let Some(selected) = list.selected_item() {
452    ///     println!("Selected: {}", selected);
453    /// }
454    /// ```
455    pub fn selected_item(&self) -> Option<&I> {
456        if self.filter_state == FilterState::Unfiltered {
457            self.items.get(self.cursor)
458        } else {
459            self.filtered_items.get(self.cursor).map(|fi| &fi.item)
460        }
461    }
462
463    /// Returns the current cursor position.
464    ///
465    /// The cursor represents the currently selected item index within the
466    /// visible (possibly filtered) list. This is always relative to the
467    /// currently visible items, not the original full list.
468    ///
469    /// # Returns
470    ///
471    /// The zero-based index of the currently selected item.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
477    /// let items = vec![
478    ///     DefaultItem::new("First", "Description"),
479    ///     DefaultItem::new("Second", "Description"),
480    /// ];
481    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
482    /// assert_eq!(list.cursor(), 0); // Initially at first item
483    /// ```
484    pub fn cursor(&self) -> usize {
485        self.cursor
486    }
487
488    /// Returns fuzzy match character indices for a given original item index.
489    ///
490    /// This method finds the character positions that matched the current filter
491    /// for a specific item identified by its original index in the full items list.
492    /// These indices can be used for character-level highlighting in custom delegates.
493    ///
494    /// # Arguments
495    ///
496    /// * `original_index` - The original index of the item in the full items list
497    ///
498    /// # Returns
499    ///
500    /// A reference to the vector of character indices that matched the filter,
501    /// or `None` if no matches exist for this item or if filtering is not active.
502    ///
503    /// # Examples
504    ///
505    /// ```
506    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
507    /// let items = vec![DefaultItem::new("Apple", "Red fruit")];
508    /// let mut list = Model::new(items, DefaultDelegate::new(), 80, 24);
509    ///
510    /// // Apply a filter first
511    /// list.set_filter_text("app");
512    /// // In a real application, this would be done through user interaction
513    ///
514    /// if let Some(matches) = list.matches_for_original_item(0) {
515    ///     // matches contains the character indices that matched "app" in "Apple"
516    ///     println!("Matched characters at indices: {:?}", matches);
517    /// }
518    /// ```
519    pub fn matches_for_original_item(&self, original_index: usize) -> Option<&Vec<usize>> {
520        self.filtered_items
521            .iter()
522            .find(|fi| fi.index == original_index)
523            .map(|fi| &fi.matches)
524    }
525
526    // === Builder Pattern Methods ===
527
528    /// Sets the list title (builder pattern).
529    ///
530    /// The title is displayed at the top of the list when not filtering.
531    /// During filtering, the title is replaced with the filter input interface.
532    ///
533    /// # Arguments
534    ///
535    /// * `title` - The new title for the list
536    ///
537    /// # Returns
538    ///
539    /// Self, for method chaining.
540    ///
541    /// # Examples
542    ///
543    /// ```
544    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
545    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
546    ///     .with_title("My Tasks");
547    /// ```
548    pub fn with_title(mut self, title: &str) -> Self {
549        self.title = title.to_string();
550        self
551    }
552
553    /// Sets pagination display visibility (builder pattern).
554    ///
555    /// # Arguments
556    ///
557    /// * `show` - `true` to show pagination, `false` to hide it
558    ///
559    /// # Returns
560    ///
561    /// Self, for method chaining.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
567    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
568    ///     .with_show_pagination(false);
569    /// assert!(!list.show_pagination());
570    /// ```
571    pub fn with_show_pagination(mut self, show: bool) -> Self {
572        self.show_pagination = show;
573        self
574    }
575
576    /// Sets the pagination type (builder pattern).
577    ///
578    /// # Arguments
579    ///
580    /// * `pagination_type` - The type of pagination to display
581    ///
582    /// # Returns
583    ///
584    /// Self, for method chaining.
585    ///
586    /// # Examples
587    ///
588    /// ```
589    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
590    /// # use bubbletea_widgets::paginator::Type;
591    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
592    ///     .with_pagination_type(Type::Dots);
593    /// assert_eq!(list.pagination_type(), Type::Dots);
594    /// ```
595    pub fn with_pagination_type(mut self, pagination_type: paginator::Type) -> Self {
596        self.paginator.paginator_type = pagination_type;
597        self
598    }
599
600    /// Sets title display visibility (builder pattern).
601    ///
602    /// # Arguments
603    ///
604    /// * `show` - `true` to show title, `false` to hide it
605    ///
606    /// # Returns
607    ///
608    /// Self, for method chaining.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
614    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
615    ///     .with_show_title(false);
616    /// assert!(!list.show_title());
617    /// ```
618    pub fn with_show_title(mut self, show: bool) -> Self {
619        self.show_title = show;
620        self
621    }
622
623    /// Sets status bar display visibility (builder pattern).
624    ///
625    /// # Arguments
626    ///
627    /// * `show` - `true` to show status bar, `false` to hide it
628    ///
629    /// # Returns
630    ///
631    /// Self, for method chaining.
632    ///
633    /// # Examples
634    ///
635    /// ```
636    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
637    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
638    ///     .with_show_status_bar(false);
639    /// assert!(!list.show_status_bar());
640    /// ```
641    pub fn with_show_status_bar(mut self, show: bool) -> Self {
642        self.show_status_bar = show;
643        self
644    }
645
646    /// Sets spinner display visibility (builder pattern).
647    ///
648    /// # Arguments
649    ///
650    /// * `show` - `true` to show spinner, `false` to hide it
651    ///
652    /// # Returns
653    ///
654    /// Self, for method chaining.
655    ///
656    /// # Examples
657    ///
658    /// ```
659    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
660    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
661    ///     .with_show_spinner(true);
662    /// assert!(list.show_spinner());
663    /// ```
664    pub fn with_show_spinner(mut self, show: bool) -> Self {
665        self.show_spinner = show;
666        self
667    }
668
669    /// Sets help display visibility (builder pattern).
670    ///
671    /// # Arguments
672    ///
673    /// * `show` - `true` to show help, `false` to hide it
674    ///
675    /// # Returns
676    ///
677    /// Self, for method chaining.
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
683    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
684    ///     .with_show_help(true);
685    /// assert!(list.show_help());
686    /// ```
687    pub fn with_show_help(mut self, show: bool) -> Self {
688        self.show_help = show;
689        self
690    }
691
692    /// Sets the list's styling configuration (builder pattern).
693    ///
694    /// This replaces all current styles with the provided configuration.
695    ///
696    /// # Arguments
697    ///
698    /// * `styles` - The styling configuration to apply
699    ///
700    /// # Returns
701    ///
702    /// Self, for method chaining.
703    ///
704    /// # Examples
705    ///
706    /// ```
707    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
708    /// # use bubbletea_widgets::list::style::ListStyles;
709    /// let custom_styles = ListStyles::default();
710    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
711    ///     .with_styles(custom_styles);
712    /// ```
713    pub fn with_styles(mut self, styles: ListStyles) -> Self {
714        self.styles = styles;
715        self
716    }
717
718    // === UI Component Toggles and Access ===
719
720    /// Returns whether pagination is currently shown.
721    ///
722    /// # Returns
723    ///
724    /// `true` if pagination is displayed, `false` otherwise.
725    ///
726    /// # Examples
727    ///
728    /// ```
729    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
730    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
731    /// assert!(list.show_pagination()); // pagination is shown by default
732    /// ```
733    pub fn show_pagination(&self) -> bool {
734        self.show_pagination
735    }
736
737    /// Sets whether pagination should be displayed.
738    ///
739    /// When disabled, the pagination section will not be rendered in the list view,
740    /// but pagination state and navigation will continue to work normally.
741    ///
742    /// # Arguments
743    ///
744    /// * `show` - `true` to show pagination, `false` to hide it
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
750    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
751    /// list.set_show_pagination(false);
752    /// assert!(!list.show_pagination());
753    /// ```
754    pub fn set_show_pagination(&mut self, show: bool) {
755        self.show_pagination = show;
756    }
757
758    /// Toggles pagination display on/off.
759    ///
760    /// This is a convenience method that flips the current pagination display state.
761    ///
762    /// # Returns
763    ///
764    /// The new pagination display state after toggling.
765    ///
766    /// # Examples
767    ///
768    /// ```
769    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
770    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
771    /// let new_state = list.toggle_pagination();
772    /// assert_eq!(new_state, list.show_pagination());
773    /// ```
774    pub fn toggle_pagination(&mut self) -> bool {
775        self.show_pagination = !self.show_pagination;
776        self.show_pagination
777    }
778
779    /// Returns the current pagination type (Arabic or Dots).
780    ///
781    /// # Returns
782    ///
783    /// The pagination type currently configured for this list.
784    ///
785    /// # Examples
786    ///
787    /// ```
788    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
789    /// # use bubbletea_widgets::paginator::Type;
790    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
791    /// let pagination_type = list.pagination_type();
792    /// ```
793    pub fn pagination_type(&self) -> paginator::Type {
794        self.paginator.paginator_type
795    }
796
797    /// Sets the pagination display type.
798    ///
799    /// This controls how pagination is rendered:
800    /// - `paginator::Type::Arabic`: Shows "1/5" style numbering
801    /// - `paginator::Type::Dots`: Shows "• ○ • ○ •" style dots
802    ///
803    /// # Arguments
804    ///
805    /// * `pagination_type` - The type of pagination to display
806    ///
807    /// # Examples
808    ///
809    /// ```
810    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
811    /// # use bubbletea_widgets::paginator::Type;
812    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
813    /// list.set_pagination_type(Type::Dots);
814    /// assert_eq!(list.pagination_type(), Type::Dots);
815    /// ```
816    pub fn set_pagination_type(&mut self, pagination_type: paginator::Type) {
817        self.paginator.paginator_type = pagination_type;
818    }
819
820    // === Item Manipulation Methods ===
821
822    /// Inserts an item at the specified index.
823    ///
824    /// All items at and after the specified index are shifted to the right.
825    /// The cursor and pagination are updated appropriately.
826    ///
827    /// # Arguments
828    ///
829    /// * `index` - The position to insert the item at
830    /// * `item` - The item to insert
831    ///
832    /// # Panics
833    ///
834    /// Panics if `index > len()`.
835    ///
836    /// # Examples
837    ///
838    /// ```
839    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
840    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
841    /// list.insert_item(0, DefaultItem::new("First", "Description"));
842    /// assert_eq!(list.len(), 1);
843    /// ```
844    pub fn insert_item(&mut self, index: usize, item: I) {
845        self.items.insert(index, item);
846        // Clear any active filter since item indices have changed
847        if self.filter_state != FilterState::Unfiltered {
848            self.filter_state = FilterState::Unfiltered;
849            self.filtered_items.clear();
850        }
851        // Update cursor if needed to maintain current selection
852        if index <= self.cursor {
853            self.cursor = self
854                .cursor
855                .saturating_add(1)
856                .min(self.items.len().saturating_sub(1));
857        }
858        self.update_pagination();
859    }
860
861    /// Removes and returns the item at the specified index.
862    ///
863    /// All items after the specified index are shifted to the left.
864    /// The cursor and pagination are updated appropriately.
865    ///
866    /// # Arguments
867    ///
868    /// * `index` - The position to remove the item from
869    ///
870    /// # Returns
871    ///
872    /// The removed item.
873    ///
874    /// # Panics
875    ///
876    /// Panics if `index >= len()`.
877    ///
878    /// # Examples
879    ///
880    /// ```
881    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
882    /// let mut list = Model::new(
883    ///     vec![DefaultItem::new("First", "Desc")],
884    ///     DefaultDelegate::new(), 80, 24
885    /// );
886    /// let removed = list.remove_item(0);
887    /// assert_eq!(list.len(), 0);
888    /// ```
889    pub fn remove_item(&mut self, index: usize) -> I {
890        if index >= self.items.len() {
891            panic!("Index out of bounds");
892        }
893
894        // Check if the item can be removed
895        if !self.delegate.can_remove(index, &self.items[index]) {
896            panic!("Item cannot be removed");
897        }
898
899        // Call the on_remove callback before removal
900        let item_ref = &self.items[index];
901        let _ = self.delegate.on_remove(index, item_ref);
902
903        let item = self.items.remove(index);
904        // Clear any active filter since item indices have changed
905        if self.filter_state != FilterState::Unfiltered {
906            self.filter_state = FilterState::Unfiltered;
907            self.filtered_items.clear();
908        }
909        // Update cursor to maintain valid position
910        if !self.items.is_empty() {
911            if index < self.cursor {
912                self.cursor = self.cursor.saturating_sub(1);
913            } else if self.cursor >= self.items.len() {
914                self.cursor = self.items.len().saturating_sub(1);
915            }
916        } else {
917            self.cursor = 0;
918        }
919        self.update_pagination();
920        item
921    }
922
923    /// Moves an item from one position to another.
924    ///
925    /// The item at `from_index` is removed and inserted at `to_index`.
926    /// The cursor is updated to follow the moved item if it was selected.
927    ///
928    /// # Arguments
929    ///
930    /// * `from_index` - The current position of the item to move
931    /// * `to_index` - The target position to move the item to
932    ///
933    /// # Panics
934    ///
935    /// Panics if either index is out of bounds.
936    ///
937    /// # Examples
938    ///
939    /// ```
940    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
941    /// let mut list = Model::new(
942    ///     vec![
943    ///         DefaultItem::new("First", "1"),
944    ///         DefaultItem::new("Second", "2"),
945    ///     ],
946    ///     DefaultDelegate::new(), 80, 24
947    /// );
948    /// list.move_item(0, 1); // Move "First" to position 1
949    /// ```
950    pub fn move_item(&mut self, from_index: usize, to_index: usize) {
951        if from_index >= self.items.len() || to_index >= self.items.len() {
952            panic!("Index out of bounds");
953        }
954        if from_index == to_index {
955            return; // No movement needed
956        }
957
958        let item = self.items.remove(from_index);
959        self.items.insert(to_index, item);
960
961        // Clear any active filter since item indices have changed
962        if self.filter_state != FilterState::Unfiltered {
963            self.filter_state = FilterState::Unfiltered;
964            self.filtered_items.clear();
965        }
966
967        // Update cursor to follow the moved item if it was selected
968        if self.cursor == from_index {
969            self.cursor = to_index;
970        } else if from_index < self.cursor && to_index >= self.cursor {
971            self.cursor = self.cursor.saturating_sub(1);
972        } else if from_index > self.cursor && to_index <= self.cursor {
973            self.cursor = self.cursor.saturating_add(1);
974        }
975
976        self.update_pagination();
977    }
978
979    /// Adds an item to the end of the list.
980    ///
981    /// This is equivalent to `insert_item(len(), item)`.
982    ///
983    /// # Arguments
984    ///
985    /// * `item` - The item to add
986    ///
987    /// # Examples
988    ///
989    /// ```
990    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
991    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
992    /// list.push_item(DefaultItem::new("New Item", "Description"));
993    /// assert_eq!(list.len(), 1);
994    /// ```
995    pub fn push_item(&mut self, item: I) {
996        self.items.push(item);
997        // Clear any active filter since item indices have changed
998        if self.filter_state != FilterState::Unfiltered {
999            self.filter_state = FilterState::Unfiltered;
1000            self.filtered_items.clear();
1001        }
1002        self.update_pagination();
1003    }
1004
1005    /// Removes and returns the last item from the list.
1006    ///
1007    /// # Returns
1008    ///
1009    /// The last item, or `None` if the list is empty.
1010    ///
1011    /// # Examples
1012    ///
1013    /// ```
1014    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1015    /// let mut list = Model::new(
1016    ///     vec![DefaultItem::new("Item", "Desc")],
1017    ///     DefaultDelegate::new(), 80, 24
1018    /// );
1019    /// let popped = list.pop_item();
1020    /// assert!(popped.is_some());
1021    /// assert_eq!(list.len(), 0);
1022    /// ```
1023    pub fn pop_item(&mut self) -> Option<I> {
1024        if self.items.is_empty() {
1025            return None;
1026        }
1027
1028        let item = self.items.pop();
1029        // Clear any active filter since item indices have changed
1030        if self.filter_state != FilterState::Unfiltered {
1031            self.filter_state = FilterState::Unfiltered;
1032            self.filtered_items.clear();
1033        }
1034        // Update cursor if it's now out of bounds
1035        if self.cursor >= self.items.len() && !self.items.is_empty() {
1036            self.cursor = self.items.len() - 1;
1037        } else if self.items.is_empty() {
1038            self.cursor = 0;
1039        }
1040        self.update_pagination();
1041        item
1042    }
1043
1044    /// Returns a reference to the underlying items collection.
1045    ///
1046    /// This provides read-only access to all items in the list,
1047    /// regardless of the current filtering state.
1048    ///
1049    /// # Returns
1050    ///
1051    /// A slice containing all items in the list.
1052    ///
1053    /// # Examples
1054    ///
1055    /// ```
1056    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1057    /// let items = vec![DefaultItem::new("First", "1"), DefaultItem::new("Second", "2")];
1058    /// let list = Model::new(items.clone(), DefaultDelegate::new(), 80, 24);
1059    /// assert_eq!(list.items().len(), 2);
1060    /// assert_eq!(list.items()[0].to_string(), items[0].to_string());
1061    /// ```
1062    pub fn items(&self) -> &[I] {
1063        &self.items
1064    }
1065
1066    /// Returns a mutable reference to the underlying items collection.
1067    ///
1068    /// This provides direct mutable access to the items. Note that after
1069    /// modifying items through this method, you should call `update_pagination()`
1070    /// to ensure pagination state remains consistent.
1071    ///
1072    /// **Warning**: Direct modification may invalidate the current filter state.
1073    /// Consider using the specific item manipulation methods instead.
1074    ///
1075    /// # Returns
1076    ///
1077    /// A mutable slice containing all items in the list.
1078    ///
1079    /// # Examples
1080    ///
1081    /// ```
1082    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1083    /// let mut list = Model::new(
1084    ///     vec![DefaultItem::new("First", "1")],
1085    ///     DefaultDelegate::new(), 80, 24
1086    /// );
1087    /// list.items_mut()[0] = DefaultItem::new("Modified", "Updated");
1088    /// assert_eq!(list.items()[0].to_string(), "Modified");
1089    /// ```
1090    pub fn items_mut(&mut self) -> &mut Vec<I> {
1091        // Clear filter state since items may be modified
1092        if self.filter_state != FilterState::Unfiltered {
1093            self.filter_state = FilterState::Unfiltered;
1094            self.filtered_items.clear();
1095        }
1096        &mut self.items
1097    }
1098
1099    /// Returns the total number of items in the list.
1100    ///
1101    /// This returns the count of all items, not just visible items.
1102    /// For visible items count, use `len()`.
1103    ///
1104    /// # Returns
1105    ///
1106    /// The total number of items in the underlying collection.
1107    ///
1108    /// # Examples
1109    ///
1110    /// ```
1111    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1112    /// let list = Model::new(
1113    ///     vec![DefaultItem::new("1", ""), DefaultItem::new("2", "")],
1114    ///     DefaultDelegate::new(), 80, 24
1115    /// );
1116    /// assert_eq!(list.items_len(), 2);
1117    /// ```
1118    pub fn items_len(&self) -> usize {
1119        self.items.len()
1120    }
1121
1122    /// Returns whether the underlying items collection is empty.
1123    ///
1124    /// This checks the total items count, not just visible items.
1125    /// For visible items check, use `is_empty()`.
1126    ///
1127    /// # Returns
1128    ///
1129    /// `true` if there are no items in the underlying collection, `false` otherwise.
1130    ///
1131    /// # Examples
1132    ///
1133    /// ```
1134    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1135    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1136    /// assert!(list.items_empty());
1137    /// ```
1138    pub fn items_empty(&self) -> bool {
1139        self.items.is_empty()
1140    }
1141
1142    // === UI Component Access and Styling ===
1143
1144    /// Returns whether the title is currently shown.
1145    ///
1146    /// # Returns
1147    ///
1148    /// `true` if the title is displayed, `false` otherwise.
1149    ///
1150    /// # Examples
1151    ///
1152    /// ```
1153    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1154    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1155    /// assert!(list.show_title()); // title is shown by default
1156    /// ```
1157    pub fn show_title(&self) -> bool {
1158        self.show_title
1159    }
1160
1161    /// Sets whether the title should be displayed.
1162    ///
1163    /// When disabled, the title section will not be rendered in the list view.
1164    ///
1165    /// # Arguments
1166    ///
1167    /// * `show` - `true` to show the title, `false` to hide it
1168    ///
1169    /// # Examples
1170    ///
1171    /// ```
1172    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1173    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1174    /// list.set_show_title(false);
1175    /// assert!(!list.show_title());
1176    /// ```
1177    pub fn set_show_title(&mut self, show: bool) {
1178        self.show_title = show;
1179    }
1180
1181    /// Toggles title display on/off.
1182    ///
1183    /// This is a convenience method that flips the current title display state.
1184    ///
1185    /// # Returns
1186    ///
1187    /// The new title display state after toggling.
1188    ///
1189    /// # Examples
1190    ///
1191    /// ```
1192    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1193    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1194    /// let new_state = list.toggle_title();
1195    /// assert_eq!(new_state, list.show_title());
1196    /// ```
1197    pub fn toggle_title(&mut self) -> bool {
1198        self.show_title = !self.show_title;
1199        self.show_title
1200    }
1201
1202    /// Returns whether the status bar is currently shown.
1203    ///
1204    /// # Returns
1205    ///
1206    /// `true` if the status bar is displayed, `false` otherwise.
1207    ///
1208    /// # Examples
1209    ///
1210    /// ```
1211    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1212    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1213    /// assert!(list.show_status_bar()); // status bar is shown by default
1214    /// ```
1215    pub fn show_status_bar(&self) -> bool {
1216        self.show_status_bar
1217    }
1218
1219    /// Sets whether the status bar should be displayed.
1220    ///
1221    /// When disabled, the status bar section will not be rendered in the list view.
1222    ///
1223    /// # Arguments
1224    ///
1225    /// * `show` - `true` to show the status bar, `false` to hide it
1226    ///
1227    /// # Examples
1228    ///
1229    /// ```
1230    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1231    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1232    /// list.set_show_status_bar(false);
1233    /// assert!(!list.show_status_bar());
1234    /// ```
1235    pub fn set_show_status_bar(&mut self, show: bool) {
1236        self.show_status_bar = show;
1237    }
1238
1239    /// Toggles status bar display on/off.
1240    ///
1241    /// This is a convenience method that flips the current status bar display state.
1242    ///
1243    /// # Returns
1244    ///
1245    /// The new status bar display state after toggling.
1246    ///
1247    /// # Examples
1248    ///
1249    /// ```
1250    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1251    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1252    /// let new_state = list.toggle_status_bar();
1253    /// assert_eq!(new_state, list.show_status_bar());
1254    /// ```
1255    pub fn toggle_status_bar(&mut self) -> bool {
1256        self.show_status_bar = !self.show_status_bar;
1257        self.show_status_bar
1258    }
1259
1260    /// Returns whether the spinner is currently shown.
1261    ///
1262    /// # Returns
1263    ///
1264    /// `true` if the spinner is displayed, `false` otherwise.
1265    ///
1266    /// # Examples
1267    ///
1268    /// ```
1269    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1270    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1271    /// assert!(!list.show_spinner()); // spinner is hidden by default
1272    /// ```
1273    pub fn show_spinner(&self) -> bool {
1274        self.show_spinner
1275    }
1276
1277    /// Sets whether the spinner should be displayed.
1278    ///
1279    /// When enabled, the spinner will be rendered as part of the list view,
1280    /// typically to indicate loading state.
1281    ///
1282    /// # Arguments
1283    ///
1284    /// * `show` - `true` to show the spinner, `false` to hide it
1285    ///
1286    /// # Examples
1287    ///
1288    /// ```
1289    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1290    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1291    /// list.set_show_spinner(true);
1292    /// assert!(list.show_spinner());
1293    /// ```
1294    pub fn set_show_spinner(&mut self, show: bool) {
1295        self.show_spinner = show;
1296    }
1297
1298    /// Toggles spinner display on/off.
1299    ///
1300    /// This is a convenience method that flips the current spinner display state.
1301    ///
1302    /// # Returns
1303    ///
1304    /// The new spinner display state after toggling.
1305    ///
1306    /// # Examples
1307    ///
1308    /// ```
1309    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1310    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1311    /// let new_state = list.toggle_spinner();
1312    /// assert_eq!(new_state, list.show_spinner());
1313    /// ```
1314    pub fn toggle_spinner(&mut self) -> bool {
1315        self.show_spinner = !self.show_spinner;
1316        self.show_spinner
1317    }
1318
1319    /// Returns a reference to the spinner model.
1320    ///
1321    /// This allows access to the underlying spinner for customization
1322    /// and state management.
1323    ///
1324    /// # Returns
1325    ///
1326    /// A reference to the spinner model.
1327    ///
1328    /// # Examples
1329    ///
1330    /// ```
1331    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1332    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1333    /// let spinner = list.spinner();
1334    /// ```
1335    pub fn spinner(&self) -> &spinner::Model {
1336        &self.spinner
1337    }
1338
1339    /// Returns a mutable reference to the spinner model.
1340    ///
1341    /// This allows modification of the underlying spinner for customization
1342    /// and state management.
1343    ///
1344    /// # Returns
1345    ///
1346    /// A mutable reference to the spinner model.
1347    ///
1348    /// # Examples
1349    ///
1350    /// ```
1351    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1352    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1353    /// let spinner = list.spinner_mut();
1354    /// ```
1355    pub fn spinner_mut(&mut self) -> &mut spinner::Model {
1356        &mut self.spinner
1357    }
1358
1359    /// Returns whether the help is currently shown.
1360    ///
1361    /// # Returns
1362    ///
1363    /// `true` if help is displayed, `false` otherwise.
1364    ///
1365    /// # Examples
1366    ///
1367    /// ```
1368    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1369    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1370    /// assert!(!list.show_help()); // help is hidden by default
1371    /// ```
1372    pub fn show_help(&self) -> bool {
1373        self.show_help
1374    }
1375
1376    /// Sets whether help should be displayed.
1377    ///
1378    /// When enabled, help text will be rendered as part of the list view,
1379    /// showing available key bindings and controls.
1380    ///
1381    /// # Arguments
1382    ///
1383    /// * `show` - `true` to show help, `false` to hide it
1384    ///
1385    /// # Examples
1386    ///
1387    /// ```
1388    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1389    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1390    /// list.set_show_help(true);
1391    /// assert!(list.show_help());
1392    /// ```
1393    pub fn set_show_help(&mut self, show: bool) {
1394        self.show_help = show;
1395    }
1396
1397    /// Toggles help display on/off.
1398    ///
1399    /// This is a convenience method that flips the current help display state.
1400    ///
1401    /// # Returns
1402    ///
1403    /// The new help display state after toggling.
1404    ///
1405    /// # Examples
1406    ///
1407    /// ```
1408    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1409    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1410    /// let new_state = list.toggle_help();
1411    /// assert_eq!(new_state, list.show_help());
1412    /// ```
1413    pub fn toggle_help(&mut self) -> bool {
1414        self.show_help = !self.show_help;
1415        self.show_help
1416    }
1417
1418    /// Returns a reference to the help model.
1419    ///
1420    /// This allows access to the underlying help system for customization
1421    /// and state management.
1422    ///
1423    /// # Returns
1424    ///
1425    /// A reference to the help model.
1426    ///
1427    /// # Examples
1428    ///
1429    /// ```
1430    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1431    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1432    /// let help = list.help();
1433    /// ```
1434    pub fn help(&self) -> &help::Model {
1435        &self.help
1436    }
1437
1438    /// Returns a mutable reference to the help model.
1439    ///
1440    /// This allows modification of the underlying help system for customization
1441    /// and state management.
1442    ///
1443    /// # Returns
1444    ///
1445    /// A mutable reference to the help model.
1446    ///
1447    /// # Examples
1448    ///
1449    /// ```
1450    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1451    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1452    /// let help = list.help_mut();
1453    /// ```
1454    pub fn help_mut(&mut self) -> &mut help::Model {
1455        &mut self.help
1456    }
1457
1458    /// Returns a reference to the list's styling configuration.
1459    ///
1460    /// This provides read-only access to all visual styles used by the list,
1461    /// including title, item, status bar, pagination, and help styles.
1462    ///
1463    /// # Returns
1464    ///
1465    /// A reference to the `ListStyles` configuration.
1466    ///
1467    /// # Examples
1468    ///
1469    /// ```
1470    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1471    /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1472    /// let styles = list.styles();
1473    /// // Access specific styles, e.g., styles.title, styles.pagination_style
1474    /// ```
1475    pub fn styles(&self) -> &ListStyles {
1476        &self.styles
1477    }
1478
1479    /// Returns a mutable reference to the list's styling configuration.
1480    ///
1481    /// This provides direct mutable access to all visual styles used by the list.
1482    /// Changes to styles take effect immediately on the next render.
1483    ///
1484    /// # Returns
1485    ///
1486    /// A mutable reference to the `ListStyles` configuration.
1487    ///
1488    /// # Examples
1489    ///
1490    /// ```
1491    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1492    /// # use lipgloss_extras::prelude::*;
1493    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1494    /// let styles = list.styles_mut();
1495    /// styles.title = Style::new().foreground("#FF0000"); // Red title
1496    /// ```
1497    pub fn styles_mut(&mut self) -> &mut ListStyles {
1498        &mut self.styles
1499    }
1500
1501    /// Sets the list's styling configuration.
1502    ///
1503    /// This replaces all current styles with the provided configuration.
1504    /// Changes take effect immediately on the next render.
1505    ///
1506    /// # Arguments
1507    ///
1508    /// * `styles` - The new styling configuration to apply
1509    ///
1510    /// # Examples
1511    ///
1512    /// ```
1513    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1514    /// # use bubbletea_widgets::list::style::ListStyles;
1515    /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1516    /// let custom_styles = ListStyles::default();
1517    /// list.set_styles(custom_styles);
1518    /// ```
1519    pub fn set_styles(&mut self, styles: ListStyles) {
1520        // Update paginator dots from styled strings
1521        self.paginator.active_dot = styles.active_pagination_dot.render("");
1522        self.paginator.inactive_dot = styles.inactive_pagination_dot.render("");
1523        self.styles = styles;
1524    }
1525
1526    /// Renders the status bar as a formatted string.
1527    ///
1528    /// The status bar shows the current selection position and total item count,
1529    /// using the custom item names if set. The format is "X/Y items" where X is
1530    /// the current cursor position + 1, and Y is the total item count.
1531    ///
1532    /// # Returns
1533    ///
1534    /// A formatted status string, or empty string if status bar is disabled.
1535    ///
1536    /// # Examples
1537    ///
1538    /// ```
1539    /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1540    /// let items = vec![
1541    ///     DefaultItem::new("First", ""),
1542    ///     DefaultItem::new("Second", ""),
1543    /// ];
1544    /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
1545    /// let status = list.status_view();
1546    /// assert!(status.contains("1/2"));
1547    /// ```
1548    pub fn status_view(&self) -> String {
1549        if !self.show_status_bar {
1550            return String::new();
1551        }
1552
1553        let mut footer = String::new();
1554        if !self.is_empty() {
1555            let singular = self.status_item_singular.as_deref().unwrap_or("item");
1556            let plural = self.status_item_plural.as_deref().unwrap_or("items");
1557            let noun = if self.len() == 1 { singular } else { plural };
1558            footer.push_str(&format!("{}/{} {}", self.cursor + 1, self.len(), noun));
1559        }
1560        let help_view = self.help.view(self);
1561        if !help_view.is_empty() {
1562            footer.push('\n');
1563            footer.push_str(&help_view);
1564        }
1565        footer
1566    }
1567
1568    // === Advanced Filtering API ===
1569}