scrawl-1.1.0/.github/workflows/rust.yml010064400017500001750000000003301357204761700163520ustar0000000000000000name: Rust on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose scrawl-1.1.0/.gitignore010064400017500001750000000001301351673325700132230ustar0000000000000000# Rust Cruft /target **/*.rs.bk Cargo.lock # MacOS Cruft .DS_Store # Vim Cruft *.swp scrawl-1.1.0/.gitlab-ci.yml010064400017500001750000000006571351612626200136750ustar0000000000000000stages: - 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.1.0/Cargo.toml.orig010064400017500001750000000007741357204753400141360ustar0000000000000000[package] name = "scrawl" version = "1.1.0" authors = ["Amy "] edition = "2018" repository = "https://github.com/xvrqt/scrawl.git" homepage = "https://github.com/xvrqt/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.1.0/Cargo.toml0000644000000020071357204765200104340ustar00# 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.1.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://github.com/xvrqt/scrawl" readme = "README.md" keywords = ["editor", "cli", "text", "xvrqt"] categories = ["config", "text-editors", "command-line-interface"] license = "BSD-3-Clause" repository = "https://github.com/xvrqt/scrawl.git" scrawl-1.1.0/LICENSE.txt010064400017500001750000000026531351612626200130620ustar0000000000000000Copyright 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.1.0/README.md010064400017500001750000000103541357204576300125230ustar0000000000000000# 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 contents 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, error}; fn main() -> Result<(), error::ScrawlError> { let editor = editor::new() .editor("vim") .contents("My favorite color is: ") .open()?; println!("About Me:\n{}", fave_color); } ``` 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").open()?; ``` #### File You can set a file from which the text buffer will be seeded. ```rust let output = editor::new().file("my_survey.txt").open()?; ``` #### Contents You can use a string to seed the text buffer. ```rust let output = editor::new().contents("Favorite Number: ").open()?; ``` #### 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").open()?; ``` #### Edit Directly Opens a file for directory editing in the text editor. Does **not** return a String with the contents of the file. ```rust editor::new().file("lib.rs").edit(true).open()?; ``` ## 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 = scrawl::new(path).unwrap(); 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 = scrawl::with("Hello World!").unwrap(); 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; fn main() { let output = scrawl::open("hello.txt").unwrap(); 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. ![Animated example of how to use the edit command](https://xvrqt.sfo2.digitaloceanspaces.com/image-cache/edit.gif) ```rust use scrawl; fn main() { scrawl::edit("README.md").unwrap(); } ``` scrawl-1.1.0/src/editor.rs010064400017500001750000000173611357204740100136630ustar0000000000000000//! # 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::{ env::{temp_dir, var}, fs, path::{Path, PathBuf}, process::Command, sync::atomic::{AtomicUsize, Ordering}, }; /* Internal Modules */ use crate::error::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); /// Returns a new Editor struct /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// /// # fn main() -> Result<(), ScrawlError> { /// let editor = scrawl::editor::new(); /// let output = editor.open()?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn new() -> Editor { Editor { editor: get_default_editor_name(), unique: InitialState { extension: String::from(".txt"), }, } } /* Marker trait that ensures valid state transitions */ /// Marker trait used to ensure valid state transitions. pub trait EditorState {} /// 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)] pub struct Editor { /// The name of the command to use instead of $EDITOR, fallback to the user's default editor editor: String, /// Captures the state of the Editor and holds additional state information. unique: S, } /* The initial state of the Editor */ #[derive(Debug)] /// State machine type marker. Initial state of the editor. pub struct InitialState { extension: String, } impl EditorState for InitialState {} impl Editor { /// Set the editor to open the buffer in. If not set, uses the user's default editor set by the $EDITOR environment variable. pub fn editor(mut self, editor_name: &str) -> Self { self.editor = editor_name.to_owned(); self } /// Set the extension of the temporary file used as a buffer. Useful for having the text editor hilight syntax appropriately. /// Open an empty buffer /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// /// # fn main() -> Result<(), ScrawlError> { /// let editor = scrawl::editor::new() /// .editor("vim") /// .extension(".rs"); /// let output = editor.open()?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn extension(mut self, ext: &str) -> Self { self.unique.extension = ext.to_owned(); self } /// Open an empty buffer /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// /// # fn main() -> Result<(), ScrawlError> { /// let editor = scrawl::editor::new(); /// let output = editor.open()?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn open(&self) -> Result { let path = create_temp_file()?; open_editor(&self.editor, &path) } /// Use the contents of this file to seed the text buffer. pub fn file>(self, file: F) -> Editor { Editor { editor: self.editor, unique: FileState { path: file.as_ref().to_owned(), }, } } /// Use the contents of this string to seed the text buffer. pub fn contents>(self, contents: S) -> Editor { Editor { editor: self.editor, unique: ContentState { contents: contents.as_ref().to_owned(), extension: self.unique.extension, }, } } } /* Editor that has its contents initialized by the contents of a file */ #[derive(Debug)] /// State machine type marker. Holds the path of the file that the text buffer will be seeded with. pub struct FileState { path: PathBuf, } impl EditorState for FileState {} impl Editor { /// Open a buffer seeded with the contents of the file at the path provided. pub fn open(&self) -> Result { /* Copy the contents of this file to the temp file */ let temp_file_path = create_temp_file()?; fs::copy(&self.unique.path, &temp_file_path).map_err(|_| { let path = self.unique.path.to_string_lossy().into(); ScrawlError::FailedToCopyToTempFile(path) })?; open_editor(&self.editor, &temp_file_path) } /// Edit the file directly. pub fn edit(self) -> Editor { Editor { editor: self.editor, unique: EditFileState { path: self.unique.path, }, } } } /* Editor that directly edit the contents of a file */ #[derive(Debug)] /// State machine type marker. Holds the file that will be edited. pub struct EditFileState { path: PathBuf, } impl EditorState for EditFileState {} impl Editor { /// Open a file in the text editor for direct editing. pub fn open(&self) -> Result<(), ScrawlError> { Command::new(&self.editor) .arg(&self.unique.path) .status() .map(|_| ()) .map_err(|_| ScrawlError::FailedToOpenEditor(self.editor.clone())) } } /* Editor that has contents initialized by a string */ #[derive(Debug)] /// State machine type marker. Holds the contents of the string that the text buffer will be seeded with. pub struct ContentState { contents: String, extension: String, } impl EditorState for ContentState {} impl Editor { /// Open a buffer seeded with the contents of a String in a text editor. pub fn open(&self) -> Result { /* Copy the contents of this file to the temp file */ let temp_file_path = create_temp_file()?; fs::write(&temp_file_path, &self.unique.contents) .map_err(|_| ScrawlError::FailedToCopyToTempFile("[String]".into()))?; open_editor(&self.editor, &temp_file_path) } } /* Utility */ /* Opens the file specified in path in the user's preferred text editor for * editing and returns the contents as a String. */ fn open_editor(editor: &str, path: &Path) -> Result { match Command::new(editor).arg(path).status() { Ok(status) if status.success() => { fs::read_to_string(path).map_err(|_| ScrawlError::FailedToCaptureInput) } _ => Err(ScrawlError::FailedToOpenEditor(String::from(editor))), } } /* Used to seed the default editor value */ fn get_default_editor_name() -> String { var("VISUAL") .or_else(|_| var("EDITOR")) .unwrap_or_else(|_| { /* Take a guess based on the system */ if cfg!(windows) { String::from("notepad.exe") } else { String::from("vi") } }) } /* Creates a thread safe, process safe tempfile to use as a buffer */ fn create_temp_file() -> 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 temp_file = format!("{}_{}_{}", PREFIX, process_id, i); /* 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(|_| ScrawlError::FailedToCreateTempfile)?; Ok(temp_dir) } scrawl-1.1.0/src/error.rs010064400017500001750000000034001357204740100135130ustar0000000000000000//! # Scrawl Error Types //! Error enum used by the Scrawl crate. use std::error::Error; 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 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) } } /* Cowards way out */ impl Error for ScrawlError { fn source(&self) -> Option<&(dyn Error + 'static)> { None } } scrawl-1.1.0/src/lib.rs010064400017500001750000000050641357204330400131360ustar0000000000000000//! # 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; pub mod editor; /* Convenience functions */ /// New opens an empty text buffer in an editor and returns a Result with the contents. /// /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// # fn main() -> Result<(), ScrawlError> { /// let output = scrawl::new()?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn new() -> Result { editor::new().open() } /// With opens a text buffer with the contents of the provided String in an editor. Returns a Result with the edited contents. /// /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// # fn main() -> Result<(), ScrawlError> { /// let output = scrawl::with("Hello World!")?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn with(content: &str) -> Result { editor::new().contents(content).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. /// /// # Example /// ```no_run /// # use scrawl::error::ScrawlError; /// # use std::path::Path; /// /// # fn main() -> Result<(), ScrawlError> { /// let output = scrawl::open("hello.txt")?; /// println!("{}", output); /// /// let path = Path::new("website.html"); /// let output = scrawl::open(path)?; /// println!("{}", output); /// # Ok(()) /// # } /// ``` pub fn open>(path: P) -> Result { editor::new().file(path.as_ref()).open() } /// 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 scrawl::error::ScrawlError; /// # use std::path::Path; /// /// # fn main() -> Result<(), ScrawlError> { /// /* Directly edits the file, no output is returned */ /// scrawl::edit("hello.txt") /// # } /// ``` pub fn edit>(path: P) -> Result<(), ScrawlError> { editor::new().file(path.as_ref()).edit().open() } scrawl-1.1.0/.cargo_vcs_info.json0000644000000001121357204765200124310ustar00{ "git": { "sha1": "4c2fdc6bc0ccc4cd0c6735182a72ad6c17c0729b" } }