plume 0.1.1

Spawn a text editor to get text
Documentation
/* Copyright (c) 2018 - Mathieu Bridon <bochecha@daitauha.fr>
 *
 * This file is part of Plume
 *
 * Plume is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Plume is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Plume.  If not, see <http://www.gnu.org/licenses/>.
 */

//! Plume enables your command-line tools to ask users to write text in their
//! favourite editor.
//!
//! This works similarly to how Git spawns your `${EDITOR}` to let you write a
//! commit message.
//!
//! Plume will first check the `${EDITOR}` environment variable. If it is set,
//! then the value is used as the text editor.
//!
//! If `${EDITOR}` is not set, then Plume will search for a well-known text
//! editor. If it finds one installed, then it will use it.
//!
//! Plume then spawns the text editor, letting the user type their text. When
//! they save and close the editor, Plume will retrieve the entered text and
//! return it.
//!
//! Currently, the list of well-known text editors are, in this order:
//!
//! * `/usr/bin/nano`
//! * `/usr/bin/vim`
//! * `/usr/bin/vi`
//!
//! This should work on most UNIX-like operating systems.

#[macro_use]
extern crate failure;
extern crate tempfile;

use std::env;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::path::Path;
use std::process::Command;

use failure::Error;

use tempfile::NamedTempFile;

static KNOWN_EDITORS: &[&str; 3] = &["/usr/bin/nano", "/usr/bin/vim", "/usr/bin/vi"];

fn get_editor() -> Result<String, Error> {
    env::var("EDITOR").or_else(|_| {
        for editor in KNOWN_EDITORS {
            if Path::new(editor).exists() {
                return Ok(editor.to_owned().to_string());
            }
        }

        bail!("Could not find a suitable text editor; Set the EDITOR environment variable")
    })
}

/// Get some text from the user
///
/// This function will:
///
/// 1.  find the text editor to use
///     *   if the ${EDITOR} environment variable is set, then its value is used;
///     *   otherwise, this will search for known text editors like nano or vim;
/// 2.  launch that text editor and capture the text entered by the user;
/// 3.  return that text.
///
/// # Examples
///
/// ```rust,no_run
/// # extern crate failure;
/// # use failure::Error;
/// #
/// # extern crate plume;
/// #
/// # fn try_main() -> Result<(), Error> {
/// let text = plume::get_text()?;
/// println!("Got text:\n{}\n----------", text);
/// #
/// #     Ok(())
/// # }
/// #
/// # fn main() {
/// #     try_main().unwrap();
/// # }
/// ```
///
/// # Errors
///
/// This function will return `failure::Error` instances in a few cases:
///
/// *   a temporary file could not be created, seeked, read or closed; (see the
///     `tempfile::NamedTempFile` documentation)
/// *   the temporary file path is not valid UTF-8; (see the
///     `std::path::Path.to_str()` documentation)
/// *   no text editor could be found, either because the `${EDITOR}`
///     environment variable was not set, or because no known text editor was
///     installed;
/// *   the command spawn to launch the text editor failed or exited with a
///     non-zero return code;
pub fn get_text() -> Result<String, Error> {
    let mut tmp_file = NamedTempFile::new()?;
    let tmp_path = match tmp_file.path().to_str() {
        Some(ref s) => s.to_owned().to_string(),
        None => bail!("Invalid temporary file path: {:?}", tmp_file),
    };

    let editor = get_editor()?;
    let status = Command::new(&editor).arg(&tmp_path).spawn()?.wait()?;

    if !status.success() {
        bail!(
            "Could not launch editor: {} returned {:?}",
            editor,
            status.code()
        )
    }

    tmp_file.seek(SeekFrom::Start(0))?;
    let mut text = String::new();
    tmp_file.read_to_string(&mut text)?;
    tmp_file.close()?;

    Ok(text)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn editor_from_environment() {
        env::set_var("EDITOR", "/plume/test");

        let editor = get_editor().unwrap();
        assert_eq!(editor, "/plume/test");
    }
}