use std::{
collections::{HashMap, HashSet},
ops::Range,
time::{Duration, Instant},
};
use super::*;
use crate::{git, worker::Task};
const MAX_CONCURRENT_DIR_SIZES: usize = 4;
type DirSize = (u64, bool);
#[derive(Debug, Default, Clone)]
pub(crate) struct AfterLoad {
pub select: Option<PathBuf>,
pub cursor: Option<usize>,
}
type LoadResult = std::io::Result<(Vec<FsEntry>, Option<crate::DiskUsage>)>;
#[derive(Debug)]
pub(crate) struct LoadJob {
task: Task<LoadResult>,
after: AfterLoad,
}
const LOAD_GRACE: Duration = Duration::from_millis(25);
#[derive(Debug, Default)]
pub(crate) struct Background {
dir_size_jobs: HashMap<PathBuf, (Task<DirSize>, usize)>,
counted: HashSet<PathBuf>,
git_task: Option<Task<Option<git::GitStatus>>>,
load: Option<LoadJob>,
}
impl Background {
pub(crate) fn clear_counted(&mut self) {
self.counted.clear();
}
pub(crate) fn clear_dir_sizes(&mut self) {
self.dir_size_jobs.clear();
}
pub(crate) fn cancel_load(&mut self) {
self.load = None;
}
}
impl FileExplorer {
pub(crate) fn fill_item_counts(&mut self, range: Range<usize>) {
for idx in range {
let Some(entry) = self.entries.get(idx) else {
break;
};
if !entry.is_dir || entry.item_count.is_some() {
continue;
}
let path = entry.path.clone();
if !self.bg.counted.insert(path.clone()) {
continue;
}
let count = self.filesystem.item_count(&path);
if let Some(entry) = self.entries.get_mut(idx) {
entry.item_count = count;
}
}
}
pub(crate) fn request_dir_sizes(&mut self, range: Range<usize>) {
for idx in range {
if self.bg.dir_size_jobs.len() >= MAX_CONCURRENT_DIR_SIZES {
break;
}
let Some(entry) = self.entries.get(idx) else {
break;
};
let (true, Some(count)) = (entry.is_dir, entry.item_count) else {
continue;
};
let path = &entry.path;
if self.bg.dir_size_jobs.contains_key(path) {
continue;
}
if self
.dir_size_cache
.get(path)
.is_some_and(|&(_, _, cached)| cached == count)
{
continue;
}
let fs = std::sync::Arc::clone(&self.filesystem);
let walk_path = path.clone();
let task = Task::spawn(move |emit| {
emit.send(fs.dir_size(&walk_path));
});
self.bg.dir_size_jobs.insert(path.clone(), (task, count));
}
}
#[must_use]
pub fn is_loading(&self) -> bool {
self.bg.load.is_some()
}
pub(crate) fn reload_with(&mut self, after: AfterLoad) {
if !self.async_loading {
self.reload_sync(&after);
return;
}
let fs = std::sync::Arc::clone(&self.filesystem);
let dir = self.current_dir.clone();
let (show_hidden, filter, sort, query) = (
self.show_hidden,
self.extension_filter.clone(),
self.sort_mode,
self.search_query.clone(),
);
let mut task = Task::spawn(move |emit| {
let result = try_load_entries(fs.as_ref(), &dir, show_hidden, &filter, sort, &query)
.map(|entries| (entries, fs.disk_usage(&dir)));
emit.send(result);
});
self.status.clear();
if let Some(result) = task.wait_next(LOAD_GRACE) {
self.bg.load = None;
self.apply_load(result, &after);
} else {
self.entries.clear();
self.bg.load = Some(LoadJob { task, after });
}
}
fn reload_sync(&mut self, after: &AfterLoad) {
self.bg.cancel_load();
self.status.clear();
let result = self.read_dir_now();
self.apply_load(result, after);
}
fn read_dir_now(&self) -> LoadResult {
try_load_entries(
self.filesystem.as_ref(),
&self.current_dir,
self.show_hidden,
&self.extension_filter,
self.sort_mode,
&self.search_query,
)
.map(|entries| (entries, self.filesystem.disk_usage(&self.current_dir)))
}
fn apply_load(&mut self, result: LoadResult, after: &AfterLoad) {
match result {
Ok((entries, usage)) => {
self.entries = entries;
self.disk_usage = usage;
self.link_cache.clear();
self.bg.clear_counted();
self.refresh_git();
if let Some(row) = after.cursor {
self.cursor = row;
}
self.clamp_cursor();
if let Some(path) = &after.select {
self.select_path(path);
}
}
Err(source) => {
let error = ExplorerError {
path: self.current_dir.clone(),
source,
};
self.entries.clear();
self.disk_usage = None;
self.status = error.to_string();
self.clamp_cursor();
}
}
}
pub fn poll_background(&mut self) -> bool {
let mut changed = false;
if let Some(job) = &mut self.bg.load {
let results = job.task.poll();
if !results.is_empty() || job.task.is_done() {
let job = self.bg.load.take().expect("checked above");
for result in results {
self.apply_load(result, &job.after);
}
changed = true;
}
}
let mut finished = Vec::new();
for (path, (task, count)) in &mut self.bg.dir_size_jobs {
for (bytes, partial) in task.poll() {
self.dir_size_cache
.insert(path.clone(), (bytes, partial, *count));
changed = true;
}
if task.is_done() {
finished.push(path.clone());
}
}
for path in finished {
self.bg.dir_size_jobs.remove(&path);
}
if let Some(task) = &mut self.bg.git_task {
for status in task.poll() {
self.git = status;
changed = true;
}
if task.is_done() {
self.bg.git_task = None;
}
}
changed
}
#[must_use]
pub fn has_pending_work(&self) -> bool {
self.bg.load.is_some() || !self.bg.dir_size_jobs.is_empty() || self.bg.git_task.is_some()
}
pub fn wait_for_background(&mut self, timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
self.poll_background();
if !self.has_pending_work() || Instant::now() >= deadline {
return;
}
std::thread::sleep(Duration::from_millis(2));
}
}
pub(crate) fn refresh_git(&mut self) {
if !self.show_git || !self.filesystem.is_local() {
self.bg.git_task = None;
return;
}
let dir = self.current_dir.clone();
self.bg.git_task = Some(Task::spawn(move |emit| {
emit.send(git::status_for(&dir));
}));
}
pub fn set_show_git(&mut self, show: bool) {
self.show_git = show;
if show {
self.refresh_git();
} else {
self.git = None;
self.bg.git_task = None;
}
}
#[must_use]
pub fn git_mark(&self, path: &Path) -> Option<git::GitMark> {
self.git.as_ref()?.mark(path)
}
}