use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyModifiers},
execute, queue,
terminal::{self, ClearType},
};
use glyphs::{style, gradient, Color};
use std::io::{stdout, Write};
const ACCENT: Color = Color::Rgb { r: 212, g: 92, b: 235 }; const ACCENT_DIM: Color = Color::Rgb { r: 147, g: 112, b: 219 }; const CURSOR_COLOR: Color = Color::Rgb { r: 255, g: 135, b: 175 }; const DIM: Color = Color::Rgb { r: 102, g: 102, b: 102 };
pub fn choose<S: Into<String>>(options: &[S]) -> Choose
where
S: Clone,
{
Choose::new(options.iter().map(|s| s.clone().into()).collect())
}
pub struct Choose {
options: Vec<String>,
header: Option<String>,
cursor: String,
selected: usize,
limit: usize,
show_help: bool,
}
impl Choose {
pub fn new(options: Vec<String>) -> Self {
Self {
options,
header: None,
cursor: "❯".to_string(),
selected: 0,
limit: 0,
show_help: true,
}
}
#[must_use]
pub fn header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
#[must_use]
pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = cursor.into();
self
}
#[must_use]
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
#[must_use]
pub fn no_help(mut self) -> Self {
self.show_help = false;
self
}
fn render(&self, offset: usize, visible: usize) -> String {
let mut buffer = String::new();
if let Some(ref header) = self.header {
buffer.push_str(&format!("{}\r\n", gradient(header, ACCENT, ACCENT_DIM)));
}
for (i, option) in self.options.iter().enumerate().skip(offset).take(visible) {
let is_selected = i == self.selected;
if is_selected {
let cursor_styled = style(&self.cursor).fg(CURSOR_COLOR).bold();
let text_styled = style(option).fg(Color::White).bold();
buffer.push_str(&format!(" {} {}\r\n", cursor_styled, text_styled));
} else {
let distance = if i > self.selected { i - self.selected } else { self.selected - i };
let alpha = match distance {
1 => Color::Rgb { r: 180, g: 180, b: 180 },
2 => Color::Rgb { r: 140, g: 140, b: 140 },
_ => DIM,
};
buffer.push_str(&format!(" {}\r\n", style(option).fg(alpha)));
}
}
if self.show_help {
buffer.push_str(&format!(
"{}\r\n",
style("↑/↓ navigate • enter select • esc cancel").fg(DIM).dim()
));
}
buffer
}
pub fn run(mut self) -> Option<String> {
if self.options.is_empty() {
return None;
}
let mut stdout = stdout();
terminal::enable_raw_mode().expect("Failed to enable raw mode");
execute!(stdout, cursor::Hide).ok();
let visible = if self.limit > 0 {
self.limit.min(self.options.len())
} else {
self.options.len()
};
let mut offset = 0;
let has_header = self.header.is_some();
let help_lines = if self.show_help { 1 } else { 0 };
let total_lines = visible + if has_header { 1 } else { 0 } + help_lines;
let buffer = self.render(offset, visible);
write!(stdout, "{}", buffer).ok();
stdout.flush().ok();
loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Enter => break,
KeyCode::Esc => {
execute!(stdout, cursor::Show).ok();
terminal::disable_raw_mode().expect("Failed to disable raw mode");
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
for _ in 0..total_lines {
queue!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::CurrentLine), cursor::MoveDown(1)).ok();
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
stdout.flush().ok();
return None;
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
execute!(stdout, cursor::Show).ok();
terminal::disable_raw_mode().expect("Failed to disable raw mode");
return None;
}
KeyCode::Up | KeyCode::Char('k') => {
if self.selected > 0 {
self.selected -= 1;
if self.selected < offset {
offset = self.selected;
}
}
}
KeyCode::Down | KeyCode::Char('j') => {
if self.selected < self.options.len() - 1 {
self.selected += 1;
if self.selected >= offset + visible {
offset = self.selected - visible + 1;
}
}
}
KeyCode::Home | KeyCode::Char('g') => {
self.selected = 0;
offset = 0;
}
KeyCode::End | KeyCode::Char('G') => {
self.selected = self.options.len() - 1;
offset = self.options.len().saturating_sub(visible);
}
_ => continue, }
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
for _ in 0..total_lines {
queue!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::CurrentLine), cursor::MoveDown(1)).ok();
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
let buffer = self.render(offset, visible);
write!(stdout, "{}", buffer).ok();
stdout.flush().ok();
}
}
execute!(stdout, cursor::Show).ok();
terminal::disable_raw_mode().expect("Failed to disable raw mode");
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
for _ in 0..total_lines {
queue!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::CurrentLine), cursor::MoveDown(1)).ok();
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
stdout.flush().ok();
let selected_text = &self.options[self.selected];
println!(
" {} {}",
style("✓").fg(Color::Green).bold(),
style(selected_text).fg(Color::White).bold()
);
Some(selected_text.clone())
}
}