git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commit32060d7def5148797164d75df064d7b52c722e88
parent5a49c1ad91
authorLucas Galante <[email protected]>
date2026-06-15 14:33
Implement Delete key functionality to delete selected file/directory

 src/main.rs           | 31 ++++++++++++++++++--
 src/pages/browse.rs   | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/pages/preview.rs  |  4 +++
 src/pages/settings.rs |  1 +
 4 files changed, 113 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index e285e88..9da8044 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -503,17 +503,28 @@ impl Application for FilesystemApp {
                     pages::browse::BrowseMessage::NavigateToPath(_) => true,
                     _ => false,
                 };
+                let is_delete = match &msg {
+                    pages::browse::BrowseMessage::DeleteEntry(_) => true,
+                    _ => false,
+                };
                 let is_directory_loaded = match &msg {
                     pages::browse::BrowseMessage::DirectoryLoaded(path, _) => Some(path.clone()),
                     _ => None,
                 };
 
                 let (target_path, handle) = pages::browse::update(&mut self.browse, msg);
-                if is_navigation {
+                if is_navigation || is_delete {
                     let sender_clone = self.sender.clone();
+                    let is_delete_clone = is_delete;
+                    let target_path_clone = target_path.clone();
                     tokio::spawn(async move {
                         let entries = handle.await.unwrap_or_default();
-                        let _ = sender_clone.send(Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(target_path, entries)));
+                        let response_msg = if is_delete_clone {
+                            Message::Browse(pages::browse::BrowseMessage::DirectoryRefreshed(target_path_clone, entries))
+                        } else {
+                            Message::Browse(pages::browse::BrowseMessage::DirectoryLoaded(target_path_clone, entries))
+                        };
+                        let _ = sender_clone.send(response_msg);
                     });
                 }
 
@@ -529,6 +540,8 @@ impl Application for FilesystemApp {
                 };
                 if let Some(path) = selected_path {
                     pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::SetPath { path });
+                } else {
+                    pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::Clear);
                 }
 
                 if let Some(idx) = self.browse.selected {
@@ -540,6 +553,13 @@ impl Application for FilesystemApp {
                             }
                         }
                     }
+                } else {
+                    if self.select_mode {
+                        self.browse.save_name_box.text.clear();
+                        if self.browse.save_name_box.editing {
+                            self.browse.save_name_box.edit_buffer.clear();
+                        }
+                    }
                 }
 
                 *needs_rebuild = true;
@@ -875,12 +895,14 @@ impl Application for FilesystemApp {
                 } else {
                     if self.browse.selected.is_some() {
                         self.browse.selected = None;
+                        pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::Clear);
                         changed = true;
                     }
                 }
             } else {
                 if self.browse.selected.is_some() {
                     self.browse.selected = None;
+                    pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::Clear);
                     changed = true;
                 }
             }
@@ -1112,6 +1134,11 @@ impl Application for FilesystemApp {
                         return Some(Message::SelectOpen);
                     }
                 }
