1use crate::key::{Binding, matches};
26use bubbletea::{Cmd, KeyMsg, Message, Model};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum Type {
31 #[default]
33 Arabic,
34 Dots,
36}
37
38#[derive(Debug, Clone)]
40pub struct KeyMap {
41 pub prev_page: Binding,
43 pub next_page: Binding,
45}
46
47impl Default for KeyMap {
48 fn default() -> Self {
49 Self {
50 prev_page: Binding::new()
51 .keys(&["pgup", "left", "h"])
52 .help("←/h", "prev page"),
53 next_page: Binding::new()
54 .keys(&["pgdown", "right", "l"])
55 .help("→/l", "next page"),
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct Paginator {
63 pub display_type: Type,
65 page: usize,
67 per_page: usize,
69 total_pages: usize,
71 pub active_dot: String,
73 pub inactive_dot: String,
75 pub arabic_format: String,
77 pub key_map: KeyMap,
79}
80
81impl Default for Paginator {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl Paginator {
88 #[must_use]
90 pub fn new() -> Self {
91 Self {
92 display_type: Type::Arabic,
93 page: 0,
94 per_page: 1,
95 total_pages: 1,
96 active_dot: "•".to_string(),
97 inactive_dot: "○".to_string(),
98 arabic_format: "{}/{}".to_string(),
99 key_map: KeyMap::default(),
100 }
101 }
102
103 #[must_use]
105 pub fn display_type(mut self, t: Type) -> Self {
106 self.display_type = t;
107 self
108 }
109
110 #[must_use]
112 pub fn per_page(mut self, n: usize) -> Self {
113 self.per_page = n.max(1);
114 self
115 }
116
117 #[must_use]
119 pub fn total_pages(mut self, n: usize) -> Self {
120 self.total_pages = n.max(1);
121 self.page = self.page.min(self.total_pages.saturating_sub(1));
122 self
123 }
124
125 #[must_use]
127 pub fn page(&self) -> usize {
128 self.page
129 }
130
131 pub fn set_page(&mut self, page: usize) {
133 self.page = page.min(self.total_pages.saturating_sub(1));
134 }
135
136 #[must_use]
138 pub fn get_per_page(&self) -> usize {
139 self.per_page
140 }
141
142 #[must_use]
144 pub fn get_total_pages(&self) -> usize {
145 self.total_pages
146 }
147
148 pub fn set_total_pages_from_items(&mut self, items: usize) -> usize {
152 if items < 1 {
153 self.total_pages = 1;
154 self.page = 0;
155 return self.total_pages;
156 }
157
158 let mut n = items / self.per_page;
159 if !items.is_multiple_of(self.per_page) {
160 n += 1;
161 }
162 self.total_pages = n;
163 self.page = self.page.min(self.total_pages.saturating_sub(1));
164 n
165 }
166
167 #[must_use]
169 pub fn items_on_page(&self, total_items: usize) -> usize {
170 if total_items < 1 {
171 return 0;
172 }
173 let (start, end) = self.get_slice_bounds(total_items);
174 end - start
175 }
176
177 #[must_use]
194 pub fn get_slice_bounds(&self, length: usize) -> (usize, usize) {
195 let start = (self.page.saturating_mul(self.per_page)).min(length);
196 let end = (start.saturating_add(self.per_page)).min(length);
197 (start, end)
198 }
199
200 pub fn prev_page(&mut self) {
202 if self.page > 0 {
203 self.page -= 1;
204 }
205 }
206
207 pub fn next_page(&mut self) {
209 if !self.on_last_page() {
210 self.page += 1;
211 }
212 }
213
214 #[must_use]
216 pub fn on_last_page(&self) -> bool {
217 self.page == self.total_pages.saturating_sub(1)
218 }
219
220 #[must_use]
222 pub fn on_first_page(&self) -> bool {
223 self.page == 0
224 }
225
226 #[must_use]
230 pub fn init(&self) -> Option<Cmd> {
231 None
232 }
233
234 pub fn update(&mut self, msg: Message) -> Option<Cmd> {
236 if let Some(key) = msg.downcast_ref::<KeyMsg>() {
237 let key_str = key.to_string();
238 if matches(&key_str, &[&self.key_map.next_page]) {
239 self.next_page();
240 } else if matches(&key_str, &[&self.key_map.prev_page]) {
241 self.prev_page();
242 }
243 }
244 None
245 }
246
247 #[must_use]
249 pub fn view(&self) -> String {
250 match self.display_type {
251 Type::Dots => self.dots_view(),
252 Type::Arabic => self.arabic_view(),
253 }
254 }
255
256 fn dots_view(&self) -> String {
257 let mut s = String::new();
258 for i in 0..self.total_pages {
259 if i == self.page {
260 s.push_str(&self.active_dot);
261 } else {
262 s.push_str(&self.inactive_dot);
263 }
264 }
265 s
266 }
267
268 fn arabic_view(&self) -> String {
269 self.arabic_format
271 .replacen("{}", &(self.page + 1).to_string(), 1)
272 .replacen("{}", &self.total_pages.to_string(), 1)
273 }
274}
275
276impl Model for Paginator {
278 fn init(&self) -> Option<Cmd> {
279 Paginator::init(self)
280 }
281
282 fn update(&mut self, msg: Message) -> Option<Cmd> {
283 Paginator::update(self, msg)
284 }
285
286 fn view(&self) -> String {
287 Paginator::view(self)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_paginator_new() {
297 let p = Paginator::new();
298 assert_eq!(p.page(), 0);
299 assert_eq!(p.get_per_page(), 1);
300 assert_eq!(p.get_total_pages(), 1);
301 }
302
303 #[test]
304 fn test_paginator_builder() {
305 let p = Paginator::new().per_page(10).total_pages(5);
306 assert_eq!(p.get_per_page(), 10);
307 assert_eq!(p.get_total_pages(), 5);
308 }
309
310 #[test]
311 fn test_paginator_navigation() {
312 let mut p = Paginator::new().total_pages(5);
313
314 assert!(p.on_first_page());
315 assert!(!p.on_last_page());
316
317 p.next_page();
318 assert_eq!(p.page(), 1);
319
320 p.next_page();
321 p.next_page();
322 p.next_page();
323 assert_eq!(p.page(), 4);
324 assert!(p.on_last_page());
325
326 p.next_page();
328 assert_eq!(p.page(), 4);
329
330 p.prev_page();
331 assert_eq!(p.page(), 3);
332
333 p.set_page(0);
335 assert!(p.on_first_page());
336
337 p.prev_page();
339 assert_eq!(p.page(), 0);
340 }
341
342 #[test]
343 fn test_paginator_slice_bounds() {
344 let mut p = Paginator::new().per_page(3);
345 p.set_total_pages_from_items(10);
346
347 assert_eq!(p.get_slice_bounds(10), (0, 3));
348
349 p.next_page();
350 assert_eq!(p.get_slice_bounds(10), (3, 6));
351
352 p.next_page();
353 assert_eq!(p.get_slice_bounds(10), (6, 9));
354
355 p.next_page();
356 assert_eq!(p.get_slice_bounds(10), (9, 10));
357 }
358
359 #[test]
360 fn test_paginator_items_on_page() {
361 let mut p = Paginator::new().per_page(3);
362 p.set_total_pages_from_items(10);
363
364 assert_eq!(p.items_on_page(10), 3);
365
366 p.set_page(3); assert_eq!(p.items_on_page(10), 1); }
369
370 #[test]
371 fn test_paginator_arabic_view() {
372 let p = Paginator::new().total_pages(5);
373 assert_eq!(p.view(), "1/5");
374 }
375
376 #[test]
377 fn test_paginator_dots_view() {
378 let mut p = Paginator::new().display_type(Type::Dots).total_pages(5);
379 assert_eq!(p.view(), "•○○○○");
380
381 p.next_page();
382 assert_eq!(p.view(), "○•○○○");
383 }
384
385 #[test]
386 fn test_set_total_pages_from_items() {
387 let mut p = Paginator::new().per_page(10);
388
389 assert_eq!(p.set_total_pages_from_items(25), 3);
390 assert_eq!(p.get_total_pages(), 3);
391
392 assert_eq!(p.set_total_pages_from_items(20), 2);
393 assert_eq!(p.get_total_pages(), 2);
394
395 assert_eq!(p.set_total_pages_from_items(0), 1);
396 assert_eq!(p.get_total_pages(), 1);
397 assert_eq!(p.page(), 0);
398 }
399
400 #[test]
401 fn test_total_pages_clamps_current_page() {
402 let mut p = Paginator::new().total_pages(5);
403 p.set_page(4);
404 assert_eq!(p.page(), 4);
405
406 p = p.total_pages(1);
407 assert_eq!(p.page(), 0);
408 }
409
410 #[test]
411 fn test_slice_bounds_clamp_when_out_of_range() {
412 let mut p = Paginator::new().per_page(10).total_pages(5);
413 p.set_page(4);
414
415 let (start, end) = p.get_slice_bounds(5);
416 assert_eq!((start, end), (5, 5));
417 }
418
419 #[test]
422 fn test_paginator_model_init_returns_none() {
423 let p = Paginator::new().total_pages(5);
424 assert!(p.init().is_none());
425 }
426
427 #[test]
428 fn test_paginator_model_update_returns_none() {
429 use bubbletea::KeyType;
430 let mut p = Paginator::new().total_pages(5);
431 let result = p.update(Message::new(KeyMsg::from_type(KeyType::Right)));
432 assert!(result.is_none());
433 }
434
435 #[test]
436 fn test_paginator_model_update_next_key() {
437 use bubbletea::KeyType;
438
439 let mut p = Paginator::new().total_pages(5);
440 assert_eq!(p.page(), 0);
441
442 let key_msg = KeyMsg::from_type(KeyType::Right);
444 p.update(Message::new(key_msg));
445 assert_eq!(p.page(), 1);
446
447 let key_msg = KeyMsg::from_char('l');
449 p.update(Message::new(key_msg));
450 assert_eq!(p.page(), 2);
451 }
452
453 #[test]
454 fn test_paginator_model_update_prev_key() {
455 use bubbletea::KeyType;
456
457 let mut p = Paginator::new().total_pages(5);
458 p.set_page(3);
459 assert_eq!(p.page(), 3);
460
461 let key_msg = KeyMsg::from_type(KeyType::Left);
463 p.update(Message::new(key_msg));
464 assert_eq!(p.page(), 2);
465
466 let key_msg = KeyMsg::from_char('h');
468 p.update(Message::new(key_msg));
469 assert_eq!(p.page(), 1);
470 }
471
472 #[test]
473 fn test_paginator_model_view_first_page() {
474 let p = Paginator::new().total_pages(5);
475 assert_eq!(p.view(), "1/5");
476 }
477
478 #[test]
479 fn test_paginator_model_view_middle_page() {
480 let mut p = Paginator::new().total_pages(5);
481 p.set_page(2);
482 assert_eq!(p.view(), "3/5");
483 }
484
485 #[test]
486 fn test_paginator_model_view_last_page() {
487 let mut p = Paginator::new().total_pages(5);
488 p.set_page(4);
489 assert_eq!(p.view(), "5/5");
490 }
491
492 #[test]
493 fn test_paginator_model_view_single_page() {
494 let p = Paginator::new().total_pages(1);
495 assert_eq!(p.view(), "1/1");
496 }
497
498 #[test]
499 fn test_paginator_model_view_dots_first_page() {
500 let p = Paginator::new().display_type(Type::Dots).total_pages(3);
501 assert_eq!(p.view(), "•○○");
502 }
503
504 #[test]
505 fn test_paginator_model_view_dots_middle_page() {
506 let mut p = Paginator::new().display_type(Type::Dots).total_pages(3);
507 p.set_page(1);
508 assert_eq!(p.view(), "○•○");
509 }
510
511 #[test]
512 fn test_paginator_model_view_dots_last_page() {
513 let mut p = Paginator::new().display_type(Type::Dots).total_pages(3);
514 p.set_page(2);
515 assert_eq!(p.view(), "○○•");
516 }
517}