use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};
use crossterm::event::{KeyCode, KeyEvent};
use crate::types::{DiskUsage, ExplorerOutcome, FsEntry, SortMode};
pub const PAGE_SIZE: usize = 10;
#[derive(Debug)]
pub struct FileExplorer {
pub current_dir: PathBuf,
pub theme_name: String,
pub editor_name: String,
pub entries: Vec<FsEntry>,
pub cursor: usize,
pub(crate) scroll_offset: usize,
pub extension_filter: Vec<String>,
pub show_hidden: bool,
pub(crate) status: String,
pub sort_mode: SortMode,
pub page_size: usize,
pub search_query: String,
pub search_active: bool,
pub marked: HashSet<PathBuf>,
pub mkdir_active: bool,
pub mkdir_input: String,
pub touch_active: bool,
pub touch_input: String,
pub rename_active: bool,
pub rename_input: String,
pub(crate) dir_size_cache: std::collections::HashMap<PathBuf, (u64, bool, usize)>,
pub disk_usage: Option<DiskUsage>,
pub show_sizes: bool,
}
macro_rules! handle_input_mode {
($self:ident, $key:ident, $active:ident, $input:ident, $on_enter:expr) => {
if $self.$active {
match $key.code {
KeyCode::Char(c)
if $key.modifiers.is_empty()
|| $key.modifiers == crossterm::event::KeyModifiers::SHIFT =>
{
$self.$input.push(c);
return ExplorerOutcome::Pending;
}
KeyCode::Backspace => {
$self.$input.pop();
return ExplorerOutcome::Pending;
}
KeyCode::Enter => $on_enter,
KeyCode::Esc => {
$self.$active = false;
$self.$input.clear();
return ExplorerOutcome::Pending;
}
_ => return ExplorerOutcome::Pending,
}
}
};
}
mod builder;
mod entries;
mod keys;
pub use self::builder::FileExplorerBuilder;
use self::entries::load_entries;
pub use self::entries::{entry_icon, fmt_size};
impl FileExplorer {
pub fn new(initial_dir: PathBuf, extension_filter: Vec<String>) -> Self {
let mut explorer = Self {
current_dir: initial_dir,
entries: Vec::new(),
cursor: 0,
scroll_offset: 0,
extension_filter,
show_hidden: false,
status: String::new(),
sort_mode: SortMode::default(),
page_size: PAGE_SIZE,
search_query: String::new(),
search_active: false,
marked: HashSet::new(),
mkdir_active: false,
mkdir_input: String::new(),
touch_active: false,
touch_input: String::new(),
rename_active: false,
rename_input: String::new(),
theme_name: String::new(),
editor_name: String::new(),
disk_usage: None,
dir_size_cache: std::collections::HashMap::new(),
show_sizes: true,
};
explorer.reload();
explorer
}
pub fn builder(initial_dir: PathBuf) -> FileExplorerBuilder {
FileExplorerBuilder::new(initial_dir)
}
pub fn navigate_to(&mut self, path: impl Into<PathBuf>) {
self.current_dir = path.into();
self.cursor = 0;
self.scroll_offset = 0;
self.reload();
}
pub fn marked_paths(&self) -> &HashSet<PathBuf> {
&self.marked
}
pub fn toggle_mark(&mut self) {
if let Some(entry) = self.entries.get(self.cursor) {
let path = entry.path.clone();
if self.marked.contains(&path) {
self.marked.remove(&path);
} else {
self.marked.insert(path);
}
}
self.move_down();
}
pub fn clear_marks(&mut self) {
self.marked.clear();
}
pub fn current_entry(&self) -> Option<&FsEntry> {
self.entries.get(self.cursor)
}
pub fn is_mkdir_active(&self) -> bool {
self.mkdir_active
}
pub fn mkdir_input(&self) -> &str {
&self.mkdir_input
}
pub fn is_touch_active(&self) -> bool {
self.touch_active
}
pub fn touch_input(&self) -> &str {
&self.touch_input
}
pub fn is_rename_active(&self) -> bool {
self.rename_active
}
pub fn rename_input(&self) -> &str {
&self.rename_input
}
pub fn is_at_root(&self) -> bool {
self.current_dir.parent().is_none()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn status(&self) -> &str {
&self.status
}
pub fn sort_mode(&self) -> SortMode {
self.sort_mode
}
pub fn search_query(&self) -> &str {
&self.search_query
}
pub fn is_searching(&self) -> bool {
self.search_active
}
pub fn set_show_hidden(&mut self, show: bool) {
self.show_hidden = show;
self.reload();
}
pub fn set_extension_filter<I, S>(&mut self, filter: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.extension_filter = filter.into_iter().map(Into::into).collect();
self.reload();
}
pub fn set_sort_mode(&mut self, mode: SortMode) {
self.sort_mode = mode;
self.reload();
}
pub fn reload(&mut self) {
self.status.clear();
self.entries = load_entries(
&self.current_dir,
self.show_hidden,
&self.extension_filter,
self.sort_mode,
&self.search_query,
);
self.disk_usage = crate::fs::disk_usage(&self.current_dir);
self.clamp_cursor();
}
pub(crate) fn ensure_dir_size_before(
&mut self,
path: &Path,
item_count: Option<usize>,
deadline: Option<std::time::Instant>,
) -> (u64, bool) {
let Some(item_count) = item_count else {
return (0, false);
};
if let Some(&(bytes, partial, cached_count)) = self.dir_size_cache.get(path) {
if cached_count == item_count {
return (bytes, partial);
}
}
if let Some(deadline) = deadline {
if std::time::Instant::now() >= deadline {
return (0, false);
}
}
let (bytes, partial) = crate::fs::dir_size(path);
self.dir_size_cache
.insert(path.to_path_buf(), (bytes, partial, item_count));
(bytes, partial)
}
pub fn clear_dir_size_cache(&mut self) {
self.dir_size_cache.clear();
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;