diff --git a/Changelog b/Changelog index a2cccfc..b7a4dea 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## Version 0.1.2 dev +- Add basic search empty folders in GTK GUI +- Remember place where button are placed ## Version 0.1.1 - 20.09.2020r - Added images to readme diff --git a/README.md b/README.md index db89a5d..32df704 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This is my first ever project in Rust so probably a lot of things are written in ## Done - Rich instruction with examples - CLI(`cargo run --bin czkawka_cli`) - GTK Frontend(Still WIP) - (`cargo run --bin czkawka_gui`) + - Basic layout + - Remembering of buttons between different tabs - Orbtk Frontend(Still very early WIP) - (`cargo run --bin czkawka_gui_orbtk`) - Saving results to file - Duplicated file finding @@ -20,12 +22,15 @@ This is my first ever project in Rust so probably a lot of things are written in ## TODO - Comments - a lot of things should be described -- Github CI - More unit tests - Finding files with debug symbols - Maybe windows support, but this will need some refactoring in code - Translation support - Add support for fast searching based on checking only first ~1MB of file. +- GTK Gui + - Selection of records(don't know how to do this) + - Popups + - Choosing directories(include, excluded) ## Usage and requirements Rustc 1.46 works fine(not sure about a minimal version) @@ -82,7 +87,7 @@ I checked my home directory without any folder exceptions(I removed all director First run reads file entry and save it to cache so this step is mostly limited by disk performance, and with second run cache helps it so searching is a lot of faster. -Duplicate Checker(Version 0.1) +Duplicate Checker(Version 0.1.0) | App| Executing Time | |:----------:|:-------------:| diff --git a/czkawka_core/src/empty_folder.rs b/czkawka_core/src/empty_folder.rs index 5794ef2..33432d9 100644 --- a/czkawka_core/src/empty_folder.rs +++ b/czkawka_core/src/empty_folder.rs @@ -1,5 +1,5 @@ use crate::common::{Common, Messages}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fs::{File, Metadata}; use std::io::Write; use std::path::Path; @@ -16,9 +16,10 @@ enum FolderEmptiness { /// Struct assigned to each checked folder with parent path(used to ignore parent if children are not empty) and flag which shows if folder is empty #[derive(Clone)] -struct FolderEntry { - parent_path: Option, +pub struct FolderEntry { + pub parent_path: Option, is_empty: FolderEmptiness, + pub modified_date: SystemTime, } /// Struct to store most basics info about all folder @@ -26,14 +27,14 @@ pub struct EmptyFolder { information: Info, delete_folders: bool, text_messages: Messages, - empty_folder_list: HashMap, // Path, FolderEntry + empty_folder_list: BTreeMap, // Path, FolderEntry included_directories: Vec, } /// Info struck with helpful information's about results pub struct Info { number_of_checked_folders: usize, - number_of_empty_folders: usize, + pub number_of_empty_folders: usize, } impl Info { pub fn new() -> Info { @@ -62,9 +63,16 @@ impl EmptyFolder { } } + pub fn get_empty_folder_list(&self) -> &BTreeMap { + &self.empty_folder_list + } + pub fn get_text_messages(&self) -> &Messages { &self.text_messages } + pub fn get_information(&self) -> &Info { + &self.information + } /// Public function used by CLI to search for empty folders pub fn find_empty_folders(&mut self) { @@ -122,7 +130,7 @@ impl EmptyFolder { /// Clean directory tree /// If directory contains only 2 empty folders, then this directory should be removed instead two empty folders inside because it will produce another empty folder. fn optimize_folders(&mut self) { - let mut new_directory_folders: HashMap = Default::default(); + let mut new_directory_folders: BTreeMap = Default::default(); for entry in &self.empty_folder_list { match &entry.1.parent_path { @@ -145,7 +153,7 @@ impl EmptyFolder { fn check_for_empty_folders(&mut self, initial_checking: bool) { let start_time: SystemTime = SystemTime::now(); let mut folders_to_check: Vec = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector - let mut folders_checked: HashMap = Default::default(); + let mut folders_checked: BTreeMap = Default::default(); if initial_checking { // Add root folders for finding @@ -155,21 +163,23 @@ impl EmptyFolder { FolderEntry { parent_path: None, is_empty: FolderEmptiness::Maybe, + modified_date: SystemTime::now(), }, ); folders_to_check.push(id.clone()); } } else { // Add folders searched before - for id in &self.empty_folder_list { + for (name, entry) in &self.empty_folder_list { folders_checked.insert( - id.0.clone(), + name.clone(), FolderEntry { parent_path: None, is_empty: FolderEmptiness::Maybe, + modified_date: entry.modified_date, }, ); - folders_to_check.push(id.0.clone()); + folders_to_check.push(name.clone()); } } @@ -206,6 +216,13 @@ impl EmptyFolder { FolderEntry { parent_path: Option::from(current_folder.clone()), is_empty: FolderEmptiness::Maybe, + modified_date: match metadata.modified() { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push(format!("Failed to read modification date of folder {}", current_folder)); + SystemTime::now() + } + }, }, ); } else { @@ -234,7 +251,7 @@ impl EmptyFolder { } } else { // We need to check if parent of folder isn't also empty, because we wan't to delete only parent with two empty folders except this folders and at the end parent folder - let mut new_folders_list: HashMap = Default::default(); + let mut new_folders_list: BTreeMap = Default::default(); for entry in folders_checked { if entry.1.is_empty != FolderEmptiness::No && self.empty_folder_list.contains_key(&entry.0) { new_folders_list.insert(entry.0, entry.1); diff --git a/czkawka_gui/czkawka.glade b/czkawka_gui/czkawka.glade index 94908e5..0025a1a 100644 --- a/czkawka_gui/czkawka.glade +++ b/czkawka_gui/czkawka.glade @@ -32,11 +32,6 @@ Author: Rafał Mikrut - - 100 - 1 - 10 - False applications-engineering @@ -272,11 +267,13 @@ Author: Rafał Mikrut - + True True + 3 1 - digits + False + number False @@ -319,20 +316,12 @@ Author: Rafał Mikrut - + True True in - - True - True - - - none - - - + diff --git a/czkawka_gui/src/main.rs b/czkawka_gui/src/main.rs index 153efc3..05f380c 100644 --- a/czkawka_gui/src/main.rs +++ b/czkawka_gui/src/main.rs @@ -5,10 +5,13 @@ use humansize::{file_size_opts as options, FileSize}; extern crate gtk; use chrono::NaiveDateTime; use czkawka_core::duplicate::CheckingMethod; +use czkawka_core::empty_folder::EmptyFolder; use duplicate::DuplicateFinder; use gtk::prelude::*; use gtk::{Builder, TreeView, TreeViewColumn}; +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::time::UNIX_EPOCH; #[derive(Debug)] @@ -19,10 +22,6 @@ enum ColumnsDuplicate { Modification, } -thread_local! { -pub static CHECK_TYPE: duplicate::CheckingMethod = duplicate::CheckingMethod::NONE; -} - fn main() { gtk::init().expect("Failed to initialize GTK."); @@ -34,17 +33,36 @@ fn main() { let main_window: gtk::Window = builder.get_object("main_window").unwrap(); main_window.show_all(); + //////////////////////////////////////////////////////////////////////////////////////////////// + // State + // Buttons State - // let shared_buttons: Rc> = Rc::new(RefCell::new( HashMap::<&str, bool>::new())); + let shared_buttons: Rc> = Rc::new(RefCell::new(HashMap::>::new())); + shared_buttons.borrow_mut().clear(); - let mut hashmap_buttons: HashMap<&str, bool> = Default::default(); for i in ["duplicate", "empty_folder"].iter() { - hashmap_buttons.insert(i, false); + let mut temp_hashmap: HashMap = Default::default(); + for j in ["search", "stop", "resume", "pause", "select", "delete", "save"].iter() { + if *j == "search" { + temp_hashmap.insert(j.to_string(), true); + } else { + temp_hashmap.insert(j.to_string(), false); + } + } + shared_buttons.borrow_mut().insert(i.to_string(), temp_hashmap); } + // Duplicate Finder state + let shared_duplication_state: Rc> = Rc::new(RefCell::new(DuplicateFinder::new())); + let shared_empty_folders_state: Rc> = Rc::new(RefCell::new(EmptyFolder::new())); + + //////////////////////////////////////////////////////////////////////////////////////////////// // GUI Notepad Buttons + // GUI Duplicate Entry + let minimal_size_entry: gtk::Entry = builder.get_object("duplicate_minimal_size").unwrap(); + // GUI Buttons let buttons_search: gtk::Button = builder.get_object("buttons_search").unwrap(); let buttons_stop: gtk::Button = builder.get_object("buttons_stop").unwrap(); @@ -66,6 +84,7 @@ fn main() { for i in notebook_chooser_tool.get_children() { notebook_chooser_tool_children_names.push(i.get_buildable_name().unwrap().to_string()); + println!("{}", i.get_buildable_name().unwrap().to_string()); } // Entry @@ -73,28 +92,93 @@ fn main() { // Scrolled window let scrolled_window_duplicate_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_duplicate_finder").unwrap(); + let scrolled_window_empty_folder_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_empty_folder_finder").unwrap(); { - // Set starting intro - // Duplicate Finder + // Set starting information in bottom panel info_entry.set_text("Duplicated Files"); - // // Disable all unused buttons + // Disable all unused buttons buttons_search.show(); buttons_save.hide(); buttons_delete.hide(); } { + // Connect Notebook Tabs + { + let shared_buttons = shared_buttons.clone(); + + let buttons_search = buttons_search.clone(); + let buttons_stop = buttons_stop.clone(); + let buttons_resume = buttons_resume.clone(); + let buttons_pause = buttons_pause.clone(); + let buttons_select = buttons_select.clone(); + let buttons_delete = buttons_delete.clone(); + let buttons_save = buttons_save.clone(); + + let notebook_chooser_tool_children_names = notebook_chooser_tool_children_names.clone(); + + notebook_chooser_tool.connect_switch_page(move |_, _, number| { + let page: &str; + match notebook_chooser_tool_children_names.get(number as usize).unwrap().as_str() { + "notebook_duplicate_finder_label" => { + page = "duplicate"; + } + "scrolled_window_empty_folder_finder" => { + page = "empty_folder"; + } + e => { + panic!("Not existent page {}", e); + } + }; + + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("search").unwrap() == true { + buttons_search.show(); + } else { + buttons_search.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("stop").unwrap() == true { + buttons_stop.show(); + } else { + buttons_stop.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("resume").unwrap() == true { + buttons_resume.show(); + } else { + buttons_resume.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("pause").unwrap() == true { + buttons_pause.show(); + } else { + buttons_pause.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("select").unwrap() == true { + buttons_select.show(); + } else { + buttons_select.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("delete").unwrap() == true { + buttons_delete.show(); + } else { + buttons_delete.hide(); + } + if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("save").unwrap() == true { + buttons_save.show(); + } else { + buttons_save.hide(); + } + }); + } + // Connect Buttons - let buttons_search_clone = buttons_search.clone(); - + assert!(notebook_chooser_tool_children_names.contains(&"notebook_duplicate_finder_label".to_string())); + assert!(notebook_chooser_tool_children_names.contains(&"scrolled_window_empty_folder_finder".to_string())); buttons_search.connect_clicked(move |_| { - assert!(notebook_chooser_tool_children_names.contains(&"notebook_duplicate_finder_label".to_string())); - assert!(notebook_chooser_tool_children_names.contains(&"notebook_empty_folders_label".to_string())); match notebook_chooser_tool_children_names.get(notebook_chooser_tool.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_duplicate_finder_label" => { + // Find duplicates // TODO Change to proper value let mut df = DuplicateFinder::new(); let check_method = duplicate::CheckingMethod::HASH; @@ -130,29 +214,52 @@ fn main() { info_entry.set_text(format!("Found {} duplicates files in {} groups which took {}.", duplicates_number, duplicates_group, duplicates_size.file_size(options::BINARY).unwrap()).as_str()); - // Set Scrolled window + // Create GUI + { + // Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears + for i in &scrolled_window_duplicate_finder.get_children() { + scrolled_window_duplicate_finder.remove(i); + } - //let results =df. + let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String]; + let list_store: gtk::ListStore = gtk::ListStore::new(&col_types); - // Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears - for i in &scrolled_window_duplicate_finder.get_children() { - scrolled_window_duplicate_finder.remove(i); - } + let mut tree_view_duplicate_finder: gtk::TreeView = TreeView::with_model(&list_store); - let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String]; - let list_store: gtk::ListStore = gtk::ListStore::new(&col_types); + create_tree_view_duplicates(&mut tree_view_duplicate_finder); - let mut tree_view_duplicate_finder: gtk::TreeView = TreeView::with_model(&list_store); + let col_indices = [0, 1, 2]; - create_tree_view_duplicates(&mut tree_view_duplicate_finder); + match check_method { + CheckingMethod::HASH => { + let hashmap = df.get_files_sorted_by_hash(); - let col_indices = [0, 1, 2]; - match check_method { - CheckingMethod::HASH => { - let hashmap = df.get_files_sorted_by_hash(); + for (size, vectors_vector) in hashmap { + for vector in vectors_vector { + let values: [&dyn ToValue; 3] = [ + &(vector.len().to_string() + " x " + size.to_string().as_str()), + &("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"), + &"Bytes lost".to_string(), + ]; + list_store.set(&list_store.append(), &col_indices, &values); + for entry in vector { + let path = &entry.path; + let index = path.rfind('/').unwrap(); - for (size, vectors_vector) in hashmap { - for vector in vectors_vector { + let values: [&dyn ToValue; 3] = [ + &(path[index + 1..].to_string()), + &(path[..index].to_string()), + &(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()), + ]; + list_store.set(&list_store.append(), &col_indices, &values); + } + } + } + } + CheckingMethod::SIZE => { + let hashmap = df.get_files_sorted_by_size(); + + for (size, vector) in hashmap { let values: [&dyn ToValue; 3] = [ &(vector.len().to_string() + " x " + size.to_string().as_str()), &("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"), @@ -172,44 +279,97 @@ fn main() { } } } - } - CheckingMethod::SIZE => { - let hashmap = df.get_files_sorted_by_size(); - - for (size, vector) in hashmap { - let values: [&dyn ToValue; 3] = [ - &(vector.len().to_string() + " x " + size.to_string().as_str()), - &("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"), - &"Bytes lost".to_string(), - ]; - list_store.set(&list_store.append(), &col_indices, &values); - for entry in vector { - let path = &entry.path; - let index = path.rfind('/').unwrap(); - - let values: [&dyn ToValue; 3] = [ - &(path[index + 1..].to_string()), - &(path[..index].to_string()), - &(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()), - ]; - list_store.set(&list_store.append(), &col_indices, &values); - } + CheckingMethod::NONE => { + panic!(); } } - CheckingMethod::NONE => { - panic!(); - } + + scrolled_window_duplicate_finder.add(&tree_view_duplicate_finder); + scrolled_window_duplicate_finder.show_all(); } - scrolled_window_duplicate_finder.add(&tree_view_duplicate_finder); - scrolled_window_duplicate_finder.show_all(); + // Set state + { + *shared_duplication_state.borrow_mut() = df; - // Buttons - buttons_search_clone.show(); - buttons_save.hide(); - buttons_delete.hide(); + if duplicates_size > 0 { + buttons_save.show(); + buttons_delete.show(); + *shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("save").unwrap() = true; + *shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("delete").unwrap() = true; + } else { + buttons_save.hide(); + buttons_delete.hide(); + *shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("save").unwrap() = false; + *shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("delete").unwrap() = false; + } + } + } + "scrolled_window_empty_folder_finder" => { + // Find empty folders + // TODO Change to proper value + let mut ef = EmptyFolder::new(); + + ef.set_include_directory("/home/rafal/Pulpit".to_string()); + ef.set_delete_folder(false); + ef.find_empty_folders(); + + let information = ef.get_information(); + + let empty_folder_number: usize = information.number_of_empty_folders; + + info_entry.set_text(format!("Found {} empty folders.", empty_folder_number).as_str()); + + // Create GUI + { + // Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears + for i in &scrolled_window_empty_folder_finder.get_children() { + scrolled_window_empty_folder_finder.remove(i); + } + + let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String]; + let list_store: gtk::ListStore = gtk::ListStore::new(&col_types); + + let mut tree_view_empty_folder_finder: gtk::TreeView = TreeView::with_model(&list_store); + + create_tree_view_empty_folders(&mut tree_view_empty_folder_finder); + + let col_indices = [0, 1, 2]; + + let hashmap = ef.get_empty_folder_list(); + + for (name, entry) in hashmap { + let name: String = name[..(name.len() - 1)].to_string(); + let index = name.rfind('/').unwrap(); + let values: [&dyn ToValue; 3] = [ + &(name[index + 1..].to_string()), + &(name[..index].to_string()), + &(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()), + ]; + list_store.set(&list_store.append(), &col_indices, &values); + } + + scrolled_window_empty_folder_finder.add(&tree_view_empty_folder_finder); + scrolled_window_empty_folder_finder.show_all(); + } + + // Set state + { + *shared_empty_folders_state.borrow_mut() = ef; + + if empty_folder_number > 0 { + buttons_save.show(); + buttons_delete.show(); + *shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("save").unwrap() = true; + *shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("delete").unwrap() = true; + } else { + buttons_save.hide(); + buttons_delete.hide(); + *shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("save").unwrap() = false; + *shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("delete").unwrap() = false; + } + } } - "notebook_empty_folders_label" => {} e => panic!("Not existent {}", e), } }); @@ -254,3 +414,34 @@ pub fn create_tree_view_duplicates(tree_view_duplicate_finder: &mut gtk::TreeVie tree_view_duplicate_finder.set_vexpand(true); } + +pub fn create_tree_view_empty_folders(tree_view_empty_folder_finder: &mut gtk::TreeView) { + let renderer = gtk::CellRendererText::new(); + let name_column: gtk::TreeViewColumn = TreeViewColumn::new(); + name_column.pack_start(&renderer, true); + name_column.set_title("Folder Name"); + name_column.set_resizable(true); + name_column.set_min_width(50); + name_column.add_attribute(&renderer, "text", ColumnsDuplicate::Name as i32); + tree_view_empty_folder_finder.append_column(&name_column); + + let renderer = gtk::CellRendererText::new(); + let path_column: gtk::TreeViewColumn = TreeViewColumn::new(); + path_column.pack_start(&renderer, true); + path_column.set_title("Path"); + path_column.set_resizable(true); + path_column.set_min_width(100); + path_column.add_attribute(&renderer, "text", ColumnsDuplicate::Path as i32); + tree_view_empty_folder_finder.append_column(&path_column); + + let renderer = gtk::CellRendererText::new(); + let modification_date_column: gtk::TreeViewColumn = TreeViewColumn::new(); + modification_date_column.pack_start(&renderer, true); + modification_date_column.set_title("Modification Date"); + modification_date_column.set_resizable(true); + modification_date_column.set_min_width(100); + modification_date_column.add_attribute(&renderer, "text", ColumnsDuplicate::Modification as i32); + tree_view_empty_folder_finder.append_column(&modification_date_column); + + tree_view_empty_folder_finder.set_vexpand(true); +}