1use std::{
33 collections::HashSet,
34 fs,
35 path::{Path, PathBuf},
36};
37
38use crossterm::event::{KeyCode, KeyEvent};
39
40use crate::types::{ExplorerOutcome, FsEntry, SortMode};
41
42#[derive(Debug)]
69pub struct FileExplorer {
70 pub current_dir: PathBuf,
72 pub theme_name: String,
74 pub editor_name: String,
76 pub entries: Vec<FsEntry>,
78 pub cursor: usize,
80 pub(crate) scroll_offset: usize,
82 pub extension_filter: Vec<String>,
86 pub show_hidden: bool,
88 pub(crate) status: String,
90 pub sort_mode: SortMode,
92 pub search_query: String,
94 pub search_active: bool,
96 pub marked: HashSet<PathBuf>,
98 pub mkdir_active: bool,
100 pub mkdir_input: String,
102 pub touch_active: bool,
104 pub touch_input: String,
106 pub rename_active: bool,
108 pub rename_input: String,
110}
111
112macro_rules! handle_input_mode {
131 ($self:ident, $key:ident, $active:ident, $input:ident, $on_enter:expr) => {
132 if $self.$active {
133 match $key.code {
134 KeyCode::Char(c)
136 if $key.modifiers.is_empty()
137 || $key.modifiers == crossterm::event::KeyModifiers::SHIFT =>
138 {
139 $self.$input.push(c);
140 return ExplorerOutcome::Pending;
141 }
142 KeyCode::Backspace => {
144 $self.$input.pop();
145 return ExplorerOutcome::Pending;
146 }
147 KeyCode::Enter => $on_enter,
149 KeyCode::Esc => {
151 $self.$active = false;
152 $self.$input.clear();
153 return ExplorerOutcome::Pending;
154 }
155 _ => return ExplorerOutcome::Pending,
157 }
158 }
159 };
160}
161
162impl FileExplorer {
163 pub fn new(initial_dir: PathBuf, extension_filter: Vec<String>) -> Self {
173 let mut explorer = Self {
174 current_dir: initial_dir,
175 entries: Vec::new(),
176 cursor: 0,
177 scroll_offset: 0,
178 extension_filter,
179 show_hidden: false,
180 status: String::new(),
181 sort_mode: SortMode::default(),
182 search_query: String::new(),
183 search_active: false,
184 marked: HashSet::new(),
185 mkdir_active: false,
186 mkdir_input: String::new(),
187 touch_active: false,
188 touch_input: String::new(),
189 rename_active: false,
190 rename_input: String::new(),
191 theme_name: String::new(),
192 editor_name: String::new(),
193 };
194 explorer.reload();
195 explorer
196 }
197
198 pub fn builder(initial_dir: PathBuf) -> FileExplorerBuilder {
213 FileExplorerBuilder::new(initial_dir)
214 }
215
216 pub fn navigate_to(&mut self, path: impl Into<PathBuf>) {
229 self.current_dir = path.into();
230 self.cursor = 0;
231 self.scroll_offset = 0;
232 self.reload();
233 }
234
235 pub fn marked_paths(&self) -> &HashSet<PathBuf> {
243 &self.marked
244 }
245
246 pub fn toggle_mark(&mut self) {
249 if let Some(entry) = self.entries.get(self.cursor) {
250 let path = entry.path.clone();
251 if self.marked.contains(&path) {
252 self.marked.remove(&path);
253 } else {
254 self.marked.insert(path);
255 }
256 }
257 self.move_down();
258 }
259
260 pub fn clear_marks(&mut self) {
262 self.marked.clear();
263 }
264
265 pub fn handle_key(&mut self, key: KeyEvent) -> ExplorerOutcome {
266 if key.kind != crossterm::event::KeyEventKind::Press {
272 return ExplorerOutcome::Pending;
273 }
274
275 handle_input_mode!(self, key, rename_active, rename_input, {
279 let new_name = self.rename_input.trim().to_string();
280 self.rename_active = false;
281 self.rename_input.clear();
282 if new_name.is_empty() {
283 return ExplorerOutcome::Pending;
284 }
285 let src = match self.entries.get(self.cursor) {
287 Some(e) => e.path.clone(),
288 None => return ExplorerOutcome::Pending,
289 };
290 let dst = self.current_dir.join(&new_name);
291 match std::fs::rename(&src, &dst) {
292 Ok(()) => {
293 self.reload();
294 if let Some(idx) = self.entries.iter().position(|e| e.path == dst) {
296 self.cursor = idx;
297 }
298 return ExplorerOutcome::RenameCompleted(dst);
299 }
300 Err(e) => {
301 self.status = format!("rename failed: {e}");
302 return ExplorerOutcome::Pending;
303 }
304 }
305 });
306
307 handle_input_mode!(self, key, touch_active, touch_input, {
311 let name = self.touch_input.trim().to_string();
312 self.touch_active = false;
313 self.touch_input.clear();
314 if name.is_empty() {
315 return ExplorerOutcome::Pending;
316 }
317 let new_file = self.current_dir.join(&name);
318 let create_result = (|| -> std::io::Result<()> {
321 if let Some(parent) = new_file.parent() {
322 std::fs::create_dir_all(parent)?;
323 }
324 std::fs::OpenOptions::new()
327 .write(true)
328 .create(true)
329 .truncate(false)
330 .open(&new_file)?;
331 Ok(())
332 })();
333 match create_result {
334 Ok(()) => {
335 self.reload();
336 if let Some(idx) = self.entries.iter().position(|e| e.path == new_file) {
338 self.cursor = idx;
339 }
340 return ExplorerOutcome::TouchCreated(new_file);
341 }
342 Err(e) => {
343 self.status = format!("touch failed: {e}");
344 return ExplorerOutcome::Pending;
345 }
346 }
347 });
348
349 handle_input_mode!(self, key, mkdir_active, mkdir_input, {
353 let name = self.mkdir_input.trim().to_string();
354 self.mkdir_active = false;
355 self.mkdir_input.clear();
356 if name.is_empty() {
357 return ExplorerOutcome::Pending;
358 }
359 let new_dir = self.current_dir.join(&name);
360 match std::fs::create_dir_all(&new_dir) {
361 Ok(()) => {
362 self.reload();
363 if let Some(idx) = self.entries.iter().position(|e| e.path == new_dir) {
365 self.cursor = idx;
366 }
367 return ExplorerOutcome::MkdirCreated(new_dir);
368 }
369 Err(e) => {
370 self.status = format!("mkdir failed: {e}");
371 return ExplorerOutcome::Pending;
372 }
373 }
374 });
375
376 if self.search_active {
382 match key.code {
383 KeyCode::Char(c) if key.modifiers.is_empty() => {
384 self.search_query.push(c);
385 self.cursor = 0;
386 self.scroll_offset = 0;
387 self.reload();
388 return ExplorerOutcome::Pending;
389 }
390 KeyCode::Backspace => {
391 if self.search_query.is_empty() {
392 self.search_active = false;
394 } else {
395 self.search_query.pop();
396 self.cursor = 0;
397 self.scroll_offset = 0;
398 self.reload();
399 }
400 return ExplorerOutcome::Pending;
401 }
402 KeyCode::Esc => {
403 self.search_active = false;
406 self.search_query.clear();
407 self.cursor = 0;
408 self.scroll_offset = 0;
409 self.reload();
410 return ExplorerOutcome::Pending;
411 }
412 _ => {} }
414 }
415
416 match key.code {
417 KeyCode::Esc => ExplorerOutcome::Dismissed,
419
420 KeyCode::Char('q') if key.modifiers.is_empty() => ExplorerOutcome::Dismissed,
422
423 KeyCode::Up | KeyCode::Char('k') => {
425 self.move_up();
426 ExplorerOutcome::Pending
427 }
428
429 KeyCode::Down | KeyCode::Char('j') => {
431 self.move_down();
432 ExplorerOutcome::Pending
433 }
434
435 KeyCode::PageUp => {
437 for _ in 0..10 {
438 self.move_up();
439 }
440 ExplorerOutcome::Pending
441 }
442
443 KeyCode::PageDown => {
445 for _ in 0..10 {
446 self.move_down();
447 }
448 ExplorerOutcome::Pending
449 }
450
451 KeyCode::Home | KeyCode::Char('g') => {
453 self.cursor = 0;
454 self.scroll_offset = 0;
455 ExplorerOutcome::Pending
456 }
457
458 KeyCode::End | KeyCode::Char('G') => {
460 if !self.entries.is_empty() {
461 self.cursor = self.entries.len() - 1;
462 }
463 ExplorerOutcome::Pending
464 }
465
466 KeyCode::Left | KeyCode::Backspace | KeyCode::Char('h') => {
469 self.ascend();
470 ExplorerOutcome::Pending
471 }
472
473 KeyCode::Right => self.navigate(),
477
478 KeyCode::Enter | KeyCode::Char('l') => self.confirm(),
482
483 KeyCode::Char('.') => {
485 self.show_hidden = !self.show_hidden;
486 let was = self.cursor;
487 self.reload();
488 self.cursor = was.min(self.entries.len().saturating_sub(1));
489 ExplorerOutcome::Pending
490 }
491
492 KeyCode::Char('/') if key.modifiers.is_empty() => {
494 self.search_active = true;
495 ExplorerOutcome::Pending
496 }
497
498 KeyCode::Char('s') if key.modifiers.is_empty() => {
500 self.sort_mode = self.sort_mode.next();
501 let was = self.cursor;
502 self.reload();
503 self.cursor = was.min(self.entries.len().saturating_sub(1));
504 ExplorerOutcome::Pending
505 }
506
507 KeyCode::Char(' ') => {
509 self.toggle_mark();
510 ExplorerOutcome::Pending
511 }
512
513 KeyCode::Char('n') if key.modifiers.is_empty() => {
515 self.mkdir_active = true;
516 self.mkdir_input.clear();
517 ExplorerOutcome::Pending
518 }
519
520 KeyCode::Char('N') if key.modifiers.is_empty() => {
523 self.touch_active = true;
524 self.touch_input.clear();
525 ExplorerOutcome::Pending
526 }
527
528 KeyCode::Char('r') if key.modifiers.is_empty() => {
532 if let Some(entry) = self.entries.get(self.cursor) {
533 self.rename_input = entry.name.clone();
534 self.rename_active = true;
535 }
536 ExplorerOutcome::Pending
537 }
538
539 _ => ExplorerOutcome::Unhandled,
540 }
541 }
542
543 pub fn current_entry(&self) -> Option<&FsEntry> {
547 self.entries.get(self.cursor)
548 }
549
550 pub fn is_mkdir_active(&self) -> bool {
552 self.mkdir_active
553 }
554
555 pub fn mkdir_input(&self) -> &str {
557 &self.mkdir_input
558 }
559
560 pub fn is_touch_active(&self) -> bool {
562 self.touch_active
563 }
564
565 pub fn touch_input(&self) -> &str {
567 &self.touch_input
568 }
569
570 pub fn is_rename_active(&self) -> bool {
572 self.rename_active
573 }
574
575 pub fn rename_input(&self) -> &str {
577 &self.rename_input
578 }
579
580 pub fn is_at_root(&self) -> bool {
592 self.current_dir.parent().is_none()
593 }
594
595 pub fn is_empty(&self) -> bool {
601 self.entries.is_empty()
602 }
603
604 pub fn entry_count(&self) -> usize {
609 self.entries.len()
610 }
611
612 pub fn status(&self) -> &str {
619 &self.status
620 }
621
622 pub fn sort_mode(&self) -> SortMode {
631 self.sort_mode
632 }
633
634 pub fn search_query(&self) -> &str {
638 &self.search_query
639 }
640
641 pub fn is_searching(&self) -> bool {
644 self.search_active
645 }
646
647 pub fn set_show_hidden(&mut self, show: bool) {
662 self.show_hidden = show;
663 self.reload();
664 }
665
666 pub fn set_extension_filter<I, S>(&mut self, filter: I)
687 where
688 I: IntoIterator<Item = S>,
689 S: Into<String>,
690 {
691 self.extension_filter = filter.into_iter().map(Into::into).collect();
692 self.reload();
693 }
694
695 pub fn set_sort_mode(&mut self, mode: SortMode) {
707 self.sort_mode = mode;
708 self.reload();
709 }
710
711 fn move_up(&mut self) {
714 if self.cursor > 0 {
715 self.cursor -= 1;
716 }
717 self.clamp_cursor();
719 }
720
721 fn move_down(&mut self) {
722 let last = self.entries.len().saturating_sub(1);
723 if !self.entries.is_empty() && self.cursor < last {
724 self.cursor += 1;
725 }
726 self.clamp_cursor();
727 }
728
729 fn clamp_cursor(&mut self) {
733 let max = self.entries.len().saturating_sub(1);
734 if self.cursor > max {
735 self.cursor = max;
736 }
737 if self.scroll_offset > self.cursor {
738 self.scroll_offset = self.cursor;
739 }
740 }
741
742 fn ascend(&mut self) {
743 if let Some(parent) = self.current_dir.parent().map(|p| p.to_path_buf()) {
744 let prev = self.current_dir.clone();
745 self.current_dir = parent;
746 self.cursor = 0;
747 self.scroll_offset = 0;
748 self.search_active = false;
750 self.search_query.clear();
751 self.marked.clear();
752 self.reload();
753 if let Some(idx) = self.entries.iter().position(|e| e.path == prev) {
755 self.cursor = idx;
756 }
757 self.clamp_cursor();
759 } else {
760 self.status = "Already at the filesystem root.".to_string();
762 }
763 }
764
765 fn navigate(&mut self) -> ExplorerOutcome {
771 let Some(entry) = self.entries.get(self.cursor) else {
772 return ExplorerOutcome::Pending;
773 };
774
775 if entry.is_dir {
776 let path = entry.path.clone();
777 self.search_active = false;
778 self.search_query.clear();
779 self.marked.clear();
780 self.navigate_to(path);
781 } else {
782 self.move_down();
783 }
784 ExplorerOutcome::Pending
785 }
786
787 fn confirm(&mut self) -> ExplorerOutcome {
788 let Some(entry) = self.entries.get(self.cursor) else {
789 return ExplorerOutcome::Pending;
790 };
791
792 if entry.is_dir {
793 let path = entry.path.clone();
794 self.search_active = false;
796 self.search_query.clear();
797 self.marked.clear();
798 self.navigate_to(path);
799 ExplorerOutcome::Pending
800 } else {
801 ExplorerOutcome::Selected(entry.path.clone())
804 }
805 }
806
807 pub fn reload(&mut self) {
815 self.status.clear();
816 self.entries = load_entries(
817 &self.current_dir,
818 self.show_hidden,
819 &self.extension_filter,
820 self.sort_mode,
821 &self.search_query,
822 );
823 self.clamp_cursor();
827 }
828}
829
830pub struct FileExplorerBuilder {
849 initial_dir: PathBuf,
850 extension_filter: Vec<String>,
851 show_hidden: bool,
852 sort_mode: SortMode,
853}
854
855impl FileExplorerBuilder {
856 pub fn new(initial_dir: PathBuf) -> Self {
858 Self {
859 initial_dir,
860 extension_filter: Vec::new(),
861 show_hidden: false,
862 sort_mode: SortMode::default(),
863 }
864 }
865
866 pub fn extension_filter(mut self, filter: Vec<String>) -> Self {
878 self.extension_filter = filter;
879 self
880 }
881
882 pub fn allow_extension(mut self, ext: impl Into<String>) -> Self {
895 self.extension_filter.push(ext.into());
896 self
897 }
898
899 pub fn show_hidden(mut self, show: bool) -> Self {
909 self.show_hidden = show;
910 self
911 }
912
913 pub fn sort_mode(mut self, mode: SortMode) -> Self {
923 self.sort_mode = mode;
924 self
925 }
926
927 pub fn build(self) -> FileExplorer {
929 let mut explorer = FileExplorer {
930 current_dir: self.initial_dir,
931 entries: Vec::new(),
932 cursor: 0,
933 scroll_offset: 0,
934 extension_filter: self.extension_filter,
935 show_hidden: self.show_hidden,
936 status: String::new(),
937 sort_mode: self.sort_mode,
938 search_query: String::new(),
939 search_active: false,
940 marked: HashSet::new(),
941 mkdir_active: false,
942 mkdir_input: String::new(),
943 touch_active: false,
944 touch_input: String::new(),
945 rename_active: false,
946 rename_input: String::new(),
947 theme_name: String::new(),
948 editor_name: String::new(),
949 };
950 explorer.reload();
951 explorer
952 }
953}
954
955pub(crate) fn load_entries(
967 dir: &Path,
968 show_hidden: bool,
969 ext_filter: &[String],
970 sort_mode: SortMode,
971 search_query: &str,
972) -> Vec<FsEntry> {
973 let read = match fs::read_dir(dir) {
974 Ok(r) => r,
975 Err(_) => return Vec::new(),
976 };
977
978 let mut dirs: Vec<FsEntry> = Vec::new();
979 let mut files: Vec<FsEntry> = Vec::new();
980
981 for entry in read.flatten() {
982 let path = entry.path();
983 let name = entry.file_name().to_string_lossy().to_string();
984
985 if !show_hidden && name.starts_with('.') {
986 continue;
987 }
988
989 let is_dir = path.is_dir();
990 let extension = if is_dir {
991 String::new()
992 } else {
993 path.extension()
994 .map(|e| e.to_string_lossy().to_lowercase())
995 .unwrap_or_default()
996 };
997
998 if !is_dir && !ext_filter.is_empty() {
1000 let matches = ext_filter
1001 .iter()
1002 .any(|f| f.eq_ignore_ascii_case(&extension));
1003 if !matches {
1004 continue;
1005 }
1006 }
1007
1008 if !search_query.is_empty() {
1010 let q = search_query.to_lowercase();
1011 if !name.to_lowercase().contains(&q) {
1012 continue;
1013 }
1014 }
1015
1016 let size = if is_dir {
1017 None
1018 } else {
1019 entry.metadata().ok().map(|m| m.len())
1020 };
1021
1022 let fs_entry = FsEntry {
1023 name,
1024 path,
1025 is_dir,
1026 size,
1027 extension,
1028 };
1029
1030 if is_dir {
1031 dirs.push(fs_entry);
1032 } else {
1033 files.push(fs_entry);
1034 }
1035 }
1036
1037 dirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
1040
1041 match sort_mode {
1042 SortMode::Name => {
1043 files.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
1044 }
1045 SortMode::SizeDesc => {
1046 files.sort_by(|a, b| b.size.unwrap_or(0).cmp(&a.size.unwrap_or(0)));
1048 }
1049 SortMode::Extension => {
1050 files.sort_by(|a, b| {
1052 a.extension
1053 .cmp(&b.extension)
1054 .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
1055 });
1056 }
1057 }
1058
1059 dirs.extend(files);
1061 dirs
1062}
1063
1064pub fn entry_icon(entry: &FsEntry) -> &'static str {
1071 if entry.is_dir {
1072 return "📁";
1073 }
1074 match entry.extension.as_str() {
1075 "iso" | "dmg" => "💿",
1077 "img" => "🖼 ",
1078 "zip" | "gz" | "xz" | "zst" | "bz2" | "tar" | "7z" | "rar" | "tgz" | "tbz2" => "📦",
1080 "pdf" => "📕",
1082 "txt" | "log" | "rst" => "📄",
1083 "md" | "mdx" | "markdown" => "📝",
1084 "toml" | "yaml" | "yml" | "json" | "xml" | "ini" | "cfg" | "conf" | "env" => "⚙ ",
1086 "lock" => "🔒",
1087 "rs" => "🦀",
1089 "py" | "pyw" => "🐍",
1090 "js" | "mjs" | "cjs" => "📜",
1091 "ts" | "mts" | "cts" => "📜",
1092 "jsx" | "tsx" => "📜",
1093 "go" => "📜",
1094 "c" | "h" => "📜",
1095 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "📜",
1096 "java" | "kt" | "kts" => "📜",
1097 "rb" | "erb" => "📜",
1098 "php" => "📜",
1099 "swift" => "📜",
1100 "cs" => "📜",
1101 "lua" => "📜",
1102 "zig" => "📜",
1103 "ex" | "exs" => "📜",
1104 "hs" | "lhs" => "📜",
1105 "ml" | "mli" => "📜",
1106 "sh" | "bash" | "zsh" | "fish" | "nu" => "📜",
1108 "bat" | "cmd" | "ps1" => "📜",
1109 "html" | "htm" | "xhtml" => "🌐",
1111 "css" | "scss" | "sass" | "less" => "🎨",
1112 "svg" => "🎨",
1113 "png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff" | "tif" | "avif"
1115 | "heic" | "heif" => "🖼 ",
1116 "mp4" | "mkv" | "avi" | "mov" | "webm" | "flv" | "wmv" | "m4v" => "🎬",
1118 "mp3" | "wav" | "flac" | "ogg" | "aac" | "m4a" | "opus" | "wma" => "🎵",
1120 "ttf" | "otf" | "woff" | "woff2" | "eot" => "🔤",
1122 "exe" | "msi" | "deb" | "rpm" | "appimage" | "apk" => "⚙ ",
1124 _ => "📄",
1125 }
1126}
1127
1128pub fn fmt_size(bytes: u64) -> String {
1142 const KB: u64 = 1_024;
1143 const MB: u64 = 1_024 * KB;
1144 const GB: u64 = 1_024 * MB;
1145 if bytes >= GB {
1146 format!("{:.1} GB", bytes as f64 / GB as f64)
1147 } else if bytes >= MB {
1148 format!("{:.1} MB", bytes as f64 / MB as f64)
1149 } else if bytes >= KB {
1150 format!("{:.1} KB", bytes as f64 / KB as f64)
1151 } else {
1152 format!("{bytes} B")
1153 }
1154}
1155
1156#[cfg(test)]
1159mod tests {
1160 use super::*;
1161 use crossterm::event::{KeyEvent, KeyModifiers};
1162 use std::fs;
1163 use tempfile::{tempdir, TempDir};
1164
1165 fn temp_dir_with_files() -> TempDir {
1168 let dir = tempfile::tempdir().expect("temp dir");
1169 fs::write(dir.path().join("ubuntu.iso"), b"fake iso content").unwrap();
1170 fs::write(dir.path().join("debian.img"), b"fake img content").unwrap();
1171 fs::write(dir.path().join("readme.txt"), b"some text").unwrap();
1172 fs::create_dir(dir.path().join("subdir")).unwrap();
1173 dir
1174 }
1175
1176 fn key(code: KeyCode) -> KeyEvent {
1177 KeyEvent::new(code, KeyModifiers::NONE)
1178 }
1179
1180 #[test]
1183 fn new_loads_entries() {
1184 let tmp = temp_dir_with_files();
1185 let explorer =
1186 FileExplorer::new(tmp.path().to_path_buf(), vec!["iso".into(), "img".into()]);
1187 assert!(explorer
1188 .entries
1189 .iter()
1190 .any(|e| e.name == "subdir" && e.is_dir));
1191 assert!(explorer.entries.iter().any(|e| e.name == "ubuntu.iso"));
1192 assert!(explorer.entries.iter().any(|e| e.name == "debian.img"));
1193 assert!(!explorer.entries.iter().any(|e| e.name == "readme.txt"));
1195 }
1196
1197 #[test]
1198 fn no_filter_shows_all_files() {
1199 let tmp = temp_dir_with_files();
1200 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1201 assert!(explorer.entries.iter().any(|e| e.name == "readme.txt"));
1202 }
1203
1204 #[test]
1205 fn dirs_listed_before_files() {
1206 let tmp = temp_dir_with_files();
1207 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1208 let first_file_idx = explorer
1209 .entries
1210 .iter()
1211 .position(|e| !e.is_dir)
1212 .unwrap_or(usize::MAX);
1213 let last_dir_idx = explorer.entries.iter().rposition(|e| e.is_dir).unwrap_or(0);
1214 assert!(
1215 last_dir_idx < first_file_idx,
1216 "all dirs must appear before any file"
1217 );
1218 }
1219
1220 #[test]
1221 fn move_down_increments_cursor() {
1222 let tmp = temp_dir_with_files();
1223 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1224 explorer.move_down();
1225 assert_eq!(explorer.cursor, 1);
1226 }
1227
1228 #[test]
1229 fn move_up_clamps_at_zero() {
1230 let tmp = temp_dir_with_files();
1231 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1232 explorer.move_up();
1233 assert_eq!(explorer.cursor, 0);
1234 }
1235
1236 #[test]
1237 fn move_down_clamps_at_last() {
1238 let tmp = temp_dir_with_files();
1239 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1240 let last = explorer.entries.len() - 1;
1241 explorer.cursor = last;
1242 explorer.move_down();
1243 assert_eq!(explorer.cursor, last);
1244 }
1245
1246 #[test]
1247 fn handle_key_down_moves_cursor() {
1248 let tmp = temp_dir_with_files();
1249 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1250 let before = explorer.cursor;
1251 explorer.handle_key(key(KeyCode::Down));
1252 assert_eq!(explorer.cursor, before + 1);
1253 }
1254
1255 #[test]
1256 fn handle_key_esc_dismisses() {
1257 let tmp = temp_dir_with_files();
1258 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1259 assert_eq!(
1260 explorer.handle_key(key(KeyCode::Esc)),
1261 ExplorerOutcome::Dismissed
1262 );
1263 }
1264
1265 #[test]
1266 fn handle_key_enter_on_dir_descends() {
1267 let tmp = temp_dir_with_files();
1268 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1269 let dir_idx = explorer
1271 .entries
1272 .iter()
1273 .position(|e| e.is_dir)
1274 .expect("no dir in fixture");
1275 explorer.cursor = dir_idx;
1276 let expected_path = explorer.entries[dir_idx].path.clone();
1277 let outcome = explorer.handle_key(key(KeyCode::Enter));
1278 assert_eq!(outcome, ExplorerOutcome::Pending);
1279 assert_eq!(explorer.current_dir, expected_path);
1280 }
1281
1282 #[test]
1283 fn handle_key_enter_on_valid_file_selects() {
1284 let tmp = temp_dir_with_files();
1285 let mut explorer =
1286 FileExplorer::new(tmp.path().to_path_buf(), vec!["iso".into(), "img".into()]);
1287 let file_idx = explorer
1288 .entries
1289 .iter()
1290 .position(|e| !e.is_dir)
1291 .expect("no file in fixture");
1292 explorer.cursor = file_idx;
1293 let expected = explorer.entries[file_idx].path.clone();
1294 let outcome = explorer.handle_key(key(KeyCode::Enter));
1295 assert_eq!(outcome, ExplorerOutcome::Selected(expected));
1296 }
1297
1298 #[test]
1299 fn handle_key_backspace_ascends() {
1300 let tmp = temp_dir_with_files();
1301 let subdir = tmp.path().join("subdir");
1302 let mut explorer = FileExplorer::new(subdir, vec![]);
1303 explorer.handle_key(key(KeyCode::Backspace));
1304 assert_eq!(explorer.current_dir, tmp.path());
1305 }
1306
1307 #[test]
1308 fn toggle_hidden_changes_visibility() {
1309 let tmp = temp_dir_with_files();
1310 fs::write(tmp.path().join(".hidden_file"), b"").unwrap();
1311 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1312 assert!(!explorer.entries.iter().any(|e| e.name == ".hidden_file"));
1313 explorer.set_show_hidden(true);
1314 assert!(explorer.entries.iter().any(|e| e.name == ".hidden_file"));
1315 }
1316
1317 #[test]
1318 fn fmt_size_formats_bytes() {
1319 assert_eq!(fmt_size(512), "512 B");
1320 assert_eq!(fmt_size(1_536), "1.5 KB");
1321 assert_eq!(fmt_size(2_097_152), "2.0 MB");
1322 assert_eq!(fmt_size(1_073_741_824), "1.0 GB");
1323 }
1324
1325 #[test]
1326 fn extension_filter_only_shows_matching_files() {
1327 let tmp = temp_dir_with_files();
1330 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec!["iso".into()]);
1331
1332 assert!(
1334 explorer.entries.iter().any(|e| e.name == "ubuntu.iso"),
1335 "iso file should appear in entries"
1336 );
1337 assert!(
1339 !explorer.entries.iter().any(|e| e.name == "debian.img"),
1340 "img file should be excluded by filter"
1341 );
1342 assert!(
1344 explorer.entries.iter().any(|e| e.is_dir),
1345 "directories should always be visible"
1346 );
1347 assert!(
1349 explorer
1350 .entries
1351 .iter()
1352 .filter(|e| !e.is_dir)
1353 .all(|e| e.extension == "iso"),
1354 "all visible files must match the active filter"
1355 );
1356 }
1357
1358 #[test]
1359 fn navigate_to_resets_cursor_and_scroll() {
1360 let tmp = temp_dir_with_files();
1361 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1362 explorer.cursor = 2;
1363 explorer.scroll_offset = 1;
1364 explorer.navigate_to(tmp.path().to_path_buf());
1365 assert_eq!(explorer.cursor, 0);
1366 assert_eq!(explorer.scroll_offset, 0);
1367 }
1368
1369 #[test]
1370 fn current_entry_returns_highlighted() {
1371 let tmp = temp_dir_with_files();
1372 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1373 explorer.cursor = 0;
1374 let entry = explorer.current_entry().expect("should have entry");
1375 assert_eq!(entry, explorer.entries.first().unwrap());
1376 }
1377
1378 #[test]
1379 fn unrecognised_key_returns_unhandled() {
1380 let tmp = temp_dir_with_files();
1381 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1382 assert_eq!(
1383 explorer.handle_key(key(KeyCode::F(5))),
1384 ExplorerOutcome::Unhandled
1385 );
1386 }
1387
1388 #[test]
1391 fn slash_activates_search_mode() {
1392 let tmp = temp_dir_with_files();
1393 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1394 assert!(!explorer.search_active);
1395 explorer.handle_key(key(KeyCode::Char('/')));
1396 assert!(explorer.search_active);
1397 assert_eq!(explorer.search_query(), "");
1398 }
1399
1400 #[test]
1401 fn search_active_chars_append_to_query() {
1402 let tmp = temp_dir_with_files();
1403 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1404 explorer.handle_key(key(KeyCode::Char('/')));
1405 explorer.handle_key(key(KeyCode::Char('u')));
1406 explorer.handle_key(key(KeyCode::Char('b')));
1407 explorer.handle_key(key(KeyCode::Char('u')));
1408 assert_eq!(explorer.search_query(), "ubu");
1409 assert!(explorer.search_active);
1410 }
1411
1412 #[test]
1413 fn search_filters_entries_by_name() {
1414 let tmp = temp_dir_with_files();
1415 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1416 explorer.handle_key(key(KeyCode::Char('/')));
1418 for c in "ubu".chars() {
1419 explorer.handle_key(key(KeyCode::Char(c)));
1420 }
1421 assert_eq!(explorer.entries.len(), 1);
1423 assert_eq!(explorer.entries[0].name, "ubuntu.iso");
1424 }
1425
1426 #[test]
1427 fn search_backspace_pops_last_char() {
1428 let tmp = temp_dir_with_files();
1429 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1430 explorer.handle_key(key(KeyCode::Char('/')));
1431 explorer.handle_key(key(KeyCode::Char('u')));
1432 explorer.handle_key(key(KeyCode::Char('b')));
1433 explorer.handle_key(key(KeyCode::Backspace));
1434 assert_eq!(explorer.search_query(), "u");
1435 assert!(explorer.search_active);
1436 }
1437
1438 #[test]
1439 fn search_backspace_on_empty_deactivates() {
1440 let tmp = temp_dir_with_files();
1441 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1442 explorer.handle_key(key(KeyCode::Char('/')));
1443 assert!(explorer.search_active);
1444 explorer.handle_key(key(KeyCode::Backspace));
1446 assert!(!explorer.search_active);
1447 assert_eq!(explorer.search_query(), "");
1448 }
1449
1450 #[test]
1451 fn search_esc_clears_and_deactivates_returns_pending() {
1452 let tmp = temp_dir_with_files();
1453 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1454 explorer.handle_key(key(KeyCode::Char('/')));
1455 explorer.handle_key(key(KeyCode::Char('u')));
1456 let outcome = explorer.handle_key(key(KeyCode::Esc));
1457 assert_eq!(
1458 outcome,
1459 ExplorerOutcome::Pending,
1460 "Esc should clear search, not dismiss"
1461 );
1462 assert!(!explorer.search_active);
1463 assert_eq!(explorer.search_query(), "");
1464 }
1465
1466 #[test]
1467 fn esc_when_not_searching_dismisses() {
1468 let tmp = temp_dir_with_files();
1469 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1470 assert!(!explorer.search_active);
1471 assert_eq!(
1472 explorer.handle_key(key(KeyCode::Esc)),
1473 ExplorerOutcome::Dismissed
1474 );
1475 }
1476
1477 #[test]
1478 fn search_clears_on_directory_descend() {
1479 let tmp = temp_dir_with_files();
1480 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1481 explorer.search_active = true;
1482 explorer.search_query = "sub".into();
1483 explorer.cursor = explorer.entries.iter().position(|e| e.is_dir).unwrap();
1485 explorer.handle_key(key(KeyCode::Enter));
1486 assert!(!explorer.search_active);
1487 assert_eq!(explorer.search_query(), "");
1488 }
1489
1490 #[test]
1491 fn search_clears_on_ascend() {
1492 let tmp = temp_dir_with_files();
1493 let subdir = tmp.path().join("subdir");
1494 let mut explorer = FileExplorer::new(subdir, vec![]);
1495
1496 explorer.search_active = true;
1507 explorer.search_query = "foo".into();
1508
1509 explorer.ascend();
1511
1512 assert!(
1513 !explorer.search_active,
1514 "search must be deactivated after ascend"
1515 );
1516 assert_eq!(
1517 explorer.search_query(),
1518 "",
1519 "query must be cleared after ascend"
1520 );
1521 assert_eq!(
1522 explorer.current_dir,
1523 tmp.path(),
1524 "must have ascended to parent"
1525 );
1526 }
1527
1528 #[test]
1529 fn backspace_in_search_pops_char_not_ascend() {
1530 let tmp = temp_dir_with_files();
1533 let subdir = tmp.path().join("subdir");
1534 let mut explorer = FileExplorer::new(subdir.clone(), vec![]);
1535 explorer.search_active = true;
1536 explorer.search_query = "foo".into();
1537
1538 explorer.handle_key(key(KeyCode::Backspace)); assert_eq!(explorer.current_dir, subdir, "must NOT have ascended");
1541 assert_eq!(
1542 explorer.search_query(),
1543 "fo",
1544 "Backspace should pop last char"
1545 );
1546 assert!(explorer.search_active, "search must still be active");
1547 }
1548
1549 #[test]
1552 fn default_sort_mode_is_name() {
1553 let tmp = temp_dir_with_files();
1554 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1555 assert_eq!(explorer.sort_mode(), SortMode::Name);
1556 }
1557
1558 #[test]
1559 fn sort_mode_cycles_on_s_key() {
1560 let tmp = temp_dir_with_files();
1561 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1562 assert_eq!(explorer.sort_mode(), SortMode::Name);
1563 explorer.handle_key(key(KeyCode::Char('s')));
1564 assert_eq!(explorer.sort_mode(), SortMode::SizeDesc);
1565 explorer.handle_key(key(KeyCode::Char('s')));
1566 assert_eq!(explorer.sort_mode(), SortMode::Extension);
1567 explorer.handle_key(key(KeyCode::Char('s')));
1568 assert_eq!(explorer.sort_mode(), SortMode::Name);
1569 }
1570
1571 #[test]
1572 fn sort_size_desc_orders_largest_first() {
1573 let tmp = tempfile::tempdir().expect("temp dir");
1574 fs::write(tmp.path().join("small.txt"), vec![0u8; 10]).unwrap();
1576 fs::write(tmp.path().join("large.txt"), vec![0u8; 10_000]).unwrap();
1577 fs::write(tmp.path().join("medium.txt"), vec![0u8; 1_000]).unwrap();
1578
1579 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1580 explorer.set_sort_mode(SortMode::SizeDesc);
1581
1582 let sizes: Vec<u64> = explorer.entries.iter().filter_map(|e| e.size).collect();
1583 let mut sorted_desc = sizes.clone();
1584 sorted_desc.sort_by(|a, b| b.cmp(a));
1585 assert_eq!(sizes, sorted_desc, "files should be sorted largest-first");
1586 }
1587
1588 #[test]
1589 fn sort_extension_groups_by_ext() {
1590 let tmp = tempfile::tempdir().expect("temp dir");
1591 fs::write(tmp.path().join("b.toml"), b"").unwrap();
1592 fs::write(tmp.path().join("a.rs"), b"").unwrap();
1593 fs::write(tmp.path().join("c.toml"), b"").unwrap();
1594 fs::write(tmp.path().join("z.rs"), b"").unwrap();
1595
1596 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1597 explorer.set_sort_mode(SortMode::Extension);
1598
1599 let exts: Vec<&str> = explorer
1600 .entries
1601 .iter()
1602 .filter(|e| !e.is_dir)
1603 .map(|e| e.extension.as_str())
1604 .collect();
1605
1606 let rs_last = exts.iter().rposition(|&e| e == "rs").unwrap_or(0);
1608 let toml_first = exts.iter().position(|&e| e == "toml").unwrap_or(usize::MAX);
1609 assert!(rs_last < toml_first, "rs group must precede toml group");
1610 }
1611
1612 #[test]
1613 fn builder_sort_mode_applied() {
1614 let tmp = temp_dir_with_files();
1615 let explorer = FileExplorer::builder(tmp.path().to_path_buf())
1616 .sort_mode(SortMode::SizeDesc)
1617 .build();
1618 assert_eq!(explorer.sort_mode(), SortMode::SizeDesc);
1619 }
1620
1621 #[test]
1622 fn set_sort_mode_reloads() {
1623 let tmp = temp_dir_with_files();
1624 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1625 explorer.set_sort_mode(SortMode::Extension);
1626 assert_eq!(explorer.sort_mode(), SortMode::Extension);
1627 assert!(!explorer.entries.is_empty());
1629 }
1630
1631 #[test]
1634 fn j_key_moves_cursor_down() {
1635 let tmp = temp_dir_with_files();
1636 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1637 let before = explorer.cursor;
1638 explorer.handle_key(key(KeyCode::Char('j')));
1639 assert_eq!(explorer.cursor, before + 1);
1640 }
1641
1642 #[test]
1643 fn k_key_moves_cursor_up() {
1644 let tmp = temp_dir_with_files();
1645 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1646 explorer.cursor = 2;
1647 explorer.handle_key(key(KeyCode::Char('k')));
1648 assert_eq!(explorer.cursor, 1);
1649 }
1650
1651 #[test]
1652 fn h_key_ascends_to_parent() {
1653 let tmp = temp_dir_with_files();
1654 let subdir = tmp.path().join("subdir");
1655 let mut explorer = FileExplorer::new(subdir, vec![]);
1656 explorer.handle_key(key(KeyCode::Char('h')));
1657 assert_eq!(explorer.current_dir, tmp.path());
1658 }
1659
1660 #[test]
1661 fn l_key_descends_into_dir() {
1662 let tmp = temp_dir_with_files();
1663 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1664 let dir_idx = explorer.entries.iter().position(|e| e.is_dir).unwrap();
1665 explorer.cursor = dir_idx;
1666 let expected = explorer.entries[dir_idx].path.clone();
1667 let outcome = explorer.handle_key(key(KeyCode::Char('l')));
1668 assert_eq!(outcome, ExplorerOutcome::Pending);
1669 assert_eq!(explorer.current_dir, expected);
1670 }
1671
1672 #[test]
1673 fn right_arrow_descends_into_dir() {
1674 let tmp = temp_dir_with_files();
1675 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1676 let dir_idx = explorer.entries.iter().position(|e| e.is_dir).unwrap();
1677 explorer.cursor = dir_idx;
1678 let expected = explorer.entries[dir_idx].path.clone();
1679 let outcome = explorer.handle_key(key(KeyCode::Right));
1680 assert_eq!(
1681 outcome,
1682 ExplorerOutcome::Pending,
1683 "Right arrow should descend into directory"
1684 );
1685 assert_eq!(
1686 explorer.current_dir, expected,
1687 "Right arrow should change into the selected directory"
1688 );
1689 }
1690
1691 #[test]
1692 fn right_arrow_on_file_moves_down_not_exits() {
1693 let tmp = temp_dir_with_files();
1694 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1695 let file_idx = explorer.entries.iter().position(|e| !e.is_dir).unwrap();
1697 assert!(
1699 file_idx + 1 < explorer.entries.len(),
1700 "fixture must have an entry after the first file"
1701 );
1702 explorer.cursor = file_idx;
1703 let original_dir = explorer.current_dir.clone();
1704 let outcome = explorer.handle_key(key(KeyCode::Right));
1705 assert_eq!(
1706 outcome,
1707 ExplorerOutcome::Pending,
1708 "Right arrow on a file must never exit (always Pending)"
1709 );
1710 assert_eq!(
1711 explorer.current_dir, original_dir,
1712 "Right arrow on a file must not change directory"
1713 );
1714 assert_eq!(
1715 explorer.cursor,
1716 file_idx + 1,
1717 "Right arrow on a file must advance the cursor by one"
1718 );
1719 }
1720
1721 #[test]
1722 fn right_arrow_on_file_at_last_entry_does_not_overflow() {
1723 let tmp = temp_dir_with_files();
1724 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1725 let last = explorer.entries.len() - 1;
1726 explorer.cursor = last;
1728 explorer.handle_key(key(KeyCode::Right));
1729 assert_eq!(
1730 explorer.cursor, last,
1731 "Right arrow at the last entry must not overflow past it"
1732 );
1733 }
1734
1735 #[test]
1736 fn enter_on_file_still_confirms_and_exits() {
1737 let tmp = temp_dir_with_files();
1738 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1739 let file_idx = explorer.entries.iter().position(|e| !e.is_dir).unwrap();
1740 explorer.cursor = file_idx;
1741 let expected = explorer.entries[file_idx].path.clone();
1742 let outcome = explorer.handle_key(key(KeyCode::Enter));
1743 assert_eq!(
1744 outcome,
1745 ExplorerOutcome::Selected(expected),
1746 "Enter on a file should confirm (select) it and exit"
1747 );
1748 }
1749
1750 #[test]
1751 fn left_arrow_ascends_to_parent() {
1752 let tmp = temp_dir_with_files();
1753 let subdir = tmp.path().join("subdir");
1754 let mut explorer = FileExplorer::new(subdir, vec![]);
1755 let outcome = explorer.handle_key(key(KeyCode::Left));
1756 assert_eq!(
1757 outcome,
1758 ExplorerOutcome::Pending,
1759 "Left arrow should return Pending after ascending"
1760 );
1761 assert_eq!(
1762 explorer.current_dir,
1763 tmp.path(),
1764 "Left arrow should ascend to the parent directory"
1765 );
1766 }
1767
1768 #[test]
1769 fn right_arrow_clears_search_on_dir_descend() {
1770 let tmp = temp_dir_with_files();
1771 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1772 explorer.search_active = true;
1774 explorer.search_query = "sub".to_string();
1775 explorer.reload();
1776 let dir_idx = explorer
1778 .entries
1779 .iter()
1780 .position(|e| e.is_dir)
1781 .expect("fixture subdir must match 'sub'");
1782 explorer.cursor = dir_idx;
1783 explorer.handle_key(key(KeyCode::Right));
1784 assert!(
1785 !explorer.search_active,
1786 "navigate() must deactivate search on directory descend"
1787 );
1788 assert!(
1789 explorer.search_query.is_empty(),
1790 "navigate() must clear search query on directory descend"
1791 );
1792 }
1793
1794 #[test]
1795 fn right_arrow_clears_marks_on_dir_descend() {
1796 let tmp = temp_dir_with_files();
1797 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1798 let dir_idx = explorer
1799 .entries
1800 .iter()
1801 .position(|e| e.is_dir)
1802 .expect("fixture has a subdir");
1803 explorer.toggle_mark();
1805 assert!(
1806 !explorer.marked.is_empty(),
1807 "should have a mark before descend"
1808 );
1809 explorer.cursor = explorer
1811 .entries
1812 .iter()
1813 .position(|e| e.is_dir)
1814 .expect("fixture has a subdir");
1815 explorer.handle_key(key(KeyCode::Right));
1816 assert!(
1817 explorer.marked.is_empty(),
1818 "navigate() must clear marks on directory descend"
1819 );
1820 let _ = dir_idx;
1821 }
1822
1823 #[test]
1824 fn backspace_still_ascends() {
1825 let tmp = temp_dir_with_files();
1826 let subdir = tmp.path().join("subdir");
1827 let mut explorer = FileExplorer::new(subdir, vec![]);
1828 explorer.handle_key(key(KeyCode::Backspace));
1829 assert_eq!(explorer.current_dir, tmp.path());
1830 }
1831
1832 #[test]
1833 fn q_key_dismisses() {
1834 let tmp = temp_dir_with_files();
1835 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1836 assert_eq!(
1837 explorer.handle_key(key(KeyCode::Char('q'))),
1838 ExplorerOutcome::Dismissed
1839 );
1840 }
1841
1842 #[test]
1845 fn page_down_advances_cursor_by_ten() {
1846 let tmp = tempfile::tempdir().unwrap();
1847 for i in 0..15 {
1848 fs::write(tmp.path().join(format!("file{i:02}.txt")), b"").unwrap();
1849 }
1850 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1851 explorer.cursor = 0;
1852 explorer.handle_key(key(KeyCode::PageDown));
1853 assert_eq!(explorer.cursor, 10);
1854 }
1855
1856 #[test]
1857 fn page_up_retreats_cursor_by_ten() {
1858 let tmp = tempfile::tempdir().unwrap();
1859 for i in 0..15 {
1860 fs::write(tmp.path().join(format!("file{i:02}.txt")), b"").unwrap();
1861 }
1862 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1863 explorer.cursor = 12;
1864 explorer.handle_key(key(KeyCode::PageUp));
1865 assert_eq!(explorer.cursor, 2);
1866 }
1867
1868 #[test]
1869 fn home_key_jumps_to_top() {
1870 let tmp = temp_dir_with_files();
1871 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1872 explorer.cursor = explorer.entries.len() - 1;
1873 explorer.handle_key(key(KeyCode::Home));
1874 assert_eq!(explorer.cursor, 0);
1875 assert_eq!(explorer.scroll_offset, 0);
1876 }
1877
1878 #[test]
1879 fn g_key_jumps_to_top() {
1880 let tmp = temp_dir_with_files();
1881 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1882 explorer.cursor = explorer.entries.len() - 1;
1883 explorer.handle_key(key(KeyCode::Char('g')));
1884 assert_eq!(explorer.cursor, 0);
1885 assert_eq!(explorer.scroll_offset, 0);
1886 }
1887
1888 #[test]
1889 fn end_key_jumps_to_bottom() {
1890 let tmp = temp_dir_with_files();
1891 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1892 explorer.cursor = 0;
1893 explorer.handle_key(key(KeyCode::End));
1894 assert_eq!(explorer.cursor, explorer.entries.len() - 1);
1895 }
1896
1897 #[test]
1898 fn capital_g_key_jumps_to_bottom() {
1899 let tmp = temp_dir_with_files();
1900 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1901 explorer.cursor = 0;
1902 let key_g = KeyEvent::new(KeyCode::Char('G'), KeyModifiers::NONE);
1903 explorer.handle_key(key_g);
1904 assert_eq!(explorer.cursor, explorer.entries.len() - 1);
1905 }
1906
1907 #[test]
1910 fn ascend_at_root_sets_status() {
1911 let root = std::path::PathBuf::from("/");
1913 let mut explorer = FileExplorer::new(root.clone(), vec![]);
1914 assert!(explorer.is_at_root());
1915 explorer.handle_key(key(KeyCode::Backspace));
1917 assert_eq!(explorer.current_dir, root);
1918 assert!(
1919 !explorer.status().is_empty(),
1920 "status should report already at root"
1921 );
1922 }
1923
1924 #[test]
1925 fn is_at_root_false_for_subdir() {
1926 let tmp = temp_dir_with_files();
1927 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1928 assert!(!explorer.is_at_root());
1929 }
1930
1931 #[test]
1934 fn is_empty_reflects_visible_entries() {
1935 let empty_dir = tempfile::tempdir().unwrap();
1936 let explorer = FileExplorer::new(empty_dir.path().to_path_buf(), vec![]);
1937 assert!(explorer.is_empty());
1938
1939 let tmp = temp_dir_with_files();
1940 let explorer2 = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1941 assert!(!explorer2.is_empty());
1942 }
1943
1944 #[test]
1945 fn entry_count_matches_entries_len() {
1946 let tmp = temp_dir_with_files();
1947 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1948 assert_eq!(explorer.entry_count(), explorer.entries.len());
1949 assert!(explorer.entry_count() > 0);
1950 }
1951
1952 #[test]
1953 fn search_query_empty_when_not_searching() {
1954 let tmp = temp_dir_with_files();
1955 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1956 assert!(!explorer.is_searching());
1957 assert_eq!(explorer.search_query(), "");
1958 }
1959
1960 #[test]
1963 fn search_is_case_insensitive() {
1964 let tmp = temp_dir_with_files();
1965 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
1966 explorer.handle_key(key(KeyCode::Char('/')));
1968 for c in "UBU".chars() {
1969 explorer.handle_key(key(KeyCode::Char(c)));
1970 }
1971 assert_eq!(explorer.entries.len(), 1);
1972 assert_eq!(explorer.entries[0].name, "ubuntu.iso");
1973 }
1974
1975 #[test]
1976 fn extension_filter_is_case_insensitive() {
1977 let tmp = tempfile::tempdir().unwrap();
1978 fs::write(tmp.path().join("disk.ISO"), b"data").unwrap();
1980 fs::write(tmp.path().join("other.txt"), b"text").unwrap();
1981
1982 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec!["iso".into()]);
1984 assert!(
1985 explorer.entries.iter().any(|e| e.name == "disk.ISO"),
1986 "upper-case extension should be matched by lower-case filter"
1987 );
1988 assert!(
1989 !explorer.entries.iter().any(|e| e.name == "other.txt"),
1990 "non-matching extension should be excluded"
1991 );
1992 }
1993
1994 #[test]
1997 fn builder_allow_extension_filters_entries() {
1998 let tmp = temp_dir_with_files();
1999 let explorer = FileExplorer::builder(tmp.path().to_path_buf())
2000 .allow_extension("iso")
2001 .build();
2002 assert!(explorer.entries.iter().any(|e| e.name == "ubuntu.iso"));
2003 assert!(!explorer.entries.iter().any(|e| e.name == "debian.img"));
2004 assert!(!explorer.entries.iter().any(|e| e.name == "readme.txt"));
2005 }
2006
2007 #[test]
2008 fn builder_show_hidden_shows_dotfiles() {
2009 let tmp = temp_dir_with_files();
2010 fs::write(tmp.path().join(".dotfile"), b"").unwrap();
2011
2012 let hidden_explorer = FileExplorer::builder(tmp.path().to_path_buf())
2013 .show_hidden(true)
2014 .build();
2015 assert!(hidden_explorer.entries.iter().any(|e| e.name == ".dotfile"));
2016
2017 let normal_explorer = FileExplorer::builder(tmp.path().to_path_buf())
2018 .show_hidden(false)
2019 .build();
2020 assert!(!normal_explorer.entries.iter().any(|e| e.name == ".dotfile"));
2021 }
2022
2023 #[test]
2024 fn set_extension_filter_updates_entries() {
2025 let tmp = temp_dir_with_files();
2026 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2027 assert!(explorer.entries.iter().any(|e| e.name == "readme.txt"));
2029
2030 explorer.set_extension_filter(["iso"]);
2031 assert!(explorer.entries.iter().any(|e| e.name == "ubuntu.iso"));
2032 assert!(!explorer.entries.iter().any(|e| e.name == "readme.txt"));
2033 }
2034
2035 #[test]
2038 fn entry_icon_directory() {
2039 let entry = FsEntry {
2040 name: "mydir".into(),
2041 path: std::path::PathBuf::from("/mydir"),
2042 is_dir: true,
2043 size: None,
2044 extension: String::new(),
2045 };
2046 assert_eq!(entry_icon(&entry), "📁");
2047 }
2048
2049 #[test]
2050 fn entry_icon_recognises_known_extensions() {
2051 let make = |name: &str, ext: &str| FsEntry {
2052 name: name.into(),
2053 path: std::path::PathBuf::from(name),
2054 is_dir: false,
2055 size: Some(0),
2056 extension: ext.into(),
2057 };
2058
2059 assert_eq!(entry_icon(&make("archive.zip", "zip")), "📦");
2060 assert_eq!(entry_icon(&make("doc.pdf", "pdf")), "📕");
2061 assert_eq!(entry_icon(&make("notes.md", "md")), "📝");
2062 assert_eq!(entry_icon(&make("config.toml", "toml")), "⚙ ");
2063 assert_eq!(entry_icon(&make("main.rs", "rs")), "🦀");
2064 assert_eq!(entry_icon(&make("script.py", "py")), "🐍");
2065 assert_eq!(entry_icon(&make("page.html", "html")), "🌐");
2066 assert_eq!(entry_icon(&make("image.png", "png")), "🖼 ");
2067 assert_eq!(entry_icon(&make("video.mp4", "mp4")), "🎬");
2068 assert_eq!(entry_icon(&make("song.mp3", "mp3")), "🎵");
2069 assert_eq!(entry_icon(&make("unknown.xyz", "xyz")), "📄");
2070 }
2071
2072 #[test]
2075 fn fmt_size_exact_boundaries() {
2076 assert_eq!(fmt_size(1_024), "1.0 KB");
2078 assert_eq!(fmt_size(1_048_576), "1.0 MB");
2079 assert_eq!(fmt_size(1_073_741_824), "1.0 GB");
2080 assert_eq!(fmt_size(1_023), "1023 B");
2082 assert_eq!(fmt_size(1_047_552), "1023.0 KB"); }
2084
2085 #[test]
2088 fn toggle_mark_adds_entry_to_marked_set() {
2089 let dir = temp_dir_with_files();
2090 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2091 assert!(!explorer.entries.is_empty(), "need at least one entry");
2092
2093 explorer.toggle_mark();
2094
2095 assert_eq!(explorer.marked.len(), 1, "one entry should be marked");
2096 }
2097
2098 #[test]
2099 fn toggle_mark_removes_already_marked_entry() {
2100 let dir = temp_dir_with_files();
2101 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2102
2103 explorer.toggle_mark(); let cursor_after_first = explorer.cursor;
2105 explorer.cursor = 0; explorer.toggle_mark(); assert!(
2109 explorer.marked.is_empty(),
2110 "second toggle on same entry should unmark it"
2111 );
2112 let _ = cursor_after_first; }
2114
2115 #[test]
2116 fn toggle_mark_advances_cursor_down() {
2117 let dir = temp_dir_with_files();
2118 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2119 assert!(
2121 explorer.entries.len() >= 2,
2122 "fixture must have at least 2 entries"
2123 );
2124
2125 let before = explorer.cursor;
2126 explorer.toggle_mark();
2127
2128 assert_eq!(
2129 explorer.cursor,
2130 before + 1,
2131 "cursor should advance by one after toggle_mark"
2132 );
2133 }
2134
2135 #[test]
2136 fn toggle_mark_at_last_entry_does_not_overflow() {
2137 let dir = temp_dir_with_files();
2138 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2139 explorer.cursor = explorer.entries.len() - 1;
2140
2141 explorer.toggle_mark();
2142
2143 assert_eq!(
2144 explorer.cursor,
2145 explorer.entries.len() - 1,
2146 "cursor should stay at the last entry, not overflow"
2147 );
2148 }
2149
2150 #[test]
2151 fn clear_marks_empties_marked_set() {
2152 let dir = temp_dir_with_files();
2153 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2154
2155 explorer.toggle_mark();
2156 assert!(
2157 !explorer.marked.is_empty(),
2158 "should have a mark before clear"
2159 );
2160
2161 explorer.clear_marks();
2162
2163 assert!(
2164 explorer.marked.is_empty(),
2165 "marked set should be empty after clear_marks"
2166 );
2167 }
2168
2169 #[test]
2170 fn space_key_marks_current_entry() {
2171 let dir = temp_dir_with_files();
2172 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2173 assert!(!explorer.entries.is_empty(), "need at least one entry");
2174
2175 let outcome = explorer.handle_key(key(KeyCode::Char(' ')));
2176
2177 assert_eq!(
2178 outcome,
2179 ExplorerOutcome::Pending,
2180 "Space should return Pending"
2181 );
2182 assert_eq!(
2183 explorer.marked.len(),
2184 1,
2185 "Space should mark the current entry"
2186 );
2187 }
2188
2189 #[test]
2190 fn space_key_toggles_mark_off() {
2191 let dir = temp_dir_with_files();
2192 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2193
2194 explorer.handle_key(key(KeyCode::Char(' '))); explorer.cursor = 0; explorer.handle_key(key(KeyCode::Char(' '))); assert!(
2199 explorer.marked.is_empty(),
2200 "second Space on same entry should unmark it"
2201 );
2202 }
2203
2204 #[test]
2205 fn marks_cleared_when_ascending_to_parent() {
2206 let dir = temp_dir_with_files();
2207 let sub = dir.path().join("subdir");
2209 fs::write(sub.join("inner.txt"), b"inner").unwrap();
2210 let mut explorer = FileExplorer::new(sub.clone(), vec![]);
2211
2212 explorer.toggle_mark();
2213 assert!(
2214 !explorer.marked.is_empty(),
2215 "should have a mark before ascend"
2216 );
2217
2218 explorer.handle_key(key(KeyCode::Backspace));
2220
2221 assert!(
2222 explorer.marked.is_empty(),
2223 "marks should be cleared after ascending to parent"
2224 );
2225 }
2226
2227 #[test]
2228 fn marks_cleared_when_descending_into_directory() {
2229 let dir = temp_dir_with_files();
2230 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2231
2232 let sub_idx = explorer
2234 .entries
2235 .iter()
2236 .position(|e| e.is_dir)
2237 .expect("fixture has a subdir");
2238 explorer.cursor = sub_idx;
2239 explorer.toggle_mark();
2240 assert!(
2241 !explorer.marked.is_empty(),
2242 "should have a mark before descend"
2243 );
2244
2245 explorer.cursor = explorer
2247 .entries
2248 .iter()
2249 .position(|e| e.is_dir)
2250 .expect("fixture has a subdir");
2251
2252 explorer.handle_key(key(KeyCode::Enter));
2254
2255 assert!(
2256 explorer.marked.is_empty(),
2257 "marks should be cleared after descending into a directory"
2258 );
2259 }
2260
2261 #[test]
2262 fn can_mark_multiple_entries() {
2263 let dir = temp_dir_with_files();
2264 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2265 let total = explorer.entries.len();
2266 assert!(total >= 2, "fixture must have at least 2 entries");
2267
2268 for _ in 0..total {
2270 explorer.toggle_mark();
2271 }
2272
2273 assert_eq!(explorer.marked.len(), total, "all entries should be marked");
2274 }
2275
2276 #[test]
2279 fn move_up_at_top_does_not_underflow() {
2280 let dir = tempdir().expect("tempdir");
2281 fs::write(dir.path().join("a.txt"), b"a").unwrap();
2282 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2283 explorer.cursor = 0;
2284 explorer.handle_key(key(KeyCode::Up));
2286 assert_eq!(explorer.cursor, 0);
2287 }
2288
2289 #[test]
2290 fn move_down_at_bottom_does_not_overflow() {
2291 let dir = tempdir().expect("tempdir");
2292 fs::write(dir.path().join("a.txt"), b"a").unwrap();
2293 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2294 let last = explorer.entries.len().saturating_sub(1);
2295 explorer.cursor = last;
2296 explorer.handle_key(key(KeyCode::Down));
2297 assert_eq!(explorer.cursor, last);
2298 }
2299
2300 #[test]
2301 fn move_down_on_empty_dir_does_not_panic() {
2302 let dir = tempdir().expect("tempdir");
2303 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2304 assert!(explorer.entries.is_empty());
2305 explorer.handle_key(key(KeyCode::Down));
2307 assert_eq!(explorer.cursor, 0);
2308 }
2309
2310 #[test]
2311 fn move_up_on_empty_dir_does_not_panic() {
2312 let dir = tempdir().expect("tempdir");
2313 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2314 assert!(explorer.entries.is_empty());
2315 explorer.handle_key(key(KeyCode::Up));
2316 assert_eq!(explorer.cursor, 0);
2317 }
2318
2319 #[test]
2320 fn page_down_at_bottom_does_not_overflow() {
2321 let dir = tempdir().expect("tempdir");
2322 for i in 0..5 {
2323 fs::write(dir.path().join(format!("{i}.txt")), b"x").unwrap();
2324 }
2325 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2326 let last = explorer.entries.len().saturating_sub(1);
2327 explorer.cursor = last;
2328 explorer.handle_key(key(KeyCode::PageDown));
2329 assert_eq!(explorer.cursor, last);
2330 }
2331
2332 #[test]
2333 fn page_up_at_top_does_not_underflow() {
2334 let dir = tempdir().expect("tempdir");
2335 fs::write(dir.path().join("a.txt"), b"a").unwrap();
2336 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2337 explorer.cursor = 0;
2338 explorer.handle_key(key(KeyCode::PageUp));
2339 assert_eq!(explorer.cursor, 0);
2340 }
2341
2342 #[test]
2343 fn ascend_at_root_does_not_panic() {
2344 let mut explorer = FileExplorer::new(std::path::PathBuf::from("/"), vec![]);
2345 explorer.handle_key(key(KeyCode::Backspace));
2347 assert_eq!(explorer.current_dir, std::path::PathBuf::from("/"));
2348 }
2349
2350 #[test]
2351 fn cursor_clamped_after_reload_with_fewer_entries() {
2352 let dir = tempdir().expect("tempdir");
2353 fs::write(dir.path().join("a.txt"), b"a").unwrap();
2354 fs::write(dir.path().join("b.txt"), b"b").unwrap();
2355 fs::write(dir.path().join("c.txt"), b"c").unwrap();
2356 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2357 explorer.cursor = explorer.entries.len() - 1;
2359 explorer.set_extension_filter(["a"]);
2361 assert!(
2363 explorer.cursor < explorer.entries.len().max(1),
2364 "cursor {} out of range for {} entries",
2365 explorer.cursor,
2366 explorer.entries.len()
2367 );
2368 }
2369
2370 #[test]
2371 fn scroll_offset_clamped_after_reload_with_empty_entries() {
2372 let dir = tempdir().expect("tempdir");
2373 fs::write(dir.path().join("test.rs"), b"fn main(){}").unwrap();
2374 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2375 explorer.scroll_offset = 5; explorer.cursor = 0;
2377 explorer.set_extension_filter(["xyz"]);
2379 assert_eq!(explorer.cursor, 0);
2380 assert_eq!(explorer.scroll_offset, 0);
2381 }
2382
2383 #[test]
2384 fn marked_paths_returns_reference_to_marked_set() {
2385 let dir = temp_dir_with_files();
2386 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2387
2388 explorer.toggle_mark();
2389
2390 assert_eq!(
2391 explorer.marked_paths().len(),
2392 explorer.marked.len(),
2393 "marked_paths() should reflect the same set as the field"
2394 );
2395 }
2396
2397 fn make_file_entry(name: &str) -> crate::types::FsEntry {
2400 let ext = std::path::Path::new(name)
2401 .extension()
2402 .map(|e| e.to_string_lossy().to_lowercase())
2403 .unwrap_or_default();
2404 crate::types::FsEntry {
2405 name: name.to_string(),
2406 path: std::path::PathBuf::from(name),
2407 is_dir: false,
2408 size: None,
2409 extension: ext,
2410 }
2411 }
2412
2413 #[test]
2414 fn entry_icon_iso_returns_disc() {
2415 let e = make_file_entry("release.iso");
2416 assert_eq!(entry_icon(&e), "💿");
2417 }
2418
2419 #[test]
2420 fn entry_icon_dmg_returns_disc() {
2421 let e = make_file_entry("app.dmg");
2422 assert_eq!(entry_icon(&e), "💿");
2423 }
2424
2425 #[test]
2426 fn entry_icon_zip_returns_package() {
2427 let e = make_file_entry("archive.zip");
2428 assert_eq!(entry_icon(&e), "📦");
2429 }
2430
2431 #[test]
2432 fn entry_icon_tar_returns_package() {
2433 let e = make_file_entry("src.tar");
2434 assert_eq!(entry_icon(&e), "📦");
2435 }
2436
2437 #[test]
2438 fn entry_icon_gz_returns_package() {
2439 let e = make_file_entry("data.gz");
2440 assert_eq!(entry_icon(&e), "📦");
2441 }
2442
2443 #[test]
2444 fn entry_icon_pdf_returns_book() {
2445 let e = make_file_entry("manual.pdf");
2446 assert_eq!(entry_icon(&e), "📕");
2447 }
2448
2449 #[test]
2450 fn entry_icon_md_returns_memo() {
2451 let e = make_file_entry("README.md");
2452 assert_eq!(entry_icon(&e), "📝");
2453 }
2454
2455 #[test]
2456 fn entry_icon_toml_returns_gear() {
2457 let e = make_file_entry("Cargo.toml");
2458 assert_eq!(entry_icon(&e), "⚙ ");
2459 }
2460
2461 #[test]
2462 fn entry_icon_json_returns_gear() {
2463 let e = make_file_entry("config.json");
2464 assert_eq!(entry_icon(&e), "⚙ ");
2465 }
2466
2467 #[test]
2468 fn entry_icon_lock_returns_lock() {
2469 let e = make_file_entry("Cargo.lock");
2470 assert_eq!(entry_icon(&e), "🔒");
2471 }
2472
2473 #[test]
2474 fn entry_icon_py_returns_snake() {
2475 let e = make_file_entry("script.py");
2476 assert_eq!(entry_icon(&e), "🐍");
2477 }
2478
2479 #[test]
2480 fn entry_icon_html_returns_globe() {
2481 let e = make_file_entry("index.html");
2482 assert_eq!(entry_icon(&e), "🌐");
2483 }
2484
2485 #[test]
2486 fn entry_icon_css_returns_palette() {
2487 let e = make_file_entry("style.css");
2488 assert_eq!(entry_icon(&e), "🎨");
2489 }
2490
2491 #[test]
2492 fn entry_icon_svg_returns_palette() {
2493 let e = make_file_entry("logo.svg");
2494 assert_eq!(entry_icon(&e), "🎨");
2495 }
2496
2497 #[test]
2498 fn entry_icon_png_returns_image() {
2499 let e = make_file_entry("photo.png");
2500 assert_eq!(entry_icon(&e), "🖼 ");
2501 }
2502
2503 #[test]
2504 fn entry_icon_jpg_returns_image() {
2505 let e = make_file_entry("photo.jpg");
2506 assert_eq!(entry_icon(&e), "🖼 ");
2507 }
2508
2509 #[test]
2510 fn entry_icon_mp4_returns_film() {
2511 let e = make_file_entry("video.mp4");
2512 assert_eq!(entry_icon(&e), "🎬");
2513 }
2514
2515 #[test]
2516 fn entry_icon_mp3_returns_music() {
2517 let e = make_file_entry("song.mp3");
2518 assert_eq!(entry_icon(&e), "🎵");
2519 }
2520
2521 #[test]
2522 fn entry_icon_ttf_returns_font() {
2523 let e = make_file_entry("font.ttf");
2524 assert_eq!(entry_icon(&e), "🔤");
2525 }
2526
2527 #[test]
2528 fn entry_icon_exe_returns_gear() {
2529 let e = make_file_entry("setup.exe");
2530 assert_eq!(entry_icon(&e), "⚙ ");
2531 }
2532
2533 #[test]
2534 fn entry_icon_unknown_extension_returns_document() {
2535 let e = make_file_entry("mystery.xyz");
2536 assert_eq!(entry_icon(&e), "📄");
2537 }
2538
2539 #[test]
2540 fn entry_icon_no_extension_returns_document() {
2541 let e = crate::types::FsEntry {
2542 name: "Makefile".into(),
2543 path: std::path::PathBuf::from("Makefile"),
2544 is_dir: false,
2545 size: None,
2546 extension: String::new(),
2547 };
2548 assert_eq!(entry_icon(&e), "📄");
2549 }
2550
2551 #[test]
2554 fn fmt_size_zero_bytes() {
2555 assert_eq!(fmt_size(0), "0 B");
2556 }
2557
2558 #[test]
2559 fn fmt_size_one_byte() {
2560 assert_eq!(fmt_size(1), "1 B");
2561 }
2562
2563 #[test]
2564 fn fmt_size_1023_bytes_stays_bytes() {
2565 assert_eq!(fmt_size(1_023), "1023 B");
2566 }
2567
2568 #[test]
2569 fn fmt_size_exactly_1_kb() {
2570 assert_eq!(fmt_size(1_024), "1.0 KB");
2571 }
2572
2573 #[test]
2574 fn fmt_size_1_5_kb() {
2575 assert_eq!(fmt_size(1_536), "1.5 KB");
2576 }
2577
2578 #[test]
2579 fn fmt_size_1_mb_boundary() {
2580 assert_eq!(fmt_size(1_048_576), "1.0 MB");
2581 }
2582
2583 #[test]
2584 fn fmt_size_2_mb() {
2585 assert_eq!(fmt_size(2_097_152), "2.0 MB");
2586 }
2587
2588 #[test]
2589 fn fmt_size_1_gb_boundary() {
2590 assert_eq!(fmt_size(1_073_741_824), "1.0 GB");
2591 }
2592
2593 #[test]
2594 fn fmt_size_large_value() {
2595 assert_eq!(fmt_size(10 * 1_073_741_824), "10.0 GB");
2597 }
2598
2599 #[test]
2602 fn navigate_to_accepts_str_slice() {
2603 let dir = tempdir().expect("tempdir");
2604 let sub = dir.path().join("sub");
2605 fs::create_dir(&sub).unwrap();
2606
2607 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2608 explorer.navigate_to(sub.to_str().unwrap());
2609 assert_eq!(explorer.current_dir, sub);
2610 }
2611
2612 #[test]
2613 fn navigate_to_accepts_path_ref() {
2614 let dir = tempdir().expect("tempdir");
2615 let sub = dir.path().join("sub2");
2616 fs::create_dir(&sub).unwrap();
2617
2618 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2619 explorer.navigate_to(sub.as_path());
2620 assert_eq!(explorer.current_dir, sub);
2621 }
2622
2623 #[test]
2624 fn navigate_to_resets_cursor_to_zero() {
2625 let dir = tempdir().expect("tempdir");
2626 let sub = dir.path().join("sub3");
2627 fs::create_dir(&sub).unwrap();
2628
2629 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2630 explorer.cursor = 99;
2631 explorer.scroll_offset = 5;
2632 explorer.navigate_to(sub.as_path());
2633 assert_eq!(explorer.cursor, 0);
2634 assert_eq!(explorer.scroll_offset, 0);
2635 }
2636
2637 #[test]
2640 fn is_searching_false_by_default() {
2641 let dir = tempdir().expect("tempdir");
2642 let explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2643 assert!(!explorer.is_searching());
2644 }
2645
2646 #[test]
2647 fn is_searching_true_after_slash_key() {
2648 let dir = tempdir().expect("tempdir");
2649 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2650 explorer.handle_key(key(KeyCode::Char('/')));
2651 assert!(explorer.is_searching());
2652 }
2653
2654 #[test]
2655 fn is_searching_false_after_esc_cancels_search() {
2656 let dir = tempdir().expect("tempdir");
2657 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2658 explorer.handle_key(key(KeyCode::Char('/')));
2659 explorer.handle_key(key(KeyCode::Esc));
2660 assert!(!explorer.is_searching());
2661 }
2662
2663 #[test]
2666 fn status_is_empty_on_fresh_explorer() {
2667 let dir = tempdir().expect("tempdir");
2668 let explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2669 assert!(explorer.status().is_empty());
2670 }
2671
2672 #[test]
2673 fn status_cleared_after_reload() {
2674 let dir = tempdir().expect("tempdir");
2675 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2676 explorer.status = "stale message".into();
2678 explorer.reload();
2679 assert!(
2680 explorer.status().is_empty(),
2681 "reload should clear the status message"
2682 );
2683 }
2684
2685 #[test]
2688 fn load_entries_empty_dir_returns_empty_vec() {
2689 let dir = tempdir().expect("tempdir");
2690 let entries = load_entries(dir.path(), false, &[], crate::types::SortMode::Name, "");
2691 assert!(
2692 entries.is_empty(),
2693 "empty directory should produce no entries"
2694 );
2695 }
2696
2697 #[test]
2698 fn load_entries_hidden_excluded_by_default() {
2699 let dir = tempdir().expect("tempdir");
2700 fs::write(dir.path().join(".hidden"), b"h").unwrap();
2701 fs::write(dir.path().join("visible.txt"), b"v").unwrap();
2702
2703 let entries = load_entries(dir.path(), false, &[], crate::types::SortMode::Name, "");
2704 assert_eq!(entries.len(), 1);
2705 assert_eq!(entries[0].name, "visible.txt");
2706 }
2707
2708 #[test]
2709 fn load_entries_hidden_included_when_show_hidden_true() {
2710 let dir = tempdir().expect("tempdir");
2711 fs::write(dir.path().join(".hidden"), b"h").unwrap();
2712 fs::write(dir.path().join("visible.txt"), b"v").unwrap();
2713
2714 let entries = load_entries(dir.path(), true, &[], crate::types::SortMode::Name, "");
2715 assert_eq!(entries.len(), 2);
2716 }
2717
2718 #[test]
2719 fn load_entries_nonexistent_dir_returns_empty_vec() {
2720 let entries = load_entries(
2721 std::path::Path::new("/nonexistent/path/that/does/not/exist"),
2722 false,
2723 &[],
2724 crate::types::SortMode::Name,
2725 "",
2726 );
2727 assert!(entries.is_empty());
2728 }
2729
2730 #[test]
2731 fn load_entries_search_query_is_case_insensitive() {
2732 let dir = tempdir().expect("tempdir");
2733 fs::write(dir.path().join("README.md"), b"r").unwrap();
2734 fs::write(dir.path().join("main.rs"), b"m").unwrap();
2735
2736 let entries = load_entries(
2737 dir.path(),
2738 false,
2739 &[],
2740 crate::types::SortMode::Name,
2741 "readme",
2742 );
2743 assert_eq!(entries.len(), 1);
2744 assert_eq!(entries[0].name, "README.md");
2745 }
2746
2747 #[test]
2748 fn load_entries_dirs_always_precede_files() {
2749 let dir = tempdir().expect("tempdir");
2750 fs::write(dir.path().join("z_file.txt"), b"z").unwrap();
2751 fs::create_dir(dir.path().join("a_dir")).unwrap();
2752
2753 let entries = load_entries(dir.path(), false, &[], crate::types::SortMode::Name, "");
2754 assert!(entries[0].is_dir, "directory must come before file");
2755 assert!(!entries[1].is_dir);
2756 }
2757
2758 #[test]
2759 fn load_entries_ext_filter_excludes_non_matching_files() {
2760 let dir = tempdir().expect("tempdir");
2761 fs::write(dir.path().join("main.rs"), b"r").unwrap();
2762 fs::write(dir.path().join("Cargo.toml"), b"t").unwrap();
2763
2764 let filter = vec!["rs".to_string()];
2765 let entries = load_entries(dir.path(), false, &filter, crate::types::SortMode::Name, "");
2766 assert_eq!(entries.len(), 1);
2767 assert_eq!(entries[0].extension, "rs");
2768 }
2769
2770 #[test]
2771 fn load_entries_ext_filter_always_includes_dirs() {
2772 let dir = tempdir().expect("tempdir");
2773 fs::create_dir(dir.path().join("subdir")).unwrap();
2774 fs::write(dir.path().join("file.txt"), b"t").unwrap();
2775
2776 let filter = vec!["rs".to_string()];
2778 let entries = load_entries(dir.path(), false, &filter, crate::types::SortMode::Name, "");
2779 assert_eq!(entries.len(), 1);
2780 assert!(entries[0].is_dir);
2781 }
2782
2783 #[test]
2786 fn r_key_activates_rename_mode_with_prefilled_name() {
2787 let tmp = temp_dir_with_files();
2788 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2789 let idx = explorer
2791 .entries
2792 .iter()
2793 .position(|e| e.name == "readme.txt")
2794 .expect("readme.txt present");
2795 explorer.cursor = idx;
2796
2797 let outcome = explorer.handle_key(key(KeyCode::Char('r')));
2798 assert_eq!(outcome, ExplorerOutcome::Pending);
2799 assert!(explorer.is_rename_active());
2800 assert_eq!(explorer.rename_input(), "readme.txt");
2801 }
2802
2803 #[test]
2804 fn r_key_on_empty_dir_does_not_activate_rename() {
2805 let dir = tempdir().expect("tempdir");
2806 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
2807 assert!(explorer.entries.is_empty());
2808
2809 let outcome = explorer.handle_key(key(KeyCode::Char('r')));
2810 assert_eq!(outcome, ExplorerOutcome::Pending);
2811 assert!(!explorer.is_rename_active());
2812 }
2813
2814 #[test]
2815 fn rename_mode_chars_append_to_input() {
2816 let tmp = temp_dir_with_files();
2817 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2818 explorer.handle_key(key(KeyCode::Char('r')));
2819 assert!(explorer.is_rename_active());
2820
2821 let original_len = explorer.rename_input().len();
2823 for _ in 0..original_len {
2824 explorer.handle_key(key(KeyCode::Backspace));
2825 }
2826 explorer.handle_key(key(KeyCode::Char('n')));
2827 explorer.handle_key(key(KeyCode::Char('e')));
2828 explorer.handle_key(key(KeyCode::Char('w')));
2829
2830 assert_eq!(explorer.rename_input(), "new");
2831 assert!(explorer.is_rename_active());
2832 }
2833
2834 #[test]
2835 fn rename_mode_backspace_pops_last_char() {
2836 let tmp = temp_dir_with_files();
2837 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2838 explorer.handle_key(key(KeyCode::Char('r')));
2839
2840 let original_len = explorer.rename_input().len();
2842 for _ in 0..original_len {
2843 explorer.handle_key(key(KeyCode::Backspace));
2844 }
2845 explorer.handle_key(key(KeyCode::Char('a')));
2846 explorer.handle_key(key(KeyCode::Char('b')));
2847 assert_eq!(explorer.rename_input(), "ab");
2848
2849 explorer.handle_key(key(KeyCode::Backspace));
2850 assert_eq!(explorer.rename_input(), "a");
2851 }
2852
2853 #[test]
2854 fn rename_mode_esc_cancels_without_renaming() {
2855 let tmp = temp_dir_with_files();
2856 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2857 let idx = explorer
2858 .entries
2859 .iter()
2860 .position(|e| e.name == "readme.txt")
2861 .expect("readme.txt present");
2862 explorer.cursor = idx;
2863
2864 explorer.handle_key(key(KeyCode::Char('r')));
2865 assert!(explorer.is_rename_active());
2866
2867 let outcome = explorer.handle_key(key(KeyCode::Esc));
2868 assert_eq!(outcome, ExplorerOutcome::Pending);
2869 assert!(!explorer.is_rename_active());
2870 assert_eq!(explorer.rename_input(), "");
2871 assert!(tmp.path().join("readme.txt").exists());
2873 }
2874
2875 #[test]
2876 fn rename_mode_enter_renames_file_and_returns_rename_completed() {
2877 let tmp = temp_dir_with_files();
2878 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2879 let idx = explorer
2880 .entries
2881 .iter()
2882 .position(|e| e.name == "readme.txt")
2883 .expect("readme.txt present");
2884 explorer.cursor = idx;
2885
2886 explorer.handle_key(key(KeyCode::Char('r')));
2888 let prefill_len = explorer.rename_input().len();
2889 for _ in 0..prefill_len {
2890 explorer.handle_key(key(KeyCode::Backspace));
2891 }
2892 for c in "notes.txt".chars() {
2893 explorer.handle_key(key(KeyCode::Char(c)));
2894 }
2895
2896 let outcome = explorer.handle_key(key(KeyCode::Enter));
2897
2898 assert!(!explorer.is_rename_active());
2899 assert_eq!(explorer.rename_input(), "");
2900 assert!(tmp.path().join("notes.txt").exists(), "new name must exist");
2901 assert!(
2902 !tmp.path().join("readme.txt").exists(),
2903 "old name must be gone"
2904 );
2905 assert!(
2906 matches!(outcome, ExplorerOutcome::RenameCompleted(p) if p.file_name().unwrap() == "notes.txt")
2907 );
2908 }
2909
2910 #[test]
2911 fn rename_mode_cursor_moves_to_renamed_entry() {
2912 let tmp = temp_dir_with_files();
2913 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2914 let idx = explorer
2915 .entries
2916 .iter()
2917 .position(|e| e.name == "readme.txt")
2918 .expect("readme.txt present");
2919 explorer.cursor = idx;
2920
2921 explorer.handle_key(key(KeyCode::Char('r')));
2922 let prefill_len = explorer.rename_input().len();
2923 for _ in 0..prefill_len {
2924 explorer.handle_key(key(KeyCode::Backspace));
2925 }
2926 for c in "zzz_last.txt".chars() {
2927 explorer.handle_key(key(KeyCode::Char(c)));
2928 }
2929 explorer.handle_key(key(KeyCode::Enter));
2930
2931 let new_idx = explorer
2932 .entries
2933 .iter()
2934 .position(|e| e.name == "zzz_last.txt")
2935 .expect("renamed entry in list");
2936 assert_eq!(explorer.cursor, new_idx);
2937 }
2938
2939 #[test]
2940 fn rename_mode_enter_with_empty_input_is_noop() {
2941 let tmp = temp_dir_with_files();
2942 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2943 let idx = explorer
2944 .entries
2945 .iter()
2946 .position(|e| e.name == "readme.txt")
2947 .expect("readme.txt present");
2948 explorer.cursor = idx;
2949
2950 explorer.handle_key(key(KeyCode::Char('r')));
2951 let prefill_len = explorer.rename_input().len();
2953 for _ in 0..prefill_len {
2954 explorer.handle_key(key(KeyCode::Backspace));
2955 }
2956 assert_eq!(explorer.rename_input(), "");
2957
2958 let outcome = explorer.handle_key(key(KeyCode::Enter));
2959 assert_eq!(outcome, ExplorerOutcome::Pending);
2960 assert!(!explorer.is_rename_active());
2961 assert!(tmp.path().join("readme.txt").exists());
2963 }
2964
2965 #[test]
2966 fn rename_mode_can_rename_directory() {
2967 let tmp = temp_dir_with_files();
2968 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2969 let idx = explorer
2970 .entries
2971 .iter()
2972 .position(|e| e.name == "subdir" && e.is_dir)
2973 .expect("subdir present");
2974 explorer.cursor = idx;
2975
2976 explorer.handle_key(key(KeyCode::Char('r')));
2977 let prefill_len = explorer.rename_input().len();
2978 for _ in 0..prefill_len {
2979 explorer.handle_key(key(KeyCode::Backspace));
2980 }
2981 for c in "renamed_dir".chars() {
2982 explorer.handle_key(key(KeyCode::Char(c)));
2983 }
2984 let outcome = explorer.handle_key(key(KeyCode::Enter));
2985
2986 assert!(tmp.path().join("renamed_dir").exists());
2987 assert!(!tmp.path().join("subdir").exists());
2988 assert!(matches!(outcome, ExplorerOutcome::RenameCompleted(_)));
2989 }
2990
2991 #[test]
2992 fn rename_mode_unrecognised_key_returns_pending_without_cancelling() {
2993 let tmp = temp_dir_with_files();
2994 let mut explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
2995 explorer.handle_key(key(KeyCode::Char('r')));
2996 assert!(explorer.is_rename_active());
2997
2998 let outcome = explorer.handle_key(key(KeyCode::F(1)));
3000 assert_eq!(outcome, ExplorerOutcome::Pending);
3001 assert!(explorer.is_rename_active(), "rename mode must stay active");
3002 }
3003
3004 #[test]
3005 fn is_rename_active_false_by_default() {
3006 let tmp = temp_dir_with_files();
3007 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
3008 assert!(!explorer.is_rename_active());
3009 }
3010
3011 #[test]
3012 fn rename_input_empty_by_default() {
3013 let tmp = temp_dir_with_files();
3014 let explorer = FileExplorer::new(tmp.path().to_path_buf(), vec![]);
3015 assert_eq!(explorer.rename_input(), "");
3016 }
3017
3018 #[test]
3025 fn mkdir_mode_char_pushes_to_input_via_macro() {
3026 let dir = tempdir().expect("tempdir");
3027 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3028 explorer.mkdir_active = true;
3029 explorer.mkdir_input.clear();
3030
3031 let outcome = explorer.handle_key(key(KeyCode::Char('a')));
3032 assert_eq!(outcome, ExplorerOutcome::Pending);
3033 assert_eq!(explorer.mkdir_input, "a");
3034 assert!(explorer.mkdir_active, "mode must remain active after Char");
3035 }
3036
3037 #[test]
3038 fn mkdir_mode_backspace_pops_via_macro() {
3039 let dir = tempdir().expect("tempdir");
3040 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3041 explorer.mkdir_active = true;
3042 explorer.mkdir_input = "ab".to_string();
3043
3044 let outcome = explorer.handle_key(key(KeyCode::Backspace));
3045 assert_eq!(outcome, ExplorerOutcome::Pending);
3046 assert_eq!(explorer.mkdir_input, "a");
3047 assert!(explorer.mkdir_active);
3048 }
3049
3050 #[test]
3051 fn mkdir_mode_esc_cancels_via_macro() {
3052 let dir = tempdir().expect("tempdir");
3053 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3054 explorer.mkdir_active = true;
3055 explorer.mkdir_input = "half".to_string();
3056
3057 let outcome = explorer.handle_key(key(KeyCode::Esc));
3058 assert_eq!(outcome, ExplorerOutcome::Pending);
3059 assert!(!explorer.mkdir_active, "mode must be deactivated by Esc");
3060 assert!(
3061 explorer.mkdir_input.is_empty(),
3062 "input must be cleared by Esc"
3063 );
3064 }
3065
3066 #[test]
3067 fn mkdir_mode_unknown_key_returns_pending_via_macro() {
3068 let dir = tempdir().expect("tempdir");
3069 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3070 explorer.mkdir_active = true;
3071 explorer.mkdir_input = "foo".to_string();
3072
3073 let outcome = explorer.handle_key(key(KeyCode::F(2)));
3074 assert_eq!(outcome, ExplorerOutcome::Pending);
3075 assert!(explorer.mkdir_active);
3077 assert_eq!(explorer.mkdir_input, "foo");
3078 }
3079
3080 #[test]
3083 fn touch_mode_char_pushes_to_input_via_macro() {
3084 let dir = tempdir().expect("tempdir");
3085 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3086 explorer.touch_active = true;
3087 explorer.touch_input.clear();
3088
3089 let outcome = explorer.handle_key(key(KeyCode::Char('z')));
3090 assert_eq!(outcome, ExplorerOutcome::Pending);
3091 assert_eq!(explorer.touch_input, "z");
3092 assert!(explorer.touch_active);
3093 }
3094
3095 #[test]
3096 fn touch_mode_backspace_pops_via_macro() {
3097 let dir = tempdir().expect("tempdir");
3098 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3099 explorer.touch_active = true;
3100 explorer.touch_input = "xy".to_string();
3101
3102 let outcome = explorer.handle_key(key(KeyCode::Backspace));
3103 assert_eq!(outcome, ExplorerOutcome::Pending);
3104 assert_eq!(explorer.touch_input, "x");
3105 assert!(explorer.touch_active);
3106 }
3107
3108 #[test]
3109 fn touch_mode_esc_cancels_via_macro() {
3110 let dir = tempdir().expect("tempdir");
3111 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3112 explorer.touch_active = true;
3113 explorer.touch_input = "half".to_string();
3114
3115 let outcome = explorer.handle_key(key(KeyCode::Esc));
3116 assert_eq!(outcome, ExplorerOutcome::Pending);
3117 assert!(!explorer.touch_active);
3118 assert!(explorer.touch_input.is_empty());
3119 }
3120
3121 #[test]
3122 fn touch_mode_unknown_key_returns_pending_via_macro() {
3123 let dir = tempdir().expect("tempdir");
3124 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3125 explorer.touch_active = true;
3126 explorer.touch_input = "bar".to_string();
3127
3128 let outcome = explorer.handle_key(key(KeyCode::F(3)));
3129 assert_eq!(outcome, ExplorerOutcome::Pending);
3130 assert!(explorer.touch_active);
3131 assert_eq!(explorer.touch_input, "bar");
3132 }
3133
3134 #[test]
3137 fn rename_mode_char_pushes_to_input_via_macro() {
3138 let dir = tempdir().expect("tempdir");
3139 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3140 explorer.rename_active = true;
3141 explorer.rename_input.clear();
3142
3143 let outcome = explorer.handle_key(key(KeyCode::Char('r')));
3144 assert_eq!(outcome, ExplorerOutcome::Pending);
3148 assert_eq!(explorer.rename_input, "r");
3149 assert!(explorer.rename_active);
3150 }
3151
3152 #[test]
3153 fn rename_mode_backspace_pops_via_macro() {
3154 let dir = tempdir().expect("tempdir");
3155 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3156 explorer.rename_active = true;
3157 explorer.rename_input = "cd".to_string();
3158
3159 let outcome = explorer.handle_key(key(KeyCode::Backspace));
3160 assert_eq!(outcome, ExplorerOutcome::Pending);
3161 assert_eq!(explorer.rename_input, "c");
3162 assert!(explorer.rename_active);
3163 }
3164
3165 #[test]
3166 fn rename_mode_esc_cancels_via_macro() {
3167 let dir = tempdir().expect("tempdir");
3168 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3169 explorer.rename_active = true;
3170 explorer.rename_input = "draft".to_string();
3171
3172 let outcome = explorer.handle_key(key(KeyCode::Esc));
3173 assert_eq!(outcome, ExplorerOutcome::Pending);
3174 assert!(!explorer.rename_active);
3175 assert!(explorer.rename_input.is_empty());
3176 }
3177
3178 #[test]
3179 fn rename_mode_unknown_key_returns_pending_via_macro() {
3180 let dir = tempdir().expect("tempdir");
3181 let mut explorer = FileExplorer::new(dir.path().to_path_buf(), vec![]);
3182 explorer.rename_active = true;
3183 explorer.rename_input = "baz".to_string();
3184
3185 let outcome = explorer.handle_key(key(KeyCode::F(4)));
3186 assert_eq!(outcome, ExplorerOutcome::Pending);
3187 assert!(explorer.rename_active);
3188 assert_eq!(explorer.rename_input, "baz");
3189 }
3190}