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}