Skip to content

Commit

Permalink
Merge pull request #18 from janpipek/split-modules
Browse files Browse the repository at this point in the history
split-modules
  • Loading branch information
janpipek authored Oct 4, 2024
2 parents cde908c + a5e148d commit db56dda
Show file tree
Hide file tree
Showing 4 changed files with 437 additions and 218 deletions.
274 changes: 274 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
use crate::extensions::{find_extensions_from_content, has_correct_extension};
use colored::Colorize;
use regex::Regex;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use unidecode::unidecode;

#[derive(Clone)]
pub struct RenameIntent {
pub old_name: PathBuf,
pub new_name: PathBuf,
}

impl RenameIntent {
/// Is the new name different from the old one?
pub fn is_changed(&self) -> bool {
self.old_name != self.new_name
}
}

impl Display for RenameIntent {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
if self.is_changed() {
write!(
f,
"{0} → {1}",
self.old_name.to_string_lossy().red(),
self.new_name.to_string_lossy().green()
)
} else {
write!(f, "{0} =", self.old_name.to_string_lossy(),)
}
}
}

pub trait RenameCommand {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf;

fn suggest_renames(&self, files: &[PathBuf]) -> Vec<RenameIntent> {
files
.iter()
.map(|path| RenameIntent {
old_name: path.clone(),
new_name: self.suggest_new_name(path),
})
.collect()
}
}

pub struct Normalize;

impl RenameCommand for Normalize {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let path_str = old_name.to_string_lossy().to_string();
let new_name = unidecode(&path_str).replace(' ', "_"); //#.to_lowercase();
PathBuf::from(new_name)
}
}

pub struct SetExtension {
pub extension: String,
}

impl RenameCommand for SetExtension {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let mut new_name = old_name.to_path_buf();
new_name.set_extension(&self.extension);
new_name
}
}

pub struct Remove {
pub pattern: String,
}

impl RenameCommand for Remove {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let new_name = old_name.to_string_lossy().replace(&self.pattern, "");
PathBuf::from(new_name)
}
}

pub struct Replace {
pub pattern: String,
pub replacement: String,
pub is_regex: bool,
}

impl RenameCommand for Replace {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let path_str = old_name.to_string_lossy().to_string();
let new_name = if self.is_regex {
let re = Regex::new(&self.pattern).unwrap();
re.replace_all(&path_str, &self.replacement).to_string()
} else {
path_str.replace(&self.pattern, &self.replacement)
};
PathBuf::from(new_name)
}
}

pub struct ChangeCase {
pub upper: bool,
}

impl RenameCommand for ChangeCase {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let path_str = old_name.to_string_lossy().to_string();
let new_name = match self.upper {
true => path_str.to_uppercase(),
false => path_str.to_lowercase(),
};
PathBuf::from(new_name)
}
}

pub struct FixExtension {
pub append: bool,
}

impl RenameCommand for FixExtension {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let possible_extensions = find_extensions_from_content(old_name);
let mut new_name = old_name.to_path_buf();
if !has_correct_extension(old_name, &possible_extensions) {
let mut new_extension = possible_extensions[0].clone();
if self.append {
if let Some(old_extension) = new_name.extension() {
new_extension.insert(0, '.');
new_extension.insert_str(0, old_extension.to_str().unwrap())
}
}
new_name.set_extension(new_extension);
};
new_name
}
}

pub struct Prefix {
pub prefix: String,
}

