mod fancy_menu;
mod utils;
use std::sync::{Arc, RwLock, RwLockWriteGuard};
use std::thread;
use std::time::Duration;
use crossterm::style::Color;
pub type TerminalMenu = Arc<RwLock<TerminalMenuStruct>>;
enum TMIKind {
Label,
Button,
BackButton,
Scroll { values: Vec<String>, selected: usize },
List { values: Vec<String>, selected: usize },
String { value: String, allow_empty: bool },
Password { value: String, allow_empty: bool },
Numeric { value: f64, step: Option<f64>, min: Option<f64>, max: Option<f64> },
Submenu(TerminalMenu),
}
pub struct TerminalMenuItem {
name: String,
kind: TMIKind,
color: crossterm::style::Color,
}
pub fn label<T: Into<String>>(text: T) -> TerminalMenuItem {
TerminalMenuItem {
name: text.into(),
kind: TMIKind::Label,
color: Color::Reset
}
}
pub fn button<T: Into<String>>(name: T) -> TerminalMenuItem {
TerminalMenuItem {
name: name.into(),
kind: TMIKind::Button,
color: Color::Reset
}
}
pub fn back_button<T: Into<String>>(name: T) -> TerminalMenuItem {
TerminalMenuItem {
name: name.into(),
kind: TMIKind::BackButton,
color: Color::Reset
}
}
pub fn scroll<T: Into<String>, T2: IntoIterator>(name: T, values: T2) -> TerminalMenuItem where T2::Item: Into<String> {
let values: Vec<String> = values.into_iter().map(|a| a.into()).collect();
if values.is_empty() {
panic!("values cannot be empty");
}
TerminalMenuItem {
name: name.into(),
kind: TMIKind::Scroll {
values,
selected: 0
},
color: Color::Reset
}
}
pub fn list<T: Into<String>, T2: IntoIterator>(name: T, values: T2) -> TerminalMenuItem where T2::Item: Into<String> {
let values: Vec<String> = values.into_iter().map(|a| a.into()).collect();
if values.is_empty() {
panic!("values cannot be empty");
}
TerminalMenuItem {
name: name.into(),
kind: TMIKind::List {
values,
selected: 0
},
color: Color::Reset
}
}
pub fn string<T: Into<String>, T2: Into<String>>(name: T, default: T2, allow_empty: bool) -> TerminalMenuItem {
TerminalMenuItem {
name: name.into(),
kind: TMIKind::String { value: default.into(), allow_empty },
color: Color::Reset,
}
}
pub fn password<T: Into<String>, T2: Into<String>>(name: T, default: T2, allow_empty: bool) -> TerminalMenuItem {
TerminalMenuItem {
name: name.into(),
kind: TMIKind::Password { value: default.into(), allow_empty },
color: Color::Reset,
}
}
pub fn numeric<T: Into<String>>(name: T, default: f64, step: Option<f64>, min: Option<f64>, max: Option<f64>) -> TerminalMenuItem {
if !utils::value_valid(default, step, min, max) {
panic!("invalid default value");
}
TerminalMenuItem {
name: name.into(),
kind: TMIKind::Numeric {
value: default,
step,
min,
max
},
color: Color::Reset
}
}
pub fn submenu<T: Into<String> + Clone>(name: T, items: Vec<TerminalMenuItem>) -> TerminalMenuItem {
let menu = menu(items);
menu.write().unwrap().name = Some(name.clone().into());
TerminalMenuItem {
name: name.into(),
kind: TMIKind::Submenu(menu),
color: Color::Reset
}
}
impl TerminalMenuItem {
pub fn name(&self) -> &str {
&self.name
}
pub fn colorize(mut self, color: Color) -> Self {
self.color = color;
self
}
}
pub(crate) enum PrintState {
None,
Big
}
pub struct TerminalMenuStruct {
name: Option<String>,
pub items: Vec<TerminalMenuItem>,
selected: usize,
active: bool,
exited: bool,
longest_name: usize,
exit: Option<String>,
canceled: bool,
printed: PrintState,
}
impl TerminalMenuStruct {
pub fn selected_item_name(&self) -> &str {
&self.items[self.selected].name
}
pub fn selected_item_index(&self) -> usize {
self.selected
}
fn index_of(&self, name: &str) -> usize {
self.items.iter().position(|a| a.name == name).expect("No item with the given name")
}
pub fn set_selected_item_with_name(&mut self, item: &str) {
self.selected = self.index_of(item);
}
pub fn set_selected_item_with_index(&mut self, item: usize) {
if item >= self.items.len() {
panic!("index out of bounds");
}
self.selected = item;
}
pub fn selection_value(&self, name: &str) -> &str {
match &self.items[self.index_of(name)].kind {
TMIKind::Scroll { values, selected } |
TMIKind::List { values, selected } => {
&values[*selected]
}
TMIKind::String { value, .. } | TMIKind::Password { value, .. } => value,
_ => panic!("item wrong kind")
}
}
pub fn numeric_value(&self, name: &str) -> f64 {
match self.items[self.index_of(name)].kind {
TMIKind::Numeric { value, .. } => value,
_ => panic!("item wrong kind")
}
}
pub fn get_submenu(&mut self, name: &str) -> RwLockWriteGuard<TerminalMenuStruct> {
for item in &self.items {
if item.name == name {
if let TMIKind::Submenu(submenu) = &item.kind {
return submenu.write().unwrap();
}
}
}
panic!("Item not found or is wrong kind");
}
pub fn get_latest_menu_name(&mut self) -> Option<&str> {
match &self.exit {
None => None,
Some(a) => Some(a)
}
}
pub fn canceled(&self) -> bool {
self.canceled
}
}
pub fn menu(items: Vec<TerminalMenuItem>) -> TerminalMenu {
for i in 0..items.len() {
if let TMIKind::Label = items[i].kind {
} else {
return Arc::new(RwLock::new(TerminalMenuStruct {
name: None,
items,
selected: i,
active: false,
exited: true,
longest_name: 0,
exit: None,
canceled: false,
printed: PrintState::None,
}))
}
}
panic!("no selectable items");
}
pub fn has_exited(menu: &TerminalMenu) -> bool {
menu.read().unwrap().exited
}
pub fn mut_menu(menu: &TerminalMenu) -> RwLockWriteGuard<TerminalMenuStruct> {
if !has_exited(menu) {
panic!("Cannot call mutable_instance if has_exited() is not true");
}
menu.write().unwrap()
}
pub fn activate(menu: &TerminalMenu) {
let menu = menu.clone();
thread::spawn(move || {
fancy_menu::run(menu.clone())
});
}
pub fn wait_for_exit(menu: &TerminalMenu) {
loop {
thread::sleep(Duration::from_millis(10));
if has_exited(menu) {
break;
}
}
}
pub fn run(menu: &TerminalMenu) {
fancy_menu::run(menu.clone());
}