Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.12] - 2025-09-10

### Changed
- **Improved variable naming**: Updated variable names to be more descriptive and consistent across the codebase.

### Fixed

- **Conflicting trash items**: Updated logic to handle conflicts when recovering files with the same name.
Now, if a file with the same name exists in the original location, the recovered file will be renamed with a timestamp suffix to avoid overwriting.


## [0.1.11] - 2025-09-07

### Changed
Expand All @@ -17,11 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.10] - 2025-09-07

### Fixed

- **Critical ignore flag bug**: Fixed issue where `-i, --ignore` flag was not functioning correctly, causing errors while trying to ingore them while using `rmxt` commands

### Changed
- Imporved different cases handling in the ignore flag logic with match block

- Imporved different cases handling in the ignore flag logic with match block

## [0.1.9] - 2025-09-01

Expand Down
26 changes: 13 additions & 13 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ pub struct Args {
#[command(subcommand)]
pub command: Option<Commands>,

/// Don't put the file in trash, remove it permanently
/// Don't put the item in trash, remove it permanently
#[arg(short = 'p', long, global = false)]
pub permanent: bool,

/// Remove directories and their contents recursively
#[arg(short = 'r', long, global = false)]
pub recursive: bool,

/// Force removal of files without prompt
/// Force removal of item without prompt
#[arg(short = 'f', long, global = false)]
pub force: bool,

Expand All @@ -30,15 +30,15 @@ pub struct Args {

#[derive(Debug, Subcommand)]
pub enum Commands {
/// List files in the trash directory
/// List items in the trash directory
#[command(name = "list")]
List {
/// Specify time from which to list files (in days)
/// Specify time from which to list items (in days)
#[arg(short = 't', long, global = false)]
time: Option<i64>,
},

/// Clean up the trash directory by removing files older than 30 days
/// Clean up the trash directory by removing files/directories older than 30 days
#[command(name = "tidy")]
Tidy {
/// Specify time to live for tidy command (in days) (default is 30 days)
Expand All @@ -49,20 +49,20 @@ pub enum Commands {
/// recover all the content of the trash
#[command(name = "recover-all")]
RecoverAll {
/// Specify time from which to recover files (in days)
/// Specify time from which to recover files/directories (in days)
#[arg(short = 't', long, global = false)]
time: Option<i64>,
},

/// Recover files from the trash directory
/// Recover items from the trash directory
#[command(name = "recover")]
Recover {
/// Name of the file to recover
/// Name of the file/directorie to recover
#[arg(help = "Name of the file to recover from trash")]
name: Vec<String>,
},

/// Purge files from the trash directory
/// Purge files/directorie from the trash directory
#[command(name = "purge")]
Purge {
/// Purge files from the trash directory
Expand All @@ -72,8 +72,8 @@ pub enum Commands {
}

impl Args {
/// Get the files to remove, handling the default case
pub fn get_files(&self) -> Vec<PathBuf> {
/// Get the items to remove, handling the default case
pub fn get_items(&self) -> Vec<PathBuf> {
match &self.command {
Some(_) => Vec::new(), // No files for list/tidy/recover commands
None => self.file.clone(), // Use the default file argument
Expand Down Expand Up @@ -113,7 +113,7 @@ impl Args {
matches!(self.command, Some(Commands::Purge { .. }))
}

/// Get the name to purge (if purge command is active)
/// Get the name of the item to purge (if purge command is active)
pub fn get_purge_name(&self) -> Vec<String> {
match &self.command {
Some(Commands::Purge { name }) => name.clone(),
Expand Down Expand Up @@ -143,7 +143,7 @@ impl Args {
}
}

/// Get the time from which to list files for list command
/// Get the time from which to list files/directories for list command
pub fn get_time_list(&self) -> i64 {
match &self.command {
// Default to 0 days (which will be evaluated to all the content)if not specified
Expand Down
95 changes: 86 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use chrono::{Local, TimeZone};
use colored::Colorize;
use tabled::{
Table, Tabled,
settings::{Alignment, Style, object::Columns},
};
use std::path::Path;
use tabled::settings::object::Columns;
use tabled::settings::{Alignment, Style};
use tabled::{Table, Tabled};
use trash::os_limited::restore_all;
mod args;
use args::Args;
use clap::Parser;
use std::fs;
use std::{fs, path::PathBuf};
use trash::os_limited;
use trash::{TrashItem, delete};

Expand Down Expand Up @@ -103,12 +103,77 @@ pub fn tidy_trash(days: i64) -> Result<(), trash::Error> {
}
Ok(())
}
pub fn resolve_conflict(path: &PathBuf) -> std::io::Result<()> {
let name = match path.file_name() {
Some(name) => name.to_string_lossy(),
None => {
eprintln!("{}", "Path does not have a valid filename".red());
return Ok(());
}
};

let timestamp = Local::now().format("%Y%m%d_%H%M%S");

let stem = path
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| name.clone());

let extension = path
.extension()
.map(|ext| format!(".{}", ext.to_string_lossy()))
.unwrap_or_default();

let new_name = format!("{}_{}{}", stem, timestamp, extension);

eprintln!(
"{}",
format!(
"Conflict detected: '{}' already exists in trash. Would be renamed to: '{}'",
name, new_name
)
.yellow()
);

fs::rename(path, &new_name)?;

if let Err(e) = delete(&new_name) {
eprintln!(
"{}",
format!("Error moving {} to trash: {e}", path.display()).red()
);
}

Ok(())
}

pub fn check_conflict(path: &Path) -> bool {
let name = match path.file_name() {
Some(name) => name.to_string_lossy(),
None => {
eprintln!("{}", "Path does not have a valid filename".red());
return false;
}
};

let trash_list = match os_limited::list() {
Ok(items) => items,
Err(e) => {
eprintln!("{}", format!("Error listing trash items: {e}").red());
return false;
}
};

trash_list
.iter()
.any(|item| item.name.to_string_lossy() == name)
}

fn main() {
// parsing the args
let args = Args::parse();

let paths = args.get_files();
let paths = args.get_items();
let recursive = args.recursive;
let force = args.force;
let dir = args.dir;
Expand Down Expand Up @@ -286,9 +351,21 @@ All the contents from the trash more then {days} days will be deleted permanentl
}
}
(false, _, _, _, _) => {
if let Err(e) = delete(&path) {
eprintln!("{}", format!("Error moving to trash: {e}").red());
}
match check_conflict(&path) {
true => {
if let Err(e) = resolve_conflict(&path) {
eprintln!("{}", format!("Error resolving conflict: {e}").red());
}
}
false => {
if let Err(e) = delete(&path) {
eprintln!(
"{}",
format!("Error moving {} to trash: {e}", path.display()).red()
);
}
}
};
}
_ => {}
}
Expand Down
Loading