Skip to content

Commit

Permalink
Passing control to the user's favorite editor from the CLI
Browse files Browse the repository at this point in the history
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](https://crates.io/crates/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.

```rust
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.

```rust
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.
  • Loading branch information
ChristopherBiscardi committed Jan 16, 2021
1 parent 75020e4 commit 8e69a12
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 2 deletions.
90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -16,6 +16,7 @@ path = "src/main.rs"
[dependencies]
color-eyre = "0.5.10"
directories = "3.0.1"
edit = "0.1.2"
structopt = "0.3.21"

[dev-dependencies]
Expand Down
23 changes: 21 additions & 2 deletions src/write.rs
@@ -1,7 +1,26 @@
use color_eyre::Result;
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<()> {
dbg!(garden_path, title);
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, title);
todo!()
}

0 comments on commit 8e69a12

Please sign in to comment.