Repository: qarmin/czkawka (language: rust)

Issue:
[Feature Request] Broken Files - Add option to rename files with incorrect file extension
I think it would be a nice feature to be able to correct the file extension on those Broken Files where the file extension does not match the file header signature / magic number (not 100% accurate due to some file formats using container formats).
This could work for images, .CHK files, etc. either applying a suggested file extension to all selected files or with a custom file extension (on GUI an extra editable column with suggested extension).

**About File Signatures**
* https://en.wikipedia.org/wiki/List_of_file_signatures
* https://www.garykessler.net/library/file_sigs.html
* https://www.nationalarchives.gov.uk/PRONOM/Default.aspx
* https://filesignatures.net/index.php?page=all  (webpage with unsecure certificate warning)

**Some programs that can detect file signatures**
* [TrID](https://mark0.net/soft-trid-e.html)
* `file` command (based on libmagic library) (GNU/Linux)
* `exiftool` (for images, can also extract metadata useful to rename filename by date or other fields)
* [DROID](https://github.com/digital-preservation/droid)
* Windows PowerShell  (`Get-FileMetaData -Signature`)
* File Carving and File Recovery software 


- https://tika.apache.org/2.3.0/detection.html

---
Feature Request slightly related to #309 


Discussion / hints:
Most tools are written in C/C++ and similar, so integration can be problematic.

From Rust side, probably something like https://crates.io/crates/mime_guess can be used
For Rust, maybe something like [mime2ext](https://crates.io/crates/mime2ext), [infer](https://crates.io/crates/infer) (or [tree_magic](https://crates.io/crates/tree_magic) or [tree_magic_mini](https://crates.io/crates/tree_magic_mini)) would help doing the job.
Regards.

Fix patch:
diff --git a/.github/workflows/linux_cli.yml b/.github/workflows/linux_cli.yml
index 0509201a..a8911561 100644
--- a/.github/workflows/linux_cli.yml
+++ b/.github/workflows/linux_cli.yml
@@ -32,6 +32,9 @@ jobs:
           name: czkawka_cli-${{ runner.os }}
           path: target/release/czkawka_cli
 
+      - name: Build test version
+        run: cargo build --profile test --bin czkawka_cli
+
       - name: Linux Regression Test
         run: |
           wget https://github.com/qarmin/czkawka/releases/download/6.0.0/TestFiles.zip
@@ -39,7 +42,7 @@ jobs:
           cargo build --release
           cd ..
           
-          ci_tester/target/release/ci_tester target/release/czkawka_cli
+          ci_tester/target/release/ci_tester target/debug/czkawka_cli
 
       - name: Prepare files to release
         run: |
diff --git a/Cargo.toml b/Cargo.toml
index 0184c4ab..b1a74c97 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,4 +28,9 @@ overflow-checks = true
 
 # Optimize all dependencies except application/workspaces, even in debug builds
 [profile.dev.package."*"]
-opt-level = 3
\ No newline at end of file
+opt-level = 3
+
+[profile.test]
+debug-assertions = true # Forces to crash when there is duplicated item in cli
+overflow-checks = true
+opt-level = 3
diff --git a/Changelog.md b/Changelog.md
index c250127f..e83ade9d 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -5,6 +5,7 @@
 - Due to the removal image_type from image struct, old cache files are incompatible with new version and should be
   regenerated
   from scratch(it uses new name)
+- Some CLI arguments could change short name, due fixing ambiguous names
 
 ### Known regressions
 
@@ -15,6 +16,7 @@
 
 - Providing nightly
   builds - [#1360](https://github.com/qarmin/czkawka/pull/1360) - https://github.com/qarmin/czkawka/releases/tag/Nightly
+- Added finding duplicated options in CLI -[#1364](https://github.com/qarmin/czkawka/pull/1364)
 
 ### Core
 
@@ -53,6 +55,7 @@
 - Added ability to show preview of referenced folders - [#1359](https://github.com/qarmin/czkawka/pull/1359)
 - Enable selecting with space and jumping over entries with
   arrows and opening with enter - [#1359](https://github.com/qarmin/czkawka/pull/1359)
+- Added button to rename files with invalid extension -[#1364](https://github.com/qarmin/czkawka/pull/1364)
 
 ### GTK GUI
 
@@ -68,6 +71,7 @@
 - Fixed and added more input parameters to the application - [#1354](https://github.com/qarmin/czkawka/pull/1354)
 - Fixed crash when stopping scan multiple times - [#1355](https://github.com/qarmin/czkawka/pull/1355)
 - Print results also in debug build - [#1355](https://github.com/qarmin/czkawka/pull/1355)
+- Added support for selecting reference directories -[#1364](https://github.com/qarmin/czkawka/pull/1364)
 
 ## Version 7.0.0 - 19.02.2024r
 
diff --git a/czkawka_cli/README.md b/czkawka_cli/README.md
index e5a5cd12..3c29dd67 100644
--- a/czkawka_cli/README.md
+++ b/czkawka_cli/README.md
@@ -47,14 +47,6 @@ cd ..
 cargo build --release --bin czkawka_cli
 ```
 
-## Limitations
-
-Not all available features in core are available in CLI.
-
-List of not available features:
-
-- Ability to use/choose referenced directories
-
 ## LICENSE
 
 MIT
\ No newline at end of file
diff --git a/czkawka_cli/src/commands.rs b/czkawka_cli/src/commands.rs
index 9d2fb8d9..3fbc6f88 100644
--- a/czkawka_cli/src/commands.rs
+++ b/czkawka_cli/src/commands.rs
@@ -25,7 +25,7 @@ pub enum Commands {
     #[clap(
         name = "dup",
         about = "Finds duplicate files",
-        after_help = "EXAMPLE:\n    czkawka dup -d /home/rafal -e /home/rafal/Obrazy  -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo"
+        after_help = "EXAMPLE:\n    czkawka dup -d /home/rafal - -e /home/rafal/Obrazy  -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo"
     )]
     Duplicates(DuplicatesArgs),
     #[clap(
@@ -88,8 +88,10 @@ pub enum Commands {
 pub struct DuplicatesArgs {
     #[clap(flatten)]
     pub common_cli_items: CommonCliItems,
+    #[clap(flatten)]
+    pub reference_directories: ReferenceDirectories,
     #[clap(
-        short = 'p',
+        short = 'Z',
         long,
         value_parser = parse_minimal_file_size,
         default_value = "257144",
@@ -198,6 +200,8 @@ pub struct TemporaryArgs {
 pub struct SimilarImagesArgs {
     #[clap(flatten)]
     pub common_cli_items: CommonCliItems,
+    #[clap(flatten)]
+    pub reference_directories: ReferenceDirectories,
     #[clap(
         short,
         long,
@@ -264,6 +268,8 @@ pub struct SameMusicArgs {
     #[clap(flatten)]
     pub common_cli_items: CommonCliItems,
     #[clap(flatten)]
+    pub reference_directories: ReferenceDirectories,
+    #[clap(flatten)]
     pub delete_method: DMethod,
     #[clap(flatten)]
     pub dry_run: DryRun,
@@ -317,7 +323,7 @@ pub struct SameMusicArgs {
     )]
     pub minimum_segment_duration: f32,
     #[clap(
-        short = 'd',
+        short = 'Y',
         long,
         value_parser = parse_maximum_difference,
         default_value = "2.0",
@@ -386,6 +392,8 @@ pub struct SimilarVideosArgs {
     #[clap(flatten)]
     pub common_cli_items: CommonCliItems,
     #[clap(flatten)]
+    pub reference_directories: ReferenceDirectories,
+    #[clap(flatten)]
     pub delete_method: DMethod,
     #[clap(flatten)]
     pub allow_hard_links: AllowHardLinks,
@@ -437,7 +445,7 @@ pub struct CommonCliItems {
         long,
         required = true,
         help = "Directorie(s) to search",
-        long_help = "List of directorie(s) which will be searched(absolute path)"
+        long_help = "List of directorie(s) which will be searched(absolute path) - this directories are not set as reference folders"
     )]
     pub directories: Vec<PathBuf>,
     #[clap(
@@ -495,6 +503,17 @@ pub struct FileToSave {
     pub file_to_save: Option<PathBuf>,
 }
 
+#[derive(Debug, clap::Args)]
+pub struct ReferenceDirectories {
+    #[clap(
+        short,
+        long,
+        help = "Reference directorie(s) to search",
+        long_help = "List of directorie(s) which will be searched(absolute path) - this directories are set as reference folders, so will not be visible in the results"
+    )]
+    pub reference_directories: Vec<PathBuf>,
+}
+
 #[derive(Debug, clap::Args)]
 pub struct JsonCompactFileToSave {
     #[clap(short = 'C', long, value_name = "json-file-name", help = "Saves the results into the compact json file")]
@@ -527,7 +546,7 @@ pub struct DryRun {
 
 #[derive(Debug, clap::Args)]
 pub struct IgnoreSameSize {
-    #[clap(short, long, help = "Ignore files with the same size, leaving only one file of each size")]
+    #[clap(short = 'J', long, help = "Ignore files with the same size, leaving only one file of each size")]
     pub ignore_same_size: bool,
 }
 
diff --git a/czkawka_cli/src/main.rs b/czkawka_cli/src/main.rs
index 8a580ef6..437409ff 100644
--- a/czkawka_cli/src/main.rs
+++ b/czkawka_cli/src/main.rs
@@ -1,6 +1,7 @@
 #![allow(clippy::needless_late_init)]
 #![warn(clippy::unwrap_used)]
 
+use std::path::PathBuf;
 use std::thread;
 
 use clap::Parser;
@@ -79,6 +80,7 @@ fn main() {
 fn duplicates(duplicates: DuplicatesArgs, stop_receiver: &Receiver<()>, progress_sender: &Sender<ProgressData>) {
     let DuplicatesArgs {
         common_cli_items,
+        reference_directories,
         minimal_file_size,
         maximal_file_size,
         minimal_cached_file_size,
@@ -103,7 +105,7 @@ fn duplicates(duplicates: DuplicatesArgs, stop_receiver: &Receiver<()>, progress
     );
     let mut item = DuplicateFinder::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));
     item.set_minimal_file_size(minimal_file_size);
     item.set_maximal_file_size(maximal_file_size);
     item.set_delete_method(delete_method.delete_method);
@@ -119,7 +121,7 @@ fn empty_folders(empty_folders: EmptyFoldersArgs, stop_receiver: &Receiver<()>,
 
     let mut item = EmptyFolder::new();
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_folders {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -141,7 +143,7 @@ fn biggest_files(biggest_files: BiggestFilesArgs, stop_receiver: &Receiver<()>,
     let params = BigFileParameters::new(number_of_files, big_files_mode);
     let mut item = BigFile::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_files {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -156,7 +158,7 @@ fn empty_files(empty_files: EmptyFilesArgs, stop_receiver: &Receiver<()>, progre
 
     let mut item = EmptyFiles::new();
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_files {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -171,7 +173,7 @@ fn temporary(temporary: TemporaryArgs, stop_receiver: &Receiver<()>, progress_se
 
     let mut item = Temporary::new();
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_files {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -184,6 +186,7 @@ fn temporary(temporary: TemporaryArgs, stop_receiver: &Receiver<()>, progress_se
 fn similar_images(similar_images: SimilarImagesArgs, stop_receiver: &Receiver<()>, progress_sender: &Sender<ProgressData>) {
     let SimilarImagesArgs {
         common_cli_items,
+        reference_directories,
         minimal_file_size,
         maximal_file_size,
         similarity_preset,
@@ -207,7 +210,7 @@ fn similar_images(similar_images: SimilarImagesArgs, stop_receiver: &Receiver<()
     );
     let mut item = SimilarImages::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));
     item.set_minimal_file_size(minimal_file_size);
     item.set_maximal_file_size(maximal_file_size);
     item.set_delete_method(delete_method.delete_method);
@@ -221,6 +224,7 @@ fn similar_images(similar_images: SimilarImagesArgs, stop_receiver: &Receiver<()
 fn same_music(same_music: SameMusicArgs, stop_receiver: &Receiver<()>, progress_sender: &Sender<ProgressData>) {
     let SameMusicArgs {
         common_cli_items,
+        reference_directories,
         delete_method,
         minimal_file_size,
         maximal_file_size,
@@ -243,7 +247,7 @@ fn same_music(same_music: SameMusicArgs, stop_receiver: &Receiver<()>, progress_
     );
     let mut item = SameMusic::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));
     item.set_minimal_file_size(minimal_file_size);
     item.set_maximal_file_size(maximal_file_size);
     item.set_delete_method(delete_method.delete_method);
@@ -259,7 +263,7 @@ fn invalid_symlinks(invalid_symlinks: InvalidSymlinksArgs, stop_receiver: &Recei
 
     let mut item = InvalidSymlinks::new();
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_files {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -283,7 +287,7 @@ fn broken_files(broken_files: BrokenFilesArgs, stop_receiver: &Receiver<()>, pro
     let params = BrokenFilesParameters::new(checked_type);
     let mut item = BrokenFiles::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
     if delete_files {
         item.set_delete_method(DeleteMethod::Delete);
     }
@@ -295,6 +299,7 @@ fn broken_files(broken_files: BrokenFilesArgs, stop_receiver: &Receiver<()>, pro
 
 fn similar_videos(similar_videos: SimilarVideosArgs, stop_receiver: &Receiver<()>, progress_sender: &Sender<ProgressData>) {
     let SimilarVideosArgs {
+        reference_directories,
         common_cli_items,
         tolerance,
         minimal_file_size,
@@ -308,7 +313,7 @@ fn similar_videos(similar_videos: SimilarVideosArgs, stop_receiver: &Receiver<()
     let params = SimilarVideosParameters::new(tolerance, ignore_same_size.ignore_same_size, !allow_hard_links.allow_hard_links);
     let mut item = SimilarVideos::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));
     item.set_minimal_file_size(minimal_file_size);
     item.set_maximal_file_size(maximal_file_size);
     item.set_delete_method(delete_method.delete_method);
@@ -325,7 +330,7 @@ fn bad_extensions(bad_extensions: BadExtensionsArgs, stop_receiver: &Receiver<()
     let params = BadExtensionsParameters::new();
     let mut item = BadExtensions::new(params);
 
-    set_common_settings(&mut item, &common_cli_items);
+    set_common_settings(&mut item, &common_cli_items, None);
 
     item.find_bad_extensions_files(Some(stop_receiver), Some(progress_sender));
 
@@ -354,13 +359,19 @@ fn save_and_print_results<T: CommonData + PrintResults>(component: &mut T, commo
     component.get_text_messages().print_messages();
 }
 
-fn set_common_settings<T>(component: &mut T, common_cli_items: &CommonCliItems)
+fn set_common_settings<T>(component: &mut T, common_cli_items: &CommonCliItems, reference_directories: Option<&Vec<PathBuf>>)
 where
     T: CommonData + PrintResults,
 {
     set_number_of_threads(common_cli_items.thread_number);
 
-    component.set_included_directory(common_cli_items.directories.clone());
+    let mut included_directories = common_cli_items.directories.clone();
+    if let Some(reference_directories) = reference_directories {
+        included_directories.extend_from_slice(reference_directories);
+        component.set_reference_directory(reference_directories.clone());
+    }
+
+    component.set_included_directory(included_directories);
     component.set_excluded_directory(common_cli_items.excluded_directories.clone());
     component.set_excluded_items(common_cli_items.excluded_items.clone());
     component.set_recursive_search(!common_cli_items.not_recursive);
diff --git a/czkawka_core/src/bad_extensions.rs b/czkawka_core/src/bad_extensions.rs
index 124417a8..cbef33aa 100644
--- a/czkawka_core/src/bad_extensions.rs
+++ b/czkawka_core/src/bad_extensions.rs
@@ -169,7 +169,8 @@ pub struct BadFileEntry {
     pub modified_date: u64,
     pub size: u64,
     pub current_extension: String,
-    pub proper_extensions: String,
+    pub proper_extensions_group: String,
+    pub proper_extension: String,
 }
 
 impl ResultEntry for BadFileEntry {
@@ -345,7 +346,8 @@ impl BadExtensions {
                     modified_date: file_entry.modified_date,
                     size: file_entry.size,
                     current_extension,
-                    proper_extensions: valid_extensions,
+                    proper_extensions_group: valid_extensions,
+                    proper_extension: proper_extension.to_string(),
                 }))
             })
             .while_some()
@@ -386,6 +388,8 @@ impl BadExtensions {
         proper_extension: &str,
     ) -> (BTreeSet<String>, String) {
         let mut all_available_extensions: BTreeSet<String> = Default::default();
+        // TODO Isn't this a bug?
+        // Why to file without extensions we set this as empty
         let valid_extensions = if current_extension.is_empty() {
             String::new()
         } else {
@@ -443,7 +447,7 @@ impl PrintResults for BadExtensions {
         writeln!(writer, "Found {} files with invalid extension.\n", self.information.number_of_files_with_bad_extension)?;
 
         for file_entry in &self.bad_extensions_files {
-            writeln!(writer, "\"{}\" ----- {}", file_entry.path.to_string_lossy(), file_entry.proper_extensions)?;
+            writeln!(writer, "\"{}\" ----- {}", file_entry.path.to_string_lossy(), file_entry.proper_extensions_group)?;
         }
 
         Ok(())
diff --git a/czkawka_core/src/common_directory.rs b/czkawka_core/src/common_directory.rs
index cf2524ca..22f96325 100644
--- a/czkawka_core/src/common_directory.rs
+++ b/czkawka_core/src/common_directory.rs
@@ -119,7 +119,7 @@ impl Directories {
         // Try to canonicalize them
         if cfg!(windows) {
             // Only canonicalize if it's not a network path
-            // This can be check by checking if path starts with \\?\UNC\
+            // This can be done by checking if path starts with \\?\UNC\
             if let Ok(dir_can) = directory.canonicalize() {
                 let dir_can_str = dir_can.to_string_lossy().to_string();
                 if let Some(dir_can_str) = dir_can_str.strip_prefix(r"\\?\") {
diff --git a/czkawka_core/src/duplicate.rs b/czkawka_core/src/duplicate.rs
index 84a847c8..69624977 100644
--- a/czkawka_core/src/duplicate.rs
+++ b/czkawka_core/src/duplicate.rs
@@ -1026,8 +1026,9 @@ impl PrintResults for DuplicateFinder {
     fn write_results<T: Write>(&self, writer: &mut T) -> io::Result<()> {
         writeln!(
             writer,
-            "Results of searching {:?} with excluded directories {:?} and excluded items {:?}",
+            "Results of searching {:?} (reference directories {:?}) with excluded directories {:?} and excluded items {:?}",
             self.common_data.directories.included_directories,
+            self.common_data.directories.reference_directories,
             self.common_data.directories.excluded_directories,
             self.common_data.excluded_items.get_excluded_items()
         )?;
diff --git a/czkawka_core/src/same_music.rs b/czkawka_core/src/same_music.rs
index ab5f2480..96defdfe 100644
--- a/czkawka_core/src/same_music.rs
+++ b/czkawka_core/src/same_music.rs
@@ -398,7 +398,7 @@ impl SameMusic {
                 }
             })
             .while_some()
-            .filter_map(|e| e)
+            .flatten()
             .collect::<Vec<_>>();
         debug!("read_tags - ended reading tags");
 
diff --git a/czkawka_gui/src/compute_results.rs b/czkawka_gui/src/compute_results.rs
index ae60b524..3db2db54 100644
--- a/czkawka_gui/src/compute_results.rs
+++ b/czkawka_gui/src/compute_results.rs
@@ -276,7 +276,7 @@ fn compute_bad_extensions(
                     (ColumnsBadExtensions::Name as u32, &file),
                     (ColumnsBadExtensions::Path as u32, &directory),
                     (ColumnsBadExtensions::CurrentExtension as u32, &file_entry.current_extension),
-                    (ColumnsBadExtensions::ValidExtensions as u32, &file_entry.proper_extensions),
+                    (ColumnsBadExtensions::ValidExtensions as u32, &file_entry.proper_extensions_group),
                     (
                         ColumnsBadExtensions::Modification as u32,
                         &(DateTime::from_timestamp(file_entry.modified_date as i64, 0)
diff --git a/justfile b/justfile
index d82ab850..e312fb09 100644
--- a/justfile
+++ b/justfile
@@ -18,10 +18,10 @@ krokiet_r:
 krokiet_dark:
     RUST_BACKTRACE=1 SLINT_STYLE=fluent-dark cargo run --bin krokiet
 
-cli:
-    RUST_BACKTRACE=1 cargo run --bin czkawka_cli
-cli_r:
-    RUST_BACKTRACE=1 cargo run --bin czkawka_cli --release
+cli +args:
+    RUST_BACKTRACE=1 cargo run --bin czkawka_cli -- {{args}}
+cli_r args:
+    RUST_BACKTRACE=1 cargo run --bin czkawka_cli --release -- {{args}}
 cli_help:
     cargo run --bin czkawka_cli -- --help
 
diff --git a/krokiet/icons/krokiet_rename.svg b/krokiet/icons/krokiet_rename.svg
new file mode 100644
index 00000000..b455cacc
--- /dev/null
+++ b/krokiet/icons/krokiet_rename.svg
@@ -0,0 +1,1 @@
+<svg clip-rule="evenodd" fill-rule="evenodd" height="2048" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 2048 2048" width="2048" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h2048v2048h-2048z" fill="none"/><path d="m87.25 611.2605h82.087448v892.82898h-82.087448z" stroke-width=".568446"/><path d="m236.19318 758.95111h866.16052v600.99158h-866.16052z" stroke-width="1.51496"/><path d="m1362.5925 758.12317h203.16624v600.84137h-203.16624z" stroke-width=".733624"/><path d="m87.248306 611.43683h1024.4435v85.122589h-1024.4435z" stroke-width=".62006"/><path d="m917.55438 359.38852h610.18457v85.122589h-610.18457z" stroke-width=".478542"/><path d="m916.36346 1618.0974h610.18457v85.122589h-610.18457z" stroke-width=".478542"/><path d="m87.277161 1418.9902h1024.4435v85.122589h-1024.4435z" stroke-width=".62006"/><path d="m1816.3279 613.24561h82.087448v892.82898h-82.087448z" stroke-width=".568446"/><path d="m1194.8612 430.29568h82.087448v1206.1714h-82.087448z" stroke-width=".660708"/><g stroke-width=".441443"><path d="m1361.6683 613.46094h536.68848v82.355583h-536.68848z"/><path d="m1361.4937 1423.62h536.68848v82.355583h-536.68848z"/></g></svg>
\ No newline at end of file
diff --git a/krokiet/src/common.rs b/krokiet/src/common.rs
index 09c9c34b..7934ee7f 100644
--- a/krokiet/src/common.rs
+++ b/krokiet/src/common.rs
@@ -190,6 +190,7 @@ pub enum StrDataBadExtensions {
     Name,
     Path,
     CurrentExtension,
+    ProperExtensionsGroup,
     ProperExtension,
 }
 
@@ -228,6 +229,14 @@ pub fn get_str_name_idx(active_tab: CurrentTab) -> usize {
     }
 }
 
+pub fn get_str_proper_extension(active_tab: CurrentTab) -> usize {
+    match active_tab {
+        CurrentTab::BadExtensions => StrDataBadExtensions::ProperExtension as usize,
+        CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"),
+        _ => panic!("Unable to get proper extension from this tab"),
+    }
+}
+
 pub fn get_int_modification_date_idx(active_tab: CurrentTab) -> usize {
     match active_tab {
         CurrentTab::EmptyFiles => IntDataEmptyFiles::ModificationDatePart1 as usize,
diff --git a/krokiet/src/connect_rename.rs b/krokiet/src/connect_rename.rs
new file mode 100644
index 00000000..b922c860
--- /dev/null
+++ b/krokiet/src/connect_rename.rs
@@ -0,0 +1,52 @@
+use std::fs;
+use std::path::{Path, MAIN_SEPARATOR};
+
+use czkawka_core::common_messages::Messages;
+use slint::{ComponentHandle, ModelRc, VecModel};
+
+use crate::common::{get_is_header_mode, get_tool_model, set_tool_model};
+use crate::model_operations::{collect_path_name_and_proper_extension_from_model, deselect_all_items, filter_out_checked_items};
+use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow};
+
+pub fn connect_rename(app: &MainWindow) {
+    let a = app.as_weak();
+    app.global::<Callabler>().on_rename_files(move || {
+        let app = a.upgrade().expect("Failed to upgrade app :(");
+        let active_tab = app.global::<GuiState>().get_active_tab();
+        let current_model = get_tool_model(&app, active_tab);
+
+        let (errors, new_model) = rename_operation(&current_model, active_tab);
+        if let Some(new_model) = new_model {
+            set_tool_model(&app, active_tab, new_model);
+        }
+        app.global::<GuiState>().set_info_text(Messages::new_from_errors(errors).create_messages_text().into());
+    });
+}
+
+fn rename_operation(items: &ModelRc<MainListModel>, active_tab: CurrentTab) -> (Vec<String>, Option<ModelRc<MainListModel>>) {
+    assert_eq!(active_tab, CurrentTab::BadExtensions);
+    let (entries_to_move, mut entries_left) = filter_out_checked_items(items, get_is_header_mode(active_tab));
+
+    if !entries_to_move.is_empty() {
+        let vec_items_to_rename = collect_path_name_and_proper_extension_from_model(&entries_to_move, active_tab);
+        let errors = rename_selected_items(vec_items_to_rename);
+        deselect_all_items(&mut entries_left);
+
+        let r = ModelRc::new(VecModel::from(entries_left));
+        return (errors, Some(r));
+    }
+    (vec![], None)
+}
+
+fn rename_selected_items(files_with_new_extensions: Vec<(String, String, String)>) -> Vec<String> {
+    let mut errors = vec![];
+    for (folder, file_name, new_extension) in files_with_new_extensions {
+        let file_stem = Path::new(&file_name).file_stem().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
+        let new_full_path = format!("{}{}{}.{}", folder, MAIN_SEPARATOR, file_stem, new_extension);
+        let old_full_path = format!("{}{}{}", folder, MAIN_SEPARATOR, file_name);
+        if let Err(e) = fs::rename(&old_full_path, &new_full_path) {
+            errors.push(format!("Failed to rename file {} to {} with error {}", old_full_path, new_full_path, e));
+        }
+    }
+    errors
+}
diff --git a/krokiet/src/connect_scan.rs b/krokiet/src/connect_scan.rs
index a618b743..4324b47c 100644
--- a/krokiet/src/connect_scan.rs
+++ b/krokiet/src/connect_scan.rs
@@ -844,7 +844,13 @@ fn write_bad_extensions_results(app: &MainWindow, vector: Vec<BadFileEntry>, mes
 
 fn prepare_data_model_bad_extensions(fe: &BadFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
     let (directory, file) = split_path(&fe.path);
-    let data_model_str = VecModel::from_slice(&[file.into(), directory.into(), fe.current_extension.clone().into(), fe.proper_extensions.clone().into()]);
+    let data_model_str = VecModel::from_slice(&[
+        file.into(),
+        directory.into(),
+        fe.current_extension.clone().into(),
+        fe.proper_extensions_group.clone().into(),
+        fe.proper_extension.clone().into(),
+    ]);
     let modification_split = split_u64_into_i32s(fe.get_modified_date());
     let size_split = split_u64_into_i32s(fe.size);
     let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]);
diff --git a/krokiet/src/connect_show_preview.rs b/krokiet/src/connect_show_preview.rs
index f3527a61..5a0e57b5 100644
--- a/krokiet/src/connect_show_preview.rs
+++ b/krokiet/src/connect_show_preview.rs
@@ -20,8 +20,8 @@ pub fn connect_show_preview(app: &MainWindow) {
 
         let active_tab = gui_state.get_active_tab();
 
-        if (active_tab == CurrentTab::SimilarImages && !settings.get_similar_images_show_image_preview())
-            || (active_tab == CurrentTab::DuplicateFiles && !settings.get_duplicate_image_preview())
+        if !((active_tab == CurrentTab::SimilarImages && settings.get_similar_images_show_image_preview())
+            || (active_tab == CurrentTab::DuplicateFiles && settings.get_duplicate_image_preview()))
         {
             set_preview_visible(&gui_state, None);
             return;
diff --git a/krokiet/src/main.rs b/krokiet/src/main.rs
index b0ffa6b5..2e86ee68 100644
--- a/krokiet/src/main.rs
+++ b/krokiet/src/main.rs
@@ -30,6 +30,7 @@ use crate::connect_directories_changes::connect_add_remove_directories;
 use crate::connect_move::connect_move;
 use crate::connect_open::connect_open_items;
 use crate::connect_progress_receiver::connect_progress_gathering;
+use crate::connect_rename::connect_rename;
 use crate::connect_scan::connect_scan_button;
 use crate::connect_select::{connect_select, connect_showing_proper_select_buttons};
 use crate::connect_show_preview::connect_show_preview;
@@ -44,6 +45,7 @@ mod connect_directories_changes;
 mod connect_move;
 mod connect_open;
 mod connect_progress_receiver;
+mod connect_rename;
 mod connect_scan;
 mod connect_select;
 mod connect_show_preview;
@@ -83,6 +85,7 @@ fn main() {
     connect_select(&app);
     connect_showing_proper_select_buttons(&app);
     connect_move(&app);
+    connect_rename(&app);
 
     app.run().expect("Failed to run app :(");
 
diff --git a/krokiet/src/model_operations.rs b/krokiet/src/model_operations.rs
index c589a6f9..574f5db0 100644
--- a/krokiet/src/model_operations.rs
+++ b/krokiet/src/model_operations.rs
@@ -1,8 +1,8 @@
 use std::path::MAIN_SEPARATOR;
 
-use slint::{Model, ModelRc};
+use slint::{Model, ModelRc, SharedString};
 
-use crate::common::{get_str_name_idx, get_str_path_idx};
+use crate::common::{get_str_name_idx, get_str_path_idx, get_str_proper_extension};
 use crate::{CurrentTab, MainListModel};
 
 pub fn deselect_all_items(items: &mut [MainListModel]) {
@@ -26,8 +26,8 @@ pub fn collect_full_path_from_model(items: &[MainListModel], active_tab: Current
     items
         .iter()
         .map(|item| {
-            let path = item.val_str.iter().nth(path_idx).unwrap_or_else(|| panic!("Failed to get {path_idx} element"));
-            let name = item.val_str.iter().nth(name_idx).unwrap_or_else(|| panic!("Failed to get {name_idx} element"));
+            let path = get_shared_str_item(item, path_idx);
+            let name = get_shared_str_item(item, name_idx);
             format!("{path}{MAIN_SEPARATOR}{name}")
         })
         .collect::<Vec<_>>()
@@ -35,16 +35,28 @@ pub fn collect_full_path_from_model(items: &[MainListModel], active_tab: Current
 pub fn collect_path_name_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec<(String, String)> {
     let path_idx = get_str_path_idx(active_tab);
     let name_idx = get_str_name_idx(active_tab);
+    items.iter().map(|item| (get_str_item(item, path_idx), get_str_item(item, name_idx))).collect::<Vec<_>>()
+}
+
+pub fn collect_path_name_and_proper_extension_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec<(String, String, String)> {
+    let path_idx = get_str_path_idx(active_tab);
+    let name_idx = get_str_name_idx(active_tab);
+    let ext_idx = get_str_proper_extension(active_tab);
     items
         .iter()
-        .map(|item| {
-            let path = item.val_str.iter().nth(path_idx).unwrap_or_else(|| panic!("Failed to get {path_idx} element"));
-            let name = item.val_str.iter().nth(name_idx).unwrap_or_else(|| panic!("Failed to get {name_idx} element"));
-            (path.to_string(), name.to_string())
-        })
+        .map(|item| (get_str_item(item, path_idx), get_str_item(item, name_idx), get_str_item(item, ext_idx)))
         .collect::<Vec<_>>()
 }
 
+#[inline]
+pub fn get_str_item(main_list_model: &MainListModel, idx: usize) -> String {
+    main_list_model.val_str.iter().nth(idx).unwrap_or_else(|| panic!("Failed to get {idx} element")).to_string()
+}
+#[inline]
+pub fn get_shared_str_item(main_list_model: &MainListModel, idx: usize) -> SharedString {
+    main_list_model.val_str.iter().nth(idx).unwrap_or_else(|| panic!("Failed to get {idx} element"))
+}
+
 pub fn filter_out_checked_items(items: &ModelRc<MainListModel>, have_header: bool) -> (Vec<MainListModel>, Vec<MainListModel>) {
     if cfg!(debug_assertions) {
         check_if_header_is_checked(items);
diff --git a/krokiet/ui/action_buttons.slint b/krokiet/ui/action_buttons.slint
index 8319eb38..7df4af91 100644
--- a/krokiet/ui/action_buttons.slint
+++ b/krokiet/ui/action_buttons.slint
@@ -27,6 +27,7 @@ export component ActionButtons inherits HorizontalLayout {
     callback scan_starting(CurrentTab);
     callback show_select_popup(length, length);
     callback show_remove_popup();
+    callback show_rename_popup();
     callback request_folder_to_move();
 
     in-out property <[MainListModel]> duplicate_files_model: [];
@@ -126,7 +127,7 @@ export component ActionButtons inherits HorizontalLayout {
     select_button := Button {
         visible: lists_enabled;
         height: parent.height;
-        enabled: !scanning && lists_enabled && results_available;
+        enabled: !scanning && self.visible && results_available;
         text: "Select";
         icon: @image-url("../icons/krokiet_select.svg");
         clicked => {
@@ -137,7 +138,7 @@ export component ActionButtons inherits HorizontalLayout {
     move_button := Button {
         visible: lists_enabled;
         height: parent.height;
-        enabled: !scanning && lists_enabled && results_available;
+        enabled: !scanning && self.visible && results_available;
         text: "Move";
         icon: @image-url("../icons/krokiet_move.svg");
         clicked => {
@@ -148,7 +149,7 @@ export component ActionButtons inherits HorizontalLayout {
     delete_button := Button {
         visible: lists_enabled;
         height: parent.height;
-        enabled: !scanning && lists_enabled && results_available;
+        enabled: !scanning && self.visible && results_available;
         text: "Delete";
         icon: @image-url("../icons/krokiet_delete.svg");
         clicked => {
@@ -156,6 +157,17 @@ export component ActionButtons inherits HorizontalLayout {
         }
     }
 
+    rename_button := Button {
+        visible: lists_enabled && GuiState.active_tab == CurrentTab.BadExtensions;
+        height: parent.height;
+        enabled: !scanning && self.visible && results_available;
+        text: "Rename";
+        icon: @image-url("../icons/krokiet_rename.svg");
+        clicked => {
+            show_rename_popup();
+        }
+    }
+
     Rectangle {
         horizontal-stretch: 0.5;
     }
diff --git a/krokiet/ui/callabler.slint b/krokiet/ui/callabler.slint
index edff596e..79dffa76 100644
--- a/krokiet/ui/callabler.slint
+++ b/krokiet/ui/callabler.slint
@@ -24,6 +24,8 @@ export global Callabler {
 
     callback move_items(bool, bool, string);
 
+    callback rename_files();
+
     // Translations
     pure callback translate(string, [{key: string, value: string}]) -> string;
 
diff --git a/krokiet/ui/main_lists.slint b/krokiet/ui/main_lists.slint
index ed433509..2b58d2ac 100644
--- a/krokiet/ui/main_lists.slint
+++ b/krokiet/ui/main_lists.slint
@@ -203,12 +203,6 @@ export component MainList {
             } else {
                 debug("Non handled key in main_lists.slint", event, GuiState.active_tab);
             }
-            
-            
-            
-            // else {
-            //     debug("Non handled key in main_lists.slint", event, GuiState.active_tab);
-            // }
             accept
         }
     }
diff --git a/krokiet/ui/main_window.slint b/krokiet/ui/main_window.slint
index 55fabdfd..eeb34ed5 100644
--- a/krokiet/ui/main_window.slint
+++ b/krokiet/ui/main_window.slint
@@ -16,6 +16,7 @@ import {PopupNewDirectories} from "popup_new_directories.slint";
 import {PopupDelete} from "popup_delete.slint";
 import {PopupMoveFolders} from "popup_move_folders.slint";
 import { PopupSelectResults } from "popup_select_results.slint";
+import {PopupRenameFiles} from "popup_rename_files.slint";
 import { ToolSettings } from "tool_settings.slint";
 
 export {Settings, Callabler, GuiState}
@@ -152,6 +153,9 @@ export component MainWindow inherits Window {
             show_remove_popup => {
                 delete_popup_window.show_popup();
             }
+            show_rename_popup => {
+                rename_popup_window.show_popup();
+            }
         }
 
         HorizontalLayout {
@@ -212,6 +216,14 @@ export component MainWindow inherits Window {
         y: parent.y + (parent.height - self.popup_height) / 2.0;
     }
 
+    rename_popup_window := PopupRenameFiles {
+        height: root.height;
+        width: root.width;
+
+        x: parent.x + (root.width - self.popup_width) / 2.0;
+        y: parent.y + (parent.height - self.popup_height) / 2.0;
+    }
+
     show_move_folders_dialog(folder_name) => {
         move_popup_window.folder_name = folder_name;
         move_popup_window.show_popup();
diff --git a/krokiet/ui/popup_rename_files.slint b/krokiet/ui/popup_rename_files.slint
new file mode 100644
index 00000000..9b043456
--- /dev/null
+++ b/krokiet/ui/popup_rename_files.slint
@@ -0,0 +1,75 @@
+import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit} from "std-widgets.slint";
+import {SelectableTableView} from "selectable_tree_view.slint";
+import {LeftSidePanel} from "left_side_panel.slint";
+import {MainList} from "main_lists.slint";
+import {CurrentTab, ProgressToSend} from "common.slint";
+import { ActionButtons } from "action_buttons.slint";
+import { Progress } from "progress.slint";
+import {MainListModel, SelectMode, SelectModel} from "common.slint";
+import {Settings} from "settings.slint";
+import {Callabler} from "callabler.slint";
+import { BottomPanel } from "bottom_panel.slint";
+import {ColorPalette} from "color_palette.slint";
+import {GuiState} from "gui_state.slint";
+import { Preview } from "preview.slint";
+
+export component PopupRenameFiles inherits Rectangle {
+    out property <length> popup_width: 500px;
+    out property <length> popup_height: 150px;
+    in-out property <string> folder_name: "";
+    callback show_popup();
+
+    popup_window := PopupWindow {
+        width: popup_width;
+        height: popup_height;
+
+        close-on-click: false;
+        Rectangle {
+            width: parent.width;
+            height: parent.height;
+            border-radius: 5px;
+            background: ColorPalette.popup_background;
+            VerticalLayout {
+                Text {
+                    vertical-stretch: 0.0;
+                    min-height: 30px;
+                    text: "Renaming files";
+                    vertical-alignment: top;
+                    horizontal-alignment: center;
+                    font-size: 13px;
+                }
+                Text {
+                    vertical-stretch: 1.0;
+                    text: "This will rename extensions of selected files to more proper\n" + "\nAre you want to continue?";
+                    vertical-alignment: center;
+                    horizontal-alignment: center;
+                    font-size: 13px;
+                    padding: 10px;
+                }
+
+                HorizontalLayout {
+                    Button {
+                        text: "Yes";
+                        clicked => {
+                            popup_window.close();
+                            Callabler.rename_files();
+                        }
+                    }
+                    Rectangle {
+
+                    }
+                    Button {
+                        text: "No";
+                        clicked => {
+                            popup_window.close();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    show_popup() => {
+        popup_window.show();
+    }
+}
\ No newline at end of file


Test patch:
diff --git a/ci_tester/Cargo.toml b/ci_tester/Cargo.toml
index 38f354a3..39f00c9d 100644
--- a/ci_tester/Cargo.toml
+++ b/ci_tester/Cargo.toml
@@ -5,6 +5,11 @@ edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+
+[profile.release]
+debug-assertions = true
+overflow-checks = true
+
 [dependencies]
 state = "0.6.0"
 handsome_logger = "0.8.0"
diff --git a/ci_tester/src/main.rs b/ci_tester/src/main.rs
index 04e47311..be1c9856 100644
--- a/ci_tester/src/main.rs
+++ b/ci_tester/src/main.rs
@@ -17,12 +17,25 @@ static COLLECTED_FILES: state::InitCell<CollectedFiles> = state::InitCell::new()
 const ATTEMPTS: u32 = 10;
 const PRINT_MESSAGES_CZKAWKA: bool = true;
 
+fn test_args() {
+    let modes = ["dup", "big", "empty-folders", "empty-files", "temp", "image", "symlinks", "broken", "ext", "video", "music"];
+    for mode in modes {
+        println!("Testing mode {}", mode);
+        let _ = fs::remove_dir_all("RandomDirWithoutContent");
+        fs::create_dir_all("RandomDirWithoutContent").expect("Should not fail in tests");
+        run_with_good_status(&[CZKAWKA_PATH.get().as_str(), mode, "-d", "RandomDirWithoutContent"], true);
+    }
+}
+
 // App runs - ./ci_tester PATH_TO_CZKAWKA
 fn main() {
     handsome_logger::init().expect("Should not fail in tests");
     let args: Vec<String> = std::env::args().collect();
     let path_to_czkawka = args[1].clone();
     CZKAWKA_PATH.set(path_to_czkawka);
+
+    test_args();
+    return;
     remove_test_dir();
     run_with_good_status(&["ls"], false);
     unzip_files();