lunar-lib 0.1.0

Common utilities for lunar applications
Documentation
use std::{
    fmt::{Arguments, Debug},
    io::{self, Write},
    sync::OnceLock,
};

use crate::error;

static PROMPTER: OnceLock<&'static dyn Prompter> = OnceLock::new();
pub fn set_prompter(prompter: &'static dyn Prompter) -> Result<(), &'static dyn Prompter> {
    PROMPTER.set(prompter)
}
pub fn get_prompter() -> Option<&'static dyn Prompter> {
    PROMPTER.get().copied()
}

/// A very simple cli logger
#[derive(Debug)]
pub struct CliPrompter;

pub trait Prompter: Sync + Debug {
    /// Tells the prompter to start an "Okay" prompt
    ///
    /// This prompt just requires user input for a confirmation something happend.
    /// An example usage is "Press any key to continue..."
    fn prompt_okay(&self, msg: Arguments);

    /// Tells the prompter to start a "Yes/No" prompt
    ///
    /// This prompt just requires user input for a confirmation if they want to do something.
    /// An example usage is "Are you sure you want to do this? \[Y/n\]"
    ///
    /// This function should return [`None`] if the result couldn't be parsed
    fn prompt_ask(&self, msg: Arguments, default: bool) -> Option<bool>;
}

impl Prompter for CliPrompter {
    fn prompt_okay(&self, msg: Arguments) {
        let mut output = io::stdout();
        let _ = output.write_fmt(msg);
        let _ = output.write_all(b"Press [ENTER] to continue.\n");
        let _ = output.flush();
        let _ = io::stdin().read_line(&mut String::new());
    }

    fn prompt_ask(&self, msg: Arguments, default: bool) -> Option<bool> {
        let prompt = if default { "[Y/n]" } else { "[y/N]" };

        let mut output = io::stdout();
        loop {
            let _ = output.write_all(b"==> Confirmation\n");
            let _ = output.write_fmt(msg);
            let _ = output.write(b"\n");
            let _ = output.write_all(prompt.as_bytes());
            let _ = output.write(b"\n");
            let _ = output.flush();

            let mut input = String::new();
            io::stdin()
                .read_line(&mut input)
                .inspect_err(|err| error!("Could not read stdin: {err}"))
                .ok()?;

            match input.trim().to_ascii_lowercase().as_str() {
                "" => return Some(default),
                "y" | "yes" => return Some(true),
                "n" | "no" => return Some(false),
                _ => {
                    let _ = output.write_all(b"Please enter 'y' for yes or 'n' for no.");
                    let _ = output.flush();
                }
            }
        }
    }
}

/// Requests an "Okay" prompt to the current prompter lazily using format args
///
/// If no prompter is set, the args will not be parsed
#[macro_export]
macro_rules! okay {
    ($($arg:tt)*) => {{
        if let Some(prompter) = $crate::prompts::get_prompter() {
            prompter.prompt_okay(format_args!($($arg)*));
        }
    }};
}

/// Requests an "Ask" or "Yes/No" prompt to the current prompter lazily using format args
///
/// If no prompter is set, the args will not be parsed
#[macro_export]
macro_rules! ask {
    ($default:expr, $($arg:tt)*) => {{
        if let Some(prompter) = $crate::prompts::get_prompter() {
            prompter.prompt_ask(format_args!($($arg)*), $default)
        } else {
            None
        }
    }};
}