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"