bubbletea_widgets/
paginator.rs

1//! A paginator component for bubbletea-rs, ported from the Go version.
2//!
3//! This component is used for calculating pagination and rendering pagination info.
4//! Note that this package does not render actual pages of content; it's purely
5//! for handling the state and view of the pagination control itself.
6
7use crate::key::{self, KeyMap as KeyMapTrait};
8use bubbletea_rs::{KeyMsg, Msg};
9
10/// The type of pagination to display.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Type {
13    /// Display pagination as Arabic numerals (e.g., "1/5").
14    #[default]
15    Arabic,
16    /// Display pagination as dots (e.g., "● ○ ○ ○ ○").
17    Dots,
18}
19
20/// Key bindings for different actions within the paginator.
21///
22/// This structure defines the key bindings that control pagination navigation.
23/// It implements the `KeyMap` trait to provide help information for the
24/// paginator component.
25///
26/// # Examples
27///
28/// ```rust
29/// use bubbletea_widgets::paginator::PaginatorKeyMap;
30/// use bubbletea_widgets::key;
31///
32/// let keymap = PaginatorKeyMap::default();
33///
34/// // Create custom key bindings
35/// let custom_keymap = PaginatorKeyMap {
36///     prev_page: key::new_binding(vec![
37///         key::with_keys_str(&["a", "left"]),
38///         key::with_help("a/←", "previous page"),
39///     ]),
40///     next_page: key::new_binding(vec![
41///         key::with_keys_str(&["d", "right"]),
42///         key::with_help("d/→", "next page"),
43///     ]),
44/// };
45/// ```
46#[derive(Debug, Clone)]
47pub struct PaginatorKeyMap {
48    /// Key binding for navigating to the previous page.
49    /// Default keys: PageUp, Left Arrow, 'h'
50    pub prev_page: key::Binding,
51    /// Key binding for navigating to the next page.
52    /// Default keys: PageDown, Right Arrow, 'l'
53    pub next_page: key::Binding,
54}
55
56impl Default for PaginatorKeyMap {
57    /// Creates default key bindings for paginator navigation.
58    ///
59    /// The default key bindings are:
60    /// - **Previous page**: PageUp, Left Arrow, 'h'
61    /// - **Next page**: PageDown, Right Arrow, 'l'
62    ///
63    /// These bindings are commonly used in terminal applications and provide
64    /// both arrow key navigation and vim-style 'h'/'l' keys.
65    ///
66    /// # Examples
67    ///
68    /// ```rust
69    /// use bubbletea_widgets::paginator::PaginatorKeyMap;
70    /// use bubbletea_widgets::key::KeyMap;
71    ///
72    /// let keymap = PaginatorKeyMap::default();
73    /// let help = keymap.short_help();
74    /// assert_eq!(help.len(), 2); // prev and next bindings
75    /// ```
76    fn default() -> Self {
77        Self {
78            prev_page: key::new_binding(vec![
79                key::with_keys_str(&["pgup", "left", "h"]),
80                key::with_help("←/h", "prev page"),
81            ]),
82            next_page: key::new_binding(vec![
83                key::with_keys_str(&["pgdown", "right", "l"]),
84                key::with_help("→/l", "next page"),
85            ]),
86        }
87    }
88}
89
90impl KeyMapTrait for PaginatorKeyMap {
91    /// Returns key bindings for the short help view.
92    ///
93    /// This provides the essential pagination key bindings that will be
94    /// displayed in compact help views.
95    ///
96    /// # Returns
97    ///
98    /// A vector containing references to the previous page and next page bindings.
99    fn short_help(&self) -> Vec<&key::Binding> {
100        vec![&self.prev_page, &self.next_page]
101    }
102
103    /// Returns key bindings for the full help view.
104    ///
105    /// This organizes all pagination key bindings into columns for display
106    /// in expanded help views. Since pagination only has two keys, they're
107    /// grouped together in a single column.
108    ///
109    /// # Returns
110    ///
111    /// A vector of vectors, where each inner vector represents a column
112    /// of related key bindings.
113    fn full_help(&self) -> Vec<Vec<&key::Binding>> {
114        vec![vec![&self.prev_page, &self.next_page]]
115    }
116}
117
118/// A paginator model for handling pagination state and rendering.
119///
120/// This component manages pagination state including current page, total pages,
121/// and pagination display style. It can render pagination in two modes:
122/// - **Arabic**: Shows page numbers (e.g., "3/10")
123/// - **Dots**: Shows dots representing pages (e.g., "○ ○ ● ○ ○")
124///
125/// The paginator handles key bindings for navigation and provides helper methods
126/// for calculating slice bounds and page information.
127///
128/// # Examples
129///
130/// ## Basic Usage
131///
132/// ```rust
133/// use bubbletea_widgets::paginator::{Model, Type};
134///
135/// let mut paginator = Model::new()
136///     .with_per_page(10)
137///     .with_total_items(150); // Creates 15 pages
138///
139/// assert_eq!(paginator.total_pages, 15);
140/// assert!(paginator.on_first_page());
141///
142/// paginator.next_page();
143/// assert_eq!(paginator.page, 1);
144/// ```
145///
146/// ## Different Display Types
147///
148/// ```rust
149/// use bubbletea_widgets::paginator::{Model, Type};
150///
151/// let mut paginator = Model::new()
152///     .with_total_items(50)
153///     .with_per_page(10);
154///
155/// // Arabic mode (default): "1/5"
156/// paginator.paginator_type = Type::Arabic;
157/// let arabic_view = paginator.view();
158///
159/// // Dots mode: "● ○ ○ ○ ○"
160/// paginator.paginator_type = Type::Dots;
161/// let dots_view = paginator.view();
162/// ```
163///
164/// ## Integration with bubbletea-rs
165///
166/// ```rust
167/// use bubbletea_widgets::paginator::Model as Paginator;
168/// use bubbletea_rs::{Model, Cmd, Msg};
169///
170/// struct App {
171///     paginator: Paginator,
172///     items: Vec<String>,
173/// }
174///
175/// impl Model for App {
176///     fn init() -> (Self, Option<Cmd>) {
177///         let items: Vec<String> = (1..=100).map(|i| format!("Item {}", i)).collect();
178///         let paginator = Paginator::new()
179///             .with_per_page(10)
180///             .with_total_items(items.len());
181///             
182///         (Self { paginator, items }, None)
183///     }
184///
185///     fn update(&mut self, msg: Msg) -> Option<Cmd> {
186///         self.paginator.update(&msg);
187///         None
188///     }
189///
190///     fn view(&self) -> String {
191///         let (start, end) = self.paginator.get_slice_bounds(self.items.len());
192///         let page_items: Vec<String> = self.items[start..end].to_vec();
193///         
194///         format!(
195///             "Items:\n{}\n\nPage: {}",
196///             page_items.join("\n"),
197///             self.paginator.view()
198///         )
199///     }
200/// }
201/// ```
202#[derive(Debug, Clone)]
203pub struct Model {
204    /// The type of pagination to display (Dots or Arabic).
205    pub paginator_type: Type,
206    /// The current page.
207    pub page: usize,
208    /// The number of items per page.
209    pub per_page: usize,
210    /// The total number of pages.
211    pub total_pages: usize,
212
213    /// The character to use for the active page in Dots mode.
214    pub active_dot: String,
215    /// The character to use for inactive pages in Dots mode.
216    pub inactive_dot: String,
217    /// The format string for Arabic mode (e.g., "%d/%d").
218    pub arabic_format: String,
219
220    /// Key bindings.
221    pub keymap: PaginatorKeyMap,
222}
223
224impl Default for Model {
225    /// Creates a paginator with default settings.
226    ///
227    /// Default configuration:
228    /// - Type: Arabic ("1/5" style)
229    /// - Current page: 0 (first page)
230    /// - Items per page: 1
231    /// - Total pages: 1
232    /// - Active dot: "•" (for dots mode)
233    /// - Inactive dot: "○" (for dots mode)
234    /// - Arabic format: "%d/%d" (current/total)
235    /// - Default key bindings
236    ///
237    /// # Examples
238    ///
239    /// ```rust
240    /// use bubbletea_widgets::paginator::{Model, Type};
241    ///
242    /// let paginator = Model::default();
243    /// assert_eq!(paginator.paginator_type, Type::Arabic);
244    /// assert_eq!(paginator.page, 0);
245    /// assert_eq!(paginator.per_page, 1);
246    /// assert_eq!(paginator.total_pages, 1);
247    /// ```
248    fn default() -> Self {
249        Self {
250            paginator_type: Type::default(),
251            page: 0,
252            per_page: 1,
253            total_pages: 1,
254            active_dot: "•".to_string(),
255            inactive_dot: "○".to_string(),
256            arabic_format: "%d/%d".to_string(),
257            keymap: PaginatorKeyMap::default(),
258        }
259    }
260}
261
262impl Model {
263    /// Creates a new paginator model with default settings.
264    ///
265    /// This is equivalent to calling `Model::default()` but provides a more
266    /// conventional constructor-style API.
267    ///
268    /// # Examples
269    ///
270    /// ```rust
271    /// use bubbletea_widgets::paginator::Model;
272    ///
273    /// let paginator = Model::new();
274    /// assert_eq!(paginator.page, 0);
275    /// assert_eq!(paginator.total_pages, 1);
276    /// ```
277    pub fn new() -> Self {
278        Self::default()
279    }
280
281    /// Sets the total number of items and calculates total pages (builder pattern).
282    ///
283    /// This method automatically calculates the total number of pages based on
284    /// the total items and the current `per_page` setting. If the current page
285    /// becomes out of bounds, it will be adjusted to the last valid page.
286    ///
287    /// # Arguments
288    ///
289    /// * `items` - The total number of items to paginate
290    ///
291    /// # Examples
292    ///
293    /// ```rust
294    /// use bubbletea_widgets::paginator::Model;
295    ///
296    /// let paginator = Model::new()
297    ///     .with_per_page(10)
298    ///     .with_total_items(95); // Will create 10 pages (95/10 = 9.5 -> 10)
299    ///
300    /// assert_eq!(paginator.total_pages, 10);
301    /// ```
302    pub fn with_total_items(mut self, items: usize) -> Self {
303        self.set_total_items(items);
304        self
305    }
306
307    /// Sets the number of items per page (builder pattern).
308    ///
309    /// The minimum value is 1; any value less than 1 will be clamped to 1.
310    /// This setting affects how total pages are calculated when using
311    /// `set_total_items()` or `with_total_items()`.
312    ///
313    /// # Arguments
314    ///
315    /// * `per_page` - Number of items to display per page (minimum 1)
316    ///
317    /// # Examples
318    ///
319    /// ```rust
320    /// use bubbletea_widgets::paginator::Model;
321    ///
322    /// let paginator = Model::new()
323    ///     .with_per_page(25)
324    ///     .with_total_items(100); // Will create 4 pages
325    ///
326    /// assert_eq!(paginator.per_page, 25);
327    /// assert_eq!(paginator.total_pages, 4);
328    ///
329    /// // Values less than 1 are clamped to 1
330    /// let clamped = Model::new().with_per_page(0);
331    /// assert_eq!(clamped.per_page, 1);
332    /// ```
333    pub fn with_per_page(mut self, per_page: usize) -> Self {
334        self.per_page = per_page.max(1);
335        self
336    }
337
338    /// Sets the number of items per page (mutable version).
339    ///
340    /// The minimum value is 1; any value less than 1 will be clamped to 1.
341    /// This method modifies the paginator in place.
342    ///
343    /// # Arguments
344    ///
345    /// * `per_page` - Number of items to display per page (minimum 1)
346    ///
347    /// # Examples
348    ///
349    /// ```rust
350    /// use bubbletea_widgets::paginator::Model;
351    ///
352    /// let mut paginator = Model::new();
353    /// paginator.set_per_page(15);
354    /// assert_eq!(paginator.per_page, 15);
355    ///
356    /// // Values less than 1 are clamped to 1
357    /// paginator.set_per_page(0);
358    /// assert_eq!(paginator.per_page, 1);
359    /// ```
360    pub fn set_per_page(&mut self, per_page: usize) {
361        self.per_page = per_page.max(1);
362    }
363
364    /// Sets the active dot character for dots mode (builder pattern).
365    ///
366    /// # Arguments
367    ///
368    /// * `dot` - The character or styled string to use for the active page
369    ///
370    /// # Examples
371    ///
372    /// ```rust
373    /// use bubbletea_widgets::paginator::Model;
374    ///
375    /// let paginator = Model::new().with_active_dot("●");
376    /// assert_eq!(paginator.active_dot, "●");
377    /// ```
378    pub fn with_active_dot(mut self, dot: &str) -> Self {
379        self.active_dot = dot.to_string();
380        self
381    }
382
383    /// Sets the inactive dot character for dots mode (builder pattern).
384    ///
385    /// # Arguments
386    ///
387    /// * `dot` - The character or styled string to use for inactive pages
388    ///
389    /// # Examples
390    ///
391    /// ```rust
392    /// use bubbletea_widgets::paginator::Model;
393    ///
394    /// let paginator = Model::new().with_inactive_dot("○");
395    /// assert_eq!(paginator.inactive_dot, "○");
396    /// ```
397    pub fn with_inactive_dot(mut self, dot: &str) -> Self {
398        self.inactive_dot = dot.to_string();
399        self
400    }
401
402    /// Sets the active dot character for dots mode (mutable version).
403    ///
404    /// # Arguments
405    ///
406    /// * `dot` - The character or styled string to use for the active page
407    ///
408    /// # Examples
409    ///
410    /// ```rust
411    /// use bubbletea_widgets::paginator::Model;
412    ///
413    /// let mut paginator = Model::new();
414    /// paginator.set_active_dot("●");
415    /// assert_eq!(paginator.active_dot, "●");
416    /// ```
417    pub fn set_active_dot(&mut self, dot: &str) {
418        self.active_dot = dot.to_string();
419    }
420
421    /// Sets the inactive dot character for dots mode (mutable version).
422    ///
423    /// # Arguments
424    ///
425    /// * `dot` - The character or styled string to use for inactive pages
426    ///
427    /// # Examples
428    ///
429    /// ```rust
430    /// use bubbletea_widgets::paginator::Model;
431    ///
432    /// let mut paginator = Model::new();
433    /// paginator.set_inactive_dot("○");
434    /// assert_eq!(paginator.inactive_dot, "○");
435    /// ```
436    pub fn set_inactive_dot(&mut self, dot: &str) {
437        self.inactive_dot = dot.to_string();
438    }
439
440    /// Sets the total number of pages directly.
441    ///
442    /// The minimum value is 1; any value less than 1 will be clamped to 1.
443    /// If the current page becomes out of bounds after setting the total pages,
444    /// it will be adjusted to the last valid page.
445    ///
446    /// **Note**: This method sets pages directly. If you want to calculate pages
447    /// based on total items, use `set_total_items()` instead.
448    ///
449    /// # Arguments
450    ///
451    /// * `pages` - The total number of pages (minimum 1)
452    ///
453    /// # Examples
454    ///
455    /// ```rust
456    /// use bubbletea_widgets::paginator::Model;
457    ///
458    /// let mut paginator = Model::new();
459    /// paginator.set_total_pages(10);
460    /// assert_eq!(paginator.total_pages, 10);
461    ///
462    /// // If current page is out of bounds, it gets adjusted
463    /// paginator.page = 15; // Out of bounds
464    /// paginator.set_total_pages(5);
465    /// assert_eq!(paginator.page, 4); // Adjusted to last page (0-indexed)
466    /// ```
467    pub fn set_total_pages(&mut self, pages: usize) {
468        self.total_pages = pages.max(1);
469        // Ensure the current page is not out of bounds
470        if self.page >= self.total_pages {
471            self.page = self.total_pages.saturating_sub(1);
472        }
473    }
474
475    /// Calculates and sets the total number of pages based on the total items.
476    ///
477    /// This method divides the total number of items by the current `per_page`
478    /// setting to calculate the total pages. The result is always at least 1,
479    /// even for 0 items. If the current page becomes out of bounds after
480    /// recalculation, it will be adjusted to the last valid page.
481    ///
482    /// # Arguments
483    ///
484    /// * `items` - The total number of items to paginate
485    ///
486    /// # Examples
487    ///
488    /// ```rust
489    /// use bubbletea_widgets::paginator::Model;
490    ///
491    /// let mut paginator = Model::new().with_per_page(10);
492    ///
493    /// // 95 items with 10 per page = 10 pages (95/10 = 9.5 -> 10)
494    /// paginator.set_total_items(95);
495    /// assert_eq!(paginator.total_pages, 10);
496    ///
497    /// // 0 items still results in 1 page minimum
498    /// paginator.set_total_items(0);
499    /// assert_eq!(paginator.total_pages, 1);
500    ///
501    /// // Exact division
502    /// paginator.set_total_items(100);
503    /// assert_eq!(paginator.total_pages, 10);
504    /// ```
505    pub fn set_total_items(&mut self, items: usize) {
506        if items == 0 {
507            self.total_pages = 1;
508        } else {
509            self.total_pages = items.div_ceil(self.per_page);
510        }
511
512        // Ensure the current page is not out of bounds
513        if self.page >= self.total_pages {
514            self.page = self.total_pages.saturating_sub(1);
515        }
516    }
517
518    /// Returns the number of items on the current page.
519    ///
520    /// This method calculates how many items are actually present on the
521    /// current page, which may be less than `per_page` on the last page
522    /// or when there are fewer total items than `per_page`.
523    ///
524    /// # Arguments
525    ///
526    /// * `total_items` - The total number of items being paginated
527    ///
528    /// # Returns
529    ///
530    /// The number of items on the current page, or 0 if there are no items.
531    ///
532    /// # Examples
533    ///
534    /// ```rust
535    /// use bubbletea_widgets::paginator::Model;
536    ///
537    /// let mut paginator = Model::new().with_per_page(10);
538    ///
539    /// // Full page
540    /// assert_eq!(paginator.items_on_page(100), 10);
541    ///
542    /// // Partial last page
543    /// paginator.page = 9; // Last page (0-indexed)
544    /// assert_eq!(paginator.items_on_page(95), 5); // Only 5 items on page 10
545    ///
546    /// // No items
547    /// assert_eq!(paginator.items_on_page(0), 0);
548    /// ```
549    pub fn items_on_page(&self, total_items: usize) -> usize {
550        if total_items == 0 {
551            return 0;
552        }
553        let (start, end) = self.get_slice_bounds(total_items);
554        end - start
555    }
556
557    /// Calculates slice bounds for the current page.
558    ///
559    /// This is a helper function for paginating slices. Given the total length
560    /// of your data, it returns the start and end indices for the current page.
561    /// The returned bounds can be used directly with slice notation.
562    ///
563    /// # Arguments
564    ///
565    /// * `length` - The total length of the data being paginated
566    ///
567    /// # Returns
568    ///
569    /// A tuple `(start, end)` where:
570    /// - `start` is the inclusive start index for the current page
571    /// - `end` is the exclusive end index for the current page
572    ///
573    /// # Examples
574    ///
575    /// ```rust
576    /// use bubbletea_widgets::paginator::Model;
577    ///
578    /// let items: Vec<i32> = (1..=100).collect();
579    /// let mut paginator = Model::new().with_per_page(10);
580    ///
581    /// // First page (0)
582    /// let (start, end) = paginator.get_slice_bounds(items.len());
583    /// assert_eq!((start, end), (0, 10));
584    /// let page_items = &items[start..end]; // Items 1-10
585    ///
586    /// // Third page (2)
587    /// paginator.page = 2;
588    /// let (start, end) = paginator.get_slice_bounds(items.len());
589    /// assert_eq!((start, end), (20, 30));
590    /// let page_items = &items[start..end]; // Items 21-30
591    /// ```
592    pub fn get_slice_bounds(&self, length: usize) -> (usize, usize) {
593        let start = self.page * self.per_page;
594        let end = (start + self.per_page).min(length);
595        (start, end)
596    }
597
598    /// Returns slice bounds assuming maximum possible data length.
599    ///
600    /// This is a convenience method that calls `get_slice_bounds()` with
601    /// the maximum possible data length (`per_page * total_pages`). It's
602    /// useful when you know your data exactly fills the pagination structure.
603    ///
604    /// # Returns
605    ///
606    /// A tuple `(start, end)` representing slice bounds for the current page.
607    ///
608    /// # Examples
609    ///
610    /// ```rust
611    /// use bubbletea_widgets::paginator::Model;
612    ///
613    /// let mut paginator = Model::new()
614    ///     .with_per_page(10)
615    ///     .with_total_items(100); // Exactly 10 pages
616    ///
617    /// paginator.page = 3;
618    /// let (start, end) = paginator.start_index_end_index();
619    /// assert_eq!((start, end), (30, 40));
620    /// ```
621    pub fn start_index_end_index(&self) -> (usize, usize) {
622        self.get_slice_bounds(self.per_page * self.total_pages)
623    }
624
625    /// Navigates to the previous page.
626    ///
627    /// If the paginator is already on the first page (page 0), this method
628    /// has no effect. The page number will not go below 0.
629    ///
630    /// # Examples
631    ///
632    /// ```rust
633    /// use bubbletea_widgets::paginator::Model;
634    ///
635    /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
636    /// paginator.page = 5;
637    ///
638    /// paginator.prev_page();
639    /// assert_eq!(paginator.page, 4);
640    ///
641    /// // Won't go below 0
642    /// paginator.page = 0;
643    /// paginator.prev_page();
644    /// assert_eq!(paginator.page, 0);
645    /// ```
646    pub fn prev_page(&mut self) {
647        if self.page > 0 {
648            self.page -= 1;
649        }
650    }
651
652    /// Navigates to the next page.
653    ///
654    /// If the paginator is already on the last page, this method has no effect.
655    /// The page number will not exceed `total_pages - 1`.
656    ///
657    /// # Examples
658    ///
659    /// ```rust
660    /// use bubbletea_widgets::paginator::Model;
661    ///
662    /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
663    /// // total_pages = 10, so last page is 9 (0-indexed)
664    ///
665    /// paginator.page = 5;
666    /// paginator.next_page();
667    /// assert_eq!(paginator.page, 6);
668    ///
669    /// // Won't go beyond last page  
670    /// paginator.page = 8; // Second to last page
671    /// paginator.next_page();
672    /// assert_eq!(paginator.page, 9); // Should go to last page (9 is the last valid page)
673    /// paginator.next_page();
674    /// assert_eq!(paginator.page, 9); // Should stay at last page
675    /// ```
676    pub fn next_page(&mut self) {
677        if !self.on_last_page() {
678            self.page += 1;
679        }
680    }
681
682    /// Returns true if the paginator is on the first page.
683    ///
684    /// The first page is always page 0 in the 0-indexed pagination system.
685    ///
686    /// # Examples
687    ///
688    /// ```rust
689    /// use bubbletea_widgets::paginator::Model;
690    ///
691    /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
692    ///
693    /// assert!(paginator.on_first_page());
694    ///
695    /// paginator.next_page();
696    /// assert!(!paginator.on_first_page());
697    /// ```
698    pub fn on_first_page(&self) -> bool {
699        self.page == 0
700    }
701
702    /// Returns true if the paginator is on the last page.
703    ///
704    /// The last page is `total_pages - 1` in the 0-indexed pagination system.
705    ///
706    /// # Examples
707    ///
708    /// ```rust
709    /// use bubbletea_widgets::paginator::Model;
710    ///
711    /// let mut paginator = Model::new().with_per_page(10).with_total_items(90);
712    /// // Creates 9 pages (0-8), so last page is 8
713    ///
714    /// assert!(!paginator.on_last_page());
715    ///
716    /// paginator.page = 8; // Last page  
717    /// assert!(paginator.on_last_page());
718    /// ```
719    pub fn on_last_page(&self) -> bool {
720        self.page == self.total_pages.saturating_sub(1)
721    }
722
723    /// Updates the paginator based on received messages.
724    ///
725    /// This method should be called from your application's `update()` method
726    /// to handle pagination key presses. It automatically responds to the
727    /// configured key bindings for next/previous page navigation.
728    ///
729    /// # Arguments
730    ///
731    /// * `msg` - The message to process, typically containing key press events
732    ///
733    /// # Examples
734    ///
735    /// ```rust
736    /// use bubbletea_widgets::paginator::Model as Paginator;
737    /// use bubbletea_rs::{Model, Msg};
738    ///
739    /// struct App {
740    ///     paginator: Paginator,
741    /// }
742    ///
743    /// impl Model for App {
744    ///     fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
745    ///         // Forward messages to paginator
746    ///         self.paginator.update(&msg);
747    ///         None
748    ///     }
749    ///     
750    ///     // ... other methods
751    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (Self { paginator: Paginator::new() }, None) }
752    /// #   fn view(&self) -> String { String::new() }
753    /// }
754    /// ```
755    pub fn update(&mut self, msg: &Msg) {
756        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
757            if self.keymap.next_page.matches(key_msg) {
758                self.next_page();
759            } else if self.keymap.prev_page.matches(key_msg) {
760                self.prev_page();
761            }
762        }
763    }
764
765    /// Renders the paginator as a string.
766    ///
767    /// The output format depends on the `paginator_type` setting:
768    /// - **Arabic**: Shows "current/total" (e.g., "3/10")
769    /// - **Dots**: Shows dots with active page highlighted (e.g., "○ ○ ● ○ ○")
770    ///
771    /// # Returns
772    ///
773    /// A string representation of the current pagination state.
774    ///
775    /// # Examples
776    ///
777    /// ```rust
778    /// use bubbletea_widgets::paginator::{Model, Type};
779    ///
780    /// let mut paginator = Model::new().with_per_page(10).with_total_items(50);
781    /// // Creates 5 pages, currently on page 0
782    ///
783    /// // Arabic mode (default)
784    /// paginator.paginator_type = Type::Arabic;
785    /// assert_eq!(paginator.view(), "1/5"); // 1-indexed for display
786    ///
787    /// // Dots mode  
788    /// paginator.paginator_type = Type::Dots;
789    /// assert_eq!(paginator.view(), "•○○○○"); // Active page shows filled bullet, others are hollow
790    ///
791    /// // Move to page 2
792    /// paginator.page = 2;
793    /// assert_eq!(paginator.view(), "○○•○○"); // Third bullet filled (active page), others hollow
794    /// ```
795    pub fn view(&self) -> String {
796        match self.paginator_type {
797            Type::Arabic => self.arabic_view(),
798            Type::Dots => self.dots_view(),
799        }
800    }
801
802    fn arabic_view(&self) -> String {
803        self.arabic_format
804            .replacen("%d", &(self.page + 1).to_string(), 1)
805            .replacen("%d", &self.total_pages.to_string(), 1)
806    }
807
808    fn dots_view(&self) -> String {
809        let mut s = String::new();
810        for i in 0..self.total_pages {
811            if i == self.page {
812                s.push_str(&self.active_dot);
813            } else {
814                s.push_str(&self.inactive_dot);
815            }
816            // Remove spacing between dots to match Go version (••••)
817            // Go version shows compact dots without spaces
818        }
819        s
820    }
821}