use std::env;
use std::error::Error;
use std::fmt;
use std::ffi::OsString;
use std::io::{
Error as IoError,
Read,
Seek,
Write
};
use std::process::Command;
use serde::{Serialize, de::DeserializeOwned};
use tempfile::Builder;
use crate::{
Claw,
Conciliator,
input::AbortRetryContinue,
Paint
};
const CRATE_NAME: &str = env!("CARGO_PKG_NAME");
pub trait Editable {
type E: Edit;
fn into_edit(self) -> Self::E;
}
pub trait Edit {
const EXTENSION: &'static str;
type T;
type Err: Error + From<IoError>;
fn write<W: Write>(&self, w: &mut W) -> Result<(), Self::Err>;
fn read<C>(&self, content: String, con: &C) -> Option<Self::T>
where C: Conciliator + ?Sized;
}
pub enum Edited<E: Edit> {
Ok(E::T),
Cancelled,
Err(E::Err)
}
#[derive(Debug)]
pub enum EditError<E: Error> {
Serialize(E),
Io(IoError)
}
pub struct TomlEditor<T: Serialize + DeserializeOwned> {
thing: T
}
#[macro_export]
macro_rules! edit_as_toml {
($x:ident) => {
impl $crate::edit::Editable for $x {
type E = $crate::edit::TomlEditor<Self>;
fn into_edit(self) -> Self::E {Self::E::new(self)}
}
};
($x:ident< $($i:ident),* >, $($i2:ident: $tp:path),*) => {
impl< $($i,)* > $crate::edit::Editable for $x< $($i),* >
where $( $i2: $tp ),*
{
type E = $crate::edit::TomlEditor<Self>;
fn into_edit(self) -> Self::E {Self::E::new(self)}
}
};
}
#[doc(inline)]
pub use edit_as_toml;
pub(crate) fn edit<E: Edit>(con: &Claw, editor: E) -> Edited<E> {
let command = match get_editor_command(con) {
Some(c) => c,
None => return Edited::Cancelled
};
let try_block = || {
let mut file = Builder::new()
.prefix(&format!("{CRATE_NAME}_edit_"))
.suffix(&format!(".{}", E::EXTENSION))
.rand_bytes(8)
.tempfile()?;
editor.write(&mut file)?;
file.as_file().sync_data()?;
let time_before = file.as_file().metadata()?.modified()?;
Ok((file, time_before))
};
let (mut file, time_before) = match try_block() {
Ok((f, t)) => (f, t),
Err(e) => return Edited::Err(e)
};
loop {
let try_block = || {
let editor_exit = Command::new(&command)
.arg(file.path())
.status()?;
let time_after = file.as_file().metadata()?.modified()?;
Ok((editor_exit, time_after))
};
let (editor_exit, time_after) = match try_block() {
Ok((e, s)) => (e, s),
Err(e) => return Edited::Err(e)
};
if !editor_exit.success() {
con.error("Editor ")
.push_bold(&command.to_string_lossy())
.push_plain(" failed! (")
.push_delta(&editor_exit)
.push_plain(")");
match con.input(AbortRetryContinue::Abort) {
AbortRetryContinue::Abort => return Edited::Cancelled,
AbortRetryContinue::Retry => continue,
AbortRetryContinue::Continue => {}
}
}
if time_before >= time_after {
con.warn("File not edited!");
match con.input(AbortRetryContinue::Abort) {
AbortRetryContinue::Abort => return Edited::Cancelled,
AbortRetryContinue::Retry => continue,
AbortRetryContinue::Continue => {}
}
}
let mut res = String::new();
if let Err(e) = file.rewind() {return Edited::Err(e.into())}
if let Err(e) = file.read_to_string(&mut res) {
return Edited::Err(e.into())
}
if let Some(thing) = editor.read(res, con) {
break Edited::Ok(thing)
}
match con.confirm(true, "Retry?") {
true => continue,
false => break Edited::Cancelled
}
}
}
fn get_editor_command(con: &Claw) -> Option<OsString> {
let editor = env::var_os("EDITOR");
if editor.is_some() {return editor}
con.error("$EDITOR environment variable is not set!");
con.info("A text editor is required to edit a temporary file");
con.confirm(false, "Enter a command to use as the editor program?")
.then(|| con.input("Enter editor command"))
.map(Into::into)
}
impl<E: Edit> Edited<E> {
pub fn unwrap(self) -> E::T {
match self {
Self::Ok(t) => t,
Self::Cancelled => panic!("Edit aborted!"),
Self::Err(e) => panic!("Edit failed: {e}")
}
}
pub fn unwrap_option(self) -> Option<E::T> {
match self {
Self::Ok(t) => Some(t),
Self::Cancelled => None,
Self::Err(e) => panic!("Edit failed: {e}")
}
}
}
impl<E: Error> From<IoError> for EditError<E> {
fn from(e: IoError) -> Self {Self::Io(e)}
}
impl<E: Error> Error for EditError<E> {}
impl<E: Error> fmt::Display for EditError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Serialize(e) => fmt::Display::fmt(&e, f),
Self::Io(e) => e.fmt(f)
}
}
}
impl<T: Serialize + DeserializeOwned> TomlEditor<T> {
pub fn new(thing: T) -> Self {Self {thing}}
}
impl<T: Serialize + DeserializeOwned> Edit for TomlEditor<T> {
const EXTENSION: &'static str = "toml";
type T = T;
type Err = EditError<toml::ser::Error>;
fn write<W: Write>(&self, w: &mut W) -> Result<(), Self::Err> {
let mut buf = String::new();
let ser = toml::Serializer::pretty(&mut buf);
if let Err(e) = self.thing.serialize(ser) {
return Err(EditError::Serialize(e))
}
w.write_all(buf.as_bytes())?;
Ok(())
}
fn read<C>(&self, content: String, con: &C) -> Option<Self::T>
where C: Conciliator + ?Sized
{
toml::from_str(&content)
.map_err(|e| {con
.error("Failed to parse file as TOML: ")
.push_plain(&e);
})
.ok()
}
}
impl<'s> Edit for &'s str {
const EXTENSION: &'static str = "txt";
type T = String;
type Err = IoError;
fn write<W: Write>(&self, w: &mut W) -> Result<(), IoError> {
w.write_all(self.as_bytes())
}
fn read<C>(&self, content: String, _con: &C) -> Option<Self::T>
where C: Conciliator + ?Sized
{
Some(content)
}
}
impl<T: Edit> Editable for T {
type E = Self;
fn into_edit(self) -> Self {self}
}