use std::io;
use crossterm::event::{KeyCode, KeyEvent};
use crate::utils::{
key_listener::{self, Typeable},
renderer::{DrawTime, Printable, Renderer},
theme,
};
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SelectOption<'a, T> {
pub value: T,
pub title: String,
pub description: Option<&'a str>,
pub disabled: bool,
pub active: bool,
}
impl<'a, T: ToString> SelectOption<'a, T> {
pub fn new(value: T) -> Self {
let title = value.to_string();
SelectOption {
value,
title,
description: None,
disabled: false,
active: false,
}
}
pub fn title(mut self, title: &'a str) -> Self {
self.title = title.to_string();
self
}
pub fn description(mut self, description: &'a str) -> Self {
self.description = Some(description);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
pub struct SelectInput {
pub focused: usize,
pub items_per_page: usize,
pub loop_mode: bool,
pub total_items: usize,
}
impl SelectInput {
pub fn count_pages(&self) -> usize {
let total = self.total_items;
let per_page = self.items_per_page;
let rem = total % per_page;
total / per_page + (rem != 0) as usize
}
pub fn get_page(&self) -> usize {
self.focused / self.items_per_page
}
}
impl SelectInput {
pub(crate) fn new(total_items: usize) -> Self {
SelectInput {
total_items,
focused: 0,
items_per_page: 10,
loop_mode: true,
}
}
pub(crate) fn set_loop_mode(&mut self, loop_mode: bool) {
self.loop_mode = loop_mode;
}
pub(crate) fn move_cursor(&mut self, direction: Direction) {
match direction {
Direction::Up => self.prev_item(),
Direction::Down => self.next_item(),
Direction::Left => self.prev_page(),
Direction::Right => self.next_page(),
};
}
pub(crate) fn set_items_per_page(&mut self, item_per_page: usize) {
self.items_per_page = item_per_page.min(self.total_items);
}
fn prev_item(&mut self) {
let max = self.total_items - 1;
self.focused = match self.loop_mode {
true => self.focused.checked_sub(1).unwrap_or(max),
false => self.focused.saturating_sub(1),
}
}
fn next_item(&mut self) {
let max = self.total_items - 1;
let new_value = self.focused + 1;
self.focused = match (new_value > max, self.loop_mode) {
(true, true) => 0,
(true, false) => max,
(false, _) => new_value,
}
}
fn prev_page(&mut self) {
self.focused = self.focused.saturating_sub(self.items_per_page)
}
fn next_page(&mut self) {
let max = self.total_items - 1;
let new_value = self.focused + self.items_per_page;
self.focused = new_value.min(max)
}
}
type Formatter<'a, T> = dyn Fn(&Select<T>, DrawTime) -> String + 'a;
pub struct Select<'a, T> {
pub message: &'a str,
pub options: Vec<SelectOption<'a, T>>,
pub input: SelectInput,
formatter: Box<Formatter<'a, T>>,
}
impl<'a, T: 'a> Select<'a, T> {
pub fn new<I>(message: &'a str, iter: I) -> Self
where
I: IntoIterator<Item = T>,
T: ToString,
{
let options = iter.into_iter().map(|o| SelectOption::new(o)).collect();
Self::new_complex(message, options)
}
pub fn new_complex(message: &'a str, options: Vec<SelectOption<'a, T>>) -> Self {
let options_len = options.len();
Select {
message,
options,
input: SelectInput::new(options_len),
formatter: Box::new(theme::fmt_select),
}
}
pub fn selected(&mut self, index: usize) -> &mut Self {
self.input.focused = index.min(self.options.len() - 1);
self
}
pub fn in_loop(&mut self, loop_mode: bool) -> &mut Self {
self.input.set_loop_mode(loop_mode);
self
}
pub fn items_per_page(&mut self, item_per_page: usize) -> &mut Self {
self.input.set_items_per_page(item_per_page);
self
}
pub fn format<F>(&mut self, formatter: F) -> &mut Self
where
F: Fn(&Select<T>, DrawTime) -> String + 'a,
{
self.formatter = Box::new(formatter);
self
}
pub fn prompt(&mut self) -> io::Result<T> {
key_listener::listen(self, true)?;
let selected = self.options.remove(self.input.focused);
Ok(selected.value)
}
}
impl<T> Select<'_, T> {
fn validate_to_submit(&self) -> bool {
let focused = &self.options[self.input.focused];
!focused.disabled
}
}
impl<T> Typeable for Select<'_, T> {
fn handle_key(&mut self, key: KeyEvent) -> bool {
let mut submit = false;
match key.code {
KeyCode::Enter | KeyCode::Backspace => submit = self.validate_to_submit(),
KeyCode::Up | KeyCode::Char('k' | 'K') => self.input.move_cursor(Direction::Up),
KeyCode::Down | KeyCode::Char('j' | 'J') => self.input.move_cursor(Direction::Down),
KeyCode::Left | KeyCode::Char('h' | 'H') => self.input.move_cursor(Direction::Left),
KeyCode::Right | KeyCode::Char('l' | 'L') => self.input.move_cursor(Direction::Right),
_ => (),
}
submit
}
}
impl<T> Printable for Select<'_, T> {
fn draw(&self, renderer: &mut Renderer) -> io::Result<()> {
let text = (self.formatter)(self, renderer.draw_time);
renderer.print(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_initial_value() {
let mut prompt = Select::new("", ["foo", "bar"]);
assert_eq!(prompt.input.focused, 0);
prompt.selected(1);
assert_eq!(prompt.input.focused, 1);
}
#[test]
fn set_loop_mode() {
let mut prompt = Select::new("", ["foo", "bar"]);
prompt.in_loop(false);
assert!(!prompt.input.loop_mode);
prompt.in_loop(true);
assert!(prompt.input.loop_mode);
}
#[test]
fn set_custom_formatter() {
let mut prompt = Select::new("", ["foo", "bar"]);
let draw_time = DrawTime::First;
const EXPECTED_VALUE: &str = "foo";
prompt.format(|_, _| String::from(EXPECTED_VALUE));
assert_eq!((prompt.formatter)(&prompt, draw_time), EXPECTED_VALUE);
}
#[test]
fn submit_selected_value() {
let events = [KeyCode::Enter, KeyCode::Backspace];
for event in events {
let mut prompt = Select::new("", ["foo", "bar"]);
let simulated_key = KeyEvent::from(event);
prompt.selected(1);
let submit = prompt.handle_key(simulated_key);
assert_eq!(prompt.input.focused, 1);
assert!(submit);
}
}
#[test]
fn not_submit_disabled() {
let events = [KeyCode::Enter, KeyCode::Backspace];
for event in events {
let mut prompt = Select::new_complex("", vec![SelectOption::new("foo").disabled(true)]);
let submit = prompt.handle_key(KeyEvent::from(event));
assert!(!submit);
}
}
#[test]
fn update_focused() {
let up_keys = [KeyCode::Up, KeyCode::Char('k'), KeyCode::Char('K')];
let down_keys = [KeyCode::Down, KeyCode::Char('j'), KeyCode::Char('j')];
let up_cases = [
(false, 0, 0),
(false, 1, 0),
(true, 0, 1),
];
let down_cases = [
(false, 1, 1),
(false, 0, 1),
(true, 1, 0),
];
for key in up_keys {
for (in_loop, initial, expected) in up_cases {
let mut prompt = Select::new("", ["foo", "bar"]);
let simulated_key = KeyEvent::from(key);
prompt.selected(initial);
prompt.in_loop(in_loop);
prompt.handle_key(simulated_key);
assert_eq!(prompt.input.focused, expected);
}
}
for key in down_keys {
for (in_loop, initial, expected) in down_cases {
let mut prompt = Select::new("", ["foo", "bar"]);
let simulated_key = KeyEvent::from(key);
prompt.selected(initial);
prompt.in_loop(in_loop);
prompt.handle_key(simulated_key);
assert_eq!(prompt.input.focused, expected);
}
}
}
}