From 0d402647cb2eb98e4fc97c3a60ab3c6cba9152d0 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 6 Nov 2024 14:14:20 +0000 Subject: [PATCH] term: allow Editor to be reusable The `Editor` is very useful for correctly opening a text editor and making changes to some initial input. The current use of `Editor` is only for getting input for commets. However, it would also be useful for opening up a text editor on some other existing files or text, for example, in the command `rad config edit`. The `Editor` struct was changed to have two new options, `truncate` and `cleanup`, to allow the user of the struct to dictate whether existing text is truncated, and if the underlying file should be remove. The original `new` method is now named `comment`, mimicing the existing construction of a temporary `RAD_COMMENT` file. The new version of `new` accepts any file for the `Editor` and will open it without truncating or removing the file. `Editor` is now used for the `rad config edit` command. --- radicle-cli/src/commands/config.rs | 19 ++--- .../src/commands/patch/review/builder.rs | 5 +- radicle-cli/src/terminal/patch.rs | 5 +- radicle-term/src/editor.rs | 81 ++++++++++++++----- radicle-tools/src/rad-cli-demo.rs | 5 +- 5 files changed, 79 insertions(+), 36 deletions(-) diff --git a/radicle-cli/src/commands/config.rs b/radicle-cli/src/commands/config.rs index 56f00c92..95d27489 100644 --- a/radicle-cli/src/commands/config.rs +++ b/radicle-cli/src/commands/config.rs @@ -1,7 +1,6 @@ #![allow(clippy::or_fun_call)] use std::ffi::OsString; use std::path::Path; -use std::process; use std::str::FromStr; use anyhow::anyhow; @@ -188,18 +187,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { path.display() ); } - Operation::Edit => { - let Some(cmd) = term::editor::default_editor() else { - anyhow::bail!("no editor configured; please set the `EDITOR` environment variable"); - }; - process::Command::new(cmd) - .stdout(process::Stdio::inherit()) - .stderr(process::Stdio::inherit()) - .stdin(process::Stdio::inherit()) - .arg(&path) - .spawn()? - .wait()?; - } + Operation::Edit => match term::editor::Editor::new(&path)?.extension("json").edit()? { + Some(_) => { + term::success!("Successfully made changes to the configuration at {path:?}") + } + None => term::info!("No changes were made to the configuration at {path:?}"), + }, } Ok(()) diff --git a/radicle-cli/src/commands/patch/review/builder.rs b/radicle-cli/src/commands/patch/review/builder.rs index f99911ff..8a5f91ba 100644 --- a/radicle-cli/src/commands/patch/review/builder.rs +++ b/radicle-cli/src/commands/patch/review/builder.rs @@ -853,7 +853,10 @@ impl CommentBuilder { for line in hunk.to_unified_string()?.lines() { writeln!(&mut input, "> {line}")?; } - let output = term::Editor::new().extension("diff").edit(input)?; + let output = term::Editor::comment() + .extension("diff") + .initial(input)? + .edit()?; if let Some(output) = output { let header = HunkHeader::try_from(hunk)?; diff --git a/radicle-cli/src/terminal/patch.rs b/radicle-cli/src/terminal/patch.rs index cb2efa24..3a40bb37 100644 --- a/radicle-cli/src/terminal/patch.rs +++ b/radicle-cli/src/terminal/patch.rs @@ -51,7 +51,10 @@ impl Message { let comment = match self { Message::Edit => { if io::stderr().is_terminal() { - term::Editor::new().extension("markdown").edit(help)? + term::Editor::comment() + .extension("markdown") + .initial(help)? + .edit()? } else { Some(help.to_owned()) } diff --git a/radicle-term/src/editor.rs b/radicle-term/src/editor.rs index c4e7bab0..3f255b4b 100644 --- a/radicle-term/src/editor.rs +++ b/radicle-term/src/editor.rs @@ -13,26 +13,52 @@ pub const PATHS: &[&str] = &["/usr/local/bin", "/usr/bin", "/bin"]; /// Allows for text input in the configured editor. pub struct Editor { path: PathBuf, -} - -impl Drop for Editor { - fn drop(&mut self) { - fs::remove_file(&self.path).ok(); - } + truncate: bool, + cleanup: bool, } impl Default for Editor { fn default() -> Self { - Self::new() + Self::comment() + } +} + +impl Drop for Editor { + fn drop(&mut self) { + if self.cleanup { + fs::remove_file(&self.path).ok(); + } } } impl Editor { /// Create a new editor. - pub fn new() -> Self { + pub fn new(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + if path.try_exists()? { + let meta = fs::metadata(path)?; + if !meta.is_file() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "must be used to edit a file", + )); + } + } + Ok(Self { + path: path.to_path_buf(), + truncate: false, + cleanup: false, + }) + } + + pub fn comment() -> Self { let path = env::temp_dir().join(COMMENT_FILE); - Self { path } + Self { + path, + truncate: true, + cleanup: true, + } } /// Set the file extension. @@ -43,26 +69,43 @@ impl Editor { self } - /// Open the editor and return the edited text. - /// - /// If the text hasn't changed from the initial contents of the editor, - /// return `None`. - pub fn edit(&mut self, initial: impl AsRef<[u8]>) -> io::Result> { - let initial = initial.as_ref(); + /// Truncate the file to length 0 when opening + pub fn truncate(mut self, truncate: bool) -> Self { + self.truncate = truncate; + self + } + + /// Clean up the file after the [`Editor`] is dropped. + pub fn cleanup(mut self, cleanup: bool) -> Self { + self.cleanup = cleanup; + self + } + + /// Initialize the file with the provided `content`, as long as the file + /// does not already contain anything. + pub fn initial(self, content: impl AsRef<[u8]>) -> io::Result { + let content = content.as_ref(); let mut file = fs::OpenOptions::new() .write(true) .create(true) - .truncate(true) + .truncate(self.truncate) .open(&self.path)?; if file.metadata()?.len() == 0 { - file.write_all(initial)?; - if !initial.ends_with(&[b'\n']) { + file.write_all(content)?; + if !content.ends_with(&[b'\n']) { file.write_all(b"\n")?; } file.flush()?; } + Ok(self) + } + /// Open the editor and return the edited text. + /// + /// If the text hasn't changed from the initial contents of the editor, + /// return `None`. + pub fn edit(&mut self) -> io::Result> { let Some(cmd) = self::default_editor() else { return Err(io::Error::new( io::ErrorKind::NotFound, @@ -126,7 +169,7 @@ impl Editor { } /// Get the default editor command. -pub fn default_editor() -> Option { +fn default_editor() -> Option { // First check the standard environment variables. if let Ok(visual) = env::var("VISUAL") { if !visual.is_empty() { diff --git a/radicle-tools/src/rad-cli-demo.rs b/radicle-tools/src/rad-cli-demo.rs index 36caf318..5630f526 100644 --- a/radicle-tools/src/rad-cli-demo.rs +++ b/radicle-tools/src/rad-cli-demo.rs @@ -35,9 +35,10 @@ fn main() -> anyhow::Result<()> { radicle_term::pager::page(table)?; } "editor" => { - let output = terminal::editor::Editor::new() + let output = terminal::editor::Editor::comment() .extension("rs") - .edit("// Enter code here."); + .initial("// Enter code here.")? + .edit(); match output { Ok(Some(s)) => {