use std::{
collections::HashSet,
fmt, io,
path::{Path, PathBuf},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::types::{DiskUsage, ExplorerCommand, ExplorerOutcome, FsEntry, SelectionMode, SortMode};
pub const PAGE_SIZE: usize = 10;
#[derive(Debug)]
pub struct ExplorerError {
pub path: PathBuf,
pub source: io::Error,
}
impl fmt::Display for ExplorerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.path.display(), self.source)
}
}
impl std::error::Error for ExplorerError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[derive(Debug)]
pub struct FileExplorer {
pub current_dir: PathBuf,
pub(crate) filesystem: crate::SharedFileSystem,
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 selection_mode: SelectionMode,
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;
#[cfg(test)]
use self::entries::load_entries;
use self::entries::try_load_entries;
pub use self::entries::{entry_icon, fmt_size};
impl FileExplorer {
pub fn new(initial_dir: PathBuf, extension_filter: Vec<String>) -> Self {
Self::builder(initial_dir)
.extension_filter(extension_filter)
.build()
}
pub fn builder(initial_dir: PathBuf) -> FileExplorerBuilder {
FileExplorerBuilder::new(initial_dir)
}
pub fn filesystem(&self) -> &crate::SharedFileSystem {
&self.filesystem
}
pub fn navigate_to(&mut self, path: impl Into<PathBuf>) {
if let Err(error) = self.try_navigate_to(path) {
self.entries.clear();
self.disk_usage = None;
self.status = error.to_string();
self.clamp_cursor();
}
}
pub fn try_navigate_to(&mut self, path: impl Into<PathBuf>) -> Result<(), ExplorerError> {
let previous_dir = std::mem::replace(&mut self.current_dir, path.into());
let previous_cursor = self.cursor;
let previous_scroll = self.scroll_offset;
let previous_search_active = self.search_active;
let previous_search_query = std::mem::take(&mut self.search_query);
self.cursor = 0;
self.scroll_offset = 0;
self.search_active = false;
if let Err(error) = self.try_reload() {
self.current_dir = previous_dir;
self.cursor = previous_cursor;
self.scroll_offset = previous_scroll;
self.search_active = previous_search_active;
self.search_query = previous_search_query;
return Err(error);
}
Ok(())
}
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 replace_entries(&mut self, entries: Vec<FsEntry>) {
self.entries = entries;
self.clamp_cursor();
}
pub fn set_status(&mut self, status: impl Into<String>) {
self.status = status.into();
}
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) {
if let Err(error) = self.try_reload() {
self.entries.clear();
self.disk_usage = None;
self.status = error.to_string();
self.clamp_cursor();
}
}
pub fn try_reload(&mut self) -> Result<(), ExplorerError> {
self.status.clear();
self.entries = try_load_entries(
self.filesystem.as_ref(),
&self.current_dir,
self.show_hidden,
&self.extension_filter,
self.sort_mode,
&self.search_query,
)
.map_err(|source| ExplorerError {
path: self.current_dir.clone(),
source,
})?;
self.disk_usage = self.filesystem.disk_usage(&self.current_dir);
self.clamp_cursor();
Ok(())
}
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) = self.filesystem.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;