1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/* 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");
    }
}