In this lesson we’ll write out a template into our temporary file, and pass control to the user to edit it. This is the code we’ll end up with.
use edit::{edit_file, Builder};
use std::{io::Write, path::PathBuf};
pub fn write(
garden_path: PathBuf,
title: Option<String>,
) -> Result<(), std::io::Error> {
let (mut file, filepath) = Builder::new()
.suffix(".md")
.rand_bytes(5)
.tempfile_in(garden_path)?
.keep()?;
dbg!(&filepath);
let template =
format!("# {}", title.unwrap_or("".to_string()));
file.write_all(template.as_bytes())?;
edit_file(&filepath)?;
Ok(())
}
Templating file content
We’re templating out a markdown file, so we can use the title
the user might have passed in to create the file contents.
In our case, there’s not much difference between having a title and having no title, so we can use Option::unwrap_or
to provide the empty string as a default value for the template.
Otherwise, this is a standard use of the format!
macro to template variables into a String
.
let template = format!("# {}", title.unwrap_or("".to_string()));
The Write trait and write_all
The write_all
function comes with File
's implementation of the Write
trait. Since write_all
accepts &mut self
as the first argument, our file
variable when we create the builder must be declared as mut
.
fn write_all(&mut self, buf: &[u8]) -> Result<()>
It it is not declared as mut
, this is the message you’ll see.
error[E0596]: cannot borrow `file` as mutable, as it is not declared as mutable
--> src/lib.rs:16:5
|
16 | file.write_all(template.as_bytes())?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
8 | let (mut file, filepath) = Builder::new()
| +++
write_all
also takes buf: &[u8]
, which can be vocalized as a “byte slice”. We don’t have a &[u8]
though, our template is a String
. Fortunately, String
has a function called as_bytes
. that will handle this conversion for us.
The implementation of a String
is actually powered by a Vec<u8>
under the hood, so this conversion ends up being very simple. It returns a shared reference to the underlying Vec<u8>
.
pub fn as_bytes(&self) -> &[u8] {
&self.vec
}
The return type here is not the same as &Vec<u8>
but it works anyway.
The reason is because Vec<T>
implements the Deref
trait for [T]
. The deref
function returns a shared reference to the target type, so in our case that’s &[u8]
.
Editing the file
The edit
crate contains a function called edit_file
.
edit_file
accepts any type that can be turned into a reference to a Path
.
fn edit_file<P: AsRef<Path>>(file: P) -> Result<()>
We know our filepath
, which is a PathBuf
fits this description because the docs show us the trait implementations for PathBuf
.
One of the trait implementations is AsRef<Path>
.
impl AsRef<Path> for PathBuf
So we can pass in our filepath
to edit_file
. I’ve chosen to use a shared reference immediately because I know we’ll use filepath
later to read the file again, but its not technically required right now.
edit_file(&filepath)?;
edit_file
returns an io::Result<()>
, which is the type alias for std::result::Result<(), io::Error>
, so the error type matches our write
function’s and we can handle the error with ?
.
The User’s Editor
If the user (or yourself at the moment) is a heavy terminal user, they probably will already have the EDITOR
environment variable set, because that’s the same variable used for tools like git commit
on the command line.
If EDITOR
isn’t set, then there’s a series of fallback conditions for choosing what editor to use. I have mine set to code -w
to bring up VSCode.
export EDITOR="code -w"
If you’ve never set one of these variables before, you might end up in any one of a large number of editors according to the lists checked in the edit
crate and you should figure out which one it is and how to save a file, or set your own preferred editor.
static ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
// TODO: should we hardcode full paths as well in case $PATH is borked?
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
#[rustfmt::skip]
static HARDCODED_NAMES: &[&str] = &[
// CLI editors
"nano", "pico", "vim", "nvim", "vi", "emacs",
// GUI editors
"code", "atom", "subl", "gedit", "gvim",
// Generic "file openers"
"xdg-open", "gnome-open", "kde-open",
];
#[cfg(target_os = "macos")]
#[rustfmt::skip]
static HARDCODED_NAMES: &[&str] = &[
// CLI editors
"nano", "pico", "vim", "nvim", "vi", "emacs",
// open has a special flag to open in the default text editor
// (this really should come before the CLI editors, but in order
// not to break compatibility, we still prefer CLI over GUI)
"open -Wt",
// GUI editors
"code -w", "atom -w", "subl -w", "gvim", "mate",
// Generic "file openers"
"open -a TextEdit",
"open -a TextMate",
// TODO: "open -f" reads input from standard input and opens with
// TextEdit. if this flag were used we could skip the tempfile
"open",
];
#[cfg(target_os = "windows")]
#[rustfmt::skip]
static HARDCODED_NAMES: &[&str] = &[
// GUI editors
"code.cmd -n -w", "atom.exe -w", "subl.exe -w",
// notepad++ does not block for input
// Installed by default
"notepad.exe",
// Generic "file openers"
"cmd.exe /C start",
];
The Result
Running the program now should result in the template file being written out, as well as the option to write in said file.
❯ cargo run -- write -t "My New Post"
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/garden write -t 'My New Post'`
[src/lib.rs:13] &filepath = "/Users/chris/garden/.tmpQzMOy.md"
❯ cat /Users/chris/garden/.tmpQzMOy.md
# My New Post
testing this post