1use crate::app::background::{CacheWrites, OwedAnswer};
6use crate::app::feedback::Confirm;
7use crate::cache::CacheManager;
8use crate::cli::FileFormat;
9use crate::loading::open_options::OpenOptions;
10#[cfg(feature = "cloud")]
11use crate::wait_on_runtime;
12use crate::{
13 APP_NAME, App, AppEvent, InputMode, cloud::source, config, home, home::catalog, home::discover,
14 loading, widgets,
15};
16use color_eyre::Result;
17use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Mutex};
20
21#[derive(Default)]
24pub struct HomeApp {
25 pub(crate) probes_inflight: Vec<PathBuf>,
29 pub(crate) probe_heard: HashMap<PathBuf, std::time::Instant>,
32 #[cfg(test)]
34 pub(crate) probe_patience: Option<std::time::Duration>,
35 #[cfg(test)]
38 pub(crate) probe_gate: Option<Arc<Mutex<()>>>,
39 pub(crate) listing_cancels: HashMap<PathBuf, Arc<std::sync::atomic::AtomicBool>>,
42 pub(crate) narrowing: Option<(PathBuf, String, Arc<std::sync::atomic::AtomicBool>)>,
45 #[cfg(feature = "cloud")]
48 pub(crate) cloud_discovery_started: bool,
49 pub(crate) search_inflight: bool,
52 pub(crate) search_generation: u64,
55 pub(crate) last_load_error: Option<String>,
58 pub preview_room: Option<u16>,
61 pub(crate) schema_reads: HashMap<PathBuf, PathBuf>,
64 pub(crate) generation: u64,
66 pub(crate) pages_owed: Vec<PathBuf>,
69 pub(crate) path_listings_out: std::collections::HashSet<String>,
72 pub(crate) rested: bool,
74 pub(crate) facts_read: bool,
77 pub(crate) left: Option<PathBuf>,
79 pub(crate) facts_dated: Arc<Mutex<std::collections::HashSet<PathBuf>>>,
81 pub(crate) schema_cache: HashMap<PathBuf, Option<discover::SchemaPreview>>,
83 pub previews: crate::home::home_preview::Previews,
85 pub(crate) remembered_moved: bool,
87 #[cfg(test)]
90 pub(crate) worker_dies: Option<crate::HomeWorkerDies>,
91 pub local_desktop: bool,
94 pub reads: crate::home::home_preview::ReadCounts,
96}
97
98const MEASURE_BATCH: usize = 12;
100
101const PROBE_MEASURE_LIMIT: usize = 24;
103
104pub(crate) const CLASSIFY_BATCH: usize = 16;
108
109const ANSWER_EVERY: std::time::Duration = std::time::Duration::from_millis(250);
112
113fn look_and_answer(
117 rows: Vec<discover::Entry>,
118 cache: &CacheManager,
119 known: &home::Known,
120 reads: home::Reads,
121 tx: &std::sync::mpsc::Sender<AppEvent>,
122 answer: impl Fn(Vec<(PathBuf, home::Measured)>, bool) -> AppEvent,
123) {
124 let mut held = Vec::new();
125 let mut since = std::time::Instant::now();
126 home::look_into_batch(rows, cache, known, reads, |path, measured| {
127 match held.iter().position(|(held, _)| *held == path) {
129 Some(at) => held[at].1 = measured,
130 None => held.push((path, measured)),
131 }
132 if since.elapsed() >= ANSWER_EVERY {
133 let _ = tx.send(answer(std::mem::take(&mut held), false));
134 since = std::time::Instant::now();
135 }
136 });
137 let _ = tx.send(answer(held, true));
138}
139
140const PROBE_PATIENCE: std::time::Duration = std::time::Duration::from_secs(30);
143
144pub(crate) const MAX_CONCURRENT_PROBES: usize = 4;
146
147fn next_search_epoch() -> u64 {
150 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
151 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
152}
153
154#[cfg(feature = "cloud")]
156pub(crate) fn home_cloud_source(
157 source: &crate::cloud::cloud_sources::Source,
158 cached: Option<&crate::cache::CloudListing>,
159 listing: bool,
160) -> home::CloudSource {
161 let mut details: Vec<(String, String)> = vec![
162 ("source".to_string(), source.id.clone()),
163 ("api".to_string(), source.kind.name().to_string()),
164 ];
165 if let Some(endpoint) = &source.s3.endpoint {
166 details.push(("endpoint".to_string(), endpoint.clone()));
167 }
168 if let Some(region) = &source.s3.region {
169 details.push(("region".to_string(), region.clone()));
170 }
171 if let Some(project) = &source.project {
172 details.push(("project".to_string(), project.clone()));
173 }
174 if let Some(profile) = &source.profile {
175 details.push(("profile".to_string(), profile.clone()));
176 }
177 if let Some(configuration) = &source.gcloud {
178 details.push(("configuration".to_string(), configuration.clone()));
179 }
180 if source.s3.virtual_hosted.is_some() {
181 let style = if source.s3.virtual_hosted_style() {
182 "virtual-hosted"
183 } else {
184 "path-style"
185 };
186 details.push(("addressing".to_string(), style.to_string()));
187 }
188 details.push(("login".to_string(), source.origin.clone()));
189
190 let short = source.problem.as_deref().map(|problem| {
191 if problem.starts_with("not signed in") {
192 "not signed in"
193 } else if problem.starts_with("unsupported login") {
194 "unsupported login"
195 } else {
196 "not configured"
197 }
198 });
199 let note = match (&source.problem, short) {
202 (Some(problem), Some(short)) => problem
203 .strip_prefix(short)
204 .map(|rest| rest.trim_start_matches([':', ' ']))
205 .filter(|rest| !rest.is_empty())
206 .unwrap_or(problem)
207 .to_string(),
208 _ => [source.detail(), Some(source.origin.clone())]
209 .into_iter()
210 .flatten()
211 .filter(|n| !n.is_empty())
212 .collect::<Vec<_>>()
213 .join(&format!(" {} ", crate::glyphs::get().middot)),
214 };
215 let mut names: Vec<String> = cached.map(|c| c.buckets.clone()).unwrap_or_default();
216 for bucket in &source.buckets {
217 if !names.contains(bucket) {
218 names.push(bucket.clone());
219 }
220 }
221 let status = match (&source.problem, short) {
222 (Some(problem), Some(short)) => home::CloudStatus::Failed {
223 short: short.to_string(),
224 detail: problem.clone(),
225 },
226 _ if cached.is_some() => home::CloudStatus::Listed,
227 _ if listing => home::CloudStatus::Listing,
228 _ => home::CloudStatus::Unlisted,
229 };
230 home::CloudSource {
231 id: source.id.clone(),
232 label: source.label.clone(),
233 api: source.kind,
234 note,
235 buckets: names
236 .iter()
237 .map(|b| PathBuf::from(source.bucket_url(b)))
238 .collect(),
239 refreshing: listing && cached.is_some() && source.problem.is_none(),
240 asked: listing || source.problem.is_some(),
242 listed_at: cached
243 .map(|c| std::time::UNIX_EPOCH + std::time::Duration::from_secs(c.listed_at)),
244 status,
245 details,
246 place_details: Default::default(),
247 }
248}
249
250#[cfg(feature = "cloud")]
252fn summarize_cloud_failure(error: &str) -> (String, String) {
253 let lower = error.to_lowercase();
254 if let Some(start) = lower.find("needs ") {
256 return (error[start..].to_string(), error.to_string());
257 }
258 let short = if lower.contains("403")
259 || lower.contains("forbidden")
260 || lower.contains("accessdenied")
261 || lower.contains("access denied")
262 {
263 "403"
264 } else if lower.contains("401")
265 || lower.contains("unauthorized")
266 || lower.contains("credential")
267 || lower.contains("invalidaccesskeyid")
268 || lower.contains("expired")
269 || lower.contains("sso")
270 || lower.contains("az login")
271 {
272 "not logged in"
273 } else if lower.contains("unsupported login") {
274 "unsupported login"
275 } else if lower.contains("no gcp project") {
276 "no project"
277 } else if lower.contains("is not set") {
278 "not configured"
279 } else if lower.contains("timed out")
280 || lower.contains("timeout")
281 || lower.contains("connection")
282 || lower.contains("dns")
283 || lower.contains("resolve")
284 {
285 "unavailable"
286 } else {
287 "error"
288 };
289 (short.to_string(), error.to_string())
290}
291
292impl App {
293 pub fn home_schema(&self, entry: &discover::Entry) -> Option<discover::SchemaPreview> {
297 self.home_app
298 .schema_cache
299 .get(&entry.path)
300 .cloned()
301 .flatten()
302 }
303
304 fn owed_answer(&mut self, instead: AppEvent) -> OwedAnswer {
306 OwedAnswer {
307 tx: self.events.clone(),
308 #[cfg(test)]
309 dies: self
310 .home_app
311 .worker_dies
312 .as_mut()
313 .is_some_and(|dies| dies(&instead)),
314 instead: Some(instead),
315 }
316 }
317
318 pub(crate) fn list_the_typed_directory(&mut self) {
321 if !self.home.path_input_active {
322 return;
323 }
324 let dir = home::typed_dir(&self.home.path_input).to_string();
325 if self
326 .home
327 .path_listing
328 .as_ref()
329 .is_some_and(|l| l.dir == dir)
330 {
331 return;
332 }
333 if home::typed_dir_is_url(&dir) {
334 self.home.path_listing = Some(home::names_under(&dir, self.home.known_urls()));
335 return;
336 }
337 if !self.home_app.path_listings_out.insert(dir.clone()) {
338 return;
339 }
340 let tx = self.events.clone();
342 let owed = self.owed_answer(AppEvent::HomePathListed {
343 listing: Box::new(home::PathListing {
344 dir: dir.clone(),
345 names: Vec::new(),
346 failed: true,
347 matched: Default::default(),
348 }),
349 });
350 std::thread::spawn(move || {
351 owed.run(|| {
352 let listing = home::list_typed_dir(&dir);
353 let _ = tx.send(AppEvent::HomePathListed {
354 listing: Box::new(listing),
355 });
356 })
357 });
358 }
359
360 pub(crate) fn request_path_completion(&mut self) {
362 let typed = self.home.path_input.clone();
363 if typed.is_empty() {
364 return;
365 }
366 let generation = self.home_app.generation;
367 let tx = self.events.clone();
368 std::thread::spawn(move || {
369 let (completed, candidates) = home::complete_path(&typed);
370 let _ = tx.send(AppEvent::HomePathCompleted {
371 generation,
372 typed,
373 completed,
374 candidates,
375 });
376 });
377 }
378
379 pub fn home_preview_rows(
383 &self,
384 entry: &discover::Entry,
385 ) -> Option<Arc<crate::home::home_preview::PreviewRows>> {
386 let stamp = crate::home::home_preview::Stamp::of_entry(entry);
387 self.home_app.previews.rows(&entry.path, stamp).flatten()
388 }
389
390 pub(crate) fn request_selected_preview(&mut self) {
395 let Some(screen_height) = self.home_app.preview_room else {
396 return;
397 };
398 if self.home_app.previews.inflight.is_some() || self.home.path_input_active {
399 return;
400 }
401 let Some(entry) = self.home.selected_entry() else {
402 return;
403 };
404 let max = self.app_config.home.preview_max.bytes();
405 if !crate::home::home_preview::previewable(entry, max) {
406 return;
407 }
408 let stamp = crate::home::home_preview::Stamp::of_entry(entry);
409 if self.home_app.previews.rows(&entry.path, stamp).is_some() {
410 return;
411 }
412 let path = entry.path.clone();
413 self.request_home_preview(path, stamp, screen_height);
414 }
415
416 pub fn home_preview_pending(&self, path: &Path) -> bool {
418 self.home_app.previews.reading(path)
419 }
420
421 fn request_home_preview(
424 &mut self,
425 path: PathBuf,
426 stamp: crate::home::home_preview::Stamp,
427 screen_height: u16,
428 ) {
429 self.home_app.previews.inflight = Some(path.clone());
430 self.home_app.reads.previews += 1;
431 let tx = self.events.clone();
432 let cloud = self.app_config.cloud.clone();
433 let formats = self.formats.clone();
434 let runtime = self.runtime.clone();
435 let cache = self.cache.clone();
436 let writes = self.cache_writes.clone();
437 let visible = (screen_height as usize).saturating_sub(3).max(1);
439 let owed = self.owed_answer(AppEvent::HomePreviewReady {
440 path: path.clone(),
441 stamp,
442 read_at: None,
443 rows: None,
444 prepared: crate::home::home_preview::Handoff::default(),
445 });
446 self.runtime.spawn_blocking(move || {
447 owed.run(|| {
448 let began = std::time::Instant::now();
449 let read_at = crate::home::home_preview::Stamp::of_file(&path);
450 let read = Self::read_home_preview(
451 &path, &cloud, &formats, &runtime, cache, writes, visible,
452 );
453 log::debug!(
454 target: "datui",
455 "home preview of {}: {:.1?}",
456 path.display(),
457 began.elapsed()
458 );
459 let (rows, prepared) = match read {
460 Some((rows, prepared)) => (Some(Arc::new(rows)), Some(Box::new(prepared))),
461 None => (None, None),
462 };
463 let _ = tx.send(AppEvent::HomePreviewReady {
464 path,
465 stamp,
466 read_at,
467 rows,
468 prepared: Arc::new(Mutex::new(prepared)),
469 });
470 })
471 });
472 }
473
474 fn read_home_preview(
478 path: &Path,
479 cloud: &crate::config::CloudConfig,
480 formats: &crate::formats::Registry,
481 runtime: &tokio::runtime::Handle,
482 cache: CacheManager,
483 writes: CacheWrites,
484 visible: usize,
485 ) -> Option<(
486 crate::home::home_preview::PreviewRows,
487 crate::home::home_preview::Prepared,
488 )> {
489 let paths = [path.to_path_buf()];
490 let scanned = Self::scan_for_open(
491 cloud,
492 formats,
493 &paths,
494 OpenOptions::default(),
495 Some(path.to_path_buf()),
496 )
497 .ok()?;
498 let loading::LoadAnswer::Scanned { lf, path, options } = scanned else {
499 return None;
500 };
501 let progress = Arc::<crate::formats::schema_union::FooterProgress>::default();
502 let report = crate::loading::measurements::OpenReport {
503 progress: progress.clone(),
504 meter: Arc::new(crate::loading::measurements::Meter::default()),
505 remembered: Some(cache),
506 writes,
507 };
508 let read = Self::read_schema_for_open(
509 *lf,
510 path,
511 options,
512 cloud,
513 runtime,
514 &report,
515 loading::Made::default(),
516 )
517 .ok()?;
518 let loading::LoadAnswer::SchemaRead {
519 mut state,
520 options,
521 debug_label,
522 ..
523 } = read
524 else {
525 return None;
526 };
527 state.visible_rows = visible;
529 let began = std::time::Instant::now();
530 let request = state.prepare_async_collect(None)?;
531 let df =
532 crate::analysis::statistics::collect_lazy(request.lf, request.polars_streaming).ok()?;
533 let result = request.plan.fit(df);
534 let rows = crate::home::home_preview::PreviewRows::from_frame(result.rows());
535 state.measurements().read_page(began.elapsed(), Some(1));
536 state.apply_async_collect(result);
537 Some((
538 rows,
539 crate::home::home_preview::Prepared {
540 state,
541 options,
542 debug_label,
543 progress,
544 },
545 ))
546 }
547
548 pub fn home_schema_pending(&self, path: &Path) -> bool {
550 self.home_app.schema_reads.values().any(|p| p == path)
551 }
552
553 pub(crate) fn request_home_schema(&mut self) {
558 if self.home.path_input_active {
559 return;
560 }
561 let Some(entry) = self.home.selected_entry() else {
562 return;
563 };
564 let previewed = self.home_app.preview_room.is_some()
567 && crate::home::home_preview::previewable(
568 entry,
569 self.app_config.home.preview_max.bytes(),
570 );
571 if home::is_cloud_place(&entry.path)
572 || home::is_object_store_url(&entry.path)
573 || self.home_app.schema_cache.contains_key(&entry.path)
574 || previewed
575 {
576 return;
577 }
578 let network = (self.home.network_check)(&entry.path) || home::could_block_path(&entry.path);
581 let slot = if network {
582 crate::home::locality::Mounts::cached().mount_point_for(&entry.path)
583 } else {
584 PathBuf::new()
585 };
586 if self.home_app.schema_reads.contains_key(&slot) {
587 return;
588 }
589 let entry = entry.clone();
590 (self.home_app.schema_reads).insert(slot, entry.path.clone());
591 self.home_app.reads.schemas += 1;
592 let tx = self.events.clone();
593 let owed = self.owed_answer(AppEvent::HomeSchemaReady {
595 path: entry.path.clone(),
596 preview: None,
597 });
598 let read = move || {
599 owed.run(|| {
600 let preview = discover::schema_preview(&entry);
601 let _ = tx.send(AppEvent::HomeSchemaReady {
602 path: entry.path,
603 preview,
604 });
605 })
606 };
607 if network {
610 std::thread::spawn(read);
611 } else {
612 self.runtime.spawn_blocking(read);
613 }
614 }
615
616 pub(crate) fn spawn_home_probes(&mut self) {
620 self.stop_listings_left_behind();
621 for root in self.home.pending_probes() {
622 if self.home_app.probes_inflight.contains(&root) {
623 if let Some(cancelled) = self.home_app.listing_cancels.get(&root) {
625 cancelled.store(false, std::sync::atomic::Ordering::Relaxed);
626 }
627 continue;
628 }
629 let browsed = self.home.browsing.as_ref() == Some(&root);
633 if !browsed && self.home_app.probes_inflight.len() >= MAX_CONCURRENT_PROBES {
634 continue;
635 }
636 self.home_app.probes_inflight.push(root.clone());
637 (self.home_app.probe_heard).insert(root.clone(), std::time::Instant::now());
638 #[cfg(test)]
639 let gate = self.home_app.probe_gate.clone();
640 let tx = self.events.clone();
641 let cache = self.cache.clone();
642 let owed = self.owed_answer(AppEvent::HomeProbeFailed {
643 root: root.clone(),
644 message: "Could not read it; see the log".to_string(),
645 });
646 #[cfg(feature = "cloud")]
647 let cloud = self.app_config.cloud.clone();
648 #[cfg(feature = "cloud")]
649 let runtime = self.runtime.clone();
650 #[cfg(feature = "cloud")]
651 let cancelled = {
652 let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
653 self.home_app
654 .listing_cancels
655 .insert(root.clone(), flag.clone());
656 flag
657 };
658 std::thread::spawn(move || {
661 owed.run(|| {
662 #[cfg(test)]
663 if let Some(gate) = gate {
664 let _held = gate.lock();
665 }
666 #[cfg(feature = "cloud")]
670 if let Some((id, account)) = home::cloud_account(&root) {
671 let listed = wait_on_runtime(&runtime, async move {
672 crate::cloud::cloud_browse::list_account(&id, &account, &cloud).await
673 });
674 match listed {
675 Some(Ok(rows)) => {
676 let _ = tx.send(AppEvent::HomeProbeReady {
677 root,
678 rows: Some(rows),
679 cut_short: false,
680 });
681 }
682 Some(Err(message)) => {
683 log::warn!(
684 target: "datui::cloud",
685 "listing {} failed: {message}",
686 root.display()
687 );
688 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
689 }
690 None => {
691 let _ = tx.send(AppEvent::HomeProbeReady {
692 root,
693 rows: None,
694 cut_short: false,
695 });
696 }
697 }
698 return;
699 }
700 #[cfg(feature = "cloud")]
701 if crate::cloud::cloud_browse::split_bucket_url(&root.to_string_lossy())
702 .is_some()
703 || source::azure_parts(&root.to_string_lossy()).is_some()
704 {
705 let url = root.to_string_lossy().into_owned();
706 let watch = crate::cloud::cloud_browse::Watch {
708 progress: Some(std::sync::Arc::new({
709 let (tx, root) = (tx.clone(), root.clone());
710 move |page: &[crate::home::discover::Entry]| {
711 let _ = tx.send(AppEvent::HomeProbeProgress {
712 root: root.clone(),
713 rows: page.to_vec(),
714 });
715 }
716 })),
717 cancelled,
718 names_from: None,
719 };
720 let listed = wait_on_runtime(&runtime, async move {
721 crate::cloud::cloud_browse::list_objects_watched(&url, &cloud, &watch)
722 .await
723 });
724 match listed {
727 Some(Err(message)) => {
728 log::warn!(
729 target: "datui::cloud",
730 "listing {} failed: {message}",
731 root.display()
732 );
733 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
734 }
735 Some(Ok(level)) if level.cancelled => {
736 let _ = tx.send(AppEvent::HomeProbeCancelled { root });
737 }
738 other => {
739 let (rows, cut_short) = match other {
740 Some(Ok(level)) => (Some(level.rows), level.truncated),
741 _ => (None, false),
742 };
743 let _ = tx.send(AppEvent::HomeProbeReady {
744 root,
745 rows,
746 cut_short,
747 });
748 }
749 }
750 return;
751 }
752 #[cfg(not(feature = "cloud"))]
754 if source::is_remote_url(&root) {
755 let message = "cloud support not in this build".to_string();
756 let _ = tx.send(AppEvent::HomeProbeFailed { root, message });
757 return;
758 }
759 let mut cut_short = false;
760 let rows = if std::fs::read_dir(&root).is_ok() {
761 let scan = crate::home::discover::scan_dir_progressive(&root, |read| {
763 let _ = tx.send(AppEvent::HomeProbeProgress {
764 root: root.clone(),
765 rows: read.to_vec(),
766 });
767 });
768 cut_short = scan.truncated;
769 let mut rows = scan.entries;
770 for row in rows.iter_mut().take(PROBE_MEASURE_LIMIT) {
772 crate::home::discover::enrich(row);
773 }
774 let mounts = crate::home::locality::Mounts::current();
777 for row in rows.iter_mut() {
778 row.cost.source = Some(mounts.describe(&row.path).fstype);
779 }
780 let facts: Vec<_> = rows.iter().filter_map(home::facts_for).collect();
781 cache.record_dataset_facts(&facts);
782 Some(rows)
783 } else {
784 None
785 };
786 let _ = tx.send(AppEvent::HomeProbeReady {
787 root,
788 rows,
789 cut_short,
790 });
791 })
792 });
793 }
794 }
795
796 fn stop_listings_left_behind(&mut self) {
799 let home = &self.home;
800 for (root, cancelled) in &self.home_app.listing_cancels {
801 let wanted = match &home.browsing {
802 Some(dir) => dir == root,
803 None => home
804 .sections
805 .iter()
806 .any(|s| s.remote_root.as_ref() == Some(root)),
807 };
808 if !wanted {
809 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
810 }
811 }
812 if let Some((dir, _, cancelled)) = &self.home_app.narrowing
813 && home.browsing.as_ref() != Some(dir)
814 {
815 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
816 self.home_app.narrowing = None;
817 }
818 if self
819 .home
820 .narrowed
821 .as_ref()
822 .is_some_and(|n| self.home.browsing.as_ref() != Some(&n.dir))
823 {
824 self.home.narrowed = None;
825 }
826 }
827
828 #[cfg(feature = "cloud")]
832 pub(crate) fn narrow_cloud_listing(&mut self) {
833 let dir = self.home.browsing.clone();
834 let prefix = dir.as_ref().and_then(|dir| {
835 if !self.home.probes.cut_short(dir) {
836 return None;
837 }
838 let rows = self.home.probes.listed(dir)?;
839 let names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
840 crate::cloud::cloud_browse::narrowing_prefix(&self.home.filter, &names)
841 });
842 let (Some(dir), Some(prefix)) = (dir, prefix) else {
843 if let Some((_, _, cancelled)) = self.home_app.narrowing.take() {
844 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
845 }
846 if self.home.narrowed.take().is_some() {
847 self.home_refresh();
848 }
849 return;
850 };
851 if self.home.narrowed.as_ref().is_some_and(|n| {
853 n.dir == dir && (n.prefix == prefix || (!n.truncated && prefix.starts_with(&n.prefix)))
854 }) {
855 return;
856 }
857 if let Some((d, p, _)) = &self.home_app.narrowing
858 && *d == dir
859 && *p == prefix
860 {
861 return;
862 }
863 if let Some((_, _, cancelled)) = self.home_app.narrowing.take() {
864 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
865 }
866 let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
867 self.home_app.narrowing = Some((dir.clone(), prefix.clone(), cancelled.clone()));
868 let tx = self.events.clone();
869 let owed = self.owed_answer(AppEvent::HomeNarrowed {
870 dir: dir.clone(),
871 prefix: prefix.clone(),
872 listed: None,
873 });
874 let cloud = self.app_config.cloud.clone();
875 let runtime = self.runtime.clone();
876 std::thread::spawn(move || {
877 owed.run(|| {
878 let url = dir.to_string_lossy().into_owned();
879 let watch = crate::cloud::cloud_browse::Watch {
880 progress: None,
881 cancelled,
882 names_from: Some(prefix.clone()),
883 };
884 let listed = wait_on_runtime(&runtime, async move {
885 crate::cloud::cloud_browse::list_objects_watched(&url, &cloud, &watch).await
886 });
887 let listed = match listed {
888 Some(Ok(level)) if !level.cancelled => Some((level.rows, level.truncated)),
889 _ => None,
890 };
891 let _ = tx.send(AppEvent::HomeNarrowed {
892 dir,
893 prefix,
894 listed,
895 });
896 })
897 });
898 }
899
900 #[cfg(feature = "cloud")]
903 fn spawn_cloud_discovery(&mut self) {
904 if self.home_app.cloud_discovery_started {
905 return;
906 }
907 self.home_app.cloud_discovery_started = true;
908 let list = self.app_config.cloud.list_on_start;
909 self.list_cloud_sources(None, list);
910 }
911
912 #[cfg(feature = "cloud")]
914 fn list_browsed_cloud_source(&mut self) {
915 let Some(id) = self
916 .home
917 .browsing
918 .as_deref()
919 .and_then(home::cloud_source_id)
920 else {
921 return;
922 };
923 let Some(source) = self.home.cloud.iter_mut().find(|s| s.id == id) else {
924 return;
925 };
926 if source.asked {
927 return;
928 }
929 source.begin_listing();
930 self.list_cloud_sources(Some(id), true);
931 }
932
933 #[cfg(feature = "cloud")]
938 fn list_cloud_sources(&mut self, only: Option<String>, list: bool) {
939 let tx = self.events.clone();
940 let cloud = self.app_config.cloud.clone();
941 let cache = self.cache.clone();
942 self.runtime.spawn(async move {
943 let mut hidden = cache.load_hidden_cloud_sources();
944 hidden.extend(cloud.hide.iter().cloned());
945 let cached_for = |source: &crate::cloud::cloud_sources::Source| {
946 cache.cloud_listing(&source.id, &source.fingerprint())
947 };
948 let found = crate::cloud::cloud_sources::rediscover(&cloud).to_vec();
950 if only.is_none() {
954 for source in found.iter().filter(|s| !hidden.contains(&s.id)) {
955 if let Some(cached) = cached_for(source) {
956 crate::cloud::cloud_sources::remember_listed(source, &cached.buckets);
957 }
958 }
959 }
960 let sources: Vec<crate::cloud::cloud_sources::Source> =
961 crate::cloud::cloud_sources::on_home(found, &cloud)
962 .into_iter()
963 .filter(|s| !hidden.contains(&s.id))
964 .collect();
965
966 match &only {
967 None => {
968 let rows = sources
969 .iter()
970 .map(|source| home_cloud_source(source, cached_for(source).as_ref(), list))
971 .collect();
972 let _ = tx.send(AppEvent::HomeCloudSources { sources: rows });
973 }
974 Some(id) if !sources.iter().any(|s| &s.id == id) => {
977 let _ = tx.send(AppEvent::HomeCloudListed {
978 id: id.clone(),
979 buckets: Vec::new(),
980 details: Vec::new(),
981 failure: Some((
982 "not found".to_string(),
983 format!("{id} is gone or hidden. Ctrl+R at the top looks again."),
984 )),
985 listed_at: std::time::SystemTime::now(),
986 });
987 return;
988 }
989 Some(_) => {}
990 }
991 if !list {
992 return;
993 }
994
995 const LISTING_AT_ONCE: usize = 4;
997 let permits = std::sync::Arc::new(tokio::sync::Semaphore::new(LISTING_AT_ONCE));
998 let mut listings = tokio::task::JoinSet::new();
999 for source in sources
1000 .into_iter()
1001 .filter(|s| only.as_ref().is_none_or(|id| &s.id == id))
1002 {
1003 let permits = permits.clone();
1004 listings.spawn(async move {
1005 let _permit = permits.acquire_owned().await;
1006 let result = crate::cloud::cloud_browse::list_first_level(&source).await;
1007 (source, result)
1008 });
1009 }
1010 while let Some(joined) = listings.join_next().await {
1011 let Ok((source, result)) = joined else {
1012 continue;
1013 };
1014 let listed_at = std::time::SystemTime::now();
1015 let mut names = source.buckets.clone();
1017 let mut details = Vec::new();
1018 let failure = match result {
1019 Ok(listed) => {
1020 for item in listed {
1021 crate::cloud::cloud_sources::remember_bucket(&source, &item.name);
1022 if !item.details.is_empty() {
1023 details.push((item.place.clone(), item.details));
1024 }
1025 let name = item.name;
1026 if !names.contains(&name) {
1027 names.push(name);
1028 }
1029 }
1030 cache.save_cloud_listing(
1031 &source.id,
1032 crate::cache::CloudListing {
1033 fingerprint: source.fingerprint(),
1034 buckets: names.clone(),
1035 listed_at: listed_at
1036 .duration_since(std::time::UNIX_EPOCH)
1037 .map(|d| d.as_secs())
1038 .unwrap_or(0),
1039 },
1040 );
1041 None
1042 }
1043 Err(e) => {
1044 log::warn!(target: "datui::cloud", "listing {} failed: {e}", source.id);
1045 Some(summarize_cloud_failure(&e))
1046 }
1047 };
1048 let _ = tx.send(AppEvent::HomeCloudListed {
1049 id: source.id.clone(),
1050 buckets: names
1051 .iter()
1052 .map(|b| PathBuf::from(source.bucket_url(b)))
1053 .collect(),
1054 details,
1055 failure,
1056 listed_at,
1057 });
1058 }
1059 });
1060 }
1061
1062 pub(crate) fn home_reload(&mut self) {
1065 #[cfg(feature = "cloud")]
1066 {
1067 let browsing = self.home.browsing.clone();
1068 match browsing.as_deref().and_then(home::cloud_source_id) {
1069 Some(id) => {
1070 if let Some(source) = self.home.cloud.iter_mut().find(|s| s.id == id) {
1071 source.begin_listing();
1072 }
1073 self.list_cloud_sources(Some(id), true);
1074 }
1075 None if browsing.is_none() && !self.home.cloud.is_empty() => {
1076 for source in &mut self.home.cloud {
1077 source.begin_listing();
1078 }
1079 self.list_cloud_sources(None, true);
1080 }
1081 None => {}
1082 }
1083 }
1084 for place in self.home.probes.silent_places() {
1087 self.home.probes.forget(&place);
1088 (self.home_app.probe_heard).insert(place, std::time::Instant::now());
1089 }
1090 if let Some(dir) = self.home.browsing.clone() {
1091 if self.home.probes.settled(&dir) {
1093 self.home.probes.forget(&dir);
1094 }
1095 }
1096 self.home.stat_epoch += 1;
1098 self.home.peek_failed.clear();
1100 for path in std::mem::take(&mut self.home.web_gone).into_keys() {
1101 self.home.sized.remove(&path);
1102 }
1103 self.home.status = None;
1104 self.home_refresh();
1105 }
1106
1107 pub(crate) fn spawn_home_search(&mut self) {
1111 if self.home_app.search_inflight || self.home.search.done {
1112 return;
1113 }
1114 let config = self.app_config.home.search.clone();
1115 if !config.enabled {
1116 return;
1117 }
1118 let Some(root) =
1119 crate::home::search::search_root(self.home.browsing.as_ref(), self.home.network_check)
1120 else {
1121 return;
1122 };
1123
1124 self.home.search.reset();
1125 self.home.search.root = Some(root.clone());
1126 self.home.search.running = true;
1127 self.home.search.epoch = next_search_epoch();
1128 self.home.search_limit = config.max_results;
1129 self.home_app.search_inflight = true;
1130
1131 let generation = self.home_app.generation;
1132 self.home_app.search_generation = generation;
1133 let tx = self.events.clone();
1134 let formats = self.formats.clone();
1135 let known = self.home.known.clone();
1136 let owed = self.owed_answer(AppEvent::HomeSearchDone {
1138 generation,
1139 root: root.clone(),
1140 scanned: 0,
1141 limited: Some(crate::glyphs::dotted("partial · failed")),
1142 });
1143 std::thread::spawn(move || {
1145 owed.run(|| {
1146 let walk_root = root.clone();
1147 let batch_tx = tx.clone();
1148 let batch_gen = generation;
1149 let batch_root = root.clone();
1150 let outcome = crate::home::search::walk_recalling(
1151 &walk_root,
1152 &config,
1153 &formats,
1154 &known,
1155 move |found, outcome| {
1156 batch_tx
1159 .send(AppEvent::HomeSearchBatch {
1160 generation: batch_gen,
1161 root: batch_root.clone(),
1162 found,
1163 scanned: outcome.scanned,
1164 })
1165 .is_ok()
1167 },
1168 );
1169 let _ = tx.send(AppEvent::HomeSearchDone {
1170 generation,
1171 root,
1172 scanned: outcome.scanned,
1173 limited: outcome.note().map(crate::glyphs::dotted),
1174 });
1175 })
1176 });
1177 }
1178
1179 pub(crate) fn home_score_search(&mut self) {
1183 if self.input_mode != InputMode::Home {
1184 return;
1185 }
1186 let Some(job) = self.home.score_job() else {
1187 return;
1188 };
1189 let epoch = job.epoch;
1190 let tx = self.events.clone();
1191 let owed = self.owed_answer(AppEvent::HomeSearchScored {
1193 epoch,
1194 matches: None,
1195 });
1196 self.runtime.spawn_blocking(move || {
1197 owed.run(move || {
1198 let matches = crate::home::search::score(
1199 &job.results,
1200 &job.query,
1201 job.base.as_ref(),
1202 job.limit,
1203 );
1204 let _ = tx.send(AppEvent::HomeSearchScored {
1205 epoch,
1206 matches: Some(Box::new(matches)),
1207 });
1208 })
1209 });
1210 }
1211
1212 pub(crate) fn home_refresh(&mut self) {
1214 #[cfg(feature = "cloud")]
1217 self.list_browsed_cloud_source();
1218 self.stop_listings_left_behind();
1220 self.home_app.generation = self.home_app.generation.wrapping_add(1);
1221 let generation = self.home_app.generation;
1222
1223 self.move_remembered_places();
1224 let mut catalogs = home::catalogs(&self.app_config);
1225 if self.cache.examples_hidden() {
1228 catalogs.retain(|c| c.origin != crate::home::catalog::Origin::Bundled);
1229 }
1230 self.home.set_catalogs(catalogs);
1231 let mut request = home::ListingRequest {
1232 recents: Vec::new(),
1235 desktop_dirs: Vec::new(),
1236 browsing: self.home.browsing.clone(),
1237 probes: self.home.probes.clone(),
1238 narrowed: self.home.narrowed.clone(),
1239 network_check: self.home.network_check,
1240 cloud: self.home.cloud.clone(),
1241 catalogs: self.home.catalogs.clone(),
1242 known: self.home.known.clone(),
1243 formats: self.formats.clone(),
1244 };
1245 let read_folds = std::mem::take(&mut self.home.folds_owed);
1246 let desktop = self.app_config.home.desktop_recents;
1247 let cache = self.cache.clone();
1248 let writes = self.cache_writes.clone();
1249 let read_facts = !self.home_app.facts_read;
1252 let left = self.home_app.left.take().filter(|_| !read_facts);
1253 let dated = self.home_app.facts_dated.clone();
1254
1255 self.home.listing_in_flight = true;
1256 let tx = self.events.clone();
1257 let owed = self.owed_answer(AppEvent::HomeListingFailed);
1258 self.runtime.spawn_blocking(move || {
1259 owed.run(move || {
1260 writes.settle();
1262 let (recents, visits) = cache.load_recents_with_visits();
1264 let newest = recents.first().cloned();
1265 request.recents = crate::cache::by_frecency(recents, &visits);
1266 if read_facts {
1267 request.known = Arc::new(cache.load_dataset_facts());
1268 }
1269 let learned: Vec<(PathBuf, crate::cache::DatasetFacts)> = (left.iter())
1271 .flat_map(|path| [path.clone(), home::index_key(path)])
1272 .filter_map(|key| Some((key.clone(), cache.dataset_facts(&key)?)))
1273 .collect();
1274 if desktop {
1275 request.desktop_dirs = home::desktop_recent_dirs();
1276 }
1277 let mut listing = home::build_listing(&request);
1278 listing.learn(&learned, request.network_check);
1279 let mut visits = visits;
1280 listing.alias_visits(&mut visits);
1281 let shown: Vec<PathBuf> = {
1284 let mut dated = dated.lock().unwrap_or_else(|e| e.into_inner());
1285 (listing.sections.iter())
1286 .flat_map(|s| s.rows.iter().chain(&s.door))
1287 .flat_map(|row| [row.path.clone(), home::index_key(&row.path)])
1288 .filter(|key| request.known.contains_key(key) && dated.insert(key.clone()))
1289 .collect()
1290 };
1291 cache.touch_dataset_facts(shown.iter().map(PathBuf::as_path));
1292 let known = read_facts.then(|| request.known.clone());
1293 drop(request);
1295 let _ = tx.send(AppEvent::HomeListingReady {
1296 generation,
1297 listing: Box::new(listing),
1298 known,
1299 learned,
1300 visits,
1301 newest,
1302 folds: read_folds.then(|| cache.load_folds()),
1303 });
1304 })
1305 });
1306 }
1307
1308 pub(crate) fn request_home_measurements(&mut self) {
1312 if self.home.measure_in_flight {
1313 return;
1314 }
1315 let (wanted, reads) = match self.home.unmeasured_visible(MEASURE_BATCH) {
1317 shown if !shown.is_empty() => (shown, home::Reads::Files),
1318 _ => (
1319 self.home.unstated_for_sort(MEASURE_BATCH),
1320 home::Reads::StatOnly,
1321 ),
1322 };
1323 if wanted.is_empty() {
1324 return;
1325 }
1326
1327 self.home.measure_in_flight = true;
1328 let tx = self.events.clone();
1329 let cache = self.cache.clone();
1330 let known = self.home.known.clone();
1331 let stat_only = reads == home::Reads::StatOnly;
1333 let owed = self.owed_answer(AppEvent::HomeMeasured {
1334 measured: (wanted.iter())
1335 .map(|entry| {
1336 let failed = home::Measured {
1337 stat_only,
1338 ..Default::default()
1339 };
1340 (entry.path.clone(), failed)
1341 })
1342 .collect(),
1343 done: true,
1344 });
1345 self.runtime.spawn_blocking(move || {
1346 owed.run(|| {
1347 look_and_answer(wanted, &cache, &known, reads, &tx, |measured, done| {
1348 AppEvent::HomeMeasured { measured, done }
1349 })
1350 })
1351 });
1352 }
1353
1354 #[cfg(feature = "http")]
1358 pub(crate) fn size_selected_web_file(&mut self) {
1359 if !self.info.head_web_rows {
1360 return;
1361 }
1362 let Some(entry) = self.home.selected_entry().cloned() else {
1363 return;
1364 };
1365 if entry.size.is_some()
1366 || !matches!(
1367 source::input_source(&entry.path),
1368 source::InputSource::Http(_)
1369 )
1370 || !self.home.sized.insert(entry.path.clone())
1371 {
1372 return;
1373 }
1374 let tx = self.events.clone();
1375 let cache = self.cache.clone();
1376 self.runtime.spawn_blocking(move || {
1377 let size = match Self::fetch_remote_size_http(&entry.path.to_string_lossy()) {
1378 Ok(Some(size)) => size,
1379 Ok(None) => return,
1380 Err(gone) => {
1381 let _ = tx.send(AppEvent::HomeWebGone {
1382 path: entry.path,
1383 gone,
1384 });
1385 return;
1386 }
1387 };
1388 let key = home::index_key(&entry.path);
1389 let mut facts = cache.dataset_facts(&key).unwrap_or_default();
1390 facts.size = size;
1391 cache.record_dataset_facts(&[(key, facts)]);
1392 let measured = home::Measured {
1393 rows: entry.rows,
1394 cols: entry.cols,
1395 cols_sampled: entry.cols_sampled,
1396 size: Some(size),
1397 modified: None,
1398 stat_only: false,
1399 columns: entry.columns.clone(),
1400 cost: entry.cost.clone(),
1401 kind: None,
1402 holds: entry.holds.clone(),
1403 };
1404 let _ = tx.send(AppEvent::HomeSized {
1405 path: entry.path,
1406 measured,
1407 });
1408 });
1409 }
1410
1411 pub(crate) fn request_home_classifications(&mut self) {
1417 let mounts = crate::home::locality::Mounts::cached();
1420 let place = |entry: &discover::Entry| mounts.mount_point_for(&entry.path);
1421 loop {
1422 let wanted = self
1423 .home
1424 .unclassified_visible_where(CLASSIFY_BATCH, |entry| {
1425 !self.home.classifying.contains(&place(entry))
1426 });
1427 let Some(first) = wanted.first() else {
1428 return;
1429 };
1430 let pass = place(first);
1431 let wanted: Vec<discover::Entry> = (wanted.into_iter())
1432 .filter(|entry| place(entry) == pass)
1433 .collect();
1434 self.home.classifying.insert(pass.clone());
1435 let tx = self.events.clone();
1436 let cache = self.cache.clone();
1437 let known = self.home.known.clone();
1438 let owed = self.owed_answer(AppEvent::HomeClassified {
1440 pass: pass.clone(),
1441 measured: (wanted.iter())
1442 .map(|entry| (entry.path.clone(), home::Measured::default()))
1443 .collect(),
1444 done: true,
1445 });
1446 std::thread::spawn(move || {
1447 owed.run(|| {
1448 look_and_answer(
1449 wanted,
1450 &cache,
1451 &known,
1452 home::Reads::Files,
1453 &tx,
1454 |measured, done| AppEvent::HomeClassified {
1455 pass: pass.clone(),
1456 measured,
1457 done,
1458 },
1459 )
1460 })
1461 });
1462 }
1463 }
1464
1465 pub fn enter_home(&mut self) {
1466 self.counting.pause_indexing();
1467 if self.return_from_quality_evidence(false) {
1468 self.analysis_modal.close();
1469 }
1470 self.close_overlays();
1471 self.stop_find();
1472 self.stop_value_count();
1474 self.export_modal.forget_counts();
1475 self.abandon_load();
1476 if let Some(state) = self.data_table_state.as_mut() {
1478 state.stop_following();
1479 }
1480 self.home.status = None;
1481 self.home.filter_selected = !self.home.filter.is_empty();
1484 self.home.folds_owed = true;
1485 if self.path.is_some() {
1487 self.home.resting = false;
1488 }
1489 self.home_app.left = self.path.clone();
1490 self.home_refresh();
1491 if let Some(open_path) = self.path.clone() {
1492 let resolved = (!home::is_remote_path(&open_path))
1495 .then(|| crate::canonical::canonicalize(&open_path).ok())
1496 .flatten();
1497 if let Some(idx) = self.home.position(|row| match row {
1498 home::Row::Entry { entry, .. } => {
1499 entry.path == open_path || resolved.as_ref() == Some(&entry.path)
1500 }
1501 home::Row::Header { .. }
1504 | home::Row::Place { .. }
1505 | home::Row::More { .. }
1506 | home::Row::Hidden { .. }
1507 | home::Row::Up { .. }
1508 | home::Row::Door { .. } => false,
1509 }) {
1510 self.home.selected = idx;
1511 }
1512 }
1513 self.input_mode = InputMode::Home;
1514 }
1515
1516 pub fn rest_at_start(&mut self) {
1519 if !std::mem::replace(&mut self.home_app.rested, true) {
1520 self.home.resting = true;
1521 }
1522 }
1523
1524 pub(crate) fn home_escape(&mut self) -> Option<AppEvent> {
1527 if !self.home.filter.is_empty() {
1528 self.home.filter.clear();
1529 self.home.sync_search_section();
1530 self.home.select_first_entry();
1532 return None;
1533 }
1534 if self.home.browsing.is_some() {
1535 if self.home.below_browse_start() {
1536 self.home_ascend();
1537 } else {
1538 self.home_leave_browsing(None);
1540 }
1541 return None;
1542 }
1543 if self.data_table_state.is_some() {
1544 self.show_table();
1545 let name = self
1547 .path
1548 .as_deref()
1549 .and_then(|p| p.file_name())
1550 .map(|n| n.to_string_lossy().into_owned());
1551 if let Some(name) = name {
1552 self.flash_note(format!("Back to {name}"));
1553 }
1554 }
1555 None
1556 }
1557
1558 pub(crate) fn home_forget_selected(&mut self) {
1561 if let Some(catalog) = self.home.selected_catalog() {
1564 if catalog.origin == crate::home::catalog::Origin::Bundled {
1565 let message = format!(
1566 "Hide {}? It comes back after datui cache clear.",
1567 catalog.label
1568 );
1569 self.confirmation_modal
1570 .show_destructive(message, "Hide", Confirm::HideExamples);
1571 } else {
1572 self.home.status = Some(format!(
1573 "[home] hide = [\"{}\"] in config.toml hides it",
1574 catalog.id
1575 ));
1576 }
1577 return;
1578 }
1579 if let Some(home::Row::Place { path, held, .. }) = self.home.selected_row() {
1582 let message = format!(
1583 "Forget {held} recently opened {} under {}?",
1584 if held == 1 { "dataset" } else { "datasets" },
1585 home::display_path(&path)
1586 );
1587 self.confirmation_modal
1588 .show(message, Confirm::ForgetPlace(path.clone()));
1589 return;
1590 }
1591 let in_mine = self
1594 .home
1595 .selected_section()
1596 .and_then(|i| self.home.sections.get(i))
1597 .is_some_and(|s| s.origin == Some("catalog.toml"));
1598 if in_mine
1599 && let Some((location, _)) = self.home_row_for_catalog()
1600 && let Some((id, name)) = self.mine_entry_at(&location)
1601 {
1602 self.home_forget_from_catalog(&id, &name);
1603 return;
1604 }
1605 let section_title = self
1606 .home
1607 .selected_section()
1608 .and_then(|i| self.home.sections.get(i))
1609 .map(|s| s.title.clone())
1610 .unwrap_or_default();
1611 if self.home.browsing.is_none()
1612 && section_title == home::HomeState::CLOUD_SECTION
1613 && let Some(id) = self
1614 .home
1615 .selected_entry()
1616 .and_then(|e| home::cloud_source_id(&e.path))
1617 {
1618 self.cache.hide_cloud_source(&id);
1619 self.home.cloud.retain(|s| s.id != id);
1620 self.home_refresh();
1621 return;
1622 }
1623 let in_recents = section_title == "Recent";
1624 if !in_recents {
1625 self.home.status = Some("Only recents and catalog.toml rows can be forgotten".into());
1626 return;
1627 }
1628 let Some(entry) = self.home.selected_entry() else {
1629 return;
1630 };
1631 self.cache.forget_recent(&entry.path);
1632 self.home.status = None;
1634 self.home_refresh();
1635 }
1636
1637 fn home_row_for_catalog(&self) -> Option<(PathBuf, String)> {
1641 match self.home.selected_row()? {
1642 home::Row::Place { path, .. } => {
1643 let name = home::display_path(&path);
1644 Some((path, name))
1645 }
1646 home::Row::Door { entry, .. } => {
1647 let path: PathBuf = if matches!(
1650 source::input_source(&entry.path),
1651 source::InputSource::Local(_)
1652 ) {
1653 entry.path.components().collect()
1654 } else {
1655 entry.path.clone()
1656 };
1657 Some((path.clone(), home::display_path(&path)))
1658 }
1659 home::Row::Entry { entry, .. } => (entry.table.is_none()
1662 && !home::is_cloud_place(&entry.path))
1663 .then(|| (entry.path.clone(), entry.name.clone())),
1664 home::Row::Header { section, .. } => {
1665 let root = self.home.sections.get(section)?.root.clone()?;
1666 let name = home::display_path(&root);
1667 Some((root, name))
1668 }
1669 home::Row::More { .. } | home::Row::Hidden { .. } | home::Row::Up { .. } => None,
1670 }
1671 }
1672
1673 fn mine_catalog_file(&self) -> Option<PathBuf> {
1675 let dir = match &self.app_config.catalog_dir {
1676 Some(dir) => dir.clone(),
1677 None => config::ConfigManager::new(APP_NAME)
1678 .ok()?
1679 .config_dir()
1680 .to_path_buf(),
1681 };
1682 Some(dir.join(catalog::MINE_FILE))
1683 }
1684
1685 fn mine_entry_at(&self, location: &Path) -> Option<(String, String)> {
1687 self.app_config
1688 .read_catalogs
1689 .iter()
1690 .find(|c| c.origin == catalog::Origin::Mine)?
1691 .dataset_at(location)
1692 .map(|d| (d.id.clone(), d.name.clone()))
1693 }
1694
1695 fn reload_catalogs(&mut self) -> Result<(), String> {
1697 let dir = self
1698 .mine_catalog_file()
1699 .and_then(|f| f.parent().map(Path::to_path_buf));
1700 self.app_config
1701 .read_catalog_files(dir.as_deref())
1702 .map_err(|e| e.to_string())?;
1703 self.info
1705 .follow_catalogs(&self.app_config, self.path.as_deref());
1706 self.open_info_documentation();
1707 self.home_refresh();
1708 Ok(())
1709 }
1710
1711 fn new_catalog_dataset(&self, location: &Path, name: &str) -> catalog::NewDataset {
1714 if let Some((_, shown)) = self.home.catalog_dataset(location) {
1715 let entry = &shown.entry;
1716 return catalog::NewDataset {
1717 name: entry.name.clone(),
1718 path: entry.local_path().map(|p| home::display_path(&p)),
1719 url: entry.url.clone(),
1720 auth: entry.auth.clone(),
1721 connection: entry.connection.clone(),
1722 description: entry.description.clone(),
1723 size: entry.size,
1724 };
1725 }
1726 let mut new = catalog::NewDataset {
1727 name: name.to_string(),
1728 ..Default::default()
1729 };
1730 if matches!(
1731 source::input_source(location),
1732 source::InputSource::Local(_)
1733 ) {
1734 let absolute = if location.is_relative() {
1735 std::env::current_dir()
1736 .map(|cwd| cwd.join(location))
1737 .unwrap_or_else(|_| location.to_path_buf())
1738 } else {
1739 location.to_path_buf()
1740 };
1741 new.path = Some(home::display_path(&absolute));
1742 return new;
1743 }
1744 let text = location.to_string_lossy();
1747 let (id, plain) = source::split_source_id(&text);
1748 new.url = Some(plain.into_owned());
1749 if let Some(id) = id {
1750 new.connection = Some(id.to_string());
1751 }
1752 new
1753 }
1754
1755 pub(crate) fn home_toggle_catalog(&mut self) {
1757 let Some((location, name)) = self.home_row_for_catalog() else {
1758 self.home.status = Some("Move to a dataset or directory to add it".into());
1759 return;
1760 };
1761 if let Some((id, name)) = self.mine_entry_at(&location) {
1762 self.home_forget_from_catalog(&id, &name);
1763 return;
1764 }
1765 let Some(file) = self.mine_catalog_file() else {
1766 self.home.status = Some("No config directory to keep catalog.toml in".into());
1767 return;
1768 };
1769 let new = self.new_catalog_dataset(&location, &name);
1770 if let Some(connection) = &new.connection
1773 && !self
1774 .app_config
1775 .cloud
1776 .connections
1777 .iter()
1778 .any(|c| c.name == *connection)
1779 {
1780 self.home.status = Some(format!(
1781 "Not added: {connection} is not a [[cloud.connections]] entry in the config"
1782 ));
1783 return;
1784 }
1785 if let Err(why) = new.check() {
1786 self.home.status = Some(format!("Not added: {why}"));
1787 return;
1788 }
1789 let label = self
1790 .app_config
1791 .read_catalogs
1792 .iter()
1793 .find(|c| c.origin == catalog::Origin::Mine)
1794 .map(|c| c.label.clone())
1795 .unwrap_or_else(|| catalog::MINE_LABEL.to_string());
1796 match catalog::add(&file, &new) {
1797 Ok(_) => match self.reload_catalogs() {
1798 Ok(()) => self.flash_note(format!("Added {} to {label}", new.name)),
1799 Err(e) => self.error_modal.show(e),
1800 },
1801 Err(e) => self.error_modal.show(e.to_string()),
1802 }
1803 }
1804
1805 fn home_forget_from_catalog(&mut self, id: &str, name: &str) {
1807 let Some(file) = self.mine_catalog_file() else {
1808 return;
1809 };
1810 match catalog::forget(&file, id) {
1811 Ok(()) => match self.reload_catalogs() {
1812 Ok(()) => self.flash_note(format!("Forgot {name}")),
1813 Err(e) => self.error_modal.show(e),
1814 },
1815 Err(e) => self.error_modal.show(e.to_string()),
1816 }
1817 }
1818
1819 fn move_remembered_places(&mut self) {
1821 if std::mem::replace(&mut self.home_app.remembered_moved, true) {
1822 return;
1823 }
1824 let places = self.cache.load_remembered_places();
1825 if places.is_empty() {
1826 return;
1827 }
1828 let Some(file) = self.mine_catalog_file() else {
1829 return;
1830 };
1831 match catalog::move_places(&file, &places) {
1834 Ok(_) => self.cache.clear_remembered_places(),
1835 Err(e) => {
1836 log::warn!(target: "datui", "moving remembered places into catalog.toml: {e:#}")
1837 }
1838 }
1839 let dir = file.parent().map(Path::to_path_buf);
1840 if let Err(e) = self.app_config.read_catalog_files(dir.as_deref()) {
1841 log::warn!(target: "datui", "reading catalog.toml: {e:#}");
1842 }
1843 }
1844
1845 pub(crate) fn home_open_documentation(&mut self) {
1847 let Some((path, doc)) = self.home_documented_row() else {
1848 self.home.status =
1849 Some("Ctrl+E shows what a catalog or a format spec says of a row".into());
1850 return;
1851 };
1852 let measured = self
1853 .home
1854 .selected_entry()
1855 .filter(|e| {
1856 e.path == path
1857 && doc
1858 .catalog
1859 .as_ref()
1860 .is_some_and(|(_, entry)| entry.location() == path)
1861 })
1862 .and_then(|e| e.size);
1863 self.info.documentation.open(doc, measured);
1864 self.info.documentation.links_open = self.home_app.local_desktop;
1865 }
1866
1867 pub(crate) fn home_documented_row(
1870 &self,
1871 ) -> Option<(PathBuf, widgets::documentation::Documented)> {
1872 let row = self.home.selected_row();
1873 let (path, file) = match &row {
1874 Some(home::Row::Entry { entry, .. }) | Some(home::Row::Door { entry, .. }) => {
1875 (Some(entry.path.clone()), Some(*entry))
1876 }
1877 Some(home::Row::Place { path, .. }) => (Some(path.clone()), None),
1878 Some(home::Row::Header { section, .. }) => (
1879 self.home
1880 .sections
1881 .get(*section)
1882 .and_then(|s| s.root.clone()),
1883 None,
1884 ),
1885 _ => (None, None),
1886 };
1887 let path = path?;
1888 let catalog = home::catalog_entry_for(&self.home.catalogs, &path);
1889 let spec = file
1890 .filter(|e| e.kind == discover::EntryKind::File)
1891 .and_then(|e| e.format_spec.as_deref())
1892 .and_then(|name| self.home.formats.get(name))
1893 .and_then(|spec| spec.docs())
1894 .map(std::sync::Arc::new);
1895 let name = file
1897 .map(|e| {
1898 let text = e.path.to_string_lossy();
1899 text.strip_suffix(e.name.as_str())
1900 .filter(|_| e.table.is_some())
1901 .map(|file| file.trim_end_matches(std::path::is_separator))
1902 .and_then(|file| Path::new(file).file_name())
1903 .map_or_else(|| e.name.clone(), |n| n.to_string_lossy().into_owned())
1904 })
1905 .unwrap_or_default();
1906 let doc = widgets::documentation::Documented::new(catalog, spec, name)?;
1907 Some((path, doc))
1908 }
1909
1910 pub(crate) fn home_hides_catalog(&self) -> bool {
1912 self.home
1913 .selected_catalog()
1914 .is_some_and(|c| c.origin == crate::home::catalog::Origin::Bundled)
1915 }
1916
1917 pub(crate) fn home_catalog_action(&self) -> Option<&'static str> {
1920 let (location, _) = self.home_row_for_catalog()?;
1921 Some(if self.mine_entry_at(&location).is_some() {
1922 "Forget"
1923 } else {
1924 "Add"
1925 })
1926 }
1927
1928 pub(crate) fn home_toggle_fold(&mut self) {
1931 if let Some(section) = self.home.selected_section() {
1932 self.home.toggle_collapsed(section);
1933 self.home.clamp_selection();
1934 self.cache.save_folds(&self.home.folds);
1935 }
1936 }
1937
1938 pub(crate) fn home_collapse(&mut self, collapse: bool) {
1939 if self.home.browsing.is_some() {
1942 return;
1943 }
1944 let Some(section) = self.home.selected_section() else {
1945 return;
1946 };
1947 if collapse && self.home.cut_again(section) {
1950 return;
1951 }
1952 if !collapse && matches!(self.home.selected_row(), Some(home::Row::More { .. })) {
1953 self.home.show_all(section);
1954 return;
1955 }
1956 if collapse && !self.home.is_collapsed(section) {
1957 self.home.set_collapsed(section, true);
1958 if let Some(idx) = self
1959 .home
1960 .visible()
1961 .iter()
1962 .position(|row| row.section() == section)
1963 {
1964 self.home.selected = idx;
1965 }
1966 } else if !collapse {
1967 self.home.set_collapsed(section, false);
1968 }
1969 self.home.clamp_selection();
1970 self.cache.save_folds(&self.home.folds);
1971 }
1972
1973 pub(crate) fn home_ascend(&mut self) {
1975 let Some(current) = self.home.browsing.clone() else {
1976 return;
1977 };
1978 let parent = self.home.parent_of(¤t);
1979 self.home_leave_browsing(parent);
1980 }
1981
1982 fn home_leave_browsing(&mut self, to: Option<PathBuf>) {
1984 self.home.status = None;
1986 let from = std::mem::replace(&mut self.home.browsing, to);
1987 if !self.home.below_browse_start() {
1990 self.home.browse_start = self.home.browsing.clone();
1991 }
1992 self.home.come_back(from);
1995 self.home.sync_search_section();
1996 self.home.selected = 0;
1997 self.home_refresh();
1998 if !self.home.filter.is_empty() {
1999 self.spawn_home_search();
2000 }
2001 }
2002
2003 #[cfg(feature = "cloud")]
2008 pub(crate) fn peek_tells_a_row_something(
2009 answer: &(discover::EntryKind, discover::Holds),
2010 ) -> bool {
2011 answer.0 != discover::EntryKind::Directory || !answer.1.is_empty()
2012 }
2013
2014 #[cfg(feature = "cloud")]
2019 pub(crate) fn peek_cloud_directories(&mut self) {
2020 const PEEKS_AT_ONCE: usize = 4;
2021 let directories = self.home.cloud_directories_to_peek(PEEKS_AT_ONCE);
2022 if directories.is_empty() {
2023 return;
2024 }
2025 for directory in &directories {
2028 self.home.peeking.insert(directory.clone());
2029 }
2030 let tx = self.events.clone();
2031 let cloud = self.app_config.cloud.clone();
2032 self.runtime.spawn(async move {
2033 let permits = Arc::new(tokio::sync::Semaphore::new(PEEKS_AT_ONCE));
2034 let mut peeks = tokio::task::JoinSet::new();
2035 let mut asked = std::collections::HashMap::new();
2038 for directory in directories {
2039 let (permits, cloud) = (permits.clone(), cloud.clone());
2040 let task_directory = directory.clone();
2041 let task = peeks.spawn(async move {
2042 let directory = task_directory;
2043 let _permit = permits.acquire_owned().await;
2044 let kind =
2045 crate::cloud::cloud_browse::peek_kind(&directory.to_string_lossy(), &cloud)
2046 .await;
2047 (directory, kind)
2048 });
2049 asked.insert(task.id(), directory);
2050 }
2051 let mut found = Vec::new();
2055 let mut failed = Vec::new();
2056 while let Some(joined) = peeks.join_next_with_id().await {
2057 match joined {
2058 Ok((_, (directory, Ok(answer)))) => {
2059 let answer = Some(answer)
2060 .filter(Self::peek_tells_a_row_something)
2061 .unwrap_or((discover::EntryKind::Directory, Default::default()));
2062 found.push((directory, answer));
2063 }
2064 Ok((_, (directory, Err(_)))) => failed.push(directory),
2065 Err(error) => failed.extend(asked.remove(&error.id())),
2066 }
2067 if found.len() + failed.len() >= PEEKS_AT_ONCE {
2068 let _ = tx.send(AppEvent::HomeCloudKinds {
2069 kinds: std::mem::take(&mut found),
2070 failed: std::mem::take(&mut failed),
2071 });
2072 }
2073 }
2074 if !found.is_empty() || !failed.is_empty() {
2075 let _ = tx.send(AppEvent::HomeCloudKinds {
2076 kinds: found,
2077 failed,
2078 });
2079 }
2080 });
2081 }
2082
2083 pub(crate) fn home_browse_into(&mut self, path: PathBuf) {
2085 self.home.leave_mark();
2086 if self.home.browsing.is_none() {
2087 self.home.browse_start = Some(path.clone());
2088 } else if self.home.browse_start.is_none() {
2089 self.home.browse_start = self.home.browsing.clone();
2090 }
2091 self.home.browsing = Some(path);
2092 self.home.status = None;
2096 self.home.search.reset();
2097 self.home.filter.clear();
2098 self.home.sync_search_section();
2099 self.home.selected = 0;
2100 self.home_refresh();
2101 }
2102
2103 fn selection_opens_the_whole_directory(&self) -> bool {
2106 self.home.selection_is_the_door()
2107 }
2108
2109 pub(crate) fn selected_directory_to_enter(&self) -> Option<PathBuf> {
2112 if let Some(home::Row::Place { path, .. }) = self.home.selected_row() {
2114 return home::place_is_browsable(&path).then_some(path);
2115 }
2116 let entry = self.home.selected_entry()?;
2117 if self.selection_opens_the_whole_directory() || self.home.missing.contains(&entry.path) {
2118 return None;
2119 }
2120 if entry.cost.tables.is_some() {
2122 return Some(entry.path.clone());
2123 }
2124 (!matches!(
2125 entry.kind,
2126 discover::EntryKind::File | discover::EntryKind::Other
2127 ))
2128 .then_some(entry.path.clone())
2129 }
2130
2131 #[cfg(feature = "cloud")]
2135 pub(crate) fn why_a_door_reads_nothing(entry: &discover::Entry) -> Option<String> {
2136 if !home::is_object_store_url(&entry.path)
2137 || matches!(
2138 entry.kind,
2139 discover::EntryKind::Hive | discover::EntryKind::MultiFile
2140 )
2141 || Self::cloud_prefix_format(&entry.holds).is_some()
2142 {
2143 return None;
2144 }
2145 Self::why_a_cloud_prefix_cannot_be_read(&entry.holds)
2146 }
2147
2148 #[cfg(feature = "cloud")]
2152 pub(crate) fn why_a_cloud_prefix_cannot_be_read(holds: &discover::Holds) -> Option<String> {
2153 let reads_parquet =
2154 |name: &str| crate::FileFormat::from_name(name) == Some(crate::FileFormat::Parquet);
2155 if holds.formats.iter().any(|(name, _)| reads_parquet(name)) {
2156 return None;
2157 }
2158 match holds.formats.as_slice() {
2159 [] => {
2162 (holds.not_read > 0 && holds.directories == 0).then(|| {
2165 "this prefix holds nothing datui can read — datui reads a directory in \
2166 an object store as Parquet only."
2167 .to_string()
2168 })
2169 }
2170 formats => {
2171 let held = formats
2172 .iter()
2173 .map(|(name, count)| format!("{count} {name}"))
2174 .collect::<Vec<_>>()
2175 .join(", ");
2176 Some(format!(
2177 "this prefix holds {held} — datui reads a directory in an object store \
2178 as Parquet only. Open one of the files below instead."
2179 ))
2180 }
2181 }
2182 }
2183
2184 #[cfg(feature = "cloud")]
2188 pub(crate) fn cloud_prefix_format(
2189 holds: &discover::Holds,
2190 ) -> Option<(FileFormat, Vec<(FileFormat, usize)>)> {
2191 if holds.dataset_dict {
2193 return Some((FileFormat::Arrow, Vec::new()));
2194 }
2195 if let Some((name, _)) = holds.model_weights() {
2198 return FileFormat::from_name(name).map(|format| (format, Vec::new()));
2199 }
2200 let (name, _) = holds.formats.first()?;
2201 let format = FileFormat::from_name(name)
2204 .filter(|f| f.reads_many_files() && !f.reads_into() && !f.is_lines())?;
2205 let left_out = holds
2208 .formats
2209 .iter()
2210 .skip(1)
2211 .filter_map(|(name, n)| FileFormat::from_name(name).map(|f| (f, *n)))
2212 .collect();
2213 Some((format, left_out))
2214 }
2215
2216 pub(crate) fn home_open_selected(&mut self) -> Option<AppEvent> {
2218 match self.home.selected_row() {
2219 Some(home::Row::Place { path, .. }) => {
2221 if home::place_is_browsable(&path) {
2222 self.home_browse_into(path);
2223 } else {
2224 self.home.status = Some(
2225 "An HTTP server has no listing to browse. Open a file under it".into(),
2226 );
2227 }
2228 return None;
2229 }
2230 Some(home::Row::More { section, .. }) => {
2232 self.home.show_all(section);
2233 return None;
2234 }
2235 Some(home::Row::Up { section }) => {
2237 if self.home.browsing.is_some() {
2238 self.home_ascend();
2239 } else if let Some(parent) = self
2240 .home
2241 .sections
2242 .get(section)
2243 .and_then(|s| s.root.as_deref())
2244 .and_then(|root| self.home.parent_of(root))
2245 {
2246 self.home_browse_into(parent);
2247 }
2248 return None;
2249 }
2250 Some(home::Row::Hidden { .. }) => {
2252 self.home.hide_unreadable = false;
2253 if let Some(idx) = self.home.position(|row| {
2254 matches!(row, home::Row::Entry { entry, .. }
2255 if entry.hidden_by_default())
2256 }) {
2257 self.home.selected = idx;
2258 }
2259 return None;
2260 }
2261 _ => {}
2262 }
2263 if self.home.selection_is_header() {
2264 self.home_toggle_fold();
2265 return None;
2266 }
2267 let entry = self.home.selected_entry()?.clone();
2268 if self.home.missing.contains(&entry.path) {
2270 self.home.status = Some(format!(
2271 "{} does not exist",
2272 home::display_path(&entry.path)
2273 ));
2274 return None;
2275 }
2276 if entry.kind != discover::EntryKind::File && self.home.bookmark(&entry.path).is_some() {
2278 #[cfg(feature = "cloud")]
2279 let reader = if home::is_object_store_url(&entry.path)
2280 && !matches!(
2281 entry.kind,
2282 discover::EntryKind::Hive | discover::EntryKind::MultiFile
2283 ) {
2284 Self::cloud_prefix_format(&entry.holds)
2285 } else {
2286 None
2287 };
2288 #[cfg(not(feature = "cloud"))]
2289 let reader = None;
2290 let directory = home::directory_dataset_url(&entry.path);
2291 return Some(self.home_open_directory_as(directory, true, None, reader));
2292 }
2293 if self.selection_opens_the_whole_directory() {
2297 let lake = entry.kind.lake_name();
2301 #[cfg(feature = "cloud")]
2306 let reader = if home::is_object_store_url(&entry.path)
2307 && !matches!(
2308 entry.kind,
2309 discover::EntryKind::Hive | discover::EntryKind::MultiFile
2310 ) {
2311 let reader = Self::cloud_prefix_format(&entry.holds);
2312 if reader.is_none()
2315 && let Some(what) = Self::why_a_cloud_prefix_cannot_be_read(&entry.holds)
2316 {
2317 self.home.status = Some(what);
2318 return None;
2319 }
2320 reader
2321 } else {
2322 None
2323 };
2324 #[cfg(not(feature = "cloud"))]
2325 let reader = None;
2326 let directory = home::directory_dataset_url(&entry.path);
2329 return Some(self.home_open_directory_as(directory, true, lake, reader));
2330 }
2331 if entry.kind == discover::EntryKind::Unknown
2335 && !home::is_cloud_place(&entry.path)
2336 && matches!(
2337 source::input_source(&entry.path),
2338 source::InputSource::Local(_)
2339 )
2340 {
2341 return Some(AppEvent::ClassifyThenOpen {
2342 path: entry.path,
2343 typed: None,
2344 });
2345 }
2346 if entry.enter_lists_tables() {
2349 self.home_browse_into(entry.path);
2350 return None;
2351 }
2352 self.open_what_it_is(entry.path, entry.kind, false)
2353 }
2354
2355 pub(crate) fn home_jump_into(&mut self, path: PathBuf) {
2358 self.home.trail.retain(|mark| mark.place.is_none());
2360 if self.home.browsing.is_none() {
2361 self.home.leave_mark();
2362 }
2363 self.home.browse_start = Some(path.clone());
2364 self.home.browsing = Some(path);
2365 self.home.status = None;
2366 self.home.search.reset();
2367 self.home.filter.clear();
2368 self.home.sync_search_section();
2369 self.home.selected = 0;
2370 self.home_refresh();
2371 }
2372
2373 pub(crate) fn home_open_path(&mut self, path: PathBuf, hive: bool) -> AppEvent {
2375 self.home_open_directory(path, hive, None)
2376 }
2377
2378 fn home_open_directory(
2381 &mut self,
2382 path: PathBuf,
2383 hive: bool,
2384 lake: Option<&'static str>,
2385 ) -> AppEvent {
2386 self.home_open_directory_as(path, hive, lake, None)
2387 }
2388
2389 fn home_open_directory_as(
2393 &mut self,
2394 path: PathBuf,
2395 hive: bool,
2396 lake: Option<&'static str>,
2397 reader: Option<(FileFormat, Vec<(FileFormat, usize)>)>,
2398 ) -> AppEvent {
2399 let (format, left_out) = match reader {
2400 Some((format, left_out)) => (Some(format), left_out),
2401 None => (None, Vec::new()),
2402 };
2403 let options = OpenOptions {
2406 hive,
2407 read_as_plain_files_of: lake,
2408 format,
2409 left_out,
2410 ..self.open_defaults()
2411 };
2412 self.show_table();
2413 self.announce_open(true, "Scanning input".to_string(), 10);
2415 self.name_what_is_loading(path.clone());
2418 AppEvent::Open(vec![path], options)
2419 }
2420
2421 pub(crate) fn take_listing_news(&mut self) {
2424 for root in std::mem::take(&mut self.home_app.pages_owed) {
2425 self.home.relist_remote(&root);
2426 }
2427 #[cfg(test)]
2428 let patience = self.home_app.probe_patience.unwrap_or(PROBE_PATIENCE);
2429 #[cfg(not(test))]
2430 let patience = PROBE_PATIENCE;
2431 let now = std::time::Instant::now();
2432 let silent: Vec<PathBuf> = (self.home_app.probe_heard.iter())
2433 .filter(|(root, heard)| {
2434 now.duration_since(**heard) >= patience
2435 && self.home_app.probes_inflight.contains(root)
2436 && !self.home.probes.settled(root)
2437 })
2438 .map(|(root, _)| root.clone())
2439 .collect();
2440 for root in silent {
2441 self.home.probes.go_silent(&root);
2442 self.home.relist_remote(&root);
2443 }
2444 }
2445
2446 pub(crate) fn home_event(&mut self, event: AppEvent) -> Option<AppEvent> {
2448 match event {
2449 AppEvent::HomeListingReady {
2450 generation,
2451 listing,
2452 known,
2453 learned,
2454 visits,
2455 newest,
2456 folds,
2457 } => {
2458 if generation == self.home_app.generation {
2461 self.home.listing_in_flight = false;
2462 }
2463 if let Some(known) = known {
2466 self.home.known = known;
2467 self.home_app.facts_read = true;
2468 }
2469 if !learned.is_empty() {
2470 std::sync::Arc::make_mut(&mut self.home.known).extend(learned);
2471 }
2472 self.home.set_visits(visits);
2473 self.home.newest_recent = newest;
2474 if let Some(folds) = folds {
2475 self.home.folds = folds;
2476 }
2477 if generation != self.home_app.generation {
2479 return None;
2480 }
2481 self.home.apply_listing(*listing);
2482 self.spawn_home_probes();
2484 #[cfg(feature = "cloud")]
2485 self.spawn_cloud_discovery();
2486 self.request_home_measurements();
2487 self.request_home_classifications();
2488 None
2489 }
2490 AppEvent::HomeListingFailed => {
2491 self.home.listing_in_flight = false;
2493 None
2494 }
2495 AppEvent::HomeMeasured { measured, done } => {
2496 for (path, m) in measured {
2497 self.home.record_measurement(path, m);
2498 }
2499 if done {
2502 self.home.measure_in_flight = false;
2503 self.request_home_measurements();
2504 }
2505 None
2506 }
2507 AppEvent::HomeSized { path, measured } => {
2508 self.home.record_size(path, measured);
2509 self.home.apply_new_measurements();
2510 None
2511 }
2512 AppEvent::HomeWebGone { path, gone } => {
2513 self.home.web_gone.insert(path, gone);
2514 None
2515 }
2516 AppEvent::HomeClassified {
2517 pass,
2518 measured,
2519 done,
2520 } => {
2521 for (path, m) in measured {
2524 self.home.record_measurement(path, m);
2525 }
2526 if done {
2530 self.home.classifying.remove(&pass);
2531 self.request_home_classifications();
2532 }
2533 None
2534 }
2535 AppEvent::HomePathListed { listing } => {
2536 self.home_app.path_listings_out.remove(&listing.dir);
2537 if self.home.path_input_active
2539 && home::typed_dir(&self.home.path_input) == listing.dir
2540 {
2541 self.home.path_listing = Some(*listing);
2542 if self.home.path_pick.is_none() {
2543 self.home.pick_first_path();
2544 }
2545 }
2546 None
2547 }
2548 AppEvent::HomePathCompleted {
2549 generation,
2550 typed,
2551 completed,
2552 candidates,
2553 } => {
2554 if generation != self.home_app.generation || self.home.path_input != typed {
2556 return None;
2557 }
2558 if candidates == 0 {
2559 self.home.status = Some("No such path".to_string());
2560 } else {
2561 self.home.status = None;
2562 if candidates > 1 {
2563 self.flash_note(format!("{candidates} matches"));
2564 }
2565 self.home.path_input = completed;
2566 self.home.pick_first_path();
2567 }
2568 None
2569 }
2570 AppEvent::HomePreviewReady {
2571 path,
2572 stamp,
2573 read_at,
2574 rows,
2575 prepared,
2576 } => {
2577 let prepared = prepared.lock().ok().and_then(|mut p| p.take());
2578 if let Some(prepared) = &prepared {
2580 let schema = prepared
2581 .state
2582 .schema()
2583 .iter()
2584 .map(|(name, dtype)| (name.to_string(), dtype.clone()))
2585 .collect();
2586 self.home_app
2587 .schema_cache
2588 .insert(path.clone(), Some(schema));
2589 }
2590 let prepared = prepared.filter(|_| read_at.is_some());
2591 self.home_app.previews.landed(
2592 path,
2593 stamp,
2594 read_at.unwrap_or(stamp),
2595 rows,
2596 prepared,
2597 );
2598 None
2599 }
2600 AppEvent::HomeSchemaReady { path, preview } => {
2601 self.home_app.schema_reads.retain(|_, read| *read != path);
2602 let known = self
2606 .home_app
2607 .schema_cache
2608 .get(&path)
2609 .is_some_and(Option::is_some);
2610 if preview.is_some() || !known {
2611 self.home_app.schema_cache.insert(path, preview);
2612 }
2613 None
2614 }
2615 AppEvent::HomeSearchBatch {
2616 generation,
2617 root,
2618 found,
2619 scanned,
2620 } => {
2621 if generation == self.home_app.search_generation {
2625 self.home.search_batch(&root, found, scanned);
2626 }
2627 None
2628 }
2629 AppEvent::HomeSearchScored { epoch, matches } => {
2630 if let Some(matches) = matches {
2632 self.home.search_scored(epoch, *matches);
2633 }
2634 None
2635 }
2636 AppEvent::HomeSearchDone {
2637 generation,
2638 root,
2639 scanned,
2640 limited,
2641 } => {
2642 if generation == self.home_app.search_generation {
2643 self.home.search_finished(&root, scanned, limited);
2644 }
2645 self.home_app.search_inflight = false;
2646 if !self.home.filter.is_empty() && self.home.search.root.is_none() {
2648 self.spawn_home_search();
2649 }
2650 None
2651 }
2652 #[cfg(feature = "cloud")]
2653 AppEvent::HomeCloudSources { sources } => {
2654 self.home.set_cloud(sources);
2656 None
2657 }
2658 #[cfg(feature = "cloud")]
2659 AppEvent::HomeCloudListed {
2660 id,
2661 buckets,
2662 details,
2663 failure,
2664 listed_at,
2665 } => {
2666 if let Some(source) = self.home.cloud.iter_mut().find(|s| s.id == id) {
2667 source.refreshing = false;
2668 for (place, lines) in details {
2669 source.place_details.insert(place, lines);
2670 }
2671 match failure {
2672 Some((short, detail)) => {
2674 for bucket in buckets {
2675 if !source.buckets.contains(&bucket) {
2676 source.buckets.push(bucket);
2677 }
2678 }
2679 source.status = home::CloudStatus::Failed { short, detail };
2680 }
2681 None => {
2682 source.buckets = buckets;
2683 source.status = home::CloudStatus::Listed;
2684 source.listed_at = Some(listed_at);
2685 }
2686 }
2687 }
2688 let cloud = std::mem::take(&mut self.home.cloud);
2689 self.home.set_cloud(cloud);
2690 None
2691 }
2692 AppEvent::HomeNarrowed {
2693 dir,
2694 prefix,
2695 listed,
2696 } => {
2697 let asked = self
2699 .home_app
2700 .narrowing
2701 .as_ref()
2702 .is_some_and(|(d, p, _)| *d == dir && *p == prefix);
2703 if asked {
2704 self.home_app.narrowing = None;
2705 }
2706 let wanted = asked
2708 && self.home.browsing.as_ref() == Some(&dir)
2709 && !self.home.filter.is_empty();
2710 if let (Some((rows, truncated)), true) = (listed, wanted) {
2711 self.home.narrowed = Some(home::Narrowed {
2712 dir: dir.clone(),
2713 prefix,
2714 rows,
2715 truncated,
2716 });
2717 self.home.relist_remote(&dir);
2718 }
2719 None
2720 }
2721 AppEvent::HomeProbeCancelled { root } => {
2722 self.home_app.probes_inflight.retain(|p| p != &root);
2723 self.home_app.probe_heard.remove(&root);
2724 self.home_app.listing_cancels.remove(&root);
2725 self.home.probes.stopped(&root);
2726 if self.home.browsing.as_ref() == Some(&root) {
2728 self.home_refresh();
2729 }
2730 None
2731 }
2732 AppEvent::HomeProbeFailed { root, message } => {
2733 self.home_app.probes_inflight.retain(|p| p != &root);
2734 self.home_app.probe_heard.remove(&root);
2735 self.home_app.listing_cancels.remove(&root);
2736 self.home.probe_failed(root, Some(message));
2737 self.home_refresh();
2738 None
2739 }
2740 AppEvent::HomeProbeProgress { root, rows } => {
2741 if self.home_app.probes_inflight.contains(&root)
2743 && self.home.probes.takes_pages(&root)
2744 {
2745 self.home.probes.read(&root, &rows);
2746 (self.home_app.probe_heard).insert(root.clone(), std::time::Instant::now());
2747 if !self.home_app.pages_owed.contains(&root) {
2749 self.home_app.pages_owed.push(root);
2750 }
2751 }
2752 None
2753 }
2754 AppEvent::HomeProbeReady {
2755 root,
2756 rows,
2757 cut_short,
2758 } => {
2759 self.home_app.probes_inflight.retain(|p| p != &root);
2762 self.home_app.probe_heard.remove(&root);
2763 self.home_app.listing_cancels.remove(&root);
2764 let landed = rows.is_some();
2765 match rows {
2766 Some(rows) => self.home.probe_ready(root.clone(), rows, cut_short),
2767 None => self.home.probe_failed(root.clone(), None),
2768 }
2769 #[cfg(feature = "cloud")]
2771 if cut_short && !self.home.filter.is_empty() {
2772 self.narrow_cloud_listing();
2773 }
2774 #[cfg(feature = "cloud")]
2776 if let Some((account, _, _)) = source::azure_parts(&root.to_string_lossy())
2777 && crate::cloud::azure::remembered_key(&account).is_some()
2778 {
2779 for source in &mut self.home.cloud {
2780 let place = source
2781 .buckets
2782 .iter()
2783 .find(|b| home::cloud_account(b).is_some_and(|(_, a)| a == account))
2784 .cloned();
2785 if let Some(place) = place {
2786 let lines = source.place_details.entry(place).or_default();
2787 if !lines.iter().any(|(k, _)| k == "access") {
2788 lines.push(("access".to_string(), "access key".to_string()));
2789 }
2790 }
2791 }
2792 }
2793 self.home_refresh();
2795 #[cfg(feature = "cloud")]
2799 if landed {
2800 self.peek_cloud_directories();
2801 }
2802 #[cfg(not(feature = "cloud"))]
2803 let _ = landed;
2804 None
2805 }
2806 AppEvent::HomeCloudKinds { kinds, failed } => {
2807 for directory in failed {
2808 self.home.peeking.remove(&directory);
2809 self.home.peek_failed.insert(directory);
2810 }
2811 for (directory, kind) in kinds {
2812 self.home.peeking.remove(&directory);
2815 self.home.cloud_kinds.insert(directory, kind);
2816 }
2817 self.home.take_cloud_kinds();
2819 None
2820 }
2821 _ => unreachable!("not a home event"),
2822 }
2823 }
2824}