#![allow(clippy::needless_return)]
#![allow(clippy::redundant_field_names)]
use console::{Key, Style, Term};
pub struct MenuOption {
label: String,
func: Box<dyn FnMut()>,
hint: Option<String>,
}
pub struct Menu {
title: Option<String>,
options: Vec<MenuOption>,
selected_option: i32,
selected_style: Style,
normal_style: Style,
hint_style: Style,
}
impl MenuOption {
pub fn new<F>(label: &str, func: F) -> MenuOption
where
F: FnMut() + 'static,
{
return MenuOption {
label: label.to_owned(),
func: Box::new(func),
hint: None,
};
}
pub fn hint(mut self, text: &str) -> MenuOption {
self.hint = Some(text.to_owned());
return self;
}
}
impl Menu {
pub fn new(options: Vec<MenuOption>) -> Menu {
return Menu {
title: None,
options: options,
selected_option: 0,
normal_style: Style::new(),
selected_style: Style::new().on_blue(),
hint_style: Style::new().color256(187),
};
}
pub fn title(mut self, text: &str) -> Menu {
self.title = Some(text.to_owned());
return self;
}
pub fn show(mut self) {
let stdout = Term::buffered_stdout();
stdout.hide_cursor().unwrap();
stdout.clear_screen().unwrap();
self.draw_menu(&stdout);
self.menu_navigation(&stdout);
stdout.clear_screen().unwrap();
stdout.flush().unwrap();
if self.selected_option == -1 {
return;
}
let option = &mut self.options[self.selected_option as usize];
(option.func)();
}
fn menu_navigation(&mut self, stdout: &Term) {
let options_limit_num: i32 = (self.options.len() - 1) as i32;
loop {
let key = match stdout.read_key() {
Ok(val) => val,
Err(_e) => {
println!("Error reading key");
return;
}
};
match key {
Key::ArrowUp => {
self.selected_option = match self.selected_option == 0 {
true => options_limit_num,
false => self.selected_option - 1,
}
}
Key::ArrowDown => {
self.selected_option = match self.selected_option == options_limit_num {
true => 0,
false => self.selected_option + 1,
}
}
Key::Escape => {
self.selected_option = -1;
stdout.show_cursor().unwrap();
return;
}
Key::Enter => {
stdout.show_cursor().unwrap();
return;
}
_ => {}
}
self.draw_menu(stdout);
}
}
fn draw_menu(&self, stdout: &Term) {
stdout.clear_screen().unwrap();
match &self.title {
Some(text) => {
let title_style = Style::new().bold();
let title = title_style.apply_to(text);
let title = format!(" {}", title);
stdout.write_line(title.as_str()).unwrap()
}
None => {}
};
for (i, option) in self.options.iter().enumerate() {
let option_idx: usize = self.selected_option as usize;
let label_style = match i == option_idx {
true => self.selected_style.clone(),
false => self.normal_style.clone(),
};
let label = label_style.apply_to(option.label.as_str());
let hint_str = match &self.options[i].hint {
Some(hint) => hint,
None => "",
};
let hint = self.hint_style.apply_to(hint_str);
let line = format!("- {: <25}\t{}", label, hint);
stdout.write_line(line.as_str()).unwrap();
}
stdout.flush().unwrap();
}
}