Log in to access Rust Adventure videos!

Lesson Details

While we can ask the user for a title if they haven’t supplied one yet, we can also implement a common confirmation pattern and require the user to sign off on the title that’s been discovered.

To do this, we’ll write a new function called confirm_filename that takes the current title as an argument and uses rprompt to ask the user if they want that title or not.

If not, then we ask the user for a new filename using the ask_for_filename function we wrote in the last lesson. Otherwise we return the discovered title.

fn confirm_filename(raw_title: &str) -> io::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(&format!(
            "current title: {}
Do you want a different title? (y/N): ",
            &raw_title,
        ))?;

        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(raw_title.to_string());
            }
            _ => {
                // ask again because something went wrong
            }
        };
    }
}

looping for input

The loop here is particularly interesting.

We want the user to be able to type y or Y for “yes” and n or N for “no”.

We also handle empty string (””) as valid input, because by convention one of the letters in the prompt is going to be capitalized, and therefore the default option.

If the user chooses anything else, we need to ask the user again because anything that isn’t one of the pre-determined letters is invalid input.

The easiest way to do this is to loop, re-executing this cycle until we break out of it.

break allows us to break the loop and return a value, which will then be returned from the loop itself and then the confirm_filename function in turn.

matching on String and &str

Our result variable from the rprompt::prompt_reply is a String, which is the owned string type in Rust. String is the string you’re probably used to from other languages. You can push onto it, etc.

String literals however, are typed as &str and you can’t modify them. So if we want to write string literals in our match, then we need to convert the String into a &str, which we can do with the as_str function, so that the types we’re using to match are the same as the type of the value we’re matching on.

as_str works because String implements Deref, which takes the underlying vec and returns a shared reference to the data, as seen here. This means we could’ve also used result.deref(), but as_str makes more sense here because that’s explicitly what we want.

impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

Confirming choice

We can now use our confirm_filename function in the same place as ask_for_filename.

let filename = match document_title {
    Some(raw_title) => confirm_filename(&raw_title)
        .map(|title| slug::slugify(title))?,
    None => ask_for_filename()
        .map(|title| slug::slugify(title))?,
};

There’s some duplication here that we can remove though. Both branches return an io::Result<String> that we map into to slugify and return with ? if there’s an error.

match, like many items in Rust, is an expression with a return value, so we can map over the io::Result<String> at the end of the match block.

This works because both of the branches of the match return the same type, so we have that type after the match completes.

let filename = match document_title {
    Some(raw_title) => confirm_filename(&raw_title),
    None => ask_for_filename(),
}
.map(|title| slug::slugify(title))?;

Running the program

Running cargo run with the write subcommand, typing some content into the file, and confirming the filename results in a new file!

❯ cargo run -- write
   Compiling garden v0.1.0 (/rust-adventure/digital-garden)
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/garden write`
[src/lib.rs:17] &filepath = "/Users/chris/garden/.tmpyEbux.md"
current title: some title
Do you want a different title? (y/N):
[src/lib.rs:41] dest = "/Users/chris/garden/some-title.md"