scrawl-1.0.0/.gitignore010064400017500001750000000001301351673325700132220ustar0000000000000000# Rust Cruft /target **/*.rs.bk Cargo.lock # MacOS Cruft .DS_Store # Vim Cruft *.swp scrawl-1.0.0/.gitlab-ci.yml010064400017500001750000000006571351612626200136740ustar0000000000000000stages: - build - test - deploy image: rust:latest build: stage: build tags: - docker script: - cargo build --release test: stage: test tags: - docker before_script: - rustup component add clippy script: - cargo clippy - cargo test deploy: stage: deploy tags: - docker script: - cargo login $CRATES_KEY - cargo publish when: manual only: - tags scrawl-1.0.0/Cargo.toml.orig010064400017500001750000000007761351773760400141420ustar0000000000000000[package] name = "scrawl" version = "1.0.0" authors = ["Amy "] edition = "2018" repository = "https://git.xvrqt.com/amy/scrawl.git" homepage = "https://git.xvrqt.com/amy/scrawl" license = "BSD-3-Clause" readme = "README.md" description = "Opens a user's preferred text editor so they can edit data inline and saves the result to a String. Useful for interactive CLI applications." keywords = ["editor", "cli", "text", "xvrqt"] categories = ["config", "text-editors", "command-line-interface"] scrawl-1.0.0/Cargo.toml0000644000000020110000000000000103540ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "scrawl" version = "1.0.0" authors = ["Amy "] description = "Opens a user's preferred text editor so they can edit data inline and saves the result to a String. Useful for interactive CLI applications." homepage = "https://git.xvrqt.com/amy/scrawl" readme = "README.md" keywords = ["editor", "cli", "text", "xvrqt"] categories = ["config", "text-editors", "command-line-interface"] license = "BSD-3-Clause" repository = "https://git.xvrqt.com/amy/scrawl.git" scrawl-1.0.0/LICENSE.txt010064400017500001750000000026531351612626200130610ustar0000000000000000Copyright 2019 Amy Jie Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.scrawl-1.0.0/README.md010064400017500001750000000122641351773757200125310ustar0000000000000000# Scrawl Rust library that opens a user's text editor and returns the results as a string. Can be used to open and edit exisiting files, or just as a scratch space for input. Useful for having a user edit text inline with a CLI program a la `git commit -m` ![Animated example of how to use the with command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/with.gif) Built for my new (under development) daily journaling program in Rust: [Echo](https://git.xvrqt.com/xvrqt/echo) ## Quick Start ```rust use scrawl; fn main() { // Open an empty buffer with the user's preferred text editor let output = scrawl::new()?; println!("User Input: {}", output); // Open a buffer with text from a string in the text editor let output = scrawl::with("Favorite color: ")?; println!("{}", output); // Open a buffer with text from a file in the text editor let output = scrawl::open("survey.txt")?; println!("{}", output); // Open a file for direct editing in the text editor let output = scrawl::edit("README.md")?; println!("{}", output); } ``` ## Editor Struct The Editor struct allows you to set certain options before opening the editor. It also allows you resuse these settings instead of having to build them each time you want to use an editor. Run `edit()` on the struct to open the buffer. ```rust use scrawl::editor::Editor; fn main() { let editor = Editor::new() .contents("My favorite color is: ") .extension(".txt") .trim(true); let fave_color = editor.edit().unwrap(); /* Change the prompt, keep other settings the same */ editor.contents("My favorite bird is: "); let fave_bird = editor.edit().unwrap(); println!("About Me:\n{}\n{}", fave_color, fave_bird); } ``` If you want to open a one off editor without using settings, see the **Functions** section below. ### Settings #### Editor You can set a preferred text editor for the user. Otherwise, $VISUAL, $EDITOR or "textpad.exe"/"vi" is used as a fallback if none is set. ```rust let output = Editor::new().editor("vim").edit()?; ``` #### File You can set a file from which the text buffer will be seeded. If the file has an extension, this will also set the extension of the temporary buffer. This will _**not**_ modify the file. ```rust let output = Editor::new().file("my_survey.txt").edit()?; ``` #### Contents You can use a string to seed the text buffer. ```rust let output = Editor::new().contents("Favorite Number: ").edit()?; ``` #### Extension Set the extension of the temporary file created as a buffer. Useful for hinting to text editors which syntax highlighting to use. ```rust let output = Editor::new().extension(".rs").edit()?; ``` #### Trim Trim leading and trailing whitespace from the result. Enabled by default. ```rust let output = Editor::new().trim(false).edit()?; ``` #### Edit Directly If **file** is set, this will open that file for editing (instead of a temporary file) and any changes made will be reflected to that file. Disabled by default. ```rust let output = Editor::new().file("lib.rs").edit_directly(true).edit()?; ``` ## Functions These functions are provided for convenience. Useful for prototyping, or if you don't want to build and maintain a struct just to open an editor. ### New Open an empty text buffer in the user's preferred editor. Returns a Result with the contents of the buffer. ![Animated example of how to use the new command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/new.gif) ```rust use scrawl; fn main() { let output = match scrawl::new(path) { Ok(s) => s, Err(e) => e.to_string() }; println!("{}", output); } ``` ### With Open an text buffer with the contents of the String slice in the user's preferred editor. Returns a Result with the contents of the buffer. ![Animated example of how to use the with command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/with.gif) ```rust use scrawl; fn main() { let output = match scrawl::with("Hello World!") { Ok(s) => s, Err(e) => e.to_string() }; println!("{}", output); } ``` ### Open Open opens a text buffer in an editor with the contents of the file specified. This does _**not**_ edit the contents of the file. Returns a Result with the contents of the buffer. ![Animated example of how to use the open command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/open.gif) ```rust use scrawl; use std::path::Path; fn main() { let path = Path::new("hello.txt"); let output = match scrawl::open(path) { Ok(s) => s, Err(e) => e.to_string() }; println!("{}", output); } ``` ### Edit Edit opens a text buffer in an editor with the contents of the file specified. This _**does**_ edit the contents of the file. Returns a Result with the contents of the buffer. ![Animated example of how to use the edit command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/edit.gif) ```rust use scrawl; use std::path::Path; fn main() { let path = Path::new("hello.txt"); let output = match scrawl::edit(path) { Ok(s) => s, Err(e) => e.to_string() }; println!("{}", output); } ``` scrawl-1.0.0/src/editor.rs010064400017500001750000000223341351715623400136620ustar0000000000000000//! # Editor //! Struct used to configure the editor before opening and using it. #![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unstable_features, unsafe_code, unused_import_braces, unused_qualifications)] /* Standard Library */ use std::{ fs, env::{temp_dir, var}, path::{Path, PathBuf}, sync::atomic::{AtomicUsize, Ordering}, process::Command }; /* Internal Modules */ use crate::error::ScrawlError as ScrawlError; /* Constants used by the struct to prevent naming collisions of buffer */ const PREFIX: &str = "xvrqt_scrawl"; static TEMP_FILE_COUNT: AtomicUsize = AtomicUsize::new(0); /// The Editor struct allows setting up the editor before opening it. Useful for setting things like a file extension for syntax highlighting, or specifying a specific editor and more. #[derive(Debug, Default)] pub struct Editor { /// The name of the command to use instead of $EDITOR, fallsback to the user's default editor editor: Option, /// Use the contents of specified file to seed the buffer. file: Option, /// Use the contents of this String slice to seed the buffer. content: Option, /// The extension to set on the file used a temporary buffer. Useful for having the correct syntax highlighting when the editor is opened. extension: Option, /// Trim the white space off the resulting string. True by default. trim: bool, /// If file is set this will enable the user to directly edit the file that is opened. If 'file' is not set then this flag is ignored. False by default. edit_directly: bool, } impl Editor { /// Returns a new Editor struct with Trim Newlines and Require Save enabled. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new().edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn new() -> Self { Editor { editor: None, file: None, content: None, extension: None, trim: true, edit_directly: false, } } /* SETTERS */ /// Sets the name of the editor to open the text buffer. If this editor is not found it will not fallback on the user's default and return an error instead. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .editor("vim") /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn editor(&mut self, command: &str) -> &mut Editor { self.editor= Some(command.to_owned()); self } /// Seeds the text buffer with the contents of the specified file. This does **not** edit the contents of the file unless 'edit_directly(true)' is set. This will set the `extension` setting to the file's extension. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .file("hello.txt") /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn file>(&mut self, file: S) -> &mut Editor { let path: &Path = file.as_ref(); /* Set the extension */ if let Some(ext) = path.extension() { if let Some(s) = ext.to_str() { self.extension(s); } } self.file = Some(path.to_owned()); self } /// Fills the text buffer with the contents of the specified string. If both 'file' and 'contents' are set, contents will take priority. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .contents("Tell me your best memory:\n") /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn contents(&mut self, contents: &str) -> &mut Editor { self.content = Some(contents.to_owned()); self } /// Sets the extension of the temporary file used as a buffer. Useful for hinting to the editor which syntax highlighting to use. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .extension(".rs") /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn extension(&mut self, ext: &str) -> &mut Editor { self.extension = Some(ext.to_owned()); self } /// Sets whether or not to trim the resulting String of whitespace /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .trim(false) /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn trim(&mut self, b: bool) -> &mut Editor { self.trim = b; self } /// Sets whether or not to save changes to the file specified in 'file'. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .edit_directly(true) /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn edit_directly(&mut self, b: bool) -> &mut Editor { self.edit_directly = b; self } /* Utility */ /* Opens the file in the user's preferred text editor, and returns the * contents as a String. */ fn open_editor(&self) -> Result { let editor_name = self.get_editor_name(); let path = self.get_file()?; match Command::new(&editor_name) .arg(&path) .status() { Ok(status) if status.success() => { fs::read_to_string(path).map_err(|_| { ScrawlError::FailedToCaptureInput }) }, _ => Err(ScrawlError::FailedToOpenEditor(editor_name)) } } /* Attempts to determine which text editor to open the text buffer with. */ fn get_editor_name(&self) -> String { /* Use the editor set by the caller */ if let Some(ref editor) = self.editor { return editor.to_owned() } /* Check env vars for a default editor */ if let Ok(editor) = var("VISUAL").or_else(|_| var("EDITOR")) { return editor } /* Take a guess based on the system */ if cfg!(windows) { String::from("notepad.exe") } else { String::from("vi") } } /* Returns the file to use as a buffer. Copies data to it if required */ fn get_file(&self) -> Result { match self.file { Some(ref path) if self.edit_directly => Ok(path.to_owned()), _ => { /* Create a tempfile to use a buffer */ let tempfile = self.create_temp_file()?; /* Seed the tempfile with content (if any) */ if let Some(ref content) = self.content { fs::write(&tempfile, content).map_err(|_| { ScrawlError::FailedToCopyToTempFile("[String]".into()) })?; } else if let Some(ref path) = self.file { fs::copy(path, &tempfile).map_err(|_| { let path = path.to_str().unwrap_or(""); ScrawlError::FailedToCopyToTempFile(String::from(path)) })?; } Ok(tempfile) } } } /* Creates a thread safe, process safe tempfile to use as a buffer */ fn create_temp_file(&self) -> Result { /* Generate unique path to a temporary file buffer */ let process_id = std::process::id(); let i = TEMP_FILE_COUNT.fetch_add(1, Ordering::SeqCst); let ext = self.extension.as_ref().map_or("", AsRef::as_ref); let temp_file = format!("{}_{}_{}{}", PREFIX, process_id, i, ext); /* Push the file to the OS's temp dir */ let mut temp_dir = temp_dir(); temp_dir.push(temp_file); /* Create the file */ fs::File::create(&temp_dir).map_err(|_e| { ScrawlError::FailedToCreateTempfile })?; Ok(temp_dir) } /// Opens a text editor with the settings in the struct. Returns a Result with the String upon success. /// # Example /// ```no_run /// use scrawl::editor::Editor; /// /// fn main() { /// let output = Editor::new() /// .file("hello.txt") /// .edit(); /// println!("{}", output.unwrap()); /// } /// ``` pub fn edit(&self) -> Result { let mut output = self.open_editor()?; if self.trim { output = output.trim().to_owned(); } Ok(output) } } scrawl-1.0.0/src/error.rs010064400017500001750000000027261351716120000135150ustar0000000000000000//! # Scrawl Error Types //! Error enum used by the Scrawl crate. use std::fmt; /// Error enum for the Scrawl crate #[derive(Debug)] pub enum ScrawlError { /// Could not create a new temporary file to use as a buffer for Scrawl. FailedToCreateTempfile, /// Could not open the editor, or the editor quit with an error. FailedToOpenEditor(String), /// Could not read the the file into a valid UTF-8 String. FailedToCaptureInput, /// Could not open the file specified in the scrawl::open function. FailedToCopyToTempFile(String) } /* Display and Debug are required to satisfy the Error trait. Debug has been derived for ScrawlError. */ impl fmt::Display for ScrawlError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let error = match self { ScrawlError::FailedToCreateTempfile => String::from("Could not \ create a temporary file to serve as a buffer for the editor."), ScrawlError::FailedToOpenEditor(editor) => format!("Failed to \ open `{}` as a text editor or editor was terminated with errors.", editor), ScrawlError::FailedToCaptureInput => String::from("Failed to \ capture input. Was not a valid UTF-8 String."), ScrawlError::FailedToCopyToTempFile(filename) => format!("Failed \ to copy the contents of the `{}` to the buffer for editing.", filename) }; write!(f, "{}", error) } } scrawl-1.0.0/src/lib.rs010064400017500001750000000047441351715623400131470ustar0000000000000000//! # Scrawl //! A library for opening a file for editing in a text editor and capturing the result as a String #![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unstable_features, unsafe_code, unused_import_braces, unused_qualifications)] /* Standard Library */ use std::path::Path; /* Internal Modules */ pub mod error; use error::ScrawlError as ScrawlError; pub mod editor; use editor::Editor as Editor; /* Convenience functions */ /// New opens an empty text buffer in an editor and returns a Result with the contents. /// /// # Example /// ```no_run /// fn main() { /// let output = match scrawl::new() { /// Ok(s) => s, /// Err(e) => e.to_string() /// }; /// println!("{}", output); /// } /// ``` pub fn new() -> Result { Editor::new().edit() } /// New opens an text buffer with the contents of the provided String in an editor. Returns a Result with the edited contents. /// /// # Example /// ```no_run /// fn main() { /// let output = match scrawl::with("Hello World!") { /// Ok(s) => s, /// Err(e) => e.to_string() /// }; /// println!("{}", output); /// } /// ``` pub fn with(content: &str) -> Result { Editor::new().contents(content).edit() } /// Open opens a text buffer in an editor with the contents of the file specified. This does **not** edit the contents of the file. Returns a Result with the contents of the buffer. /// /// # Example /// ```no_run /// use std::path::Path; /// /// fn main() { /// let path = Path::new("hello.txt"); /// let output = match scrawl::open(path) { /// Ok(s) => s, /// Err(e) => e.to_string() /// }; /// println!("{}", output); /// } /// ``` pub fn open(p: &Path) -> Result { Editor::new().file(p).edit() } /// Edit opens a text buffer in an editor with the contents of the file specified. This **does** edit the contents of the file. Returns a Result with the contents of the buffer. /// /// # Example /// ```no_run /// use std::path::Path; /// /// fn main() { /// let path = Path::new("hello.txt"); /// let output = match scrawl::edit(path) { /// Ok(s) => s, /// Err(e) => e.to_string() /// }; /// println!("{}", output); /// } /// ``` pub fn edit(p: &Path) -> Result { Editor::new().file(p).edit_directly(true).edit() } scrawl-1.0.0/.cargo_vcs_info.json0000644000000001120000000000000123560ustar00{ "git": { "sha1": "193078eb7eab146ff15604ec5932799706529059" } }