promptuity 0.0.5

Promptuity is a library that provides interactive prompts.
Documentation
use promptuity::prompts::{Input, Select, SelectOption};
use promptuity::style::*;
use promptuity::{Error, PromptBody, PromptInput, PromptState, Promptuity, Term, Terminal, Theme};

const S_STEP: Symbol = Symbol("ℹ️", "i");

struct SimpleTheme {
    prev_lines: u16,
}

impl SimpleTheme {
    fn new() -> Self {
        Self { prev_lines: 0 }
    }

    fn fmt_message(&self, message: String) -> String {
        format!("{} {}:", S_STEP, message)
    }

    fn fmt_placeholder(&self, placeholder: Option<String>) -> String {
        Styled::new(placeholder.unwrap_or_default())
            .fg(Color::DarkGrey)
            .to_string()
    }

    fn fmt_hint(&self, hint: Option<String>) -> String {
        Styled::new(hint.unwrap_or_default())
            .fg(Color::DarkGrey)
            .to_string()
    }

    fn fmt_input_active(&self, input: PromptInput, placeholder: Option<String>) -> String {
        match input {
            PromptInput::Raw(raw) => {
                format!(
                    " {}",
                    if raw.is_empty() {
                        self.fmt_placeholder(placeholder)
                    } else {
                        raw
                    }
                )
            }
            PromptInput::Cursor(cursor) => {
                format!(
                    " {}",
                    if cursor.is_empty() && placeholder.is_some() {
                        self.fmt_placeholder(placeholder)
                    } else {
                        let (left, cursor, right) = cursor.split();
                        format!("{left}{}{right}", Styled::new(cursor).rev())
                    }
                )
            }
            _ => String::new(),
        }
    }

    fn fmt_input_submit(&self, input: PromptInput) -> String {
        match input {
            PromptInput::Raw(raw) => format!(" {}", Styled::new(raw).fg(Color::Green)),
            PromptInput::Cursor(cursor) => {
                format!(" {}", Styled::new(cursor.value()).fg(Color::Green))
            }
            _ => String::new(),
        }
    }

    fn fmt_body_active(&self, body: PromptBody) -> String {
        match body {
            PromptBody::Raw(raw) => {
                format!("\n{}", raw)
            }
            _ => String::new(),
        }
    }

    fn fmt_body_submit(&self, body: PromptBody) -> String {
        match body {
            PromptBody::Raw(raw) => {
                format!("\n{}", Styled::new(raw).fg(Color::Green))
            }
            _ => String::new(),
        }
    }
}

impl<W: std::io::Write> Theme<W> for SimpleTheme {
    fn log(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        term.writeln(&message)?;
        term.flush()?;
        Ok(())
    }

    fn info(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        self.log(
            term,
            format!("{} {}", Styled::new("[info]").fg(Color::Cyan), message),
        )
    }

    fn warn(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        self.log(
            term,
            format!("{} {}", Styled::new("[warn]").fg(Color::Yellow), message),
        )
    }

    fn error(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        self.log(
            term,
            format!("{} {}", Styled::new("[error]").fg(Color::Red), message),
        )
    }

    fn success(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        self.log(
            term,
            format!("{} {}", Styled::new("[success]").fg(Color::Green), message),
        )
    }

    fn step(&mut self, term: &mut dyn Terminal<W>, message: String) -> Result<(), Error> {
        self.log(
            term,
            format!("{} {}", Styled::new(S_STEP).fg(Color::Cyan), message),
        )
    }

    fn begin(&mut self, term: &mut dyn Terminal<W>, intro: Option<String>) -> Result<(), Error> {
        term.cursor_hide()?;
        if let Some(intro) = intro {
            term.writeln(&format!("INTRO: '{}'", intro))?;
        }
        term.flush()?;
        Ok(())
    }

    fn render(
        &mut self,
        term: &mut dyn Terminal<W>,
        payload: promptuity::RenderSnapshot,
    ) -> Result<(), Error> {
        if self.prev_lines > 0 {
            term.move_previous_line(self.prev_lines)?;
            term.clear_cursor_down()?;
        }

        let mut output = String::new();

        match payload.state {
            PromptState::Active => {
                output.push_str(&self.fmt_message(payload.message));
                output.push_str(&self.fmt_input_active(payload.input, payload.placeholder));
                output.push_str(&self.fmt_body_active(payload.body));
                output.push_str(&self.fmt_hint(payload.hint));
                self.prev_lines = output.lines().count() as u16;
            }
            PromptState::Error(msg) | PromptState::Fatal(msg) => {
                output.push_str(&self.fmt_message(payload.message));
                output.push_str(&self.fmt_input_active(payload.input, payload.placeholder));
                output.push_str(&self.fmt_body_active(payload.body));
                output.push_str(&self.fmt_hint(payload.hint));
                output.push_str(&format!("\n{}", Styled::new(msg).fg(Color::Red)));
                self.prev_lines = output.lines().count() as u16;
            }
            PromptState::Submit => {
                output.push_str(&self.fmt_message(payload.message));
                output.push_str(&self.fmt_input_submit(payload.input));
                output.push_str(&self.fmt_body_submit(payload.body));
                output.push_str(&self.fmt_hint(payload.hint));
                self.prev_lines = 0;
            }
            PromptState::Cancel => {
                output.push_str(&self.fmt_message(payload.message));
                self.prev_lines = 0;
            }
        }

        term.writeln(&output)?;
        term.flush()?;

        Ok(())
    }

    fn finish(
        &mut self,
        term: &mut dyn Terminal<W>,
        _: &promptuity::PromptState,
        outro: Option<String>,
    ) -> Result<(), Error> {
        term.cursor_show()?;
        if let Some(outro) = outro {
            term.writeln(&format!("OUTRO: '{}'", outro))?;
        }
        term.flush()?;
        Ok(())
    }
}

fn main() -> Result<(), Error> {
    let mut term = Term::default();
    let mut theme = SimpleTheme::new();
    let mut p = Promptuity::new(&mut term, &mut theme);

    p.with_intro("Custom Theme DEMO").begin()?;

    let _ = p.prompt(Input::new("input message").as_mut())?;

    let _ = p.prompt(
        Select::new(
            "select message",
            vec![
                SelectOption::new("value1", "value1"),
                SelectOption::new("value2", "value2"),
                SelectOption::new("value3", "value3"),
            ],
        )
        .as_mut(),
    )?;

    p.with_outro("Outro Message").finish()?;

    Ok(())
}