use anyhow::Result;
use crossterm::{
cursor::{position, MoveTo},
event::{self, Event, KeyCode, KeyEvent},
execute,
style::{Print, ResetColor, SetForegroundColor},
terminal::{disable_raw_mode, enable_raw_mode, size},
};
use std::io::{stdout, Write};
use crate::color::Color;
#[derive(Debug, Clone)]
pub struct ListConfig {
pub items_per_row: usize,
pub rows_per_page: usize,
pub cell_width: u16,
pub normal_fg: Color,
pub highlight_fg: Color,
}
impl Default for ListConfig {
fn default() -> Self {
Self {
items_per_row: 3,
rows_per_page: 5,
cell_width: 20,
normal_fg: Color::White,
highlight_fg: Color::Yellow,
}
}
}
impl ListConfig {
pub fn items_per_row(mut self, val: usize) -> Self {
self.items_per_row = val;
self
}
pub fn rows_per_page(mut self, val: usize) -> Self {
self.rows_per_page = val;
self
}
pub fn cell_width(mut self, val: u16) -> Self {
self.cell_width = val;
self
}
pub fn normal_fg(mut self, color: Color) -> Self {
self.normal_fg = color;
self
}
pub fn highlight_fg(mut self, color: Color) -> Self {
self.highlight_fg = color;
self
}
}
pub fn choose_from_list<T: ToString>(items: &[T], config: &ListConfig) -> Result<Option<usize>> {
enable_raw_mode()?;
let (start_col, start_row) = position()?;
let mut stdout = stdout();
let display_start_row = ensure_display_space(start_row, config)?;
let total = items.len();
let per_page = config.items_per_row * config.rows_per_page;
let mut selected = 0;
let mut digit_buffer = String::new();
render_page(
items,
selected,
&digit_buffer,
config,
start_col,
display_start_row,
)?;
loop {
if let Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Char(c) if c.is_digit(10) => digit_buffer.push(c),
KeyCode::Backspace if !digit_buffer.is_empty() => {
digit_buffer.pop();
}
KeyCode::Left if selected > 0 => {
digit_buffer.clear();
selected -= 1;
}
KeyCode::Right if selected + 1 < total => {
digit_buffer.clear();
selected += 1;
}
KeyCode::Up if selected >= config.items_per_row => {
digit_buffer.clear();
selected -= config.items_per_row;
}
KeyCode::Down if selected + config.items_per_row < total => {
digit_buffer.clear();
selected += config.items_per_row;
}
KeyCode::PageDown if selected + per_page < total => {
digit_buffer.clear();
selected += per_page;
}
KeyCode::PageUp if selected >= per_page => {
digit_buffer.clear();
selected -= per_page;
}
KeyCode::Enter => {
let choice = if !digit_buffer.is_empty() {
digit_buffer
.parse::<usize>()
.ok()
.and_then(|n| (1..=total).contains(&n).then(|| n - 1))
} else {
Some(selected)
};
cleanup(&mut stdout, start_col, display_start_row, config)?;
disable_raw_mode()?;
return Ok(choice);
}
KeyCode::Esc => {
cleanup(&mut stdout, start_col, display_start_row, config)?;
disable_raw_mode()?;
return Ok(None);
}
_ => {}
}
render_page(
items,
selected,
&digit_buffer,
config,
start_col,
display_start_row,
)?;
}
}
}
fn ensure_display_space(current_row: u16, config: &ListConfig) -> Result<u16> {
let mut stdout = stdout();
let (_, terminal_height) = size()?;
let required_lines = config.rows_per_page as u16 + 1;
let available_lines = terminal_height.saturating_sub(current_row);
if available_lines < required_lines {
let lines_to_create = required_lines - available_lines;
for _ in 0..lines_to_create {
execute!(stdout, Print("\n"))?;
}
stdout.flush()?;
let (_, new_row) = position()?;
Ok(new_row.saturating_sub(required_lines))
} else {
Ok(current_row)
}
}
fn calculate_page_start(selected: usize, per_page: usize) -> usize {
(selected / per_page) * per_page
}
fn cleanup(
stdout: &mut impl Write,
start_col: u16,
start_row: u16,
config: &ListConfig,
) -> anyhow::Result<()> {
let page_size = config.items_per_row * config.rows_per_page;
for idx in 0..page_size {
let row = idx / config.items_per_row;
let col = idx % config.items_per_row;
execute!(
stdout,
MoveTo(
start_col + col as u16 * config.cell_width,
start_row + row as u16
),
Print(" ".repeat(config.cell_width as usize))
)?;
}
execute!(
stdout,
MoveTo(start_col, start_row + config.rows_per_page as u16),
Print(" ".repeat(config.cell_width as usize)),
MoveTo(start_col, start_row + config.rows_per_page as u16)
)?;
Ok(())
}
fn render_page<T: ToString>(
items: &[T],
selected: usize,
digit_buffer: &str,
config: &ListConfig,
start_col: u16,
start_row: u16,
) -> Result<()> {
let mut stdout = stdout();
let page_size = config.items_per_row * config.rows_per_page;
let page_start = calculate_page_start(selected, page_size);
for idx in 0..page_size {
let row = idx / config.items_per_row;
let col = idx % config.items_per_row;
execute!(
stdout,
MoveTo(
start_col + col as u16 * config.cell_width,
start_row + row as u16
),
Print(" ".repeat(config.cell_width as usize))
)?;
}
for idx in 0..page_size {
let global = page_start + idx;
if global >= items.len() {
break;
}
let row = idx / config.items_per_row;
let col = idx % config.items_per_row;
let x = start_col + col as u16 * config.cell_width;
let y = start_row + row as u16;
execute!(stdout, MoveTo(x, y))?;
let fg = if global == selected {
config.highlight_fg
} else {
config.normal_fg
};
execute!(
stdout,
SetForegroundColor(fg.into()),
Print(format!(
"{num:>2}. {text:<width$}",
num = global + 1,
text = items[global].to_string(),
width = (config.cell_width as usize - 4)
))
)?;
}
execute!(
stdout,
MoveTo(start_col, start_row + config.rows_per_page as u16),
Print(" ".repeat(config.cell_width as usize)),
MoveTo(start_col, start_row + config.rows_per_page as u16)
)?;
if !digit_buffer.is_empty() {
execute!(
stdout,
SetForegroundColor(Color::White.into()),
Print(format!("Input: {}", digit_buffer))
)?;
}
execute!(
stdout,
ResetColor,
MoveTo(start_col, start_row + config.rows_per_page as u16)
)?;
stdout.flush()?;
Ok(())
}