tui_file_explorer/explorer/
background.rs1use std::{
10 collections::{HashMap, HashSet},
11 ops::Range,
12 time::{Duration, Instant},
13};
14
15use super::*;
16use crate::{git, worker::Task};
17
18const MAX_CONCURRENT_DIR_SIZES: usize = 4;
20
21type DirSize = (u64, bool);
23
24#[derive(Debug, Default, Clone)]
26pub(crate) struct AfterLoad {
27 pub select: Option<PathBuf>,
29 pub cursor: Option<usize>,
31}
32
33type LoadResult = std::io::Result<(Vec<FsEntry>, Option<crate::DiskUsage>)>;
35
36#[derive(Debug)]
38pub(crate) struct LoadJob {
39 task: Task<LoadResult>,
40 after: AfterLoad,
41}
42
43const LOAD_GRACE: Duration = Duration::from_millis(25);
47
48#[derive(Debug, Default)]
50pub(crate) struct Background {
51 dir_size_jobs: HashMap<PathBuf, (Task<DirSize>, usize)>,
54 counted: HashSet<PathBuf>,
57 git_task: Option<Task<Option<git::GitStatus>>>,
58 load: Option<LoadJob>,
60}
61
62impl Background {
63 pub(crate) fn clear_counted(&mut self) {
64 self.counted.clear();
65 }
66
67 pub(crate) fn clear_dir_sizes(&mut self) {
68 self.dir_size_jobs.clear();
69 }
70
71 pub(crate) fn cancel_load(&mut self) {
72 self.load = None;
73 }
74}
75
76impl FileExplorer {
77 pub(crate) fn fill_item_counts(&mut self, range: Range<usize>) {
82 for idx in range {
83 let Some(entry) = self.entries.get(idx) else {
84 break;
85 };
86 if !entry.is_dir || entry.item_count.is_some() {
87 continue;
88 }
89 let path = entry.path.clone();
90 if !self.bg.counted.insert(path.clone()) {
91 continue;
92 }
93 let count = self.filesystem.item_count(&path);
94 if let Some(entry) = self.entries.get_mut(idx) {
95 entry.item_count = count;
96 }
97 }
98 }
99
100 pub(crate) fn request_dir_sizes(&mut self, range: Range<usize>) {
105 for idx in range {
106 if self.bg.dir_size_jobs.len() >= MAX_CONCURRENT_DIR_SIZES {
107 break;
108 }
109 let Some(entry) = self.entries.get(idx) else {
110 break;
111 };
112 let (true, Some(count)) = (entry.is_dir, entry.item_count) else {
113 continue;
114 };
115 let path = &entry.path;
116 if self.bg.dir_size_jobs.contains_key(path) {
117 continue;
118 }
119 if self
120 .dir_size_cache
121 .get(path)
122 .is_some_and(|&(_, _, cached)| cached == count)
123 {
124 continue;
125 }
126 let fs = std::sync::Arc::clone(&self.filesystem);
127 let walk_path = path.clone();
128 let task = Task::spawn(move |emit| {
129 emit.send(fs.dir_size(&walk_path));
130 });
131 self.bg.dir_size_jobs.insert(path.clone(), (task, count));
132 }
133 }
134
135 #[must_use]
140 pub fn is_loading(&self) -> bool {
141 self.bg.load.is_some()
142 }
143
144 pub(crate) fn reload_with(&mut self, after: AfterLoad) {
148 if !self.async_loading {
149 self.reload_sync(&after);
150 return;
151 }
152
153 let fs = std::sync::Arc::clone(&self.filesystem);
154 let dir = self.current_dir.clone();
155 let (show_hidden, filter, sort, query) = (
156 self.show_hidden,
157 self.extension_filter.clone(),
158 self.sort_mode,
159 self.search_query.clone(),
160 );
161 let mut task = Task::spawn(move |emit| {
163 let result = try_load_entries(fs.as_ref(), &dir, show_hidden, &filter, sort, &query)
164 .map(|entries| (entries, fs.disk_usage(&dir)));
165 emit.send(result);
166 });
167 self.status.clear();
168
169 if let Some(result) = task.wait_next(LOAD_GRACE) {
170 self.bg.load = None;
171 self.apply_load(result, &after);
172 } else {
173 self.entries.clear();
176 self.bg.load = Some(LoadJob { task, after });
177 }
178 }
179
180 fn reload_sync(&mut self, after: &AfterLoad) {
182 self.bg.cancel_load();
183 self.status.clear();
184 let result = self.read_dir_now();
185 self.apply_load(result, after);
186 }
187
188 fn read_dir_now(&self) -> LoadResult {
189 try_load_entries(
190 self.filesystem.as_ref(),
191 &self.current_dir,
192 self.show_hidden,
193 &self.extension_filter,
194 self.sort_mode,
195 &self.search_query,
196 )
197 .map(|entries| (entries, self.filesystem.disk_usage(&self.current_dir)))
198 }
199
200 fn apply_load(&mut self, result: LoadResult, after: &AfterLoad) {
202 match result {
203 Ok((entries, usage)) => {
204 self.entries = entries;
205 self.disk_usage = usage;
206 self.link_cache.clear();
207 self.bg.clear_counted();
208 self.refresh_git();
209 if let Some(row) = after.cursor {
210 self.cursor = row;
211 }
212 self.clamp_cursor();
214 if let Some(path) = &after.select {
215 self.select_path(path);
216 }
217 }
218 Err(source) => {
219 let error = ExplorerError {
220 path: self.current_dir.clone(),
221 source,
222 };
223 self.entries.clear();
224 self.disk_usage = None;
225 self.status = error.to_string();
226 self.clamp_cursor();
227 }
228 }
229 }
230
231 pub fn poll_background(&mut self) -> bool {
234 let mut changed = false;
235
236 if let Some(job) = &mut self.bg.load {
238 let results = job.task.poll();
239 if !results.is_empty() || job.task.is_done() {
240 let job = self.bg.load.take().expect("checked above");
241 for result in results {
242 self.apply_load(result, &job.after);
243 }
244 changed = true;
245 }
246 }
247
248 let mut finished = Vec::new();
249 for (path, (task, count)) in &mut self.bg.dir_size_jobs {
250 for (bytes, partial) in task.poll() {
251 self.dir_size_cache
252 .insert(path.clone(), (bytes, partial, *count));
253 changed = true;
254 }
255 if task.is_done() {
256 finished.push(path.clone());
257 }
258 }
259 for path in finished {
260 self.bg.dir_size_jobs.remove(&path);
261 }
262
263 if let Some(task) = &mut self.bg.git_task {
264 for status in task.poll() {
265 self.git = status;
266 changed = true;
267 }
268 if task.is_done() {
269 self.bg.git_task = None;
270 }
271 }
272 changed
273 }
274
275 #[must_use]
277 pub fn has_pending_work(&self) -> bool {
278 self.bg.load.is_some() || !self.bg.dir_size_jobs.is_empty() || self.bg.git_task.is_some()
279 }
280
281 pub fn wait_for_background(&mut self, timeout: Duration) {
286 let deadline = Instant::now() + timeout;
287 loop {
288 self.poll_background();
289 if !self.has_pending_work() || Instant::now() >= deadline {
290 return;
291 }
292 std::thread::sleep(Duration::from_millis(2));
293 }
294 }
295
296 pub(crate) fn refresh_git(&mut self) {
301 if !self.show_git || !self.filesystem.is_local() {
302 self.bg.git_task = None;
303 return;
304 }
305 let dir = self.current_dir.clone();
306 self.bg.git_task = Some(Task::spawn(move |emit| {
307 emit.send(git::status_for(&dir));
308 }));
309 }
310
311 pub fn set_show_git(&mut self, show: bool) {
313 self.show_git = show;
314 if show {
315 self.refresh_git();
316 } else {
317 self.git = None;
318 self.bg.git_task = None;
319 }
320 }
321
322 #[must_use]
324 pub fn git_mark(&self, path: &Path) -> Option<git::GitMark> {
325 self.git.as_ref()?.mark(path)
326 }
327}