use std::{
collections::HashSet,
fmt, io,
path::{Path, PathBuf},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::keymap::Keymap;
use crate::types::{
DiskUsage, ExplorerCommand, ExplorerOutcome, FsEntry, Prompt, PromptKind, 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 prompt: Option<Prompt>,
pub keymap: Keymap,
pub async_loading: bool,
pub(crate) bg: background::Background,
pub show_git: bool,
pub(crate) git: Option<crate::git::GitStatus>,
pub(crate) link_cache: std::collections::HashMap<PathBuf, Option<PathBuf>>,
pub(crate) dir_size_cache: std::collections::HashMap<PathBuf, (u64, bool, usize)>,
pub disk_usage: Option<DiskUsage>,
pub show_sizes: bool,
pub show_dir_sizes: bool,
}
mod background;
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 {
#[must_use]
pub fn new(initial_dir: PathBuf, extension_filter: Vec<String>) -> Self {
Self::builder(initial_dir)
.extension_filter(extension_filter)
.build()
}
#[must_use]
pub fn builder(initial_dir: PathBuf) -> FileExplorerBuilder {
FileExplorerBuilder::new(initial_dir)
}
#[must_use]
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(())
}
#[must_use]
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();
}
#[must_use]
pub fn current_entry(&self) -> Option<&FsEntry> {
self.entries.get(self.cursor)
}
fn prompt_text(&self, kind: PromptKind) -> &str {
match &self.prompt {
Some(p) if p.kind == kind => &p.text,
_ => "",
}
}
#[must_use]
pub fn is_prompt_active(&self, kind: PromptKind) -> bool {
self.prompt.as_ref().is_some_and(|p| p.kind == kind)
}
#[must_use]
pub fn has_prompt(&self) -> bool {
self.prompt.is_some()
}
pub fn open_prompt(&mut self, kind: PromptKind, text: impl Into<String>) {
self.prompt = Some(Prompt {
kind,
text: text.into(),
});
}
#[must_use]
pub fn is_mkdir_active(&self) -> bool {
self.is_prompt_active(PromptKind::Mkdir)
}
#[must_use]
pub fn mkdir_input(&self) -> &str {
self.prompt_text(PromptKind::Mkdir)
}
#[must_use]
pub fn is_touch_active(&self) -> bool {
self.is_prompt_active(PromptKind::Touch)
}
#[must_use]
pub fn touch_input(&self) -> &str {
self.prompt_text(PromptKind::Touch)
}
#[must_use]
pub fn is_rename_active(&self) -> bool {
self.is_prompt_active(PromptKind::Rename)
}
#[must_use]
pub fn rename_input(&self) -> &str {
self.prompt_text(PromptKind::Rename)
}
#[must_use]
pub fn is_at_root(&self) -> bool {
self.current_dir.parent().is_none()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
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();
}
#[must_use]
pub fn status(&self) -> &str {
&self.status
}
#[must_use]
pub fn sort_mode(&self) -> SortMode {
self.sort_mode
}
#[must_use]
pub fn search_query(&self) -> &str {
&self.search_query
}
#[must_use]
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) {
let cursor = self.cursor;
self.reload_with(background::AfterLoad {
select: None,
cursor: Some(cursor),
});
}
pub fn try_reload(&mut self) -> Result<(), ExplorerError> {
self.bg.cancel_load();
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.link_cache.clear();
self.bg.clear_counted();
self.refresh_git();
self.clamp_cursor();
Ok(())
}
pub(crate) fn link_target(&mut self, path: &Path) -> Option<PathBuf> {
if !self.filesystem.is_local() {
return None;
}
self.link_cache
.entry(path.to_path_buf())
.or_insert_with(|| std::fs::read_link(path).ok())
.clone()
}
pub fn clear_dir_size_cache(&mut self) {
self.dir_size_cache.clear();
self.bg.clear_dir_sizes();
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;