use crate::hooks::Key;
pub trait SelectionState {
fn selected(&self) -> Option<usize>;
fn select(&mut self, index: Option<usize>);
fn offset(&self) -> usize;
fn set_offset(&mut self, offset: usize);
fn select_next(&mut self, len: usize) {
if len == 0 {
self.select(None);
return;
}
let new_index = match self.selected() {
Some(i) => (i + 1).min(len - 1),
None => 0,
};
self.select(Some(new_index));
}
fn select_previous(&mut self, len: usize) {
if len == 0 {
self.select(None);
return;
}
let new_index = match self.selected() {
Some(i) => i.saturating_sub(1),
None => 0,
};
self.select(Some(new_index));
}
fn select_first(&mut self, len: usize) {
if len > 0 {
self.select(Some(0));
}
}
fn select_last(&mut self, len: usize) {
if len > 0 {
self.select(Some(len - 1));
}
}
fn scroll_to_selected(&mut self, viewport_height: usize) {
if let Some(selected) = self.selected() {
let offset = self.offset();
if selected < offset {
self.set_offset(selected);
}
else if selected >= offset + viewport_height {
self.set_offset(selected.saturating_sub(viewport_height - 1));
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationResult {
Moved(usize),
None,
}
impl NavigationResult {
pub fn unwrap_or(self, current: usize) -> usize {
match self {
NavigationResult::Moved(pos) => pos,
NavigationResult::None => current,
}
}
pub fn is_moved(&self) -> bool {
matches!(self, NavigationResult::Moved(_))
}
}
#[derive(Debug, Clone)]
pub struct NavigationConfig {
pub vim_navigation: bool,
pub number_shortcuts: bool,
pub page_size: usize,
}
impl Default for NavigationConfig {
fn default() -> Self {
Self {
vim_navigation: false,
number_shortcuts: false,
page_size: 5,
}
}
}
impl NavigationConfig {
pub fn new() -> Self {
Self::default()
}
pub fn vim_navigation(mut self, enabled: bool) -> Self {
self.vim_navigation = enabled;
self
}
pub fn number_shortcuts(mut self, enabled: bool) -> Self {
self.number_shortcuts = enabled;
self
}
pub fn page_size(mut self, size: usize) -> Self {
self.page_size = size;
self
}
}
pub fn handle_list_navigation(
current: usize,
total: usize,
input: &str,
key: Key,
config: &NavigationConfig,
) -> NavigationResult {
if total == 0 {
return NavigationResult::None;
}
let max_index = total.saturating_sub(1);
if key.up_arrow {
return NavigationResult::Moved(current.saturating_sub(1));
}
if key.down_arrow {
return NavigationResult::Moved((current + 1).min(max_index));
}
if config.vim_navigation {
if input == "k" {
return NavigationResult::Moved(current.saturating_sub(1));
}
if input == "j" {
return NavigationResult::Moved((current + 1).min(max_index));
}
}
if config.number_shortcuts {
if let Some(num) = input.chars().next().and_then(|c| c.to_digit(10)) {
if (1..=9).contains(&num) {
let index = (num as usize) - 1;
if index < total {
return NavigationResult::Moved(index);
}
}
}
}
if key.home {
return NavigationResult::Moved(0);
}
if key.end {
return NavigationResult::Moved(max_index);
}
if key.page_up {
return NavigationResult::Moved(current.saturating_sub(config.page_size));
}
if key.page_down {
return NavigationResult::Moved((current + config.page_size).min(max_index));
}
NavigationResult::None
}
pub fn calculate_visible_range(
highlighted: usize,
total: usize,
limit: Option<usize>,
) -> (usize, usize) {
if let Some(limit) = limit {
let half = limit / 2;
let start = if highlighted <= half {
0
} else if highlighted >= total.saturating_sub(half) {
total.saturating_sub(limit)
} else {
highlighted.saturating_sub(half)
};
let end = (start + limit).min(total);
(start, end)
} else {
(0, total)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_navigation_result() {
let moved = NavigationResult::Moved(5);
assert!(moved.is_moved());
assert_eq!(moved.unwrap_or(0), 5);
let none = NavigationResult::None;
assert!(!none.is_moved());
assert_eq!(none.unwrap_or(3), 3);
}
#[test]
fn test_navigation_config_builder() {
let config = NavigationConfig::new()
.vim_navigation(true)
.number_shortcuts(true)
.page_size(10);
assert!(config.vim_navigation);
assert!(config.number_shortcuts);
assert_eq!(config.page_size, 10);
}
#[test]
fn test_arrow_navigation() {
let config = NavigationConfig::default();
let mut key = Key::default();
key.down_arrow = true;
let result = handle_list_navigation(0, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(1));
key.down_arrow = false;
key.up_arrow = true;
let result = handle_list_navigation(5, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(4));
let result = handle_list_navigation(0, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(0));
}
#[test]
fn test_vim_navigation() {
let config = NavigationConfig::default().vim_navigation(true);
let key = Key::default();
let result = handle_list_navigation(0, 10, "j", key, &config);
assert_eq!(result, NavigationResult::Moved(1));
let result = handle_list_navigation(5, 10, "k", key, &config);
assert_eq!(result, NavigationResult::Moved(4));
}
#[test]
fn test_vim_navigation_disabled() {
let config = NavigationConfig::default().vim_navigation(false);
let key = Key::default();
let result = handle_list_navigation(0, 10, "j", key, &config);
assert_eq!(result, NavigationResult::None);
}
#[test]
fn test_number_shortcuts() {
let config = NavigationConfig::default().number_shortcuts(true);
let key = Key::default();
let result = handle_list_navigation(5, 10, "1", key, &config);
assert_eq!(result, NavigationResult::Moved(0));
let result = handle_list_navigation(0, 10, "5", key, &config);
assert_eq!(result, NavigationResult::Moved(4));
let result = handle_list_navigation(0, 5, "9", key, &config);
assert_eq!(result, NavigationResult::None);
}
#[test]
fn test_home_end_navigation() {
let config = NavigationConfig::default();
let mut key = Key::default();
key.home = true;
let result = handle_list_navigation(5, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(0));
key.home = false;
key.end = true;
let result = handle_list_navigation(0, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(9));
}
#[test]
fn test_page_navigation() {
let config = NavigationConfig::default().page_size(5);
let mut key = Key::default();
key.page_down = true;
let result = handle_list_navigation(0, 20, "", key, &config);
assert_eq!(result, NavigationResult::Moved(5));
key.page_down = false;
key.page_up = true;
let result = handle_list_navigation(10, 20, "", key, &config);
assert_eq!(result, NavigationResult::Moved(5));
}
#[test]
fn test_boundary_conditions() {
let config = NavigationConfig::default();
let mut key = Key::default();
key.down_arrow = true;
let result = handle_list_navigation(9, 10, "", key, &config);
assert_eq!(result, NavigationResult::Moved(9));
let result = handle_list_navigation(0, 0, "", key, &config);
assert_eq!(result, NavigationResult::None);
}
#[test]
fn test_calculate_visible_range_no_limit() {
let (start, end) = calculate_visible_range(5, 20, None);
assert_eq!(start, 0);
assert_eq!(end, 20);
}
#[test]
fn test_calculate_visible_range_with_limit() {
let (start, end) = calculate_visible_range(0, 20, Some(5));
assert_eq!(start, 0);
assert_eq!(end, 5);
let (start, end) = calculate_visible_range(10, 20, Some(5));
assert_eq!(start, 8);
assert_eq!(end, 13);
let (start, end) = calculate_visible_range(19, 20, Some(5));
assert_eq!(start, 15);
assert_eq!(end, 20);
}
}