diff --git a/Cargo.lock b/Cargo.lock index 70033a5..a405e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,50 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "errno" version = "0.3.9" @@ -30,6 +68,23 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.164" @@ -46,6 +101,8 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" name = "mvw" version = "0.1.0" dependencies = [ + "dialoguer", + "rand", "tempfile", ] @@ -55,6 +112,63 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rustix" version = "0.38.41" @@ -68,6 +182,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.14.0" @@ -81,6 +212,44 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "windows-sys" version = "0.52.0" @@ -162,3 +331,30 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 7ed2320..2859dac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] tempfile = "3.14.0" +dialoguer = "0.11.0" +rand = "0.8.5" diff --git a/src/main.rs b/src/main.rs index 300fdd6..5abca2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,24 @@ use std::fs; use std::fs::File; -use std::path::PathBuf; -use std::path::Path; -use std::io::ErrorKind; -use std::io::Write; +use std::path::{Path, PathBuf}; +use std::io::{ErrorKind, Write, BufRead, BufReader}; +use std::process; +use std::process::Command; +use std::collections::HashMap; use tempfile::NamedTempFile; +use dialoguer::Confirm; +use rand::distributions::{Alphanumeric, DistString}; const LOCK_FILE: &str = ".mvwrap"; +struct Cleanup; + +impl Drop for Cleanup { + fn drop(&mut self) { + unlock_dir(); + } +} + fn read_current_dir() -> Vec { let paths : Vec = match fs::read_dir(".") { Err(e) if e.kind() == ErrorKind::NotFound => Vec::new(), @@ -47,7 +58,7 @@ fn unlock_dir() { } } -fn create_temp_file(paths: Vec) -> String { +fn create_temp_file(paths: &Vec) -> String { let mut tmpfile = match NamedTempFile::new(){ Ok(file) => file, Err(e) => panic!("Could not create tempfile: {}", e), @@ -67,35 +78,135 @@ fn create_temp_file(paths: Vec) -> String { filepath } -fn remove_temp_file(tmpfile: String) { - if Path::new(&tmpfile).is_file() { - let _result = match fs::remove_file(&tmpfile){ +fn remove_temp_file(tmpfile: &String) { + if Path::new(tmpfile).is_file() { + let _result = match fs::remove_file(tmpfile){ Ok(result) => result, - Err(_) => panic!("Could not remove tempfile: {}", &tmpfile), + Err(_) => panic!("Could not remove tempfile: {}", tmpfile), }; } } -fn main() { - lock_dir(); +fn edit_temp_file(tmpfile: &String) { + let _output = Command::new("vi") + .arg(tmpfile) + .status() + .expect("failed to execute editor process"); +} + +fn read_temp_file(tmpfile: &String) -> Vec { + let file = File::open(tmpfile).expect("no such file"); + let buf = BufReader::new(file); + buf.lines() + .map(|l| l.expect("Could not parse line")) + .collect() +} + +fn unique_length(dst_paths: &Vec) -> usize { + let map = dst_paths.into_iter().map(|x| (x, x)).collect::>(); + map.len() +} + +fn unique_filename(dst_paths: &Vec) -> String { + let mut string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + + // Generate unique name that does not exist in dir and is not in dst_paths + while dst_paths.iter().any(|j| j==&string) || Path::new(&string).is_file() { + string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + } + string +} + +fn move_safely(src_paths: &Vec, dst_paths: &Vec) { + let src_len = src_paths.len(); + let mut intermediate_files: HashMap = HashMap::new(); + + for i in 0..src_len { + if src_paths[i] != dst_paths[i] { + // is the destination already in the source list? + // if so, an intermediate file is needed + if src_paths.iter().any(|j| j==&dst_paths[i]) { + let unique = unique_filename(&dst_paths); + intermediate_files.insert(unique.clone(), dst_paths[i].clone()); + fs::rename(&src_paths[i], &unique).expect("failed to rename file"); + println!("Moving {} -> {}", src_paths[i], unique); + } else { + fs::rename(&src_paths[i], &dst_paths[i]).expect("failed to rename file"); + println!("Moving {} -> {}", src_paths[i], dst_paths[i]); + } + } + } + for (src, dst) in intermediate_files.iter() { + fs::rename(&src, &dst).expect("failed to rename file"); + println!("Moving {} -> {}", src, dst); + } +} + +fn run_checks(src_paths: &Vec, dst_paths: &Vec) -> bool { + // Make sure there are an equal number of sources and destinations + if src_paths.len() != dst_paths.len() { + println!("ERROR: Source and target list don't have the same number of lines."); + return true + } + + // Make sure destination names are not empty + if dst_paths.iter().any(|i| i=="") { + println!("ERROR: You can't move to empty names."); + return true + } + + // Make sure all destination files are unique + let dst_paths_length_unique = unique_length(&dst_paths); + if dst_paths.len() != dst_paths_length_unique { + println!("Clashing destination names!"); + // TODO: show which ones! + return true + } + false +} + +fn main() { + // Place lockfile to not have multiple mvw processes running in the same dir at the same time + lock_dir(); + let _cleanup = Cleanup; // Cleanup LOCK_FILE on panic + + // read directory contents into vector of Strings let paths = read_current_dir(); // create named tempfile and fill with paths - let temp_file = create_temp_file(paths); + let temp_file = create_temp_file(&paths); - // edit tempfile + //display_temp_file(&temp_file); - // read and process tempfile - println!("In file: {}", temp_file); + edit_temp_file(&temp_file); - let contents = fs::read_to_string(temp_file.clone()) - .expect("Should have been able to read the file"); + let mut new_paths = read_temp_file(&temp_file); - println!("With text:\n{contents}"); - println!("----- END ----"); + // Compare length, if not equal offer to try again (or panic for now) + while run_checks(&paths, &new_paths) { - remove_temp_file(temp_file); + let confirmation = Confirm::new() + .with_prompt("Continue editing?") + .interact() + .unwrap(); + if ! confirmation { + unlock_dir(); + process::exit(1); + } + + edit_temp_file(&temp_file); + new_paths = read_temp_file(&temp_file); + } + + // (also don't overwrite existing files that are not in the input list!) + + + move_safely(&paths, &new_paths); + + //display_temp_file(&temp_file); + + remove_temp_file(&temp_file); unlock_dir(); }