use crate::key::{Binding, matches};
use bubbletea::{Cmd, KeyMsg, Message, Model};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Type {
#[default]
Arabic,
Dots,
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub prev_page: Binding,
pub next_page: Binding,
}
impl Default for KeyMap {
fn default() -> Self {
Self {
prev_page: Binding::new()
.keys(&["pgup", "left", "h"])
.help("←/h", "prev page"),
next_page: Binding::new()
.keys(&["pgdown", "right", "l"])
.help("→/l", "next page"),
}
}
}
#[derive(Debug, Clone)]
pub struct Paginator {
pub display_type: Type,
page: usize,
per_page: usize,
total_pages: usize,
pub active_dot: String,
pub inactive_dot: String,
pub arabic_format: String,
pub key_map: KeyMap,
}
impl Default for Paginator {
fn default() -> Self {
Self::new()
}
}
impl Paginator {
#[must_use]
pub fn new() -> Self {
Self {
display_type: Type::Arabic,
page: 0,
per_page: 1,
total_pages: 1,
active_dot: "•".to_string(),
inactive_dot: "○".to_string(),
arabic_format: "{}/{}".to_string(),
key_map: KeyMap::default(),
}
}
#[must_use]
pub fn display_type(mut self, t: Type) -> Self {
self.display_type = t;
self
}
#[must_use]
pub fn per_page(mut self, n: usize) -> Self {
self.per_page = n.max(1);
self
}
#[must_use]
pub fn total_pages(mut self, n: usize) -> Self {
self.total_pages = n.max(1);
self.page = self.page.min(self.total_pages.saturating_sub(1));
self
}
#[must_use]
pub fn page(&self) -> usize {
self.page
}
pub fn set_page(&mut self, page: usize) {
self.page = page.min(self.total_pages.saturating_sub(1));
}
#[must_use]
pub fn get_per_page(&self) -> usize {
self.per_page
}
#[must_use]
pub fn get_total_pages(&self) -> usize {
self.total_pages
}
pub fn set_total_pages_from_items(&mut self, items: usize) -> usize {
if items < 1 {
self.total_pages = 1;
self.page = 0;
return self.total_pages;
}
let mut n = items / self.per_page;
if !items.is_multiple_of(self.per_page) {
n += 1;
}
self.total_pages = n;
self.page = self.page.min(self.total_pages.saturating_sub(1));
n
}
#[must_use]
pub fn items_on_page(&self, total_items: usize) -> usize {
if total_items < 1 {
return 0;
}
let (start, end) = self.get_slice_bounds(total_items);
end - start
}
#[must_use]
pub fn get_slice_bounds(&self, length: usize) -> (usize, usize) {
let start = (self.page.saturating_mul(self.per_page)).min(length);
let end = (start.saturating_add(self.per_page)).min(length);
(start, end)
}
pub fn prev_page(&mut self) {
if self.page > 0 {
self.page -= 1;
}
}
pub fn next_page(&mut self) {
if !self.on_last_page() {
self.page += 1;
}
}
#[must_use]
pub fn on_last_page(&self) -> bool {
self.page == self.total_pages.saturating_sub(1)
}
#[must_use]
pub fn on_first_page(&self) -> bool {
self.page == 0
}
#[must_use]
pub fn init(&self) -> Option<Cmd> {
None
}
pub fn update(&mut self, msg: Message) -> Option<Cmd> {
if let Some(key) = msg.downcast_ref::<KeyMsg>() {
let key_str = key.to_string();
if matches(&key_str, &[&self.key_map.next_page]) {
self.next_page();
} else if matches(&key_str, &[&self.key_map.prev_page]) {
self.prev_page();
}
}
None
}
#[must_use]
pub fn view(&self) -> String {
match self.display_type {
Type::Dots => self.dots_view(),
Type::Arabic => self.arabic_view(),
}
}
fn dots_view(&self) -> String {
let mut s = String::new();
for i in 0..self.total_pages {
if i == self.page {
s.push_str(&self.active_dot);
} else {
s.push_str(&self.inactive_dot);
}
}
s
}
fn arabic_view(&self) -> String {
self.arabic_format
.replacen("{}", &(self.page + 1).to_string(), 1)
.replacen("{}", &self.total_pages.to_string(), 1)
}
}
impl Model for Paginator {
fn init(&self) -> Option<Cmd> {
Paginator::init(self)
}
fn update(&mut self, msg: Message) -> Option<Cmd> {
Paginator::update(self, msg)
}
fn view(&self) -> String {
Paginator::view(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_paginator_new() {
let p = Paginator::new();
assert_eq!(p.page(), 0);
assert_eq!(p.get_per_page(), 1);
assert_eq!(p.get_total_pages(), 1);
}
#[test]
fn test_paginator_builder() {
let p = Paginator::new().per_page(10).total_pages(5);
assert_eq!(p.get_per_page(), 10);
assert_eq!(p.get_total_pages(), 5);
}
#[test]
fn test_paginator_navigation() {
let mut p = Paginator::new().total_pages(5);
assert!(p.on_first_page());
assert!(!p.on_last_page());
p.next_page();
assert_eq!(p.page(), 1);
p.next_page();
p.next_page();
p.next_page();
assert_eq!(p.page(), 4);
assert!(p.on_last_page());
p.next_page();
assert_eq!(p.page(), 4);
p.prev_page();
assert_eq!(p.page(), 3);
p.set_page(0);
assert!(p.on_first_page());
p.prev_page();
assert_eq!(p.page(), 0);
}
#[test]
fn test_paginator_slice_bounds() {
let mut p = Paginator::new().per_page(3);
p.set_total_pages_from_items(10);
assert_eq!(p.get_slice_bounds(10), (0, 3));
p.next_page();
assert_eq!(p.get_slice_bounds(10), (3, 6));
p.next_page();
assert_eq!(p.get_slice_bounds(10), (6, 9));
p.next_page();
assert_eq!(p.get_slice_bounds(10), (9, 10));
}
#[test]
fn test_paginator_items_on_page() {
let mut p = Paginator::new().per_page(3);
p.set_total_pages_from_items(10);
assert_eq!(p.items_on_page(10), 3);
p.set_page(3); assert_eq!(p.items_on_page(10), 1); }
#[test]
fn test_paginator_arabic_view() {
let p = Paginator::new().total_pages(5);
assert_eq!(p.view(), "1/5");
}
#[test]
fn test_paginator_dots_view() {
let mut p = Paginator::new().display_type(Type::Dots).total_pages(5);
assert_eq!(p.view(), "•○○○○");
p.next_page();
assert_eq!(p.view(), "○•○○○");
}
#[test]
fn test_set_total_pages_from_items() {
let mut p = Paginator::new().per_page(10);
assert_eq!(p.set_total_pages_from_items(25), 3);
assert_eq!(p.get_total_pages(), 3);
assert_eq!(p.set_total_pages_from_items(20), 2);
assert_eq!(p.get_total_pages(), 2);
assert_eq!(p.set_total_pages_from_items(0), 1);
assert_eq!(p.get_total_pages(), 1);
assert_eq!(p.page(), 0);
}
#[test]
fn test_total_pages_clamps_current_page() {
let mut p = Paginator::new().total_pages(5);
p.set_page(4);
assert_eq!(p.page(), 4);
p = p.total_pages(1);
assert_eq!(p.page(), 0);
}
#[test]
fn test_slice_bounds_clamp_when_out_of_range() {
let mut p = Paginator::new().per_page(10).total_pages(5);
p.set_page(4);
let (start, end) = p.get_slice_bounds(5);
assert_eq!((start, end), (5, 5));
}
#[test]
fn test_paginator_model_init_returns_none() {
let p = Paginator::new().total_pages(5);
assert!(p.init().is_none());
}
#[test]
fn test_paginator_model_update_returns_none() {
use bubbletea::KeyType;
let mut p = Paginator::new().total_pages(5);
let result = p.update(Message::new(KeyMsg::from_type(KeyType::Right)));
assert!(result.is_none());
}
#[test]
fn test_paginator_model_update_next_key() {
use bubbletea::KeyType;
let mut p = Paginator::new().total_pages(5);
assert_eq!(p.page(), 0);
let key_msg = KeyMsg::from_type(KeyType::Right);
p.update(Message::new(key_msg));
assert_eq!(p.page(), 1);
let key_msg = KeyMsg::from_char('l');
p.update(Message::new(key_msg));
assert_eq!(p.page(), 2);
}
#[test]
fn test_paginator_model_update_prev_key() {
use bubbletea::KeyType;
let mut p = Paginator::new().total_pages(5);
p.set_page(3);
assert_eq!(p.page(), 3);
let key_msg = KeyMsg::from_type(KeyType::Left);
p.update(Message::new(key_msg));
assert_eq!(p.page(), 2);
let key_msg = KeyMsg::from_char('h');
p.update(Message::new(key_msg));
assert_eq!(p.page(), 1);
}
#[test]
fn test_paginator_model_view_first_page() {
let p = Paginator::new().total_pages(5);
assert_eq!(p.view(), "1/5");
}
#[test]
fn test_paginator_model_view_middle_page() {
let mut p = Paginator::new().total_pages(5);
p.set_page(2);
assert_eq!(p.view(), "3/5");
}
#[test]
fn test_paginator_model_view_last_page() {
let mut p = Paginator::new().total_pages(5);
p.set_page(4);
assert_eq!(p.view(), "5/5");
}
#[test]
fn test_paginator_model_view_single_page() {
let p = Paginator::new().total_pages(1);
assert_eq!(p.view(), "1/1");
}
#[test]
fn test_paginator_model_view_dots_first_page() {
let p = Paginator::new().display_type(Type::Dots).total_pages(3);
assert_eq!(p.view(), "•○○");
}
#[test]
fn test_paginator_model_view_dots_middle_page() {
let mut p = Paginator::new().display_type(Type::Dots).total_pages(3);
p.set_page(1);
assert_eq!(p.view(), "○•○");
}
#[test]
fn test_paginator_model_view_dots_last_page() {
let mut p = Paginator::new().display_type(Type::Dots).total_pages(3);
p.set_page(2);
assert_eq!(p.view(), "○○•");
}
}