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)]
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(())

View File

@ -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)?;

View File

@ -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())
}

View File

@ -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<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);
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<Option<String>> {
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<Self> {
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<Option<String>> {
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<OsString> {
fn default_editor() -> Option<OsString> {
// First check the standard environment variables.
if let Ok(visual) = env::var("VISUAL") {
if !visual.is_empty() {

View File

@ -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)) => {