+                clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Delete) => {
+                    if let Some(idx) = self.browse.selected {
+                        return Some(Message::Browse(pages::browse::BrowseMessage::DeleteEntry(idx)));
+                    }
+                }
                 clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::Escape) => {
                     if self.select_mode {
                         std::process::exit(1);
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index b2804fd..595b293 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -104,6 +104,7 @@ pub enum BrowseMessage {
     DirectoryLoaded(PathBuf, Vec<DirEntry>),
     DirectoryRefreshed(PathBuf, Vec<DirEntry>),
     ToggleHidden,
+    DeleteEntry(usize),
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -473,6 +474,31 @@ pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> (PathBuf, tokio::t
             apply_filters(state);
             (state.current_dir.clone(), tokio::spawn(async { Vec::new() }))
         }
+        BrowseMessage::DeleteEntry(idx) => {
+            if let Some(entry) = state.entries.get(idx) {
+                let path = entry.path.clone();
+                let is_dir = entry.is_dir;
+                let res = if is_dir {
+                    std::fs::remove_dir_all(&path)
+                } else {
+                    std::fs::remove_file(&path)
+                };
+                if let Err(e) = res {
+                    eprintln!("Failed to delete {}: {:?}", path.display(), e);
+                } else {
+                    // Remove from local entries list immediately for responsive UI
+                    state.all_entries.retain(|e| e.path != path);
+                    state.entries.retain(|e| e.path != path);
+                    if state.entries.is_empty() {
+                        state.selected = None;
+                    } else {
+                        state.selected = Some(idx.min(state.entries.len() - 1));
+                    }
+                }
+            }
+            let p = state.current_dir.clone();
+            (state.current_dir.clone(), tokio::spawn(async move { read_directory(&p) }))
+        }
     }
 }
 
@@ -616,4 +642,57 @@ mod tests {
         let _ = std::fs::remove_dir_all(&mock_home);
         let _ = std::fs::remove_dir(&test_dir);
     }
+
+    #[tokio::test]
+    async fn test_delete_entry() {
+        let temp_dir = std::env::temp_dir();
+        let test_subdir = temp_dir.join(format!("cce_test_delete_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
+        std::fs::create_dir_all(&test_subdir).unwrap();
+
+        let file_path = test_subdir.join("delete_me.txt");
+        std::fs::write(&file_path, "test delete content").unwrap();
+
+        let mut state = BrowseState {
+            current_dir: test_subdir.clone(),
+            all_entries: vec![
+                DirEntry {
+                    name: "delete_me.txt".to_string(),
+                    path: file_path.clone(),
+                    is_dir: false,
+                    size: 19,
+                    permissions: 0o644,
+                    modified: String::new(),
+                }
+            ],
+            entries: vec![
+                DirEntry {
+                    name: "delete_me.txt".to_string(),
+                    path: file_path.clone(),
+                    is_dir: false,
+                    size: 19,
+                    permissions: 0o644,
+                    modified: String::new(),
+                }
+            ],
+            selected: Some(0),
+            ..BrowseState::default()
+        };
+
+        // Assert file exists before deletion
+        assert!(file_path.exists());
+
+        // Perform update call
+        let (_, _handle) = update(&mut state, BrowseMessage::DeleteEntry(0));
+
+        // Check if file is deleted from disk
+        assert!(!file_path.exists());
+
+        // Check if state entries are updated
+        assert!(state.entries.is_empty());
+        assert!(state.all_entries.is_empty());
+        assert_eq!(state.selected, None);
+
+        // Clean up directory
+        let _ = std::fs::remove_dir_all(&test_subdir);
+    }
 }
diff --git a/src/pages/preview.rs b/src/pages/preview.rs
index 22f7940..1e20466 100644
--- a/src/pages/preview.rs
+++ b/src/pages/preview.rs
@@ -25,6 +25,7 @@ pub struct PreviewState {
 #[derive(Debug, Clone)]
 pub enum PreviewMessage {
     SetPath { path: PathBuf },
+    Clear,
 }
 
 // ── Helpers ─────────────────────────────────────────────────────────
@@ -237,6 +238,9 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageCon
 
 pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
     match msg {
+        PreviewMessage::Clear => {
+            *state = PreviewState::default();
+        }
         PreviewMessage::SetPath { path } => {
             let meta = fs::symlink_metadata(&path).ok();
             let name = path
diff --git a/src/pages/settings.rs b/src/pages/settings.rs
index 7b44b7a..9b70920 100644
--- a/src/pages/settings.rs
+++ b/src/pages/settings.rs
@@ -55,6 +55,7 @@ const SECTIONS: &[SectionData] = &[
             Binding { keys: "Click", action: "Select file or directory" },
             Binding { keys: "Double-click", action: "Open directory" },
             Binding { keys: "Search", action: "Filter files by name" },
+            Binding { keys: "Delete", action: "Delete selected file or directory" },
         ],
     },
     SectionData {