What user experience do we want for the act of writing?
We have a few options, including letting users write directly in the console or giving them a prepared file that they can then open and write in.
Writing directly in the console doesn't give a user access to their text editor of choice. Which means lacking autocomplete, syntax highlighting, snippets, and other functionality.
Creating a file and handling the path back to
the user doesn't quite work for us either, as
that reduces the utility of the garden write
command to the equivalent of running touch
filename.md
.
So for our use case, we'd ideally like to open the user's preferred editing environment, whatever that happens to be, then wait for them to be done with the file before continuing.
Since we're working with a CLI tool, we
can safely assume that our users either
know how to, or are willingly to learn how
to, set environment variables. This means we
can use the EDITOR
variable to select an
editor to use. This is the same way git commit
works.
In Rust, there is a crate that handles not only
EDITOR
, but also various fallbacks per-platform.
We'll take advantage of edit
to call out to the user's choice of editor.
The quick usage of edit allows us to call the
edit::edit
function, and get the user's data.
pub fn write(garden_path: PathBuf, title: Option<String>) -> Result<()> {
dbg!(garden_path, title);
let template = "# ";
let content_from_user = edit::edit(template).wrap_err("unable to read writing")?;
dbg!(content_from_user);
todo!()
}
This results in a filename that looks like:
.tmpBy0Yun
"somewhere else" on the filesystem,
in a location the user would never reasonably
find it. Ideally, if anything went wrong, the
user would be able to take a look at the
in-progress tempfile they were just working on,
which should be in an easily discoverable place
like the garden path.
We don't want to lose a user's work.
Additionally, the tempfile doesn't have a file
extension, which means that the user's editor
will be less likely to recognize it as a
markdown file, so we want to add .md
to the
filepath.
use color_eyre::{eyre::WrapErr, Result};
use edit::{edit_file, Builder};
use std::io::{Read, Write};
use std::path::PathBuf;
const TEMPLATE: &[u8; 2] = b"# ";
pub fn write(garden_path: PathBuf, title: Option<String>) -> Result<()> {
let (mut file, filepath) = Builder::new()
.suffix(".md")
.rand_bytes(5)
.tempfile_in(&garden_path)
.wrap_err("Failed to create wip file")?
.keep()
.wrap_err("Failed to keep tempfile")?;
file.write_all(TEMPLATE)?;
// let the user write whatever they want in their favorite editor
// before returning to the cli and finishing up
edit_file(filepath)?;
// Read the user's changes back from the file into a string
let mut contents = String::new();
file.read_to_string(&mut contents)?;
dbg!(contents);
todo!()
}
We will use the edit::Builder
API to generate
a random tempfile to let the user write content
into. The suffix is going to be .md
and the
filename will be 5 random bytes. We also put
the tempfile in the garden path, which ensures
a user will be able to find it if necessary.
wrap_err
(which requires the eyre::WrapErr
trait in scope) wraps the potential error
resulting from these calls with additional context,
making the original error the source, and we can
keep chaining after that. We keep
the tempfile,
which would otherwise be deleted when all handles
closed, because if anything goes wrong after the
user inputs data, we want to make sure we don't
lose that data.
After the file is created, we write our template
out to the file before passing control to the
user. This requires the std::io::Write
trait
in scope. We use a const for the file template
because it won't change. To make the TEMPLATE
a const, we also need to give it a type, which
is a two element byte array. Change the string
to see how the byte array length is checked at
compile time.
Since we have access to the file
already, we
can read the contents back into a string after
the user is done editing. This requires having
the std::io::Read
trait in scope.
And now we've let the user write into a file which will stick around as long as we need it to, and importantly will stick around if any errors happen in the execution of the program, so we lose no user data and can remove the temporary file at the end of the program ourselves if all goes well.
If you have trouble getting EDITOR
to take effect, it may be because the edit
crate looks at the VISUAL
environment variable before it looks at EDITOR
, so if VISUAL
is set, it takes precedence.
For people wondering why their code fails with Failed to create wip file, you need to create your ~/.garden directory first (or pass an existing one)!