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.
This commit is contained in:
Fintan Halpenny 2024-11-06 14:14:20 +00:00 committed by cloudhead
parent 23f8cf0d84
commit 0d402647cb
No known key found for this signature in database
5 changed files with 79 additions and 36 deletions

View File

@ -1,7 +1,6 @@
#![allow(clippy::or_fun_call)] #![allow(clippy::or_fun_call)]
use std::ffi::OsString; use std::ffi::OsString;
use std::path::Path; use std::path::Path;
use std::process;
use std::str::FromStr; use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
@ -188,18 +187,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
path.display() path.display()
); );
} }
Operation::Edit => { Operation::Edit => match term::editor::Editor::new(&path)?.extension("json").edit()? {
let Some(cmd) = term::editor::default_editor() else { Some(_) => {
anyhow::bail!("no editor configured; please set the `EDITOR` environment variable"); term::success!("Successfully made changes to the configuration at {path:?}")
}; }
process::Command::new(cmd) None => term::info!("No changes were made to the configuration at {path:?}"),
.stdout(process::Stdio::inherit()) },
.stderr(process::Stdio::inherit())
.stdin(process::Stdio::inherit())
.arg(&path)
.spawn()?
.wait()?;
}
} }
Ok(()) Ok(())

View File

@ -853,7 +853,10 @@ impl CommentBuilder {
for line in hunk.to_unified_string()?.lines() { for line in hunk.to_unified_string()?.lines() {
writeln!(&mut input, "> {line}")?; 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 { if let Some(output) = output {
let header = HunkHeader::try_from(hunk)?; let header = HunkHeader::try_from(hunk)?;

View File

@ -51,7 +51,10 @@ impl Message {
let comment = match self { let comment = match self {
Message::Edit => { Message::Edit => {
if io::stderr().is_terminal() { if io::stderr().is_terminal() {
term::Editor::new().extension("markdown").edit(help)? term::Editor::comment()
.extension("markdown")
.initial(help)?
.edit()?
} else { } else {
Some(help.to_owned()) Some(help.to_owned())
} }

View File

@ -13,26 +13,52 @@ pub const PATHS: &[&str] = &["/usr/local/bin", "/usr/bin", "/bin"];
/// Allows for text input in the configured editor. /// Allows for text input in the configured editor.
pub struct Editor { pub struct Editor {
path: PathBuf, path: PathBuf,
} truncate: bool,
cleanup: bool,
impl Drop for Editor {
fn drop(&mut self) {
fs::remove_file(&self.path).ok();
}
} }
impl Default for Editor { impl Default for Editor {
fn default() -> Self { 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 { impl Editor {
/// Create a new editor. /// Create a new editor.
pub fn new() -> Self { pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
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); let path = env::temp_dir().join(COMMENT_FILE);
Self { path } Self {
path,
truncate: true,
cleanup: true,
}
} }
/// Set the file extension. /// Set the file extension.
@ -43,26 +69,43 @@ impl Editor {
self self
} }
/// Open the editor and return the edited text. /// Truncate the file to length 0 when opening
/// pub fn truncate(mut self, truncate: bool) -> Self {
/// If the text hasn't changed from the initial contents of the editor, self.truncate = truncate;
/// return `None`. self
pub fn edit(&mut self, initial: impl AsRef<[u8]>) -> io::Result<Option<String>> { }
let initial = initial.as_ref();
/// 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<Self> {
let content = content.as_ref();
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.truncate(true) .truncate(self.truncate)
.open(&self.path)?; .open(&self.path)?;
if file.metadata()?.len() == 0 { if file.metadata()?.len() == 0 {
file.write_all(initial)?; file.write_all(content)?;
if !initial.ends_with(&[b'\n']) { if !content.ends_with(&[b'\n']) {
file.write_all(b"\n")?; file.write_all(b"\n")?;
} }
file.flush()?; 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<Option<String>> {
let Some(cmd) = self::default_editor() else { let Some(cmd) = self::default_editor() else {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::NotFound, io::ErrorKind::NotFound,
@ -126,7 +169,7 @@ impl Editor {
} }
/// Get the default editor command. /// Get the default editor command.
pub fn default_editor() -> Option<OsString> { fn default_editor() -> Option<OsString> {
// First check the standard environment variables. // First check the standard environment variables.
if let Ok(visual) = env::var("VISUAL") { if let Ok(visual) = env::var("VISUAL") {
if !visual.is_empty() { if !visual.is_empty() {

View File

@ -35,9 +35,10 @@ fn main() -> anyhow::Result<()> {
radicle_term::pager::page(table)?; radicle_term::pager::page(table)?;
} }
"editor" => { "editor" => {
let output = terminal::editor::Editor::new() let output = terminal::editor::Editor::comment()
.extension("rs") .extension("rs")
.edit("// Enter code here."); .initial("// Enter code here.")?
.edit();
match output { match output {
Ok(Some(s)) => { Ok(Some(s)) => {