use std::{
sync::{Arc, RwLock},
thread,
io::{stdout, Write},
time::Duration
};
use crossterm::{
execute,
cursor,
terminal,
screen::RawScreen,
input::{input, InputEvent, KeyEvent}
};
type TerminalMenu = Arc<RwLock<TerminalMenuStruct>>;
#[derive(Eq, PartialEq)]
enum TMIKind {
Button,
ScrollSelection,
ListSelection,
Numeric,
}
pub struct TerminalMenuItem {
name: String,
kind: TMIKind,
s_values: Vec<String>,
s_selected: usize,
n_value: f64,
n_step: f64,
n_min: f64,
n_max: f64
}
pub fn button(name: &str) -> TerminalMenuItem {
TerminalMenuItem {
name: name.to_owned(),
kind: TMIKind::Button,
s_values: vec![],
s_selected: 0,
n_value: 0.0,
n_step: 0.0,
n_min: 0.0,
n_max: 0.0,
}
}
pub fn scroll_selection(name: &str, values: Vec<&str>) -> TerminalMenuItem {
if values.len() == 0 {
panic!("values cannot be empty");
}
TerminalMenuItem {
name: name.to_owned(),
kind: TMIKind::ScrollSelection,
s_values: values.iter().map(|&s| s.to_owned()).collect(),
s_selected: 0,
n_value: 0.0,
n_step: 0.0,
n_min: 0.0,
n_max: 0.0,
}
}
pub fn list_selection(name: &str, values: Vec<&str>) -> TerminalMenuItem {
if values.len() == 0 {
panic!("values cannot be empty");
}
TerminalMenuItem {
name: name.to_owned(),
kind: TMIKind::ListSelection,
s_values: values.iter().map(|&s| s.to_owned()).collect(),
s_selected: 0,
n_value: 0.0,
n_step: 0.0,
n_min: 0.0,
n_max: 0.0,
}
}
pub fn numeric(name: &str, default: f64, step: f64, min: f64, max: f64) -> TerminalMenuItem {
TerminalMenuItem {
name: name.to_owned(),
kind: TMIKind::Numeric,
s_values: vec![],
s_selected: 0,
n_value: default,
n_step: step,
n_min: min,
n_max: max,
}
}
pub struct TerminalMenuStruct {
items: Vec<TerminalMenuItem>,
selected: usize,
active: bool,
exited: bool,
}
impl TerminalMenuStruct {
pub fn is_active(&self) -> bool {
!self.exited
}
pub fn selected_item(&self) -> &str {
&self.items[self.selected].name
}
pub fn selection_value(&self, name: &str) -> Option<&str> {
for item in &self.items {
if (item.kind == TMIKind::ListSelection
|| item.kind == TMIKind::ScrollSelection)
&& item.name.eq(name) {
return Some(&item.s_values[item.s_selected]);
}
}
None
}
pub fn numeric_value(&self, name: &str) -> Option<f64> {
for item in &self.items {
if item.kind == TMIKind::Numeric && item.name.eq(name) {
return Some(item.n_value);
}
}
None
}
}
pub fn menu(items: Vec<TerminalMenuItem>) -> TerminalMenu {
if items.len() == 0 {
panic!("items cannot be empty");
}
Arc::new(RwLock::new(TerminalMenuStruct {
items,
selected: 0,
active: false,
exited: true
}))
}
pub fn selected_item(menu: &TerminalMenu) -> String {
menu.read().unwrap().selected_item().to_owned()
}
pub fn selection_value(menu: &TerminalMenu, item: &str) -> Option<String> {
menu.read().unwrap().selection_value(item).map(|s| s.to_owned())
}
pub fn numeric_value(menu: &TerminalMenu, item: &str) -> Option<f64> {
menu.read().unwrap().numeric_value(item)
}
fn move_up(a: u16) {
if a != 0 {
execute!(stdout(), cursor::MoveUp(a)).unwrap();
}
}
fn move_down(a: u16) {
if a != 0 {
execute!(stdout(), cursor::MoveDown(a)).unwrap();
}
}
fn move_left(a: u16) {
if a != 0 {
execute!(stdout(), cursor::MoveLeft(a)).unwrap();
}
}
fn move_right(a: u16) {
if a != 0 {
execute!(stdout(), cursor::MoveRight(a)).unwrap();
}
}
fn move_to_beginning() {
for _ in 0..terminal::size().unwrap().0 {
print!("\u{8}");
}
}
fn clear_rest_of_line() {
execute!(stdout(), terminal::Clear(terminal::ClearType::UntilNewLine)).unwrap();
}
fn save_pos() {
execute!(stdout(), cursor::SavePosition).unwrap();
}
fn restore_pos() {
execute!(stdout(), cursor::RestorePosition).unwrap();
}
fn print_menu(menu: &TerminalMenuStruct, longest_name: usize, selected: usize) {
for i in 0..menu.items.len() {
print!("{} {} ", if i == selected { '>' } else { ' ' }, menu.items[i].name);
for _ in menu.items[i].name.len()..longest_name {
print!(" ");
}
match menu.items[i].kind {
TMIKind::Button => {},
TMIKind::ScrollSelection => print!("{}", menu.items[i].s_values[menu.items[i].s_selected]),
TMIKind::ListSelection => {
move_left(1);
for j in 0..menu.items[i].s_values.len() {
print!("{}{}{}",
if j == menu.items[i].s_selected {'['} else {' '},
menu.items[i].s_values[j],
if j == menu.items[i].s_selected {']'} else {' '},
);
}
}
TMIKind::Numeric => print!("{}", menu.items[i].n_value)
}
if i != menu.items.len() - 1 {
println!();
} else {
move_to_beginning();
stdout().flush().unwrap();
}
}
}
fn run_menu(menu: TerminalMenu) {
{
let mut menu = menu.write().unwrap();
menu.active = true;
menu.exited = false;
}
execute!(stdout(), cursor::Hide).unwrap();
let mut longest_name = 0;
{
let menu = menu.read().unwrap();
for item in &menu.items {
if item.name.len() > longest_name {
longest_name = item.name.len();
}
}
print_menu(&menu, longest_name, menu.selected);
}
let _raw = RawScreen::into_raw_mode().unwrap();
let input = input();
let mut stdin = input.read_async();
use KeyEvent::*;
while menu.read().unwrap().active {
if let Some(InputEvent::Keyboard(k)) = stdin.next() {
match k {
Up | Char('w') => {
let mut menu = menu.write().unwrap();
save_pos();
move_up((menu.items.len() - menu.selected - 1) as u16);
print!(" ");
if menu.selected == 0 {
menu.selected = menu.items.len() - 1;
move_down(menu.items.len() as u16 - 1);
}
else {
menu.selected -= 1;
move_up(1);
}
print!("\u{8}>");
restore_pos();
}
Down | Char('s') => {
let mut menu = menu.write().unwrap();
save_pos();
move_up((menu.items.len() - menu.selected - 1) as u16);
print!(" ");
if menu.selected == menu.items.len() - 1 {
menu.selected = 0;
move_up(menu.items.len() as u16 - 1);
}
else {
menu.selected += 1;
move_down(1);
}
print!("\u{8}>");
restore_pos();
}
Left | Char('a') => {
let mut menu = menu.write().unwrap();
let s = menu.selected;
save_pos();
move_up((menu.items.len() - s - 1) as u16);
move_right(longest_name as u16 + 6);
clear_rest_of_line();
match menu.items[s].kind {
TMIKind::Button => {}
TMIKind::ScrollSelection => {
if menu.items[s].s_selected == 0 {
menu.items[s].s_selected =
menu.items[s].s_values.len() - 1;
}
else {
menu.items[s].s_selected -= 1;
}
print!("{}", menu.items[s].s_values[
menu.items[s].s_selected
]);
}
TMIKind::ListSelection => {
if menu.items[s].s_selected == 0 {
menu.items[s].s_selected =
menu.items[s].s_values.len() - 1;
}
else {
menu.items[s].s_selected -= 1;
}
move_left(1);
for i in 0..menu.items[s].s_values.len() {
print!("{}{}{}",
if i == menu.items[s].s_selected {'['} else {' '},
menu.items[s].s_values[i],
if i == menu.items[s].s_selected {']'} else {' '},
);
}
}
TMIKind::Numeric => {
menu.items[s].n_value -=
menu.items[s].n_step;
if menu.items[s].n_value <
menu.items[s].n_min {
menu.items[s].n_value =
menu.items[s].n_min;
}
print!("{}", menu.items[s].n_value);
}
}
restore_pos();
}
Right | Char('d') => {
let mut menu = menu.write().unwrap();
let s = menu.selected;
save_pos();
move_up((menu.items.len() - s - 1) as u16);
move_right(longest_name as u16 + 6);
clear_rest_of_line();
match menu.items[s].kind {
TMIKind::Button => {}
TMIKind::ScrollSelection => {
if menu.items[s].s_selected ==
menu.items[s].s_values.len() - 1 {
menu.items[s].s_selected = 0;
}
else {
menu.items[s].s_selected += 1;
}
print!("{}", menu.items[s].s_values[
menu.items[s].s_selected
]);
}
TMIKind::ListSelection => {
if menu.items[s].s_selected ==
menu.items[s].s_values.len() - 1 {
menu.items[s].s_selected = 0;
}
else {
menu.items[s].s_selected += 1;
}
move_left(1);
for i in 0..menu.items[s].s_values.len() {
print!("{}{}{}",
if i == menu.items[s].s_selected {'['} else {' '},
menu.items[s].s_values[i],
if i == menu.items[s].s_selected {']'} else {' '},
);
}
}
TMIKind::Numeric => {
menu.items[s].n_value +=
menu.items[s].n_step;
if menu.items[s].n_value >
menu.items[s].n_max {
menu.items[s].n_value =
menu.items[s].n_max;
}
print!("{}", menu.items[s].n_value);
}
}
restore_pos();
}
Enter => {
let mut menu = menu.write().unwrap();
let s = menu.selected;
match menu.items[s].kind {
TMIKind::Button => {
menu.active = false;
}
TMIKind::ScrollSelection => {
save_pos();
move_up((menu.items.len() - s - 1) as u16);
move_right(longest_name as u16 + 6);
clear_rest_of_line();
if menu.items[s].s_selected ==
menu.items[s].s_values.len() - 1 {
menu.items[s].s_selected = 0;
} else {
menu.items[s].s_selected += 1;
}
print!("{}", menu.items[s].s_values[
menu.items[s].s_selected
]);
restore_pos();
}
TMIKind::ListSelection => {
save_pos();
move_up((menu.items.len() - s - 1) as u16);
move_right(longest_name as u16 + 6);
clear_rest_of_line();
if menu.items[s].s_selected ==
menu.items[s].s_values.len() - 1 {
menu.items[s].s_selected = 0;
}
else {
menu.items[s].s_selected += 1;
}
move_left(1);
for i in 0..menu.items[s].s_values.len() {
print!("{}{}{}",
if i == menu.items[s].s_selected {'['} else {' '},
menu.items[s].s_values[i],
if i == menu.items[s].s_selected {']'} else {' '},
);
}
restore_pos();
}
_ => ()
}
}
_ => ()
}
}
thread::sleep(Duration::from_millis(10));
}
execute!(stdout(),
cursor::MoveUp(menu.read().unwrap().items.len() as u16 - 1),
terminal::Clear(terminal::ClearType::FromCursorDown),
cursor::Show
).unwrap();
menu.write().unwrap().exited = true;
}
pub fn activate(menu: &TerminalMenu) {
let menu = menu.clone();
thread::spawn(move || {
run_menu(menu);
});
}
pub fn deactivate(menu: &TerminalMenu) {
menu.write().unwrap().active = false;
wait_for_exit(menu);
}
pub fn wait_for_exit(menu: &TerminalMenu) {
loop {
thread::sleep(Duration::from_millis(10));
if menu.read().unwrap().exited {
break;
}
}
}
pub fn run(menu: &TerminalMenu) {
run_menu(menu.clone());
}