minimo 0.5.42

terminal ui library combining alot of things from here and there and making it slightly easier to play with
Documentation
use super::*;

pub type AsyncClosure<T> = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<T, Box<dyn std::error::Error>>> + Send>> + Send + Sync>;

#[derive(Clone)]
pub struct AsyncChoice<T> {
    pub name: String,
    pub description: String,
    action: AsyncClosure<T>,
}

impl<T> Pickable for AsyncChoice<T> {
    fn get_title(&self) -> String {
        self.name.clone()
    }
    fn get_description(&self) -> String {
        self.description.clone()
    }
}

impl<T> AsyncChoice<T> {
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        action: AsyncClosure<T>,
    ) -> Self {
        AsyncChoice {
            name: name.into(),
            description: description.into(),
            action,
        }
    }

    pub async fn run(&self) -> Result<T, Box<dyn std::error::Error>> {
        (self.action)().await
    }
}

pub async fn async_menu<T: Clone>(message: impl Into<String>, options: &Vec<AsyncChoice<T>>) -> Option<AsyncChoice<T>> {
    let message = message.into();
    let mut filter = String::new();
    for _ in 0..options.len() + 2 {
        println!();
    }
    let (_, y) = cursor::position().unwrap();
    let mut corrected_y = y - options.len() as u16 - 2;
    terminal::enable_raw_mode().unwrap();
    let mut selected_index = 0;

    read().unwrap();

    loop {
        execute!(
            io::stdout(),
            cursor::MoveTo(0, corrected_y),
            terminal::Clear(terminal::ClearType::FromCursorDown)
        )
        .unwrap();

        execute!(io::stdout(), cursor::MoveTo(0, corrected_y)).unwrap();
        showln!(yellow_bold, "╭─ ", cyan_bold, message, yellow_bold, "");
        let filtered_options: Vec<&AsyncChoice<T>> = options
            .iter()
            .filter(|o| {
                filter.is_empty()
                    || o.get_title().to_lowercase().contains(&filter.to_lowercase())
                    || o.get_description().to_lowercase().contains(&filter.to_lowercase())
            })
            .collect();

        for (index, option) in filtered_options.iter().enumerate() {
            if index == selected_index {
                showln!(
                    yellow_bold,
                    "",
                    yellowbg,
                    format!(" {} ", option.get_title()),
                    white,
                    " ",
                    yellow_bold,
                    option.get_description()
                );
            } else {
                showln!(
                    yellow_bold,
                    "",
                    white,
                    format!(" {} ", option.get_title()),
                    white,
                    " ",
                    gray_dim,
                    option.get_description()
                );
            }
        }
        show!(yellow_bold, "╰─→ ", white_bold, filter);
        io::stdout().flush().unwrap();
        match read().unwrap() {
            event::Event::Key(KeyEvent { code, kind, .. }) => match kind {
                KeyEventKind::Press => match code {
                    KeyCode::Up => {
                        if selected_index > 0 {
                            selected_index -= 1;
                        }
                    }
                    KeyCode::Down => {
                        if selected_index < filtered_options.len() - 1 {
                            selected_index += 1;
                        }
                    }
                    KeyCode::Enter => {
                        execute!(
                            io::stdout(),
                            cursor::MoveTo(0, corrected_y + 1),
                            terminal::Clear(terminal::ClearType::FromCursorDown)
                        )
                        .unwrap();
                        showln!(
                            yellow_bold,
                            "╰→ ",
                            white_bold,
                            filtered_options[selected_index].get_title()
                        );
                        terminal::disable_raw_mode().unwrap();
                        return Some(filtered_options[selected_index].clone());
                    }
                    KeyCode::Char(c) => {
                        filter.push(c);
                        selected_index = 0;
                    }
                    KeyCode::Backspace => {
                        filter.pop();
                        selected_index = 0;
                    }
                    KeyCode::Esc => {
                        terminal::disable_raw_mode().unwrap();
                        return None;
                    }
                    _ => {}
                },
                _ => {}
            },
            _ => {}
        }
    }
}

#[macro_export]
macro_rules! async_choice {
    ($name:expr, $description:expr, $action:expr) => {
        $crate::async_impl::AsyncChoice::new(
            $name,
            $description,
            std::sync::Arc::new(|| Box::pin($action())),
        )
    };
    ($name:expr, $action:expr) => {
        $crate::async_impl::AsyncChoice::new(
            $name,
            "",
            std::sync::Arc::new(|| Box::pin($action())),
        )
    };
}

#[macro_export]
macro_rules! async_selection {
    ($message:expr, $choices:expr) => {
        $crate::async_impl::async_menu($message, $choices).await
    };
    ($choices:expr) => {
        $crate::async_impl::async_menu("", $choices).await
    };
}