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.