impl RenameCommand for Prefix {
fn suggest_new_name(&self, old_name: &Path) -> PathBuf {
let mut new_name = self.prefix.clone();
new_name.push_str(old_name.to_string_lossy().to_string().as_str());
PathBuf::from(new_name)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

/// Compare whether old_names are converted to new expected_names using command.
fn assert_renames_correctly(
command: &dyn RenameCommand,
old_names: &[&str],
expected_names: &[&str],
) {
let old: Vec<PathBuf> = old_names.iter().map(|&x| PathBuf::from(x)).collect();
let new_intents = command.suggest_renames(&old);
let new: Vec<PathBuf> = new_intents
.iter()
.map(|intent| intent.new_name.clone())
.collect();
let expected: Vec<PathBuf> = expected_names.iter().map(|&x| PathBuf::from(x)).collect();
assert_eq!(expected, new);
}

#[test]
fn test_prefix() {
assert_renames_correctly(
&Prefix { prefix: String::from("a") },
&["b", "a"],
&["ab", "aa"]
);
}

mod test_replace {
use super::*;

#[test]
fn test_regex() {
let command = Replace {
pattern: String::from("\\d"),
replacement: String::from("a"),
is_regex: true,
};
assert_renames_correctly(
&command,
&["222", "abc", "answer_is_42", "\\d2"],
&["aaa", "abc", "answer_is_aa", "\\da"]
);
}

#[test]
fn test_non_regex() {
let command = Replace {
pattern: String::from("a.c"),
replacement: String::from("def"),
is_regex: false,
};
assert_renames_correctly(
&command,
&["a.c", "abc", "ABC"],
&["def", "abc", "ABC"]
);
}
}

mod test_change_case {
use super::*;

#[test]
fn test_upper() {
assert_renames_correctly(
&ChangeCase { upper: true },
&["Abc", "hnědý", "Αθήνα", "mountAIN🗻"],
&["ABC", "HNĚDÝ", "ΑΘΉΝΑ", "MOUNTAIN🗻"]
);
}

#[test]
fn test_lower() {
assert_renames_correctly(
&ChangeCase { upper: false },
&["Abc", "hnědý", "Αθήνα", "mountAIN🗻"],
&["abc", "hnědý", "αθήνα", "mountain🗻"]
);
}
}

#[test]
fn test_normalize() {
assert_renames_correctly(
&Normalize,
&["Abc", "hnědý", "Αθήνα & Σπάρτη", "mountain🗻"],
&["Abc", "hnedy", "Athena_&_Sparte", "mountain"]
);
}

mod test_set_extension {
use super::*;

#[test]
fn test_no_extension() {
assert_renames_correctly(
&SetExtension{ extension: String::from("") },
&["a", "b", "c.jpg", ".gitignore"],
&["a", "b", "c", ".gitignore"],
);
}

#[test]
fn test_some_extension() {
assert_renames_correctly(
&SetExtension{ extension: String::from("jpg") },
&["a", "b", "c.jpg", ".gitignore"],
&["a.jpg", "b.jpg", "c.jpg", ".gitignore.jpg"],
);
}
}

#[test]
fn test_remove() {
assert_renames_correctly(
&Remove{ pattern: String::from("ab") },
&[".gitignore", "babe", "abABab"],
&[".gitignore", "be", "AB"]
)
}
}
81 changes: 81 additions & 0 deletions src/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::io::ErrorKind;
use std::path::Path;
use std::process::{self, exit};

fn infer_mimetype(path: &Path, mime_type: bool) -> Option<String> {
// TODO: Do something on windows :see_no_evil:
let mut cmd = process::Command::new("file");
let cmd_with_args = cmd.arg(path).arg("--brief");
let cmd_with_args = if mime_type {
cmd_with_args.arg("--mime-type")
} else {
cmd_with_args
};

let output = cmd_with_args.output();
match output {
Ok(output) => {
let output_str = String::from_utf8(output.stdout).unwrap();
let mime_type = match output_str.strip_suffix('\n') {
Some(s) => String::from(s),
None => output_str,
};
Some(mime_type)
}
Err(e) => match e.kind() {
ErrorKind::NotFound => {
eprintln!("Error: `file` probably not installed");
exit(-1);
}
_ => panic!("{e}"),
},
}
}

pub fn find_extensions_from_content(path: &Path) -> Vec<String> {
let mime_type_based = match infer_mimetype(path, true) {
None => vec![],
Some(mime_type) => {
let mime_type_str = mime_type.as_str();
match mime_type_str {
"application/pdf" => vec![String::from("pdf")],
"image/jpeg" => vec![String::from("jpeg"), String::from("jpg")],
"image/png" => vec![String::from("png")],
"text/csv" => vec![String::from("csv")],
"text/html" => vec![String::from("html"), String::from("htm")],
"text/x-script.python" => vec![String::from("py"), String::from("pyw")],
_other => vec![],
}
}
};

let mut description_based = match infer_mimetype(path, false) {
None => vec![],
Some(description) => {
let description_str = description.as_str();
match description_str {
"Apache Parquet" => vec![String::from("parquet"), String::from("pq")],
_other => vec![],
}
}
};

let mut extensions = mime_type_based.clone();
extensions.append(&mut description_based);
extensions
}

pub fn has_correct_extension(path: &Path, possible_extensions: &[String]) -> bool {
if possible_extensions.is_empty() {
true
} else {
let current_extension = path.extension();
if current_extension.is_none() {
false
} else {
let extension = current_extension.unwrap().to_ascii_lowercase();
let extension_str = String::from(extension.to_string_lossy());
possible_extensions.contains(&extension_str)
}
}
}
Loading

0 comments on commit db56dda

Please sign in to comment.