Skip to main content

lunar_lib/
prompts.rs

1use std::{
2    fmt::{Arguments, Debug},
3    io::{self, Write},
4    sync::OnceLock,
5};
6
7use crate::error;
8
9static PROMPTER: OnceLock<&'static dyn Prompter> = OnceLock::new();
10pub fn set_prompter(prompter: &'static dyn Prompter) -> Result<(), &'static dyn Prompter> {
11    PROMPTER.set(prompter)
12}
13pub fn get_prompter() -> Option<&'static dyn Prompter> {
14    PROMPTER.get().copied()
15}
16
17/// A very simple cli logger
18#[derive(Debug)]
19pub struct CliPrompter;
20
21pub trait Prompter: Sync + Debug {
22    /// Tells the prompter to start an "Okay" prompt
23    ///
24    /// This prompt just requires user input for a confirmation something happend.
25    /// An example usage is "Press any key to continue..."
26    fn prompt_okay(&self, msg: Arguments);
27
28    /// Tells the prompter to start a "Yes/No" prompt
29    ///
30    /// This prompt just requires user input for a confirmation if they want to do something.
31    /// An example usage is "Are you sure you want to do this? \[Y/n\]"
32    ///
33    /// This function should return [`None`] if the result couldn't be parsed
34    fn prompt_ask(&self, msg: Arguments, default: bool) -> Option<bool>;
35}
36
37impl Prompter for CliPrompter {
38    fn prompt_okay(&self, msg: Arguments) {
39        let mut output = io::stdout();
40        let _ = output.write_fmt(msg);
41        let _ = output.write_all(b"Press [ENTER] to continue.\n");
42        let _ = output.flush();
43        let _ = io::stdin().read_line(&mut String::new());
44    }
45
46    fn prompt_ask(&self, msg: Arguments, default: bool) -> Option<bool> {
47        let prompt = if default { "[Y/n]" } else { "[y/N]" };
48
49        let mut output = io::stdout();
50        loop {
51            let _ = output.write_all(b"==> Confirmation\n");
52            let _ = output.write_fmt(msg);
53            let _ = output.write(b"\n");
54            let _ = output.write_all(prompt.as_bytes());
55            let _ = output.write(b"\n");
56            let _ = output.flush();
57
58            let mut input = String::new();
59            io::stdin()
60                .read_line(&mut input)
61                .inspect_err(|err| error!("Could not read stdin: {err}"))
62                .ok()?;
63
64            match input.trim().to_ascii_lowercase().as_str() {
65                "" => return Some(default),
66                "y" | "yes" => return Some(true),
67                "n" | "no" => return Some(false),
68                _ => {
69                    let _ = output.write_all(b"Please enter 'y' for yes or 'n' for no.");
70                    let _ = output.flush();
71                }
72            }
73        }
74    }
75}
76
77/// Requests an "Okay" prompt to the current prompter lazily using format args
78///
79/// If no prompter is set, the args will not be parsed
80#[macro_export]
81macro_rules! okay {
82    ($($arg:tt)*) => {{
83        if let Some(prompter) = $crate::prompts::get_prompter() {
84            prompter.prompt_okay(format_args!($($arg)*));
85        }
86    }};
87}
88
89/// Requests an "Ask" or "Yes/No" prompt to the current prompter lazily using format args
90///
91/// If no prompter is set, the args will not be parsed
92#[macro_export]
93macro_rules! ask {
94    ($default:expr, $($arg:tt)*) => {{
95        if let Some(prompter) = $crate::prompts::get_prompter() {
96            prompter.prompt_ask(format_args!($($arg)*), $default)
97        } else {
98            None
99        }
100    }};
101}