aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2022-11-04 11:32:07 +0100
committerThomas Voss <mail@thomasvoss.com> 2022-11-04 11:37:07 +0100
commitbb0f6f3a76d9e1099460fb2b3b7b41653d697898 (patch)
treed2d44ff8073bceacbbfb92814921f72fad29c035 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/encoded_string.rs26
-rw-r--r--src/error.rs34
-rw-r--r--src/main.rs112
-rw-r--r--src/main_result.rs32
4 files changed, 204 insertions, 0 deletions
diff --git a/src/encoded_string.rs b/src/encoded_string.rs
new file mode 100644
index 0000000..55090a9
--- /dev/null
+++ b/src/encoded_string.rs
@@ -0,0 +1,26 @@
+use std::str;
+
+pub struct EncodedString<'a> {
+ pub s: str::Bytes<'a>,
+}
+
+impl<'a> Iterator for EncodedString<'a> {
+ type Item = u8;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.s.next().map(|c| match c {
+ b'\\' => match self.s.next() {
+ Some(b'n') => b'\n',
+ Some(b'\\') | None => b'\\',
+ Some(c) => c
+ },
+ c => c
+ })
+ }
+}
+
+impl<'a> EncodedString<'a> {
+ pub fn decode(self) -> String {
+ String::from_utf8(self.s.collect()).unwrap()
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..06bfbbe
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,34 @@
+use std::{
+ env,
+ fmt::{self, Display},
+ io
+};
+
+pub enum Error {
+ BadLengths,
+ DuplicateElems(Vec<String>),
+ IOError(io::Error),
+ NoEditor,
+ SpawnFailed(String, io::Error),
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let p = env::args().next().unwrap();
+ match self {
+ Self::BadLengths => writeln!(f, "{p}: Files have been added or removed during editing"),
+ Self::DuplicateElems(ds) => ds.iter().try_for_each(
+ |d| writeln!(f, "{p}: Multiple files named \"{}\" specified", d)
+ ),
+ Self::IOError(e) => writeln!(f, "{p}: {e}"),
+ Self::NoEditor => writeln!(f, "{p}: \"EDITOR\" environment variable is not set"),
+ Self::SpawnFailed(ed, e) => writeln!(f, "{p}: Failed to spawn editor \"{ed}\": {e}")
+ }
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Self::IOError(e)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e6b1b78
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,112 @@
+mod encoded_string;
+mod error;
+mod main_result;
+
+use std::{
+ collections::HashSet,
+ env,
+ hash::Hash,
+ io::{self, BufRead, BufReader, BufWriter, Write, Seek, SeekFrom},
+ process::Command
+};
+
+use encoded_string::*;
+use error::*;
+use main_result::*;
+
+use {
+ itertools::Itertools,
+ tempfile::NamedTempFile
+};
+
+fn main() -> MainResult {
+ work().into()
+}
+
+fn work() -> Result<(), Error> {
+ let files = env::args().skip(1).collect::<Vec<String>>();
+ let dups = duplicate_elements(files.clone());
+ if !dups.is_empty() {
+ return Err(Error::DuplicateElems(dups));
+ }
+
+ let mut tmpfile = NamedTempFile::new()?;
+ let mut writer = BufWriter::new(&tmpfile);
+
+ files.iter().try_for_each(|f| encode_to_file(&mut writer, f))?;
+ writer.flush()?;
+ drop(writer);
+
+ let editor = env::var("EDITOR")
+ .ok()
+ .filter(|e| !e.is_empty())
+ .ok_or(Error::NoEditor)?;
+
+ Command::new(&editor)
+ .arg(tmpfile.path().as_os_str())
+ .spawn()
+ .map_err(|err| Error::SpawnFailed(editor, err))?
+ .wait()?;
+
+ tmpfile.seek(SeekFrom::Start(0))?;
+ let new_files = decode_from_file(&tmpfile)?;
+
+ assert_changes(
+ files.iter().cloned().collect(),
+ new_files.iter().cloned().collect()
+ )?;
+ new_files.iter().for_each(|f| println!("{}", f));
+
+ Ok(())
+}
+
+fn assert_changes(old: Vec<String>, new: Vec<String>) -> Result<(), Error> {
+ if old.len() != new.len() {
+ return Err(Error::BadLengths);
+ }
+
+ let dups = duplicate_elements(new);
+ if !dups.is_empty() {
+ return Err(Error::DuplicateElems(dups));
+ }
+
+ Ok(())
+}
+
+fn duplicate_elements<T>(iter: T) -> Vec<T::Item>
+where
+ T: IntoIterator,
+ T::Item: Clone + Eq + Hash
+{
+ let mut uniq = HashSet::new();
+ iter
+ .into_iter()
+ .filter(|x| !uniq.insert(x.clone()))
+ .unique()
+ .collect::<Vec<_>>()
+}
+
+fn encode_to_file<W: Write>(f: &mut W, s: &str) -> io::Result<()> {
+ s.chars().try_for_each(|c| {
+ write!(f, "{}", match c {
+ '\\' => "\\\\",
+ '\n' => "\\n",
+ _ => return write!(f, "{}", c),
+ }
+ )
+ })?;
+ write!(f, "{}", '\n')
+}
+
+fn decode_from_file(tmpfile: &NamedTempFile) -> Result<Vec<String>, io::Error> {
+ BufReader::new(tmpfile)
+ .lines()
+ .map(|r| match r {
+ Ok(s) => {
+ let es = EncodedString { s: s.bytes() };
+ Ok(es.decode())
+ },
+ Err(_) => r
+ })
+ .collect::<Result<Vec<String>, _>>()
+}
diff --git a/src/main_result.rs b/src/main_result.rs
new file mode 100644
index 0000000..2d28874
--- /dev/null
+++ b/src/main_result.rs
@@ -0,0 +1,32 @@
+use std::{
+ io::{self, Write},
+ process::{ExitCode, Termination}
+};
+
+use super::error::Error;
+
+pub enum MainResult {
+ Success,
+ Failure(Error)
+}
+
+impl Termination for MainResult {
+ fn report(self) -> ExitCode {
+ match self {
+ Self::Success => ExitCode::SUCCESS,
+ Self::Failure(e) => {
+ let _ = write!(io::stderr(), "{e}");
+ ExitCode::FAILURE
+ }
+ }
+ }
+}
+
+impl From<Result<(), Error>> for MainResult {
+ fn from(r: Result<(), Error>) -> Self {
+ match r {
+ Ok(()) => Self::Success,
+ Err(e) => Self::Failure(e)
+ }
+ }
+}