file manager
git clone https://git.lucas.co/cce-files.git
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 {