Prompting the user for input with rprompt and loop

Chris Biscardi
InstructorChris Biscardi

Share this video with your friends

Send Tweet

Given the potential document_title we now want to ask the user if the title is still what they want to use so that we can write the file to disk. This will involve two paths depending on if we have an existing potential option or not.

We will need to use ask_for_filename in confirm_filename, so we'll write that branch first.

let filename = match document_title {
    Some(raw_title) => {
        // confirm_filename()?
        todo!()
    }
    None => ask_for_filename(),
};

rprompt allows us to ask for a small amount of input from the user (compared to the much larger input possible by passing control to the user's editor).

We'll use rprompt::prompt_reply_stderr to get a response from the user, and .wrap_err to add context to the error if anything goes wrong.

fn ask_for_filename() -> Result<String> {
    rprompt::prompt_reply_stderr(
        "\
Enter filename
> ",
    )
    .wrap_err("Failed to get filename")
}

We'll use the same behavior for the first part of confirm_filename. We'll also need to use our first lifetime to account for the shared reference argument.

match can match against multiple values for the same branch, so we'll take advantage of that to handle branches for Ns and Ys, as well as the default case. If anything goes wrong, such as someone inputting an "a", we'll fall through using loop and ask again until we get a usable answer

fn confirm_filename(raw_title: &str) -> Result<String> {
    loop {
        // prompt defaults to uppercase character in question
        // this is a convention, not a requirement enforced by
        // the code
        let result = rprompt::prompt_reply_stderr(&format!(
            "\
current title: `{}`
Do you want a different title? (y/N): ",
            raw_title,
        ))
        .wrap_err("Failed to get input for y/n question")?;

        match result.as_str() {
            "y" | "Y" => break ask_for_filename(),
            "n" | "N" | "" => {
                // the capital N in the prompt means "default",
                // so we handle "" as input here
                break Ok(slug::slugify(raw_title));
            }
            _ => {
                // ask again because something went wrong
            }
        };
    }
}

While filenames can technically have spaces in them, we're going to slugify our filenames like urls for a few reasons. One is that when starting out, many programmers fail to quote filename arguments in their bash scripts, which results in filenames with spaces being treated as separate arguments. Another is that this is a digital garden CLI, and digital gardens are often stored in git, accessed from multiple file systems, as well as from URLs. While strictly speaking we don't need to slugify our filenames, we will here so as to adhere to a safer set of characters.

cargo add slug

We'll map over the Result from rprompt which allows us to operate on the internal value if it's Ok.

fn ask_for_filename() -> Result<String> {
    rprompt::prompt_reply_stderr(
        "\
Enter filename
> ",
    )
    .wrap_err("Failed to get filename")
    .map(|title| slug::slugify(title))
}

We'll also use slugify in confirm_filename

match result.as_str() {
    "y" | "Y" => break ask_for_filename(),
    "n" | "N" | "" => {
        // the capital N in the prompt means "default",
        // so we handle "" as input here
        break Ok(slug::slugify(raw_title));
    }
    _ => {
        // ask again because something went wrong
    }
};

The fact that confirm_filename and ask_for_filename have the same return type is important because the branches of a match need to return the same type.