pub mod catalog;
pub mod codebook;
pub mod discover;
pub mod fuzzy;
pub(crate) mod home_app;
pub(crate) mod home_keys;
pub mod home_preview;
pub mod locality;
pub mod search;
use crate::home::discover::{Entry, EntryKind};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootOrigin {
Cwd,
Desktop,
}
impl RootOrigin {
pub fn note(self) -> &'static str {
match self {
RootOrigin::Cwd => "current directory",
RootOrigin::Desktop => "opened elsewhere",
}
}
}
pub fn desktop_recent_dirs() -> Vec<PathBuf> {
let Some(data_dir) = dirs::data_dir() else {
return Vec::new();
};
let path = data_dir.join("recently-used.xbel");
let Ok(contents) = std::fs::read_to_string(&path) else {
return Vec::new();
};
dirs_from_xbel(&contents)
}
pub fn dirs_from_xbel(contents: &str) -> Vec<PathBuf> {
const PREFIX: &str = "href=\"file://";
let mut dirs: Vec<PathBuf> = Vec::new();
for chunk in contents.split(PREFIX).skip(1) {
let Some(end) = chunk.find('"') else { continue };
let decoded = percent_decode(&chunk[..end]);
let file = PathBuf::from(decoded);
if !crate::home::discover::is_data_file(&file) || !file.is_file() {
continue;
}
let Some(parent) = file.parent() else {
continue;
};
if parent.as_os_str().is_empty() {
continue;
}
let parent = parent.to_path_buf();
if !dirs.contains(&parent) {
dirs.push(parent);
}
}
dirs
}
fn percent_decode(raw: &str) -> String {
let bytes = raw.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
if let Some(byte) = hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
pub fn object_place_label(path: &Path) -> Option<&'static str> {
if cloud_source_id(path).is_some() {
return Some("source");
}
if cloud_account(path).is_some() {
return Some("account");
}
let text = path.to_string_lossy();
if let Some((_, _, key)) = crate::cloud::source::azure_parts(&text) {
return key.trim_matches('/').is_empty().then_some("container");
}
let (scheme, rest) = text.split_once("://")?;
if !matches!(scheme, "s3" | "s3a" | "gs" | "gcs") {
return None;
}
let rest = rest.trim_end_matches('/');
if rest.is_empty() {
return None;
}
(!rest.contains('/')).then_some("bucket")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloudLook {
Waiting,
Looking,
Failed,
}
#[derive(Debug, Default, PartialEq)]
pub struct RowLabel {
pub short: String,
pub words: String,
pub curated: bool,
pub source: bool,
pub missing_source: bool,
}
pub fn describe(
entry: &Entry,
place_kind: Option<&'static str>,
look: Option<CloudLook>,
frame: usize,
known_sources: Option<&[crate::config::CloudConnectionConfig]>,
) -> RowLabel {
if entry.opens_whole_directory {
return RowLabel::default();
}
let g = crate::glyphs::get();
let curated =
place_kind.filter(|_| matches!(entry.kind, EntryKind::Directory | EntryKind::Unknown));
let look_glyph = look.map(|look| match look {
CloudLook::Waiting => g.ellipsis,
CloudLook::Looking => g.spinner[frame % g.spinner.len()],
CloudLook::Failed => "?",
});
let short = match curated {
Some(word) => word.to_string(),
None if entry.kind == EntryKind::Directory && entry.holds.formats.is_empty() => {
object_place_label(&entry.path)
.or(look_glyph)
.map(str::to_string)
.unwrap_or_else(|| entry.label().into_owned())
}
None => entry.label().into_owned(),
};
let path_text = entry.path.to_string_lossy();
let named = crate::cloud::source::split_source_id(&path_text).0;
let missing_source = named
.is_some_and(|id| known_sources.is_some_and(|known| !known.iter().any(|k| k.name == id)));
let (short, source) = match (named, known_sources) {
(Some(id), Some(_)) if short.is_empty() && missing_source => {
(format!("source not found: {id}"), true)
}
(Some(id), Some(_)) if short.is_empty() => (id.to_string(), true),
_ if short.is_empty() && entry.kind == EntryKind::Unknown => {
(g.ellipsis.to_string(), false)
}
_ => (short, false),
};
let words = match (&entry.table, entry.kind) {
(Some(table), _) => {
let of = match (&entry.format_spec, table.format) {
(Some(spec), _) => spec.clone(),
(None, Some(format)) => format.name().to_string(),
(None, None) => String::new(),
};
format!("{of} {}", table.kind).trim_start().to_string()
}
(None, _) if curated.is_some() => curated.unwrap_or_default().to_string(),
(None, EntryKind::File) => match (
&entry.format_spec,
crate::FileFormat::from_path(&entry.path),
) {
(Some(spec), _) => format!("{spec} file"),
(None, Some(format)) => format!("{} file", format.name()),
(None, None) => "data file".to_string(),
},
(None, EntryKind::Hive) => "hive table".to_string(),
(None, EntryKind::MultiFile) => "multi-file table".to_string(),
(None, kind) if kind.is_lake_table() => {
format!(
"{} table",
kind.lake_name().unwrap_or_default().to_lowercase()
)
}
(None, EntryKind::Directory) => match look {
Some(CloudLook::Failed) => format!("? {} listing failed, Ctrl+R retries", g.middot),
Some(_) => String::new(),
None => object_place_label(&entry.path)
.unwrap_or("directory")
.to_string(),
},
_ => String::new(),
};
RowLabel {
short,
words,
curated: curated.is_some(),
source,
missing_source,
}
}
pub const CLOUD_PLACE: &str = "cloud://";
const SCROLL_MARGIN: usize = 2;
pub(crate) fn settle_top(top: usize, selected: usize, height: usize, total: usize) -> usize {
if height == 0 {
return top.min(selected);
}
let margin = SCROLL_MARGIN.min((height - 1) / 2);
let top = if selected < top + margin {
selected.saturating_sub(margin)
} else if selected + margin >= top + height {
selected + margin + 1 - height
} else {
top
};
top.min(total.saturating_sub(height))
}
pub fn cloud_place(id: &str) -> PathBuf {
PathBuf::from(format!("{CLOUD_PLACE}{id}"))
}
pub fn cloud_source_id(path: &Path) -> Option<String> {
let text = path.to_string_lossy();
let id = text.strip_prefix(CLOUD_PLACE)?.trim_end_matches('/');
(!id.is_empty() && !id.contains('/')).then(|| id.to_string())
}
pub fn cloud_account(path: &Path) -> Option<(String, String)> {
let text = path.to_string_lossy();
let rest = text.strip_prefix(CLOUD_PLACE)?.trim_end_matches('/');
let (id, account) = rest.split_once('/')?;
(!id.is_empty() && !account.is_empty() && !account.contains('/'))
.then(|| (id.to_string(), account.to_string()))
}
fn within(url: &str, root: &str) -> bool {
#[cfg(feature = "cloud")]
{
crate::cloud::cloud_sources::is_within(url, root)
}
#[cfg(not(feature = "cloud"))]
{
let (url, root) = (url.trim_end_matches('/'), root.trim_end_matches('/'));
url == root || url.strip_prefix(root).is_some_and(|r| r.starts_with('/'))
}
}
fn same_place(a: &Path, b: &Path) -> bool {
place_key(a) == place_key(b)
}
fn place_key(path: &Path) -> String {
let text = path.to_string_lossy();
if !text.contains("://") {
return crate::config::path_place(path).display().to_string();
}
#[cfg(feature = "cloud")]
{
crate::cloud::source::canonical_cloud_place(&text)
}
#[cfg(not(feature = "cloud"))]
{
text.trim_end_matches('/').to_string()
}
}
#[derive(Debug, Default)]
pub struct CatalogPlaces {
datasets: std::collections::HashMap<String, (usize, usize)>,
bookmarks: std::collections::HashMap<String, (usize, usize, usize)>,
counted: (usize, usize),
}
impl CatalogPlaces {
fn of(catalogs: &[ShownCatalog]) -> Self {
let mut places = CatalogPlaces {
counted: Self::count(catalogs),
..Default::default()
};
for (c, catalog) in catalogs.iter().enumerate() {
for (d, dataset) in catalog.datasets.iter().enumerate() {
places
.datasets
.entry(place_key(&dataset.location))
.or_insert((c, d));
for (b, (_, place)) in dataset.bookmarks.iter().enumerate() {
places
.bookmarks
.entry(place_key(place))
.or_insert((c, d, b));
}
}
}
places
}
fn count(catalogs: &[ShownCatalog]) -> (usize, usize) {
let datasets = catalogs.iter().flat_map(|c| c.datasets.iter());
(
datasets.clone().count(),
datasets.map(|d| d.bookmarks.len()).sum(),
)
}
fn indexes(&self, catalogs: &[ShownCatalog]) -> bool {
self.counted == Self::count(catalogs)
}
}
fn within_rest(url: &str, root: &str) -> String {
let canonical = |u: &str| match crate::cloud::source::azure_parts(u) {
Some((account, container, path)) => {
crate::cloud::source::azure_url(&account, &container, &path)
}
None => u.to_string(),
};
let (url, root) = (canonical(url), canonical(root));
url.strip_prefix(root.trim_end_matches('/'))
.unwrap_or("")
.to_string()
}
pub fn is_object_store_url(path: &Path) -> bool {
let text = path.to_string_lossy();
let scheme = text
.split_once("://")
.map(|(s, _)| s.to_ascii_lowercase())
.unwrap_or_default();
matches!(scheme.as_str(), "s3" | "s3a" | "gs" | "gcs")
|| crate::cloud::source::azure_parts(&text).is_some()
}
pub fn directory_dataset_url(path: &Path) -> PathBuf {
let text = path.to_string_lossy();
if text.ends_with('/') {
path.to_path_buf()
} else {
PathBuf::from(format!("{text}/"))
}
}
fn whole_directory_row(dir: &Path, rows: &[Entry], remote: bool) -> Option<Entry> {
if cloud_account(dir).is_some() {
return None;
}
let directories: Vec<String> = rows
.iter()
.filter(|r| !matches!(r.kind, EntryKind::File | EntryKind::Other))
.map(|r| format!("{}/", r.name))
.collect();
let objects: Vec<(String, u64)> = rows
.iter()
.filter(|r| matches!(r.kind, EntryKind::File | EntryKind::Other))
.map(|r| (r.path.to_string_lossy().into_owned(), r.size.unwrap_or(1)))
.collect();
let (kind, holds) = if remote || is_object_store_url(dir) {
#[cfg(feature = "cloud")]
{
crate::cloud::cloud_browse::look_at_listing(
&dir.to_string_lossy(),
&directories,
&objects,
)
}
#[cfg(not(feature = "cloud"))]
{
let _ = (&directories, &objects);
(EntryKind::Unknown, Default::default())
}
} else {
crate::home::discover::look_at_directory(dir)
};
let openable_row = rows.iter().any(|r| r.kind != EntryKind::Other);
if !openable_row && holds_nothing_to_open(&holds) {
return None;
}
let mut entry = Entry::directory(&directory_dataset_url(dir));
entry.kind = kind;
entry.holds = holds;
entry.opens_whole_directory = true;
entry.name = door_name(&entry, rows);
Some(entry)
}
fn door_base_name(dir: &Path) -> String {
let text = dir.to_string_lossy();
if let Some((_, container, key)) = crate::cloud::source::azure_parts(&text) {
let leaf = key.trim_matches('/').rsplit('/').next().unwrap_or("");
if leaf.is_empty() {
container
} else {
leaf.to_string()
}
} else {
let (_, plain) = crate::cloud::source::split_source_id(&text);
std::path::Path::new(plain.as_ref())
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| plain.into_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoorKind {
Hive,
OneSchema,
SchemasDiffer,
Single,
Lake,
Mixed,
Unknown,
}
pub fn door_kind(door: &Entry) -> DoorKind {
let holds = &door.holds;
match door.kind {
EntryKind::Hive => DoorKind::Hive,
EntryKind::MultiFile => DoorKind::OneSchema,
k if k.is_lake_table() => DoorKind::Lake,
EntryKind::Unknown => DoorKind::Unknown,
_ => {
let Some(format) = holds.one_format() else {
return DoorKind::Mixed;
};
if holds.directories > 0 {
return DoorKind::Mixed;
}
let files = holds.data_files();
if files == 1 {
return DoorKind::Single;
}
let mostly_data = files * 2 >= files + holds.not_read + holds.unnamed;
let reads_many = crate::FileFormat::from_name(format)
.is_some_and(crate::FileFormat::reads_many_files);
if mostly_data && reads_many {
DoorKind::SchemasDiffer
} else {
DoorKind::Mixed
}
}
}
}
pub fn door_lands(door: &Entry) -> bool {
matches!(door_kind(door), DoorKind::Hive | DoorKind::OneSchema)
}
fn format_title(name: &str) -> String {
crate::FileFormat::from_name(name)
.map_or_else(|| name.to_ascii_uppercase(), |f| f.title().to_string())
}
fn door_keys(door: &Entry, rows: &[Entry]) -> Vec<String> {
if let Some(layout) = door.cost.partitions.as_ref()
&& !layout.keys.is_empty()
{
return layout.keys.clone();
}
let mut keys: Vec<String> = Vec::new();
for row in rows {
if let Some((key, _)) = row.name.split_once('=')
&& !key.is_empty()
&& !keys.iter().any(|k| k == key)
{
keys.push(key.to_string());
}
}
keys
}
pub fn door_name(door: &Entry, rows: &[Entry]) -> String {
let name = door_base_name(&door.path);
let holds = &door.holds;
let more = if holds.truncated { "+" } else { "" };
let files = |format: &str| {
let count = holds.data_files();
let word = if count == 1 { "file" } else { "files" };
format!("{count}{more} {} {word}", format_title(format))
};
let what = match door_kind(door) {
DoorKind::Hive => {
let keys = door_keys(door, rows);
if keys.is_empty() {
"hive table".to_string()
} else {
format!("hive table: {}", keys.join(", "))
}
}
DoorKind::OneSchema => match (holds.model_weights(), holds.one_format()) {
(Some((format, count)), _) => {
let word = if count == 1 { "file" } else { "files" };
format!("model, {count}{more} {} {word}", format_title(format))
}
(None, Some(format)) => format!("{}, one schema", files(format)),
(None, None) => "one table".to_string(),
},
DoorKind::SchemasDiffer => {
format!(
"{}, schemas differ",
files(holds.one_format().unwrap_or(""))
)
}
DoorKind::Single => files(holds.one_format().unwrap_or("")),
DoorKind::Lake => format!(
"{} files, not the table",
door.kind.lake_name().unwrap_or_default()
),
DoorKind::Mixed => "all files, mixed".to_string(),
DoorKind::Unknown => "all files".to_string(),
};
format!("{name} ({what})")
}
pub fn door_reads(door: &Entry) -> Option<(String, Option<String>)> {
if !matches!(door_kind(door), DoorKind::Mixed | DoorKind::Single) {
return None;
}
let holds = &door.holds;
let more = if holds.truncated { "+" } else { "" };
let plain = holds.directories.saturating_sub(holds.partitions);
let directories = |n: usize| {
let word = if n == 1 { "directory" } else { "directories" };
format!("{n}{more} {word}")
};
let Some((format, count)) = holds.formats.first() else {
return (holds.directories > 0).then(|| ("every Parquet file below".to_string(), None));
};
let remote = is_object_store_url(&door.path);
let below = remote || holds.partitions > 0 || (holds.formats.len() == 1 && format == "parquet");
let below = below && holds.directories > 0;
let reads = if below && remote {
format!("every {format} file below")
} else if below {
"every Parquet file below".to_string()
} else {
format!("{count}{more} {format}")
};
let mut skips: Vec<String> = holds
.formats
.iter()
.skip(1)
.map(|(name, n)| format!("{n}{more} {name}"))
.collect();
if !below && plain > 0 {
skips.push(directories(plain));
}
Some((reads, (!skips.is_empty()).then(|| skips.join(", "))))
}
pub fn name_by_spec(formats: &crate::formats::Registry, rows: &mut [Entry]) {
if formats.is_empty() {
return;
}
let mut named = false;
for row in rows
.iter_mut()
.filter(|r| r.kind == EntryKind::Other && r.format_spec.is_none())
{
if let Some(spec) = formats.by_glob(&row.path, false).first() {
discover::name_spec_file(row, spec);
named = true;
}
}
for row in rows.iter_mut().filter(|r| {
r.kind == EntryKind::File
&& r.format_spec.is_none()
&& discover::data_format(&r.path).is_some_and(|f| f.separator().is_some())
}) {
if let Some(spec) = formats
.by_glob(&row.path, false)
.into_iter()
.find(|s| s.is_delimited())
{
row.format_spec = Some(spec.name.clone());
}
}
if named {
discover::sort_entries(rows);
}
}
pub fn holds_nothing_to_open(holds: &discover::Holds) -> bool {
holds.formats.is_empty() && holds.directories == 0 && holds.unnamed == 0
}
pub fn is_cloud_place(path: &Path) -> bool {
path.to_string_lossy().starts_with(CLOUD_PLACE)
}
fn is_bucket_root(path: &Path) -> bool {
let text = path.to_string_lossy();
if let Some((_, _, key)) = crate::cloud::source::azure_parts(&text) {
return key.trim_matches('/').is_empty();
}
let Some((scheme, rest)) = text.split_once("://") else {
return false;
};
matches!(scheme, "s3" | "s3a" | "gs" | "gcs") && {
let rest = rest.trim_end_matches('/');
!rest.is_empty() && !rest.contains('/')
}
}
pub fn parent_location(path: &Path) -> Option<PathBuf> {
if !matches!(
crate::cloud::source::input_source(path),
crate::cloud::source::InputSource::Local(_)
) {
let s = path.to_string_lossy();
let (scheme, rest) = s.split_once("://")?;
let (up, _) = rest.trim_end_matches('/').rsplit_once('/')?;
return Some(PathBuf::from(format!("{scheme}://{up}")));
}
path.parent()
.filter(|p| !p.as_os_str().is_empty() && *p != path)
.map(Path::to_path_buf)
}
pub fn is_remote_path(path: &Path) -> bool {
is_cloud_place(path)
|| !matches!(
crate::cloud::source::input_source(path),
crate::cloud::source::InputSource::Local(_)
)
|| is_network_path(path)
}
pub fn is_network_path(path: &Path) -> bool {
crate::home::locality::Mounts::cached().is_network(path)
}
pub fn could_block_path(path: &Path) -> bool {
crate::home::locality::Mounts::cached().could_block(path)
}
#[doc(hidden)]
pub fn network_fs_for_test(mountinfo: &str, path: &Path) -> bool {
crate::home::locality::Mounts::parse(mountinfo).is_network(path)
}
#[derive(Debug, Clone)]
pub struct Root {
pub path: PathBuf,
pub origin: RootOrigin,
pub network: bool,
pub available: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Section {
pub title: String,
pub subtitle: Option<String>,
pub origin: Option<&'static str>,
pub root: Option<PathBuf>,
pub rows: Vec<Entry>,
pub door: Option<Entry>,
pub unavailable: bool,
pub unavailable_note: Option<String>,
pub folded_by_default: bool,
pub remote_root: Option<PathBuf>,
pub waiting: bool,
pub grouped_by_place: bool,
pub place_labels: std::collections::HashMap<PathBuf, String>,
}
impl Section {
pub fn titled(title: impl Into<String>, rows: Vec<Entry>) -> Self {
Section {
title: title.into(),
rows,
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum CloudStatus {
#[default]
Listing,
Unlisted,
Listed,
Failed { short: String, detail: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CloudSource {
pub id: String,
pub label: String,
pub api: crate::cloud::source::ProviderKind,
pub note: String,
pub buckets: Vec<PathBuf>,
pub status: CloudStatus,
pub listed_at: Option<std::time::SystemTime>,
pub refreshing: bool,
pub asked: bool,
pub details: Vec<(String, String)>,
pub place_details: std::collections::HashMap<PathBuf, Vec<(String, String)>>,
}
impl CloudSource {
pub fn count_text(&self) -> String {
match &self.status {
CloudStatus::Failed { short, .. } if self.buckets.is_empty() => short.clone(),
CloudStatus::Listing if self.buckets.is_empty() => String::new(),
CloudStatus::Unlisted if self.buckets.is_empty() => "not listed".to_string(),
_ => {
let (one, many) = match self.api {
crate::cloud::source::ProviderKind::Azure => ("account", "accounts"),
crate::cloud::source::ProviderKind::Gcs => ("project", "projects"),
crate::cloud::source::ProviderKind::S3 => ("bucket", "buckets"),
};
match self.buckets.len() {
0 => format!("no {many}"),
1 => format!("1 {one}"),
n => format!("{n} {many}"),
}
}
}
}
pub fn begin_listing(&mut self) {
self.asked = true;
if self.status == CloudStatus::Unlisted && self.buckets.is_empty() {
self.status = CloudStatus::Listing;
} else {
self.refreshing = true;
}
}
pub fn busy(&self) -> bool {
self.refreshing || (self.status == CloudStatus::Listing && self.buckets.is_empty())
}
pub fn failed(&self) -> bool {
matches!(self.status, CloudStatus::Failed { .. })
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShownCatalog {
pub id: String,
pub label: String,
pub origin: crate::home::catalog::Origin,
pub description: String,
pub file: Option<PathBuf>,
pub datasets: Vec<ShownDataset>,
pub broken: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ShownDataset {
pub name: String,
pub location: PathBuf,
pub details: Vec<(String, String)>,
pub size: Option<u64>,
pub codebook: Option<std::sync::Arc<crate::home::codebook::Codebook>>,
pub bookmarks: Vec<(String, PathBuf)>,
pub entry: std::sync::Arc<crate::home::catalog::Dataset>,
}
impl ShownCatalog {
pub fn from_catalog(catalog: &crate::home::catalog::Catalog) -> Self {
Self {
id: catalog.id.clone(),
label: catalog.label.clone(),
origin: catalog.origin,
description: catalog.description.clone(),
file: catalog.file.clone(),
datasets: catalog
.datasets
.iter()
.map(|dataset| {
let location = dataset.location();
let mut details: Vec<(String, String)> = [
("about", &dataset.description),
("publisher", &dataset.publisher),
("license", &dataset.license),
("homepage", &dataset.homepage),
("documentation", &dataset.documentation),
]
.into_iter()
.filter(|(_, value)| !value.is_empty())
.map(|(key, value)| (key.to_string(), value.clone()))
.collect();
match &dataset.url {
None => details.push(("path".to_string(), display_path(&location))),
Some(url) => {
details.push(("url".to_string(), url.clone()));
details.push(("login".to_string(), login_of(dataset)));
}
}
ShownDataset {
name: dataset.name.clone(),
location,
details,
size: dataset.size,
codebook: crate::home::codebook::Codebook::of(dataset)
.map(std::sync::Arc::new),
bookmarks: dataset
.bookmarks
.iter()
.map(|(name, path)| (name.clone(), dataset.bookmark_location(path)))
.collect(),
entry: std::sync::Arc::new(dataset.clone()),
}
})
.collect(),
broken: None,
}
}
pub fn from_broken(broken: &crate::home::catalog::Broken) -> Self {
Self {
id: broken.id.clone(),
label: broken.id.clone(),
origin: broken.origin,
description: String::new(),
file: None,
datasets: Vec::new(),
broken: Some(broken.callout()),
}
}
pub fn origin_note(&self) -> &'static str {
match self.origin {
crate::home::catalog::Origin::Mine => "catalog.toml",
crate::home::catalog::Origin::Listed | crate::home::catalog::Origin::Folder => {
"catalog"
}
crate::home::catalog::Origin::Bundled => BUNDLED_ORIGIN,
}
}
}
pub const BUNDLED_ORIGIN: &str = "comes with datui";
pub const CATALOG_ORIGINS: [&str; 3] = ["catalog.toml", "catalog", BUNDLED_ORIGIN];
pub fn is_catalog_origin(origin: &str) -> bool {
CATALOG_ORIGINS.contains(&origin)
}
pub fn login_of(dataset: &crate::home::catalog::Dataset) -> String {
match dataset.object_store_auth() {
Some(crate::config::DatasetAuth::Connection(connection)) => connection,
Some(crate::config::DatasetAuth::Anonymous) | None => "none".to_string(),
Some(crate::config::DatasetAuth::Auto) => "auto".to_string(),
}
}
pub fn catalogs(config: &crate::config::AppConfig) -> Vec<ShownCatalog> {
let mut out: Vec<ShownCatalog> = config
.shown_catalogs()
.iter()
.filter_map(|catalog| {
let mut shown = ShownCatalog::from_catalog(catalog);
if catalog.origin == crate::home::catalog::Origin::Bundled {
shown
.datasets
.retain(|d| crate::cloud::source::opens_in_this_build(&d.location));
}
(!shown.datasets.is_empty()).then_some(shown)
})
.collect();
let at = out
.iter()
.position(|c| c.origin == crate::home::catalog::Origin::Bundled)
.unwrap_or(out.len());
let broken: Vec<ShownCatalog> = config
.broken_catalogs
.iter()
.filter(|b| !config.home.hide.contains(&b.id))
.map(ShownCatalog::from_broken)
.collect();
out.splice(at..at, broken);
out
}
fn catalog_entry(
dataset: &ShownDataset,
network_check: fn(&Path) -> bool,
missing: &mut std::collections::HashSet<PathBuf>,
) -> Entry {
let path = &dataset.location;
let local = matches!(
crate::cloud::source::input_source(path),
crate::cloud::source::InputSource::Local(_)
);
let mut entry = if is_object_store_url(path) {
if names_a_file(path) {
entry_for_path(path, true)
} else {
Entry::directory(path)
}
} else if !local || network_check(path) {
entry_for_path(path, true)
} else if path.exists() {
entry_for_path(path, false)
} else {
missing.insert(path.clone());
let mut entry = entry_for_path(path, true);
entry.kind = EntryKind::Unknown;
entry
};
entry.name = dataset.name.clone();
entry
}
pub fn codebook_for(
catalogs: &[ShownCatalog],
path: &Path,
) -> Option<std::sync::Arc<crate::home::codebook::Codebook>> {
let text = path.to_string_lossy();
catalogs
.iter()
.flat_map(|c| c.datasets.iter())
.filter(|d| d.codebook.is_some())
.filter(|d| d.location == path || within(&text, &d.location.to_string_lossy()))
.max_by_key(|d| d.location.to_string_lossy().trim_end_matches('/').len())
.and_then(|d| d.codebook.clone())
}
pub fn catalog_entry_for(
catalogs: &[ShownCatalog],
path: &Path,
) -> Option<(String, std::sync::Arc<crate::home::catalog::Dataset>)> {
let text = path.to_string_lossy();
catalogs
.iter()
.flat_map(|c| c.datasets.iter().map(move |d| (c, d)))
.filter(|(_, d)| {
d.location == path
|| same_place(&d.location, path)
|| within(&text, &d.location.to_string_lossy())
})
.rev()
.max_by_key(|(_, d)| d.location.to_string_lossy().trim_end_matches('/').len())
.map(|(c, d)| (c.label.clone(), d.entry.clone()))
}
fn bookmark_entry(name: &str, place: &Path, network_check: fn(&Path) -> bool) -> Entry {
let local = matches!(
crate::cloud::source::input_source(place),
crate::cloud::source::InputSource::Local(_)
);
let mut entry = if is_object_store_url(place) && !names_a_file(place) {
Entry::directory(place)
} else {
entry_for_path(place, !local || network_check(place) || !place.exists())
};
entry.name = name.to_string();
entry
}
fn catalog_section(
catalog: &ShownCatalog,
network_check: fn(&Path) -> bool,
missing: &mut std::collections::HashSet<PathBuf>,
) -> Section {
let rows = catalog
.datasets
.iter()
.flat_map(|dataset| {
std::iter::once(catalog_entry(dataset, network_check, missing)).chain(
dataset
.bookmarks
.iter()
.map(|(name, place)| bookmark_entry(name, place, network_check)),
)
})
.collect();
Section {
origin: Some(catalog.origin_note()),
unavailable: catalog.broken.is_some(),
unavailable_note: catalog.broken.clone(),
..Section::titled(catalog.label.clone(), rows)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Measured {
pub rows: Option<usize>,
pub cols: Option<usize>,
pub cols_sampled: bool,
pub size: Option<u64>,
pub modified: Option<std::time::SystemTime>,
pub stat_only: bool,
pub columns: Vec<String>,
pub cost: crate::home::discover::Cost,
pub kind: Option<crate::home::discover::EntryKind>,
pub holds: crate::home::discover::Holds,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SortMode {
#[default]
Natural,
Size,
Modified,
Rows,
}
impl SortMode {
pub fn label_in(self, section_is_recency_ordered: bool) -> &'static str {
match self {
SortMode::Natural if section_is_recency_ordered => "recent",
SortMode::Natural => "name",
SortMode::Size => "size",
SortMode::Modified => "modified",
SortMode::Rows => "rows",
}
}
pub fn next(self) -> Self {
match self {
SortMode::Natural => SortMode::Size,
SortMode::Size => SortMode::Modified,
SortMode::Modified => SortMode::Rows,
SortMode::Rows => SortMode::Natural,
}
}
}
#[derive(Debug, Clone)]
pub enum Row<'a> {
Header {
section: usize,
matches: usize,
collapsed: bool,
},
Entry {
section: usize,
entry: &'a Entry,
nested: bool,
hit: Hit,
},
Place {
section: usize,
path: PathBuf,
label: Option<String>,
source: Option<String>,
held: usize,
},
Door { section: usize, entry: &'a Entry },
More {
section: usize,
hidden: usize,
places: usize,
measuring: bool,
},
Hidden { section: usize, count: usize },
Up { section: usize },
}
impl Row<'_> {
pub fn section(&self) -> usize {
match self {
Row::Header { section, .. }
| Row::Entry { section, .. }
| Row::Door { section, .. }
| Row::Place { section, .. }
| Row::More { section, .. }
| Row::Hidden { section, .. }
| Row::Up { section } => *section,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Hit {
pub score: i32,
pub column: Option<usize>,
}
impl Hit {
pub fn column_of<'a>(&self, entry: &'a Entry) -> Option<&'a str> {
self.column
.and_then(|i| entry.columns.get(i))
.map(String::as_str)
}
pub fn positions(&self, filter: &str, entry: &Entry) -> Vec<usize> {
match self.column_of(entry) {
Some(column) => substring_positions(filter, column),
None => fuzzy_positions(filter, &entry.name),
}
}
}
#[derive(Debug, Default)]
pub struct RowsCache {
built: std::cell::RefCell<Option<View>>,
builds: std::cell::Cell<usize>,
rescored: std::cell::RefCell<Option<Hits>>,
}
#[derive(Debug, Clone)]
struct Hits {
filter: String,
sections: Vec<Option<Vec<Option<Hit>>>>,
}
#[derive(Debug)]
struct View {
key: ViewKey,
slots: Vec<Slot>,
headers: Vec<usize>,
hits: Hits,
has_dataset: bool,
}
#[derive(Debug, PartialEq)]
struct ViewKey {
filter: String,
sort: SortMode,
hide_unreadable: bool,
recent_expanded: bool,
shown_whole: std::collections::HashSet<PathBuf>,
view_height: usize,
browsing: Option<PathBuf>,
folds: std::collections::HashMap<String, bool>,
shape: Vec<(usize, bool)>,
}
impl ViewKey {
fn of(home: &HomeState) -> Self {
ViewKey {
filter: home.filter.clone(),
sort: home.sort,
hide_unreadable: home.hide_unreadable,
recent_expanded: home.recent_expanded,
shown_whole: home.shown_whole.clone(),
view_height: home.view_height,
browsing: home.browsing.clone(),
folds: home.folds.clone(),
shape: (home.sections.iter())
.map(|s| (s.rows.len(), s.door.is_some()))
.collect(),
}
}
fn matches(&self, home: &HomeState) -> bool {
self.filter == home.filter
&& self.sort == home.sort
&& self.hide_unreadable == home.hide_unreadable
&& self.recent_expanded == home.recent_expanded
&& self.shown_whole == home.shown_whole
&& self.view_height == home.view_height
&& self.browsing == home.browsing
&& self.folds == home.folds
&& self.shape.iter().copied().eq(home
.sections
.iter()
.map(|s| (s.rows.len(), s.door.is_some())))
}
}
#[derive(Debug)]
enum Slot {
Plain(Row<'static>),
Entry {
section: usize,
index: usize,
nested: bool,
hit: Hit,
},
Door {
section: usize,
},
}
#[derive(Debug, Clone)]
pub struct ListLines {
headers: Vec<usize>,
rows: usize,
spaced: bool,
}
impl ListLines {
pub fn rows(&self) -> usize {
self.rows
}
pub fn headers(&self) -> usize {
self.headers.len()
}
pub fn line_of(&self, row: usize) -> usize {
if !self.spaced {
return row;
}
let above = self.headers.partition_point(|&h| h <= row);
let first_at_top = self.headers.first() == Some(&0);
row + above.saturating_sub(usize::from(first_at_top))
}
pub fn total(&self) -> usize {
match self.rows {
0 => 0,
n => self.line_of(n - 1) + 1,
}
}
pub fn first_row_from(&self, line: usize) -> usize {
let (mut lo, mut hi) = (0, self.rows);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self.line_of(mid) < line {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
}
pub fn row_on(&self, line: usize) -> Option<usize> {
let row = self.first_row_from(line);
(row < self.rows && self.line_of(row) == line).then_some(row)
}
}
pub fn place_of(path: &Path) -> PathBuf {
parent_location(path).unwrap_or_else(|| path.to_path_buf())
}
pub fn place_is_browsable(path: &Path) -> bool {
is_cloud_place(path)
|| is_object_store_url(path)
|| matches!(
crate::cloud::source::input_source(path),
crate::cloud::source::InputSource::Local(_)
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RowKey {
Header(String),
Entry(PathBuf),
Door(PathBuf),
Place(PathBuf),
More(String),
Hidden(String),
Up(String),
}
#[derive(Debug)]
pub struct HomeState {
pub sections: Vec<Section>,
pub filter: String,
pub filter_selected: bool,
pub search_limit: usize,
pub hide_unreadable: bool,
pub formats: std::sync::Arc<crate::formats::Registry>,
pub lake_here: Option<(PathBuf, &'static str)>,
pub selected: usize,
pub scroll: usize,
pub view_height: usize,
pub path_input_active: bool,
pub path_input: String,
pub path_listing: Option<PathListing>,
pub path_pick: Option<usize>,
pub browsing: Option<PathBuf>,
pub browse_start: Option<PathBuf>,
pub status: Option<String>,
pub network_check: fn(&Path) -> bool,
pub visits: std::collections::HashMap<PathBuf, crate::cache::Visits>,
pub newest_recent: Option<PathBuf>,
pub probes: Probes,
pub narrowed: Option<Narrowed>,
pub cloud_kinds: std::collections::HashMap<PathBuf, (EntryKind, crate::home::discover::Holds)>,
pub sort: SortMode,
pub listing_in_flight: bool,
pub measure_in_flight: bool,
pub classifying: std::collections::HashSet<PathBuf>,
pub peeking: std::collections::HashSet<PathBuf>,
pub peek_failed: std::collections::HashSet<PathBuf>,
pub enriched: std::collections::HashMap<PathBuf, Measured>,
pub stated: std::collections::HashMap<PathBuf, Stated>,
pub stat_epoch: u64,
pub stale: std::collections::HashSet<PathBuf>,
pub unapplied: std::collections::HashSet<PathBuf>,
pub folds: std::collections::HashMap<String, bool>,
pub folds_owed: bool,
pub search: SearchState,
pub known: Known,
pub cloud: Vec<CloudSource>,
pub catalogs: Vec<ShownCatalog>,
pub sized: std::collections::HashSet<PathBuf>,
pub web_gone: std::collections::HashMap<PathBuf, crate::error_display::HttpGone>,
pub missing: std::collections::HashSet<PathBuf>,
pub waiting_since: Option<std::time::Instant>,
pub recent_expanded: bool,
pub shown_whole: std::collections::HashSet<PathBuf>,
pub trail: Vec<Mark>,
pub returning: Option<RowKey>,
pub returning_line: Option<usize>,
pub landing: bool,
pub resting: bool,
pub rows_cache: RowsCache,
pub catalog_places: CatalogPlaces,
}
#[derive(Debug, Clone)]
pub struct Mark {
pub place: Option<PathBuf>,
pub key: Option<RowKey>,
pub filter: String,
pub search: Option<SearchState>,
pub line: usize,
}
#[derive(Debug, Clone, Default)]
pub struct SearchState {
pub root: Option<PathBuf>,
pub epoch: u64,
pub results: Vec<std::sync::Arc<[Entry]>>,
pub indexed: usize,
pub matches: Option<crate::home::search::Matches>,
pub scoring: bool,
pub scanned: usize,
pub running: bool,
pub done: bool,
pub limited: Option<String>,
}
const SCORE_INLINE_MAX: usize = 2_000;
const FRECENCY_LIFT: f64 = 3.0;
#[derive(Debug, Clone)]
pub struct ScoreJob {
pub epoch: u64,
pub results: Vec<std::sync::Arc<[Entry]>>,
pub query: String,
pub base: Option<crate::home::search::Matches>,
pub limit: usize,
}
impl SearchState {
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn set_results(&mut self, results: Vec<Entry>) {
self.indexed = results.len();
self.results = vec![results.into()];
self.matches = None;
}
pub fn files(&self) -> impl Iterator<Item = &Entry> {
self.results.iter().flat_map(|batch| batch.iter())
}
fn scored_for(&self, query: &str) -> bool {
self.matches
.as_ref()
.is_some_and(|m| m.query == query && m.upto == self.indexed)
}
fn base_for(&self, query: &str) -> (Option<&crate::home::search::Matches>, usize) {
match self.matches.as_ref() {
Some(m) if m.narrows_to(query) && m.upto <= self.indexed => {
(Some(m), m.ids.len() + self.indexed - m.upto)
}
_ => (None, self.indexed),
}
}
}
impl Default for HomeState {
fn default() -> Self {
Self {
sections: Vec::new(),
cloud: Vec::new(),
catalogs: Vec::new(),
sized: std::collections::HashSet::new(),
web_gone: Default::default(),
missing: Default::default(),
filter: String::new(),
search_limit: crate::config::SearchConfig::default().max_results,
rows_cache: RowsCache::default(),
catalog_places: CatalogPlaces::default(),
hide_unreadable: true,
formats: Default::default(),
lake_here: None,
selected: 0,
scroll: 0,
view_height: 0,
path_input_active: false,
path_input: String::new(),
path_listing: None,
path_pick: None,
filter_selected: false,
browsing: None,
browse_start: None,
status: None,
network_check: is_remote_path,
visits: Default::default(),
newest_recent: None,
sort: SortMode::default(),
listing_in_flight: false,
measure_in_flight: false,
classifying: Default::default(),
peeking: std::collections::HashSet::new(),
probes: Probes::default(),
narrowed: None,
cloud_kinds: std::collections::HashMap::new(),
peek_failed: std::collections::HashSet::new(),
waiting_since: None,
enriched: std::collections::HashMap::new(),
stated: Default::default(),
stat_epoch: 0,
stale: Default::default(),
unapplied: Default::default(),
folds: std::collections::HashMap::new(),
folds_owed: false,
search: SearchState::default(),
known: Default::default(),
recent_expanded: false,
shown_whole: Default::default(),
trail: Vec::new(),
returning: None,
returning_line: None,
landing: false,
resting: false,
}
}
}
pub type Known = std::sync::Arc<std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>>;
#[derive(Debug, Clone)]
pub struct ListingRequest {
pub recents: Vec<PathBuf>,
pub desktop_dirs: Vec<PathBuf>,
pub browsing: Option<PathBuf>,
pub probes: Probes,
pub narrowed: Option<Narrowed>,
pub network_check: fn(&Path) -> bool,
pub cloud: Vec<CloudSource>,
pub catalogs: Vec<ShownCatalog>,
pub known: Known,
pub formats: std::sync::Arc<crate::formats::Registry>,
}
#[derive(Debug, Clone, Default)]
pub struct Listing {
pub sections: Vec<Section>,
pub missing: std::collections::HashSet<PathBuf>,
}
impl Listing {
pub fn learn(
&mut self,
learned: &[(PathBuf, crate::cache::DatasetFacts)],
network_check: fn(&Path) -> bool,
) {
if learned.is_empty() {
return;
}
let learned: std::collections::HashMap<PathBuf, crate::cache::DatasetFacts> =
learned.iter().cloned().collect();
for row in self.sections.iter_mut().flat_map(|s| s.rows.iter_mut()) {
if known_facts(&learned, &row.path).is_some() {
apply_known_facts(row, &learned, network_check(&row.path));
}
}
}
pub fn alias_visits(
&self,
visits: &mut std::collections::HashMap<PathBuf, crate::cache::Visits>,
) {
let names: std::collections::HashSet<std::ffi::OsString> = visits
.keys()
.filter_map(|p| p.file_name().map(|n| n.to_os_string()))
.collect();
let mut aliases = Vec::new();
for row in self.sections.iter().flat_map(|s| &s.rows) {
let path = &row.path;
if visits.contains_key(path)
|| row.table.is_some()
|| !path.file_name().is_some_and(|n| names.contains(n))
|| is_network_path(path)
{
continue;
}
if let Some(v) = crate::canonical::canonicalize(path)
.ok()
.and_then(|canonical| visits.get(&canonical))
{
aliases.push((path.clone(), *v));
}
}
visits.extend(aliases);
}
}
pub fn look_into_as(entry: &Entry, as_read: &crate::formats::schema_union::ReadAs) -> Entry {
let mut probe = classify_row(entry);
measure_row(&mut probe, entry, as_read, None);
probe
}
fn classify_row(entry: &Entry) -> Entry {
let mut probe = entry.clone();
if probe.kind == EntryKind::Unknown && probe.path.is_dir() {
let (kind, holds) = discover::look_at_directory(&probe.path);
probe.kind = kind;
probe.holds = holds;
}
probe
}
fn stat_and_recall(
probe: &mut Entry,
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
) -> bool {
if discover::unstated(probe) && !discover::stat_row(probe) {
return false;
}
if probe.rows.is_none() && probe.columns.is_empty() && discover::on_disk(probe) {
apply_known_facts(probe, known, false);
}
true
}
fn measure_row(
probe: &mut Entry,
entry: &Entry,
as_read: &crate::formats::schema_union::ReadAs,
remembered: Option<&crate::cache::CacheManager>,
) {
discover::enrich_with(probe, as_read, remembered);
probe.size = probe.size.or(entry.size);
probe.modified = probe.modified.or(entry.modified);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reads {
Files,
StatOnly,
}
pub fn look_into_batch(
rows: Vec<Entry>,
cache: &crate::cache::CacheManager,
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
reads: Reads,
mut each: impl FnMut(PathBuf, Measured),
) {
let as_read = crate::formats::schema_union::ReadAs::default();
let answer = |probe: &Entry, entry: &Entry| Measured {
stat_only: reads == Reads::StatOnly,
..measured_from(probe, entry)
};
let classified: Vec<(Entry, Entry)> = rows
.into_iter()
.map(|entry| {
let mut probe = entry.clone();
if !stat_and_recall(&mut probe, known) {
return (probe, entry);
}
let probe = match reads {
Reads::Files => classify_row(&probe),
Reads::StatOnly => probe,
};
if probe.kind != entry.kind || probe.modified != entry.modified {
each(entry.path.clone(), answer(&probe, &entry));
}
(probe, entry)
})
.collect();
let mut facts = Vec::new();
for (mut probe, entry) in classified {
if reads == Reads::Files && probe.rows.is_none() && probe.columns.is_empty() {
measure_row(&mut probe, &entry, &as_read, Some(cache));
facts.extend(facts_for(&probe));
}
each(entry.path.clone(), answer(&probe, &entry));
}
cache.record_dataset_facts(&facts);
}
pub fn measured_from(probe: &Entry, original: &Entry) -> Measured {
Measured {
rows: probe.rows,
cols: probe.cols,
cols_sampled: probe.cols_sampled,
size: probe.size.or(original.size),
modified: probe.modified.or(original.modified),
stat_only: false,
columns: probe.columns.clone(),
kind: (probe.kind != original.kind).then_some(probe.kind),
holds: probe.holds.clone(),
cost: crate::home::discover::Cost {
source: None,
..probe.cost.clone()
},
}
}
#[derive(Debug, Clone)]
pub enum Probe {
Listing(Vec<Entry>),
Listed {
rows: std::sync::Arc<[Entry]>,
cut_short: bool,
},
Unreachable(Option<String>),
Silent(Vec<Entry>),
}
pub const NOT_ANSWERING: &str = "not answering · Ctrl+R retries";
#[derive(Debug, Clone, Default)]
pub struct Probes(std::collections::HashMap<PathBuf, Probe>);
impl Probes {
pub fn listed(&self, place: &Path) -> Option<&[Entry]> {
match self.0.get(place)? {
Probe::Listed { rows, .. } => Some(rows),
_ => None,
}
}
pub fn so_far(&self, place: &Path) -> Option<&[Entry]> {
match self.0.get(place)? {
Probe::Listing(rows) => Some(rows),
_ => None,
}
}
pub fn settled(&self, place: &Path) -> bool {
matches!(
self.0.get(place),
Some(Probe::Listed { .. } | Probe::Unreachable(_) | Probe::Silent(_))
)
}
pub fn silent(&self, place: &Path) -> bool {
matches!(self.0.get(place), Some(Probe::Silent(_)))
}
pub fn silent_places(&self) -> Vec<PathBuf> {
(self.0.iter())
.filter(|(_, probe)| matches!(probe, Probe::Silent(_)))
.map(|(place, _)| place.clone())
.collect()
}
pub fn takes_pages(&self, place: &Path) -> bool {
matches!(
self.0.get(place),
None | Some(Probe::Listing(_) | Probe::Silent(_))
)
}
pub fn go_silent(&mut self, place: &Path) {
let rows = match self.0.remove(place) {
Some(Probe::Listing(rows) | Probe::Silent(rows)) => rows,
None => Vec::new(),
Some(settled) => {
self.0.insert(place.to_path_buf(), settled);
return;
}
};
self.0.insert(place.to_path_buf(), Probe::Silent(rows));
}
pub fn cut_short(&self, place: &Path) -> bool {
matches!(
self.0.get(place),
Some(Probe::Listed {
cut_short: true,
..
})
)
}
pub fn unreachable(&self, place: &Path) -> bool {
matches!(self.0.get(place), Some(Probe::Unreachable(_)))
}
pub fn error(&self, place: &Path) -> Option<&str> {
match self.0.get(place)? {
Probe::Unreachable(why) => why.as_deref(),
_ => None,
}
}
fn rows(&self, place: &Path) -> Vec<Entry> {
match self.0.get(place) {
Some(Probe::Listed { rows, .. }) => rows.to_vec(),
Some(Probe::Listing(rows) | Probe::Silent(rows)) => {
let mut rows = rows.clone();
if is_object_store_url(place) {
rows.sort_by_key(|row| row.kind != EntryKind::Directory);
} else {
discover::sort_entries(&mut rows);
}
rows
}
_ => Vec::new(),
}
}
pub fn answered(&self) -> impl Iterator<Item = (&PathBuf, &[Entry])> {
self.0.iter().filter_map(|(place, probe)| match probe {
Probe::Listed { rows, .. } => Some((place, &rows[..])),
_ => None,
})
}
fn entry(&self, path: &Path) -> Option<Entry> {
self.answered()
.flat_map(|(_, rows)| rows.iter())
.find(|e| e.path == path)
.cloned()
}
pub fn read(&mut self, place: &Path, rows: &[Entry]) {
let probe = (self.0)
.entry(place.to_path_buf())
.or_insert(Probe::Listing(Vec::new()));
if let Probe::Silent(so_far) = probe {
*probe = Probe::Listing(std::mem::take(so_far));
}
if let Probe::Listing(so_far) = probe {
so_far.extend_from_slice(rows);
}
}
pub fn insert(&mut self, place: PathBuf, probe: Probe) {
self.0.insert(place, probe);
}
pub fn forget(&mut self, place: &Path) {
self.0.remove(place);
}
pub fn stopped(&mut self, place: &Path) {
if let Some(Probe::Listing(_) | Probe::Silent(_)) = self.0.get(place) {
self.0.remove(place);
}
}
fn listed_mut(&mut self, place: &Path) -> Option<&mut std::sync::Arc<[Entry]>> {
match self.0.get_mut(place)? {
Probe::Listed { rows, .. } => Some(rows),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Narrowed {
pub dir: PathBuf,
pub prefix: String,
pub rows: Vec<Entry>,
pub truncated: bool,
}
pub fn build_listing(request: &ListingRequest) -> Listing {
let ListingRequest {
recents,
desktop_dirs,
browsing,
probes,
narrowed,
network_check,
cloud,
catalogs,
known,
formats,
} = request;
let network_check = *network_check;
let mounts = crate::home::locality::Mounts::current();
let mut sections: Vec<Section> = Vec::new();
if let Some(id) = browsing.as_deref().and_then(cloud_source_id) {
sections.push(source_section(&id, cloud));
annotate(&mut sections, known, network_check, &mounts);
return Listing {
sections,
..Default::default()
};
}
if let Some(dir) = browsing.clone() {
let remote = network_check(&dir);
sections.push(browsed_section(
&dir,
remote,
&Listed {
probes,
narrowed: narrowed.as_ref(),
catalogs,
known,
formats,
},
));
annotate(&mut sections, known, network_check, &mounts);
return Listing {
sections,
..Default::default()
};
}
let recent_rows: Vec<Entry> = recents
.iter()
.filter(|p| {
network_check(p)
|| p.exists()
|| crate::formats::members::split(p).is_some()
|| crate::formats::members::split_variant(p, formats).is_some()
|| crate::formats::hf_splits::split_place(p).is_some()
})
.map(|p| {
if let Some(known) = probes.entry(p) {
return known;
}
if let Some(variant) = discover::variant_row(p, formats) {
return variant;
}
if !network_check(p)
&& let Some(split) = discover::split_row(p)
{
return split;
}
let mut entry = entry_for_path(p, network_check(p));
if !network_check(p) {
discover::name_unlisted_file(&mut entry, formats);
}
if let Some(dataset) = catalogs
.iter()
.flat_map(|c| &c.datasets)
.find(|d| d.location == *p)
{
entry.name = dataset.name.clone();
}
entry
})
.collect();
let mut elsewhere: Vec<Entry> = Vec::new();
let roots = HomeState::roots_with(desktop_dirs, network_check);
let mut root_sections: Vec<(RootOrigin, Section)> = Vec::new();
let mut cwd_listing: Option<(PathBuf, std::collections::HashSet<std::ffi::OsString>)> = None;
for root in roots {
if root.origin == RootOrigin::Desktop {
if root.available {
let mut entry = Entry::directory(&root.path);
entry.name = display_path(&root.path);
elsewhere.push(entry);
}
continue;
}
let mut truncated = false;
let rows = if root.network {
truncated = probes.cut_short(&root.path);
probes.rows(&root.path)
} else if root.available {
let scan = discover::scan_dir_specs(&root.path, formats);
truncated = scan.truncated;
scan.entries
} else {
Vec::new()
};
if root.origin == RootOrigin::Cwd {
let key = if root.network {
root.path.clone()
} else {
crate::canonical::canonicalize(&root.path).unwrap_or_else(|_| root.path.clone())
};
let names = rows
.iter()
.filter_map(|row| row.path.file_name().map(|n| n.to_os_string()))
.collect();
cwd_listing = Some((key, names));
}
let unreachable = root.network && probes.unreachable(&root.path);
let silent = root.network && probes.silent(&root.path);
let waiting = root.network && !probes.settled(&root.path);
let described = mounts.describe(&root.path);
let fstype = if described.network() {
described.fstype
} else {
"network".to_string()
};
let mut state: Vec<String> = Vec::new();
if truncated {
state.push(format!(
"first {}",
crate::numfmt::group_chrome(discover::MAX_ENTRIES_PER_DIR)
));
}
if root.network {
state.push(fstype);
}
root_sections.push((
root.origin,
Section {
subtitle: (!state.is_empty()).then(|| crate::glyphs::dotted(&state.join(" · "))),
origin: Some(root.origin.note()),
root: Some(root.path.clone()),
unavailable: !root.available || unreachable || silent,
unavailable_note: silent.then(|| NOT_ANSWERING.to_string()),
remote_root: root.network.then(|| root.path.clone()),
waiting,
..Section::titled(display_path(&root.path), rows)
},
));
}
let recent_rows: Vec<Entry> = match &cwd_listing {
None => recent_rows,
Some((cwd, names)) => recent_rows
.into_iter()
.filter(|row| {
let place = place_of(&row.path);
let place = if network_check(&place) {
place
} else {
crate::canonical::canonicalize(&place).unwrap_or(place)
};
place != *cwd || !row.path.file_name().is_some_and(|n| names.contains(n))
})
.collect(),
};
if !recent_rows.is_empty() {
let place_labels = place_labels(&recent_rows, known, network_check);
sections.push(Section {
grouped_by_place: true,
place_labels,
..Section::titled(HomeState::RECENT_SECTION, recent_rows)
});
}
sections.extend(root_sections.into_iter().map(|(_, s)| s));
sections.extend(cloud_section(cloud));
let mut missing = std::collections::HashSet::new();
for catalog in catalogs {
let mut section = catalog_section(catalog, network_check, &mut missing);
for row in section.rows.iter_mut().filter(|r| {
r.kind == EntryKind::File
&& r.format_spec.is_none()
&& r.table.is_none()
&& !network_check(&r.path)
&& !discover::is_data_file(&r.path)
}) {
if let Some(spec) = formats.by_glob(&row.path, false).first() {
discover::name_spec_file(row, spec);
}
}
sections.push(section);
}
if !elsewhere.is_empty() {
sections.push(Section {
folded_by_default: true,
..Section::titled("Elsewhere", elsewhere)
});
}
annotate(&mut sections, known, network_check, &mounts);
Listing { sections, missing }
}
fn source_section(id: &str, cloud: &[CloudSource]) -> Section {
let source = cloud.iter().find(|s| s.id == id);
let rows = source
.map(|s| s.buckets.iter().map(|b| bucket_entry(b)).collect())
.unwrap_or_default();
let failure = source.and_then(|s| match &s.status {
CloudStatus::Failed { short, .. } => Some(short.clone()),
_ => None,
});
Section {
subtitle: source.map(|s| s.note.clone()).filter(|n| !n.is_empty()),
unavailable: source.is_none() || failure.is_some(),
unavailable_note: if source.is_none() {
Some("source not found".to_string())
} else {
failure
},
waiting: source.is_some_and(|s| s.busy()),
..Section::titled(
source.map_or_else(|| id.to_string(), |s| s.label.clone()),
rows,
)
}
}
fn cloud_section(cloud: &[CloudSource]) -> Option<Section> {
(!cloud.is_empty()).then(|| {
Section::titled(
HomeState::CLOUD_SECTION.to_string(),
cloud.iter().map(source_entry).collect(),
)
})
}
pub(crate) struct Listed<'a> {
pub probes: &'a Probes,
pub narrowed: Option<&'a Narrowed>,
pub catalogs: &'a [ShownCatalog],
pub known: &'a std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
pub formats: &'a crate::formats::Registry,
}
fn browsed_section(dir: &Path, remote: bool, listed: &Listed) -> Section {
let Listed {
probes,
narrowed,
catalogs,
known,
formats,
} = *listed;
let dir = dir.to_path_buf();
let so_far = remote && probes.so_far(&dir).is_some();
let database = !remote && dir.is_file();
let (mut rows, truncated) = if remote {
(probes.rows(&dir), probes.cut_short(&dir))
} else if database {
let tables = discover::database_rows(&dir);
let rows = if tables.is_empty() {
discover::variant_rows(&dir, formats)
} else {
tables
};
(rows, false)
} else {
let scan = discover::scan_dir_specs(&dir, formats);
let mut rows = discover::split_rows(&dir);
rows.extend(scan.entries);
(rows, scan.truncated)
};
let narrowed = narrowed.filter(|n| remote && truncated && n.dir == dir);
if let Some(narrowed) = narrowed {
let listed: std::collections::HashSet<PathBuf> =
rows.iter().map(|row| row.path.clone()).collect();
rows.extend(
narrowed
.rows
.iter()
.filter(|row| !listed.contains(&row.path))
.cloned(),
);
}
let subtitle = if so_far {
Some(format!(
"{} so far",
crate::numfmt::group_chrome(rows.len())
))
} else if truncated {
let first = format!(
"first {}",
crate::numfmt::group_chrome(discover::MAX_ENTRIES_PER_DIR)
);
Some(match narrowed {
Some(n) => format!(
"{first} + {}{} {}*",
crate::numfmt::group_chrome(n.rows.len()),
if n.truncated { "+" } else { "" },
n.prefix
),
None => first,
})
} else {
None
};
let unavailable = remote && (probes.unreachable(&dir) || probes.silent(&dir));
let mut door = (!database)
.then(|| whole_directory_row(&dir, &rows, remote))
.flatten();
if !remote
&& let Some(door) = door.as_mut()
&& let Ok(meta) = std::fs::metadata(&dir)
{
door.modified = meta.modified().ok();
apply_known_facts(door, known, false);
door.modified = None;
door.name = door_name(door, &rows);
}
let title = {
let text = dir.to_string_lossy();
if let Some(dataset) = catalogs
.iter()
.flat_map(|c| c.datasets.iter())
.find(|d| is_object_store_url(&d.location) && same_place(&d.location, &dir))
{
dataset.name.clone()
} else if let Some((_, account)) = cloud_account(&dir) {
account
} else if let Some((_, container, key)) = crate::cloud::source::azure_parts(&text) {
format!("{container}/{}", key.trim_matches('/'))
.trim_end_matches('/')
.to_string()
} else {
match crate::cloud::source::split_source_id(&text) {
(Some(_), plain) => plain.into_owned(),
(None, _) => display_path(&dir),
}
}
};
Section {
subtitle,
root: Some(dir.clone()),
unavailable,
unavailable_note: probes
.error(&dir)
.map(str::to_string)
.or_else(|| probes.silent(&dir).then(|| NOT_ANSWERING.to_string())),
waiting: so_far,
door,
..Section::titled(title, rows)
}
}
pub fn index_key(path: &Path) -> PathBuf {
let text = path.to_string_lossy();
if let Some((account, container, key)) = crate::cloud::source::azure_parts(&text) {
return PathBuf::from(crate::cloud::source::azure_url(&account, &container, &key));
}
match crate::cloud::source::split_source_id(&text) {
(Some(_), plain) => PathBuf::from(plain.into_owned()),
(None, _) => path.to_path_buf(),
}
}
fn known_facts<'a>(
known: &'a std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
path: &Path,
) -> Option<&'a crate::cache::DatasetFacts> {
known.get(path).or_else(|| known.get(&index_key(path)))
}
fn place_labels(
rows: &[Entry],
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
network_check: fn(&Path) -> bool,
) -> std::collections::HashMap<PathBuf, String> {
let mut labels = std::collections::HashMap::new();
for row in rows {
let place = place_of(&row.path);
if labels.contains_key(&place) {
continue;
}
let Some(facts) = known_facts(known, &place) else {
continue;
};
if facts.classified_by != crate::home::discover::CLASSIFIER_VERSION {
continue;
}
if !network_check(&place) {
let same_mtime = std::fs::metadata(&place)
.and_then(|m| m.modified())
.ok()
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.is_some_and(|d| d.as_secs() == facts.mtime);
if !same_mtime {
continue;
}
}
let Some(kind) = facts.kind else {
continue;
};
let mut probe = Entry::directory(&place);
probe.kind = kind;
probe.holds = facts.holds.clone();
let label = probe.label();
if !label.is_empty() && !label.starts_with("dir") {
labels.insert(place, label.into_owned());
}
}
labels
}
fn annotate(
sections: &mut [Section],
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
network_check: fn(&Path) -> bool,
mounts: &crate::home::locality::Mounts,
) {
for section in sections {
for row in &mut section.rows {
if is_cloud_place(&row.path) {
row.cost.source = Some("cloud".to_string());
continue;
}
let remote = network_check(&row.path);
if !remote && discover::unstated(row) && known_facts(known, &row.path).is_some() {
discover::stat_row(row);
}
apply_known_facts(row, known, remote);
row.cost.source = Some(mounts.describe(&row.path).fstype);
}
if let Some(door) = section.door.as_mut()
&& !is_cloud_place(&door.path)
{
door.cost.source = Some(mounts.describe(&door.path).fstype);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stated {
pub size: Option<u64>,
pub modified: Option<std::time::SystemTime>,
pub epoch: u64,
}
fn fold_known(
row: &mut Entry,
enriched: &std::collections::HashMap<PathBuf, Measured>,
stated: &std::collections::HashMap<PathBuf, Stated>,
stale: &std::collections::HashSet<PathBuf>,
) -> bool {
let mut any = false;
if stale.contains(&row.path) {
forget_counts(row);
any = true;
}
if let Some(m) = enriched.get(&row.path) {
fold_measured(row, m);
any = true;
}
if let Some(stat) = stated.get(&row.path) {
if stat.size.is_some() {
row.size = stat.size;
}
if stat.modified.is_some() {
row.modified = stat.modified;
}
any = true;
}
any
}
fn forget_counts(row: &mut Entry) {
row.measured = false;
row.rows = None;
row.cols = None;
row.cols_sampled = false;
row.columns.clear();
take_cost(row, &discover::Cost::default());
}
#[derive(PartialEq)]
struct Standing {
hidden: bool,
dataset: bool,
sorted_by: Option<u64>,
}
impl Standing {
fn of(row: &Entry, sort: SortMode) -> Self {
Standing {
hidden: row.hidden_by_default(),
dataset: row.kind.is_dataset() || row.kind.is_lake_table(),
sorted_by: match sort {
SortMode::Size => Some(row.size.unwrap_or(0)),
SortMode::Rows => Some(row.rows.unwrap_or(0) as u64),
SortMode::Modified => Some(
(row.modified)
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |d| d.as_secs()),
),
SortMode::Natural => None,
},
}
}
}
fn fold_measured(row: &mut Entry, m: &Measured) {
row.measured = true;
row.rows = m.rows;
row.cols = m.cols;
row.cols_sampled = m.cols_sampled;
if let Some(kind) = m.kind {
row.kind = kind;
}
if m.size.is_some() {
row.size = m.size;
}
if m.modified.is_some() {
row.modified = m.modified;
}
if !m.columns.is_empty() && row.columns != m.columns {
row.columns.clone_from(&m.columns);
}
if !m.holds.is_empty() && row.holds != m.holds {
row.holds.clone_from(&m.holds);
}
take_cost(row, &m.cost);
}
fn take_cost(row: &mut Entry, cost: &discover::Cost) {
let source = row.cost.source.take();
let variants = row.cost.tables.filter(|_| row.format_spec.is_some());
row.cost = cost.clone();
row.cost.source = source;
if variants.is_some() {
row.cost.tables = variants;
}
}
fn apply_known_facts(
row: &mut Entry,
known: &std::collections::HashMap<PathBuf, crate::cache::DatasetFacts>,
remote: bool,
) {
let Some(facts) = known_facts(known, &row.path) else {
return;
};
if !remote
&& matches!(row.kind, EntryKind::Unknown | EntryKind::MultiFile)
&& facts.classified_by == crate::home::discover::CLASSIFIER_VERSION
&& let Some(kind) = facts.kind
{
let same_mtime = row
.modified
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.is_some_and(|d| d.as_secs() == facts.mtime);
if same_mtime {
row.kind = kind;
if row.holds.is_empty() {
row.holds = facts.holds.clone();
}
}
}
if !remote {
let same_bytes = row.size.map(|s| s == facts.size).unwrap_or(false)
&& row
.modified
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() == facts.mtime)
.unwrap_or(false);
if !same_bytes {
return;
}
}
row.rows = facts.rows;
row.cols = facts.cols;
row.cols_sampled = facts.cols_sampled;
if !facts.columns.is_empty() {
row.columns = facts.columns.clone();
}
take_cost(row, &facts.cost);
if remote {
if facts.size > 0 {
row.size = row.size.or(Some(facts.size));
}
if row.kind == EntryKind::Unknown
&& facts.classified_by == crate::home::discover::CLASSIFIER_VERSION
&& let Some(kind) = facts.kind
{
row.kind = kind;
if row.holds.is_empty() {
row.holds = facts.holds.clone();
}
}
}
}
pub fn facts_for(entry: &Entry) -> Option<(PathBuf, crate::cache::DatasetFacts)> {
let size = entry.size?;
let mtime = entry
.modified?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let cost = crate::home::discover::Cost {
source: None,
..entry.cost.clone()
};
if entry.rows.is_none() && entry.columns.is_empty() && cost == Default::default() {
return None; }
Some((
entry.path.clone(),
crate::cache::DatasetFacts {
mtime,
size,
rows: entry.rows,
cols: entry.cols,
cols_sampled: entry.cols_sampled,
columns: entry.columns.clone(),
kind: Some(entry.kind),
holds: entry.holds.clone(),
classified_by: crate::home::discover::CLASSIFIER_VERSION,
cost: crate::home::discover::Cost {
source: None,
..entry.cost.clone()
},
},
))
}
pub fn match_score(filter: &str, entry: &Entry) -> Option<i32> {
match crate::home::fuzzy::best_match(filter, &entry.name) {
Some(m) => Some(m.score),
None => matching_column(filter, entry).map(|_| -COLUMN_MATCH_PENALTY),
}
}
pub fn match_hit(filter: &str, entry: &Entry) -> Option<Hit> {
Needle::new(filter).hit(entry)
}
struct Needle<'a> {
filter: &'a str,
lower: String,
}
impl<'a> Needle<'a> {
fn new(filter: &'a str) -> Self {
Needle {
filter,
lower: filter.to_lowercase(),
}
}
fn hit(&self, entry: &Entry) -> Option<Hit> {
let named = crate::home::fuzzy::best_match_with(self.filter, &entry.name, |score, _| Hit {
score,
column: None,
});
if named.is_some() {
return named;
}
Some(Hit {
score: -COLUMN_MATCH_PENALTY,
column: Some(self.column(entry)?),
})
}
fn column(&self, entry: &Entry) -> Option<usize> {
if self.filter.is_empty() {
return None;
}
(entry.columns.iter()).position(|c| contains_folded(c, &self.lower))
}
}
fn contains_folded(haystack: &str, lower: &str) -> bool {
if haystack.is_ascii() && lower.is_ascii() {
let (hay, needle) = (haystack.as_bytes(), lower.as_bytes());
return needle.is_empty()
|| hay
.windows(needle.len())
.any(|w| w.eq_ignore_ascii_case(needle));
}
haystack.to_lowercase().contains(lower)
}
const COLUMN_MATCH_PENALTY: i32 = 1_000_000;
pub fn matching_column<'a>(filter: &str, entry: &'a Entry) -> Option<&'a str> {
(Needle::new(filter).column(entry)).map(|i| entry.columns[i].as_str())
}
pub fn fuzzy_positions(needle: &str, haystack: &str) -> Vec<usize> {
crate::home::fuzzy::best_match(needle, haystack)
.map(|m| m.positions)
.unwrap_or_default()
}
pub fn substring_positions(needle: &str, haystack: &str) -> Vec<usize> {
if needle.is_empty() {
return Vec::new();
}
let hay: Vec<char> = haystack.to_lowercase().chars().collect();
let need: Vec<char> = needle.to_lowercase().chars().collect();
if need.len() > hay.len() {
return Vec::new();
}
for start in 0..=(hay.len() - need.len()) {
if hay[start..start + need.len()] == need[..] {
return (start..start + need.len()).collect();
}
}
Vec::new()
}
pub fn fuzzy_score(needle: &str, haystack: &str) -> Option<i32> {
crate::home::fuzzy::best_match(needle, haystack).map(|m| m.score)
}
impl HomeState {
pub fn roots(desktop_dirs: &[PathBuf]) -> Vec<Root> {
Self::roots_with(desktop_dirs, is_remote_path)
}
pub fn roots_with(desktop_dirs: &[PathBuf], is_network: fn(&Path) -> bool) -> Vec<Root> {
let mut roots: Vec<Root> = Vec::new();
let mut seen: Vec<PathBuf> = Vec::new();
let push =
|path: PathBuf, origin: RootOrigin, roots: &mut Vec<Root>, seen: &mut Vec<PathBuf>| {
let network = is_network(&path);
let key = if network {
path.clone()
} else {
crate::canonical::canonicalize(&path).unwrap_or_else(|_| path.clone())
};
if seen.contains(&key) {
return;
}
seen.push(key);
let available = if network {
true } else {
std::fs::read_dir(&path).is_ok()
};
roots.push(Root {
path,
origin,
available,
network,
});
};
if let Ok(cwd) = std::env::current_dir() {
push(cwd, RootOrigin::Cwd, &mut roots, &mut seen);
}
for dir in desktop_dirs {
push(dir.clone(), RootOrigin::Desktop, &mut roots, &mut seen);
}
roots
}
pub fn rebuild(&mut self, recents: &[PathBuf]) {
self.rebuild_with(recents, &[])
}
pub fn rebuild_with(&mut self, recents: &[PathBuf], desktop_dirs: &[PathBuf]) {
let request = ListingRequest {
recents: recents.to_vec(),
desktop_dirs: desktop_dirs.to_vec(),
browsing: self.browsing.clone(),
probes: self.probes.clone(),
narrowed: self.narrowed.clone(),
network_check: self.network_check,
cloud: self.cloud.clone(),
catalogs: self.catalogs.clone(),
known: Default::default(),
formats: self.formats.clone(),
};
let listing = build_listing(&request);
self.apply_listing(listing);
}
pub fn apply_listing(&mut self, listing: Listing) {
let mut listing = listing;
for section in &mut listing.sections {
name_by_spec(&self.formats, &mut section.rows);
}
self.missing = listing.missing;
self.replace_sections(|home| home.sections = listing.sections);
}
pub fn relist_remote(&mut self, root: &Path) {
let browsed = self.browsing.as_deref() == Some(root)
&& (self.network_check)(root)
&& cloud_source_id(root).is_none();
let at = (self.sections.iter()).position(|s| {
s.root.as_deref() == Some(root) && (browsed || s.remote_root.as_deref() == Some(root))
});
let Some(at) = at else {
return;
};
let mut section = if browsed {
browsed_section(
root,
true,
&Listed {
probes: &self.probes,
narrowed: self.narrowed.as_ref(),
catalogs: &self.catalogs,
known: &self.known,
formats: &self.formats,
},
)
} else {
Section::titled("", self.probes.rows(root))
};
let mounts = crate::home::locality::Mounts::cached();
annotate(
std::slice::from_mut(&mut section),
&self.known,
self.network_check,
&mounts,
);
name_by_spec(&self.formats, &mut section.rows);
let silent = self.probes.silent(root);
let unreachable = self.probes.unreachable(root);
let waiting = !self.probes.settled(root);
self.replace_sections(|home| {
if browsed {
home.sections[at] = section;
return;
}
let shown = &mut home.sections[at];
shown.rows = section.rows;
shown.waiting = waiting;
shown.unavailable = silent || unreachable;
shown.unavailable_note = silent.then(|| NOT_ANSWERING.to_string());
});
}
pub fn set_cloud(&mut self, cloud: Vec<CloudSource>) {
self.cloud = cloud;
let mounts = crate::home::locality::Mounts::cached();
let annotated = |home: &Self, mut section: Section| {
annotate(
std::slice::from_mut(&mut section),
&home.known,
home.network_check,
&mounts,
);
section
};
match self.browsing.as_deref().map(cloud_source_id) {
Some(Some(id)) => {
let section = annotated(self, source_section(&id, &self.cloud));
self.replace_sections(|home| home.sections = vec![section]);
}
Some(None) => {}
None => {
let section = cloud_section(&self.cloud).map(|s| annotated(self, s));
self.replace_sections(|home| {
let at = (home.sections.iter()).position(|s| s.title == Self::CLOUD_SECTION);
match (at, section) {
(Some(at), Some(section)) => home.sections[at] = section,
(Some(at), None) => {
home.sections.remove(at);
}
(None, Some(section)) => {
let at = (home.sections.iter())
.position(|s| {
s.origin.is_some_and(is_catalog_origin)
|| s.title == "Elsewhere"
|| s.title == Self::SEARCH_SECTION
})
.unwrap_or(home.sections.len());
home.sections.insert(at, section);
}
(None, None) => {}
}
});
}
}
}
fn replace_sections(&mut self, change: impl FnOnce(&mut Self)) {
let returning = self.returning.take();
let previous = returning.clone().or_else(|| self.selected_key());
let line = self.selected.saturating_sub(self.scroll);
change(self);
self.changed();
if let (Some(browsing), Some((dir, format))) = (&self.browsing, &self.lake_here)
&& browsing == dir
&& let Some(section) = self.sections.first_mut()
{
let note = crate::glyphs::dotted(&format!(
"{} · not read as a table",
format.to_ascii_lowercase()
));
section.subtitle = Some(match section.subtitle.take() {
Some(state) => crate::glyphs::dotted(&format!("{note} · {state}")),
None => note,
});
}
self.sync_search_section();
self.apply_measurements();
let placed = self.reselect(previous);
if placed && returning.is_some() {
self.landing = false;
}
if !placed {
self.select_first_entry();
if self.rows_still_arriving() {
self.returning = returning;
}
} else if returning.is_some() {
self.scroll_to_returning_line();
} else {
self.scroll = self.selected.saturating_sub(line);
}
self.follow_selection();
}
pub fn leave_mark(&mut self) {
let mark = Mark {
place: self.browsing.clone(),
key: self.selected_key(),
filter: self.filter.clone(),
search: (!self.search.running).then(|| SearchState {
scoring: false,
..self.search.clone()
}),
line: self.selected.saturating_sub(self.scroll),
};
self.trail.retain(|m| m.place != mark.place);
self.trail.push(mark);
}
pub fn come_back(&mut self, from: Option<PathBuf>) {
let to = self.browsing.clone();
let mark = self
.trail
.iter()
.rposition(|m| m.place == to)
.map(|at| self.trail.split_off(at).remove(0));
match mark {
Some(mark) => {
self.filter = mark.filter;
self.search = mark.search.unwrap_or_default();
self.returning = mark.key;
self.returning_line = Some(mark.line);
}
None => {
self.filter.clear();
self.search.reset();
self.returning = from.map(RowKey::Entry);
self.returning_line = None;
}
}
}
fn rows_still_arriving(&self) -> bool {
self.listing_in_flight
|| self.sections_waiting()
|| self.awaiting_listing().is_some()
|| self.search.running
}
fn settle_return(&mut self) {
let Some(key) = self.returning.clone() else {
return;
};
if let Some(idx) = self.place_key(&key) {
self.selected = idx;
self.returning = None;
self.landing = false;
self.scroll_to_returning_line();
self.follow_selection();
} else if !self.rows_still_arriving() {
self.returning = None;
}
}
fn scroll_to_returning_line(&mut self) {
if let Some(line) = self.returning_line.take() {
self.scroll = self.selected.saturating_sub(line);
}
}
pub fn selected_key(&self) -> Option<RowKey> {
let title = |section: usize| self.sections.get(section).map(|s| s.title.clone());
Some(match self.cursor_row()? {
Row::Header { section, .. } => RowKey::Header(title(section)?),
Row::More { section, .. } => RowKey::More(title(section)?),
Row::Hidden { section, .. } => RowKey::Hidden(title(section)?),
Row::Up { section } => RowKey::Up(title(section)?),
Row::Entry { entry, .. } => RowKey::Entry(entry.path.clone()),
Row::Door { entry, .. } => RowKey::Door(entry.path.clone()),
Row::Place { path, .. } => RowKey::Place(path),
})
}
pub fn reselect(&mut self, key: Option<RowKey>) -> bool {
let Some(key) = key else {
self.clamp_selection();
return false;
};
match self.place_key(&key) {
Some(idx) => {
self.selected = idx;
true
}
None => {
self.clamp_selection();
false
}
}
}
fn place_key(&mut self, key: &RowKey) -> Option<usize> {
if let Some(found) = self.listed(key) {
return Some(found);
}
if let RowKey::Entry(path) = key
&& let Some(root) = self
.sections
.iter()
.find(|s| !s.grouped_by_place && s.rows.iter().any(|r| r.path == *path))
.and_then(|s| s.root.clone())
&& self.shown_whole.insert(root.clone())
{
match self.listed(key) {
Some(found) => return Some(found),
None => {
self.shown_whole.remove(&root);
}
}
}
self.behind_recent_cap(key)
}
fn listed(&self, key: &RowKey) -> Option<usize> {
self.position(|row| match (row, key) {
(Row::Entry { entry, .. }, RowKey::Entry(path)) => entry.path == *path,
(Row::Door { entry, .. }, RowKey::Door(path)) => entry.path == *path,
(Row::Place { path, .. }, RowKey::Place(wanted)) => path == wanted,
(Row::Header { section, .. }, RowKey::Header(title))
| (Row::More { section, .. }, RowKey::More(title))
| (Row::Hidden { section, .. }, RowKey::Hidden(title))
| (Row::Up { section }, RowKey::Up(title)) => self
.sections
.get(*section)
.is_some_and(|s| s.title == *title),
_ => false,
})
}
fn behind_recent_cap(&self, key: &RowKey) -> Option<usize> {
let (RowKey::Entry(path) | RowKey::Place(path)) = key else {
return None;
};
self.position(|row| {
matches!(row, Row::More { section, .. }
if self.sections.get(*section).is_some_and(|s| {
s.grouped_by_place
&& s.rows
.iter()
.any(|r| r.path == *path || place_of(&r.path) == *path)
}))
})
}
pub fn set_view_height(&mut self, height: usize) {
if height == self.view_height {
return;
}
let key = self.selected_key();
self.view_height = height;
self.reselect(key);
}
fn follow_selection(&mut self) {
let rows = self.row_count();
self.scroll = settle_top(self.scroll, self.selected, self.view_height, rows);
}
fn section_folded(&self, section: &Section) -> bool {
if self.browsing.is_some() {
return false;
}
self.folds
.get(§ion.title)
.copied()
.unwrap_or(section.folded_by_default)
}
pub fn is_collapsed(&self, section: usize) -> bool {
self.sections
.get(section)
.is_some_and(|s| self.section_folded(s))
}
pub fn toggle_collapsed(&mut self, section: usize) {
let folded = self.is_collapsed(section);
self.set_collapsed(section, !folded);
}
pub fn set_collapsed(&mut self, section: usize, collapsed: bool) {
if self.browsing.is_some() {
return;
}
let Some(title) = self.sections.get(section).map(|s| s.title.clone()) else {
return;
};
self.folds.insert(title, collapsed);
}
pub fn jump_section(&mut self, delta: isize) {
self.resting = false;
self.returning = None;
self.landing = false;
let headers = self.view().headers.clone();
if headers.is_empty() {
return;
}
let current = self.selected;
self.selected = if delta > 0 {
headers
.iter()
.copied()
.find(|&h| h > current)
.unwrap_or(headers[0])
} else {
headers
.iter()
.rev()
.copied()
.find(|&h| h < current)
.unwrap_or(*headers.last().unwrap())
};
}
pub fn has_any_dataset(&self) -> bool {
self.view().has_dataset
}
pub const SEARCH_SECTION: &'static str = "Found";
pub const CLOUD_SECTION: &'static str = "Cloud";
pub const RECENT_SECTION: &'static str = "Recent";
pub fn cloud_source_of(&self, path: &Path) -> Option<&CloudSource> {
if let Some(id) = cloud_source_id(path) {
return self.cloud.iter().find(|s| s.id == id);
}
if let Some((id, _)) = cloud_account(path) {
return self.cloud.iter().find(|s| s.id == id);
}
let text = path.to_string_lossy();
if let Some((account, _, _)) = crate::cloud::source::azure_parts(&text) {
return self.azure_account_place(&account).and_then(|place| {
cloud_account(&place).and_then(|(id, _)| self.cloud.iter().find(|s| s.id == id))
});
}
if let (Some(id), _) = crate::cloud::source::split_source_id(&text) {
return self.cloud.iter().find(|s| s.id == id);
}
if let Some(project) =
Self::google_bucket_root(path).and_then(|b| self.project_of_bucket(&b))
{
return cloud_account(&project)
.and_then(|(id, _)| self.cloud.iter().find(|s| s.id == id));
}
let (_, plain) = crate::cloud::source::split_source_id(&text);
let (scheme, rest) = plain.split_once("://")?;
let bucket = rest.split('/').next()?;
let root = PathBuf::from(format!("{scheme}://{bucket}"));
self.cloud.iter().find(|s| s.buckets.contains(&root))
}
pub fn parent_of(&self, path: &Path) -> Option<PathBuf> {
if cloud_source_id(path).is_some() {
return None;
}
if let Some((_, dataset)) = self.remote_dataset_of(path) {
let place = &dataset.location;
if same_place(path, place) {
return None;
}
let up = self.parent_within(path)?;
return Some(if same_place(&up, place) {
place.clone()
} else {
up
});
}
if let Some((id, _)) = cloud_account(path) {
return Some(cloud_place(&id));
}
let text = path.to_string_lossy();
if let Some((account, container, key)) = crate::cloud::source::azure_parts(&text) {
let key = key.trim_matches('/');
if key.is_empty() {
return self.azure_account_place(&account);
}
let up = key.rsplit_once('/').map(|(up, _)| up).unwrap_or("");
let up = if up.is_empty() {
String::new()
} else {
format!("{up}/")
};
return Some(PathBuf::from(crate::cloud::source::azure_url(
&account, &container, &up,
)));
}
if is_bucket_root(path) {
if let Some(project) = self.project_of_bucket(path) {
return Some(project);
}
return self.cloud_source_of(path).map(|s| cloud_place(&s.id));
}
parent_location(path)
}
fn parent_within(&self, path: &Path) -> Option<PathBuf> {
let text = path.to_string_lossy();
if let Some((account, container, key)) = crate::cloud::source::azure_parts(&text) {
let key = key.trim_matches('/');
let up = key.rsplit_once('/').map(|(up, _)| up).unwrap_or("");
let up = if up.is_empty() {
String::new()
} else {
format!("{up}/")
};
return Some(PathBuf::from(crate::cloud::source::azure_url(
&account, &container, &up,
)));
}
parent_location(path)
}
fn remote_dataset_of(&self, path: &Path) -> Option<(&ShownCatalog, &ShownDataset)> {
if !is_object_store_url(path) {
return None;
}
let text = path.to_string_lossy();
self.catalogs
.iter()
.flat_map(|c| c.datasets.iter().map(move |d| (c, d)))
.filter(|(_, d)| {
is_object_store_url(&d.location) && within(&text, &d.location.to_string_lossy())
})
.rev()
.max_by_key(|(_, d)| d.location.to_string_lossy().trim_end_matches('/').len())
}
pub fn catalog_dataset(&self, path: &Path) -> Option<(&ShownCatalog, &ShownDataset)> {
let places = &self.catalog_places;
if places.indexes(&self.catalogs) {
let key = place_key(path);
let &(c, d) = places.datasets.get(&key)?;
let catalog = self.catalogs.get(c)?;
if let Some(dataset) = catalog.datasets.get(d)
&& place_key(&dataset.location) == key
{
return Some((catalog, dataset));
}
}
self.catalogs
.iter()
.flat_map(|c| c.datasets.iter().map(move |d| (c, d)))
.find(|(_, d)| same_place(&d.location, path))
}
pub fn bookmark(&self, path: &Path) -> Option<(&ShownDataset, &str)> {
let places = &self.catalog_places;
if places.indexes(&self.catalogs) {
let key = place_key(path);
let &(c, d, b) = places.bookmarks.get(&key)?;
if let Some(dataset) = self.catalogs.get(c).and_then(|c| c.datasets.get(d))
&& let Some((name, place)) = dataset.bookmarks.get(b)
&& place_key(place) == key
{
return Some((dataset, name.as_str()));
}
}
self.catalogs
.iter()
.flat_map(|c| c.datasets.iter())
.find_map(|d| {
d.bookmarks
.iter()
.find(|(_, place)| same_place(place, path))
.map(|(name, _)| (d, name.as_str()))
})
}
pub fn size_hint(&self, path: &Path) -> Option<u64> {
self.catalog_dataset(path).and_then(|(_, d)| d.size)
}
fn azure_account_place(&self, account: &str) -> Option<PathBuf> {
self.cloud
.iter()
.flat_map(|s| s.buckets.iter())
.find(|place| cloud_account(place).is_some_and(|(_, a)| a == account))
.cloned()
}
pub fn cloud_look(&self, entry: &Entry) -> Option<CloudLook> {
if entry.kind != EntryKind::Directory
|| !entry.holds.is_empty()
|| !is_object_store_url(&entry.path)
|| is_cloud_place(&entry.path)
|| object_place_label(&entry.path).is_some()
{
return None;
}
if self.peeking.contains(&entry.path) {
return Some(CloudLook::Looking);
}
if self.peek_failed.contains(&entry.path) {
return Some(CloudLook::Failed);
}
match self.cloud_kinds.get(&entry.path) {
None => Some(CloudLook::Waiting),
Some((kind, holds)) if *kind != EntryKind::Directory || !holds.is_empty() => {
Some(CloudLook::Looking)
}
Some(_) => None,
}
}
pub fn place_kind(&self, path: &Path) -> Option<&'static str> {
if self.missing.contains(path) {
return Some("missing");
}
if let Some((id, _)) = cloud_account(path) {
return self
.cloud
.iter()
.any(|s| s.id == id && s.api == crate::cloud::source::ProviderKind::Gcs)
.then_some("project");
}
(is_object_store_url(path)
&& (self
.catalog_dataset(path)
.is_some_and(|(_, d)| is_object_store_url(&d.location))
|| (self.browsing.is_none() && self.bookmark(path).is_some())))
.then_some("dataset")
}
fn project_of_bucket(&self, bucket_root: &Path) -> Option<PathBuf> {
let root = bucket_root.to_string_lossy();
let root = root.trim_end_matches('/');
self.probes
.answered()
.filter(|(place, _)| cloud_account(place).is_some())
.find(|(_, rows)| {
rows.iter()
.any(|row| row.path.to_string_lossy().trim_end_matches('/') == root)
})
.map(|(place, _)| place.clone())
}
fn google_bucket_root(path: &Path) -> Option<PathBuf> {
let text = path.to_string_lossy();
let rest = text
.strip_prefix("gs://")
.or_else(|| text.strip_prefix("gcs://"))?;
let bucket = rest.split('/').next().filter(|b| !b.is_empty())?;
Some(PathBuf::from(format!("gs://{bucket}")))
}
pub fn place_details(&self, path: &Path) -> Option<&[(String, String)]> {
self.cloud
.iter()
.find_map(|s| s.place_details.get(path))
.or_else(|| self.catalog_dataset(path).map(|(_, d)| &d.details))
.or_else(|| self.bookmark(path).map(|(d, _)| &d.details))
.map(Vec::as_slice)
}
pub fn location_label(&self, path: &Path) -> String {
let sep = crate::glyphs::get().trail;
if let Some((catalog, dataset)) = self.remote_dataset_of(path) {
let text = path.to_string_lossy();
let rest = within_rest(&text, &dataset.location.to_string_lossy());
let mut parts = vec![catalog.label.clone(), dataset.name.clone()];
parts.extend(
rest.split('/')
.filter(|p| !p.is_empty())
.map(str::to_string),
);
return parts.join(&format!(" {sep} "));
}
if let Some(source) = self.cloud_source_of(path) {
let mut parts = vec!["cloud".to_string(), source.label.clone()];
let text = path.to_string_lossy();
if let Some((_, account)) = cloud_account(path) {
parts.push(account);
} else if let Some((account, container, key)) = crate::cloud::source::azure_parts(&text)
{
parts.push(account);
parts.push(container);
parts.extend(key.split('/').filter(|p| !p.is_empty()).map(str::to_string));
} else if cloud_source_id(path).is_none() {
if let Some((_, project)) = Self::google_bucket_root(path)
.and_then(|b| self.project_of_bucket(&b))
.as_deref()
.and_then(cloud_account)
{
parts.push(project);
}
let (_, plain) = crate::cloud::source::split_source_id(&text);
if let Some((_, rest)) = plain.split_once("://") {
parts.extend(
rest.split('/')
.filter(|p| !p.is_empty())
.map(str::to_string),
);
}
}
return parts.join(&format!(" {sep} "));
}
display_path(path)
}
pub fn sync_search_section(&mut self) {
let found = (self.sections.iter()).position(|s| s.title == Self::SEARCH_SECTION);
let mut kept = self.take_hits();
self.sections.retain(|s| s.title != Self::SEARCH_SECTION);
self.changed();
if let (Some(hits), Some(at)) = (kept.as_mut(), found)
&& at < hits.sections.len()
{
hits.sections.remove(at);
}
*self.rows_cache.rescored.get_mut() = kept;
if self.filter.is_empty() {
return;
}
let cloud_rows: Vec<Entry> = if self.browsing.is_none() {
self.cloud
.iter()
.flat_map(|source| {
source.buckets.iter().map(move |bucket| {
let mut entry = bucket_entry(bucket);
entry.name = format!(
"{} {} {}",
source.label,
crate::glyphs::get().trail,
entry.name
);
entry.cost.source = Some(source.api.name().to_string());
entry
})
})
.filter(|e| match_score(&self.filter, e).is_some())
.collect()
} else {
Vec::new()
};
self.score_search_inline();
let local = self.search.root.is_some()
&& (self.search.indexed > 0 || self.search.running || self.search.limited.is_some());
if !local {
if !cloud_rows.is_empty() {
let subtitle =
crate::glyphs::dotted(&format!("cloud · {} names", cloud_rows.len()));
self.sections.push(Section {
subtitle: Some(subtitle),
..Section::titled(Self::SEARCH_SECTION, cloud_rows)
});
}
return;
}
let matches = self.search.matches.as_ref();
let names: std::collections::HashSet<&std::ffi::OsStr> = (matches.iter())
.flat_map(|m| m.top.iter())
.map(|e| e.path.file_name().unwrap_or(e.path.as_os_str()))
.collect();
let listed: std::collections::HashSet<&PathBuf> = (self.sections.iter())
.flat_map(|s| s.rows.iter().map(|r| &r.path))
.filter(|path| path.file_name().is_none_or(|name| names.contains(name)))
.collect();
let kept: Vec<(&Entry, i32)> = matches
.map(|m| {
let fresh = m.query == self.filter;
m.top
.iter()
.zip(m.scores.iter().copied())
.filter(|(e, _)| !listed.contains(&e.path))
.filter_map(|(e, score)| {
if fresh {
Some((e, score))
} else {
match_score(&self.filter, e).map(|s| (e, s))
}
})
.collect()
})
.unwrap_or_default();
let mut rows: Vec<Entry> = kept.into_iter().map(|(e, _)| e.clone()).collect();
rows.extend(cloud_rows);
for row in &mut rows {
fold_known(row, &self.enriched, &self.stated, &self.stale);
}
let partial = self.search.limited.is_some();
let scored = self.search.scored_for(&self.filter);
if rows.is_empty() && !self.search.running && !partial && scored {
return;
}
let subtitle = self.found_subtitle(rows.is_empty());
self.sections.push(Section {
subtitle: Some(subtitle),
..Section::titled(Self::SEARCH_SECTION, rows)
});
}
fn found_subtitle(&self, empty: bool) -> String {
let root = self.search.root.clone().unwrap_or_default();
let dot = crate::glyphs::get().middot;
let files = crate::numfmt::group_chrome(self.search.indexed);
let files = if self.search.indexed == 1 {
format!("{files} file")
} else {
format!("{files} files")
};
let subtitle = display_path(&root);
let counted = self
.search
.matches
.as_ref()
.filter(|m| m.query == self.filter && m.ids.len() > m.top.len())
.map(|m| {
format!(
" {dot} {} of {} matches",
crate::numfmt::group_chrome(m.top.len()),
crate::numfmt::group_chrome(m.ids.len())
)
})
.unwrap_or_default();
if self.search.running {
format!(
"{subtitle} {dot} searching {}{counted}",
crate::numfmt::group_chrome(self.search.scanned)
)
} else if !self.search.scored_for(&self.filter) {
format!("{subtitle} {dot} matching {files}")
} else if empty {
match &self.search.limited {
Some(limit) => format!("{subtitle} {dot} no match in {files} {dot} {limit}"),
None => format!("{subtitle} {dot} no match in {files}"),
}
} else {
let searched = crate::numfmt::group_chrome(self.search.scanned);
match &self.search.limited {
Some(limit) => {
format!("{subtitle} {dot} {limit} {dot} {searched} searched{counted}")
}
None => format!("{subtitle} {dot} {searched} searched{counted}"),
}
}
}
pub fn search_batch(&mut self, root: &Path, mut found: Vec<Entry>, scanned: usize) {
if self.search.root.as_deref() != Some(root) {
return;
}
for row in &mut found {
apply_known_facts(row, &self.known, false);
}
self.search.scanned = scanned;
let start = self.search.indexed;
let batch: std::sync::Arc<[Entry]> = found.into();
if !batch.is_empty() {
self.search.indexed += batch.len();
self.search.results.push(batch.clone());
}
let limit = self.search_limit;
let changed = match self.search.matches.as_mut() {
Some(m) if m.query == self.filter && m.upto == start => m.extend(&batch, start, limit),
_ => {
let before = self.search.matches.as_ref().map(|m| m.upto);
self.score_search_inline();
self.search.matches.as_ref().map(|m| m.upto) != before
}
};
let found_listed = self
.sections
.iter()
.position(|s| s.title == Self::SEARCH_SECTION);
match found_listed {
Some(at) if !changed => {
let empty = self.sections[at].rows.is_empty();
self.sections[at].subtitle = Some(self.found_subtitle(empty));
}
_ => self.sync_search_section(),
}
self.settle_return();
}
fn score_search_inline(&mut self) {
if self.filter.is_empty() || self.search.scored_for(&self.filter) {
return;
}
let (base, looks_at) = self.search.base_for(&self.filter);
if looks_at > SCORE_INLINE_MAX {
return;
}
let scored =
crate::home::search::score(&self.search.results, &self.filter, base, self.search_limit);
self.search.matches = Some(scored);
}
pub fn score_job(&mut self) -> Option<ScoreJob> {
if self.filter.is_empty() || self.search.scoring || self.search.scored_for(&self.filter) {
return None;
}
let base = self.search.base_for(&self.filter).0.cloned();
self.search.scoring = true;
Some(ScoreJob {
epoch: self.search.epoch,
results: self.search.results.clone(),
query: self.filter.clone(),
base,
limit: self.search_limit,
})
}
pub fn search_scored(&mut self, epoch: u64, scored: crate::home::search::Matches) {
if epoch != self.search.epoch {
return;
}
self.search.scoring = false;
if self
.search
.matches
.as_ref()
.is_some_and(|m| m.query == scored.query && m.upto >= scored.upto)
{
return;
}
self.search.matches = Some(scored);
self.sync_search_section();
if !matches!(self.selected_row(), Some(Row::Entry { .. })) {
self.select_first_entry();
}
self.settle_return();
}
pub fn search_finished(&mut self, root: &Path, scanned: usize, limited: Option<String>) {
if self.search.root.as_deref() != Some(root) {
return;
}
self.search.running = false;
self.search.done = true;
self.search.scanned = self.search.scanned.max(scanned);
self.search.limited = limited;
self.sync_search_section();
self.settle_return();
}
pub fn visible(&self) -> Vec<Row<'_>> {
let view = self.view();
view.slots.iter().map(|slot| self.row(slot)).collect()
}
pub fn list_lines(&self, spaced: bool) -> ListLines {
let view = self.view();
ListLines {
headers: view.headers.clone(),
rows: view.slots.len(),
spaced,
}
}
pub fn matched(&self) -> usize {
let view = self.view();
(view.headers.iter())
.map(|&i| match &view.slots[i] {
Slot::Plain(Row::Header { matches, .. }) => *matches,
_ => 0,
})
.sum()
}
pub fn position(&self, mut wanted: impl FnMut(&Row<'_>) -> bool) -> Option<usize> {
let view = self.view();
view.slots.iter().position(|slot| wanted(&self.row(slot)))
}
pub fn row_count(&self) -> usize {
self.view().slots.len()
}
pub fn row_at(&self, index: usize) -> Option<Row<'_>> {
self.view().slots.get(index).map(|slot| self.row(slot))
}
pub fn rows_built(&self) -> usize {
self.rows_cache.builds.get()
}
fn changed(&mut self) {
*self.rows_cache.built.get_mut() = None;
*self.rows_cache.rescored.get_mut() = None;
}
fn take_hits(&mut self) -> Option<Hits> {
let built = self.rows_cache.built.get_mut().take();
match self.rows_cache.rescored.get_mut().take() {
Some(hits) => Some(hits),
None => built
.filter(|view| view.key.matches(self))
.map(|view| view.hits),
}
.filter(|hits| hits.filter == self.filter)
}
fn rows_changed(&mut self, touched: &[(usize, usize)]) {
let Some(mut hits) = self.take_hits() else {
return;
};
let needle = Needle::new(&self.filter);
for &(si, i) in touched {
if let Some(Some(section)) = hits.sections.get_mut(si)
&& let Some(hit) = section.get_mut(i)
{
*hit = needle.hit(&self.sections[si].rows[i]);
}
}
*self.rows_cache.rescored.get_mut() = Some(hits);
}
pub fn set_catalogs(&mut self, catalogs: Vec<ShownCatalog>) {
self.catalog_places = CatalogPlaces::of(&catalogs);
self.catalogs = catalogs;
self.changed();
}
pub fn set_visits(&mut self, visits: std::collections::HashMap<PathBuf, crate::cache::Visits>) {
self.visits = visits;
self.changed();
}
pub fn show_all(&mut self, section: usize) {
match self.sections.get(section) {
Some(s) if s.grouped_by_place => self.recent_expanded = true,
Some(Section {
root: Some(root), ..
}) => {
self.shown_whole.insert(root.clone());
}
_ => {}
}
}
pub fn cut_again(&mut self, section: usize) -> bool {
let Some(key) = self.selected_key() else {
return false;
};
let Some(s) = self.sections.get(section) else {
return false;
};
let title = s.title.clone();
let whole = if s.grouped_by_place {
None
} else {
match s.root.clone() {
Some(root) => Some(root),
None => return false,
}
};
let was_whole = match &whole {
None => std::mem::replace(&mut self.recent_expanded, false),
Some(root) => self.shown_whole.remove(root),
};
if !was_whole {
return false;
}
if self.listed(&key).is_some() {
match whole {
None => self.recent_expanded = true,
Some(root) => {
self.shown_whole.insert(root);
}
}
return false;
}
self.reselect(Some(RowKey::More(title)));
true
}
pub fn sections_mut(&mut self) -> &mut Vec<Section> {
self.changed();
&mut self.sections
}
fn view(&self) -> std::cell::Ref<'_, View> {
let fresh = self
.rows_cache
.built
.borrow()
.as_ref()
.is_some_and(|view| view.key.matches(self));
if !fresh {
let view = self.build_view();
self.rows_cache.builds.set(self.rows_cache.builds.get() + 1);
*self.rows_cache.built.borrow_mut() = Some(view);
}
std::cell::Ref::map(self.rows_cache.built.borrow(), |view| {
view.as_ref().expect("built above")
})
}
fn row<'a>(&'a self, slot: &Slot) -> Row<'a> {
match slot {
Slot::Plain(row) => row.clone(),
Slot::Entry {
section,
index,
nested,
hit,
} => Row::Entry {
section: *section,
entry: &self.sections[*section].rows[*index],
nested: *nested,
hit: *hit,
},
Slot::Door { section } => Row::Door {
section: *section,
entry: self.sections[*section]
.door
.as_ref()
.expect("the shape says it has a door"),
},
}
}
fn build_view(&self) -> View {
let kept = (self.rows_cache.rescored.take()).filter(|hits| {
hits.filter == self.filter && hits.sections.len() <= self.sections.len()
});
let hits = self.score_rows(kept);
let slots = self.slots(&hits);
View {
key: ViewKey::of(self),
headers: (slots.iter().enumerate())
.filter(|(_, slot)| matches!(slot, Slot::Plain(Row::Header { .. })))
.map(|(i, _)| i)
.collect(),
slots,
hits,
has_dataset: self
.sections
.iter()
.flat_map(|s| s.rows.iter())
.any(|e| e.kind.is_dataset() || e.kind.is_lake_table()),
}
}
fn score_rows(&self, kept: Option<Hits>) -> Hits {
let needle = Needle::new(&self.filter);
let mut kept = kept.map(|hits| hits.sections).unwrap_or_default();
let sections =
(self.sections.iter().enumerate())
.map(|(si, section)| {
let reused = (kept.get_mut(si).and_then(Option::take))
.filter(|hits| hits.len() == section.rows.len());
Some(reused.unwrap_or_else(|| {
(section.rows.iter()).map(|row| needle.hit(row)).collect()
}))
})
.collect();
Hits {
filter: self.filter.clone(),
sections,
}
}
fn slots(&self, hits: &Hits) -> Vec<Slot> {
let mut out: Vec<Slot> = Vec::new();
for (si, section) in self.sections.iter().enumerate() {
let mut matched: Vec<(usize, Hit)> = (section.rows.iter())
.zip(hits.sections[si].iter().flatten())
.enumerate()
.filter(|(_, (row, _))| !(self.hide_unreadable && row.hidden_by_default()))
.filter_map(|(i, (_, hit))| hit.map(|hit| (i, hit)))
.collect();
let keep_empty = section.unavailable || section.waiting || section.origin.is_some();
let has_door = section.door.is_some() && self.filter.is_empty();
let hidden =
if self.browsing.is_some() && self.hide_unreadable && self.filter.is_empty() {
section
.rows
.iter()
.filter(|row| row.hidden_by_default())
.count()
} else {
0
};
let says_why = section.title == Self::SEARCH_SECTION;
if matched.is_empty()
&& !has_door
&& hidden == 0
&& !says_why
&& !(keep_empty && self.filter.is_empty())
{
continue;
}
let entry = |i: usize| §ion.rows[i];
if !self.filter.is_empty() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default();
let lifted = |i: usize, score: i32| {
let frecency = self
.visits
.get(&entry(i).path)
.map_or(0.0, |v| v.frecency(now));
score.saturating_add((frecency.min(10.0) * FRECENCY_LIFT) as i32)
};
matched.sort_by_cached_key(|(i, hit)| {
(
std::cmp::Reverse(lifted(*i, hit.score)),
entry(*i).name.len(),
)
});
}
match self.sort {
SortMode::Natural => {}
SortMode::Size => {
matched.sort_by_key(|(i, _)| std::cmp::Reverse(entry(*i).size.unwrap_or(0)));
}
SortMode::Rows => {
matched.sort_by_key(|(i, _)| std::cmp::Reverse(entry(*i).rows.unwrap_or(0)));
}
SortMode::Modified => {
matched.sort_by_key(|(i, _)| {
std::cmp::Reverse(
entry(*i)
.modified
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0),
)
});
}
}
let collapsed = self.section_folded(section);
out.push(Slot::Plain(Row::Header {
section: si,
matches: matched.len(),
collapsed,
}));
if collapsed {
continue;
}
let root = section.root.as_deref();
if self.filter.is_empty()
&& root
.is_some_and(|root| self.browsing.is_some() || self.parent_of(root).is_some())
{
out.push(Slot::Plain(Row::Up { section: si }));
}
if has_door {
out.push(Slot::Door { section: si });
}
let shown = match root {
Some(_)
if self.browsing.is_none()
&& self.filter.is_empty()
&& self.view_height > 0
&& !root.is_some_and(|root| self.shown_whole.contains(root)) =>
{
(self.view_height * 2 / 5).max(8)
}
_ => usize::MAX,
};
let rest = if matched.len() > shown.saturating_add(1) {
matched.split_off(shown)
} else {
Vec::new()
};
let measuring = rest.iter().any(|(i, _)| self.sort_wants(entry(*i)));
if section.grouped_by_place {
self.slots_by_place(si, section, &matched, &mut out);
} else {
let in_order = self.sort == SortMode::Natural
&& self.filter.is_empty()
&& section.origin.is_some_and(is_catalog_origin)
&& section.root.is_none();
out.extend(matched.into_iter().map(|(index, hit)| Slot::Entry {
section: si,
index,
nested: in_order && self.bookmark(&entry(index).path).is_some(),
hit,
}));
}
if !rest.is_empty() {
out.push(Slot::Plain(Row::More {
section: si,
hidden: rest.len(),
places: 0,
measuring,
}));
}
if hidden > 0 {
out.push(Slot::Plain(Row::Hidden {
section: si,
count: hidden,
}));
}
}
out
}
fn slots_by_place(
&self,
si: usize,
section: &Section,
matched: &[(usize, Hit)],
out: &mut Vec<Slot>,
) {
let places: Vec<PathBuf> = section.rows.iter().map(|row| place_of(&row.path)).collect();
let mut order: Vec<&PathBuf> = Vec::new();
for place in &places {
if !order.contains(&place) {
order.push(place);
}
}
let groups: Vec<(&PathBuf, Vec<&(usize, Hit)>)> = order
.into_iter()
.filter_map(|place| {
let rows: Vec<&(usize, Hit)> = matched
.iter()
.filter(|(i, _)| places[*i] == *place)
.collect();
(!rows.is_empty()).then_some((place, rows))
})
.collect();
let capped = !self.recent_expanded && self.filter.is_empty() && self.view_height > 0;
let budget = self.view_height / 3;
let mut used = 0usize;
let mut shown = 0usize;
for (place, rows) in &groups {
let cost = 1 + rows.len();
if capped && shown > 0 && used + cost > budget {
break;
}
out.push(Slot::Plain(Row::Place {
section: si,
path: (*place).clone(),
label: section.place_labels.get(*place).cloned(),
source: section.rows[rows[0].0].cost.source.clone(),
held: places.iter().filter(|p| p == place).count(),
}));
out.extend(rows.iter().map(|(index, hit)| Slot::Entry {
section: si,
index: *index,
nested: true,
hit: *hit,
}));
used += cost;
shown += 1;
}
if shown < groups.len() {
out.push(Slot::Plain(Row::More {
section: si,
hidden: groups[shown..].iter().map(|(_, rows)| rows.len()).sum(),
places: groups.len() - shown,
measuring: false,
}));
}
}
pub fn path_candidates(&self) -> Vec<&PathName> {
let Some(listing) = self
.path_listing
.as_ref()
.filter(|l| l.dir == typed_dir(&self.path_input))
else {
return Vec::new();
};
let segment = &self.path_input[listing.dir.len()..];
let mut memo = (listing.matched.0.lock()).unwrap_or_else(|e| e.into_inner());
let at = match memo.as_ref() {
Some((typed, at)) if typed == segment => at.clone(),
_ => {
let at = path_matches(&listing.names, segment);
*memo = Some((segment.to_string(), at.clone()));
at
}
};
drop(memo);
at.iter().map(|&i| &listing.names[i]).collect()
}
pub fn pick_first_path(&mut self) {
self.path_pick = (!self.path_candidates().is_empty()).then_some(0);
}
pub fn picked_path(&self) -> Option<String> {
let pick = self.path_pick?;
let name = *self.path_candidates().get(pick)?;
let dir = typed_dir(&self.path_input);
let mut path = format!("{dir}{}", name.name);
if name.dir {
path.push(separator_in(dir));
}
Some(path)
}
pub fn path_completion(&self) -> Option<String> {
let dir = typed_dir(&self.path_input);
let segment = &self.path_input[dir.len()..];
let candidates = self.path_candidates();
match candidates.as_slice() {
[] => None,
[one] => {
let mut path = format!("{dir}{}", one.name);
if one.dir {
path.push(separator_in(dir));
}
Some(path)
}
many => {
let starting: Vec<&str> = many
.iter()
.map(|n| n.name.as_str())
.filter(|n| n.starts_with(segment))
.collect();
let first = starting.first()?;
let shared = starting
.iter()
.skip(1)
.fold(first.to_string(), |acc, n| common_prefix(&acc, n));
(shared.len() > segment.len()).then(|| format!("{dir}{shared}"))
}
}
}
pub fn known_urls(&self) -> Vec<String> {
let mut urls: Vec<String> = Vec::new();
let mut add = |path: &Path| {
let text = path.to_string_lossy();
if text.contains("://") && !is_cloud_place(path) {
urls.push(text.into_owned());
}
};
for catalog in &self.catalogs {
for dataset in &catalog.datasets {
add(&dataset.location);
}
}
for source in &self.cloud {
for bucket in &source.buckets {
add(bucket);
}
}
for (root, rows) in self.probes.answered() {
add(root);
for row in rows.iter() {
add(&row.path);
}
}
for path in self.known.keys() {
add(path);
}
for section in &self.sections {
for row in §ion.rows {
add(&row.path);
}
}
urls
}
pub fn selected_row(&self) -> Option<Row<'_>> {
if self.resting {
return None;
}
self.row_at(self.selected)
}
pub fn cursor_row(&self) -> Option<Row<'_>> {
self.row_at(self.selected)
}
pub fn selected_entry(&self) -> Option<&Entry> {
match self.selected_row()? {
Row::Entry { entry, .. } | Row::Door { entry, .. } => Some(entry),
_ => None,
}
}
pub fn selection_is_the_door(&self) -> bool {
matches!(self.selected_row(), Some(Row::Door { .. }))
}
pub fn recents_in(&self, place: &Path) -> Vec<PathBuf> {
self.sections
.iter()
.filter(|s| s.grouped_by_place)
.flat_map(|s| s.rows.iter())
.filter(|row| place_of(&row.path) == place)
.map(|row| row.path.clone())
.collect()
}
pub fn selected_section(&self) -> Option<usize> {
self.selected_row().map(|r| r.section())
}
pub fn selected_catalog(&self) -> Option<&ShownCatalog> {
if !self.selection_is_header() {
return None;
}
let section = self.sections.get(self.selected_section()?)?;
let origin = section.origin?;
self.catalogs
.iter()
.find(|c| c.label == section.title && c.origin_note() == origin)
}
pub fn selection_is_header(&self) -> bool {
matches!(self.selected_row(), Some(Row::Header { .. }))
}
pub fn pending_probes(&self) -> Vec<PathBuf> {
let check = self.network_check;
let mut out = Vec::new();
for root in self.sections.iter().filter_map(|s| s.remote_root.as_ref()) {
if !self.probes.settled(root) && !out.contains(root) {
out.push(root.clone());
}
}
if let Some(dir) = &self.browsing
&& check(dir)
&& cloud_source_id(dir).is_none()
&& !self.probes.settled(dir)
&& !out.contains(dir)
{
out.push(dir.clone());
}
out
}
pub fn below_browse_start(&self) -> bool {
let (Some(dir), Some(start)) = (&self.browsing, &self.browse_start) else {
return false;
};
if dir == start {
return false;
}
let mut current = self.parent_of(dir);
let mut steps = 0;
while let Some(place) = current {
if &place == start {
return true;
}
steps += 1;
if steps > 64 {
break;
}
current = self.parent_of(&place);
}
false
}
pub fn sections_waiting(&self) -> bool {
self.sections.iter().any(|s| s.waiting)
}
pub fn awaiting_listing(&self) -> Option<&Path> {
let dir = self.browsing.as_deref()?;
if cloud_source_id(dir).is_some() {
return self
.cloud_source_of(dir)
.is_some_and(|s| s.status == CloudStatus::Listing && s.buckets.is_empty())
.then_some(dir);
}
((self.network_check)(dir) && !self.probes.settled(dir)).then_some(dir)
}
pub fn probe_ready(&mut self, root: PathBuf, rows: Vec<Entry>, cut_short: bool) {
self.probes.insert(
root.clone(),
Probe::Listed {
rows: rows.into(),
cut_short,
},
);
self.apply_cloud_kinds(&root);
}
pub fn take_cloud_kinds(&mut self) {
let roots: Vec<PathBuf> = (self.probes.answered())
.map(|(root, _)| root.clone())
.collect();
for root in &roots {
self.apply_cloud_kinds(root);
}
let mut labeled = false;
for section in &mut self.sections {
for row in &mut section.rows {
if row.kind == EntryKind::Directory
&& let Some((kind, holds)) = self.cloud_kinds.get(&row.path)
{
row.kind = *kind;
if !holds.is_empty() {
row.holds = holds.clone();
}
labeled = true;
}
}
}
if labeled {
self.changed();
}
if let Some(dir) = self.browsing.clone()
&& roots.contains(&dir)
{
self.relist_remote(&dir);
}
}
pub fn apply_cloud_kinds(&mut self, root: &Path) {
let Some(rows) = self.probes.listed_mut(root) else {
return;
};
for row in std::sync::Arc::make_mut(rows).iter_mut() {
if row.kind == EntryKind::Directory
&& let Some((kind, holds)) = self.cloud_kinds.get(&row.path)
{
row.kind = *kind;
if !holds.is_empty() {
row.holds = holds.clone();
}
}
}
}
pub fn cloud_directories_to_peek(&self, limit: usize) -> Vec<PathBuf> {
if limit == 0 {
return Vec::new();
}
let view = self.view();
let mut out: Vec<PathBuf> = Vec::new();
for entry in self.entries_near_cursor(&view, limit) {
if !is_object_store_url(&entry.path) || is_cloud_place(&entry.path) {
continue;
}
if !matches!(entry.kind, EntryKind::Directory | EntryKind::Unknown) {
continue;
}
if self.browsing.is_none()
&& (self.catalog_dataset(&entry.path).is_some()
|| self.bookmark(&entry.path).is_some())
{
continue;
}
if self.cloud_kinds.contains_key(&entry.path)
|| self.peeking.contains(&entry.path)
|| self.peek_failed.contains(&entry.path)
{
continue;
}
if out.contains(&entry.path) {
continue;
}
out.push(entry.path.clone());
if out.len() >= limit {
break;
}
}
out
}
pub fn probe_failed(&mut self, root: PathBuf, why: Option<String>) {
self.probes.insert(root, Probe::Unreachable(why));
}
pub fn measure_now(&mut self, limit: usize) -> bool {
let wanted = self.unmeasured_visible(limit);
let more = self.unmeasured_visible(limit + 1).len() > wanted.len();
for entry in wanted {
let mut probe = entry.clone();
stat_and_recall(&mut probe, &self.known);
if probe.rows.is_none() && probe.columns.is_empty() {
discover::enrich(&mut probe);
}
self.record_measurement(entry.path.clone(), measured_from(&probe, &entry));
}
self.apply_new_measurements();
more
}
pub fn unmeasured_visible(&self, limit: usize) -> Vec<Entry> {
let view = self.view();
let mut out: Vec<Entry> = Vec::new();
for entry in self.entries_on_screen(&view) {
if self.wants_measuring(entry) && !out.iter().any(|e| e.path == entry.path) {
out.push(entry.clone());
if out.len() >= limit {
return out;
}
}
}
if self.sort == SortMode::Rows {
self.sorted_rest(&view, limit, &mut out);
}
out
}
pub fn unstated_for_sort(&self, limit: usize) -> Vec<Entry> {
let view = self.view();
let mut out: Vec<Entry> = Vec::new();
for entry in self.entries_on_screen(&view) {
let stale =
(self.stated.get(&entry.path)).is_some_and(|stated| stated.epoch < self.stat_epoch);
if stale && self.needs_stat(entry) && !out.iter().any(|e| e.path == entry.path) {
out.push(entry.clone());
if out.len() >= limit {
break;
}
}
}
if out.len() < limit && matches!(self.sort, SortMode::Size | SortMode::Modified) {
self.sorted_rest(&view, limit, &mut out);
}
for entry in &mut out {
entry.modified = None;
}
out
}
fn sorted_rest(&self, view: &View, limit: usize, out: &mut Vec<Entry>) {
let mut take = |entry: &Entry| {
if self.sort_wants(entry) && !out.iter().any(|e| e.path == entry.path) {
out.push(entry.clone());
}
out.len() >= limit
};
for entry in view.slots.iter().filter_map(|slot| self.entry_of(slot)) {
if take(entry) {
return;
}
}
for slot in &view.slots {
let Slot::Plain(Row::More {
section,
measuring: true,
..
}) = slot
else {
continue;
};
for entry in &self.sections[*section].rows {
if take(entry) {
return;
}
}
}
}
fn sort_wants(&self, entry: &Entry) -> bool {
match self.sort {
SortMode::Natural => false,
SortMode::Rows => self.wants_measuring(entry),
SortMode::Size | SortMode::Modified => {
(self.sort == SortMode::Modified || entry.kind != EntryKind::Unknown)
&& self.needs_stat(entry)
}
}
}
fn needs_stat(&self, entry: &Entry) -> bool {
let due = match self.stated.get(&entry.path) {
Some(stated) => stated.epoch < self.stat_epoch,
None => entry.modified.is_none(),
};
due && discover::on_disk(entry) && !(self.network_check)(&entry.path)
}
fn wants_measuring(&self, entry: &Entry) -> bool {
if entry.measured || self.enriched.contains_key(&entry.path) {
return false;
}
if entry.kind != EntryKind::Unknown
&& discover::unstated(entry)
&& !self.stated.contains_key(&entry.path)
&& !(self.network_check)(&entry.path)
{
return true;
}
if entry.rows.is_some() {
return false;
}
if matches!(
entry.kind,
EntryKind::Directory | EntryKind::Unknown | EntryKind::Other
) || entry.kind.is_lake_table()
{
return false;
}
!(self.network_check)(&entry.path)
}
pub fn classify_now(&mut self, limit: usize) -> bool {
let wanted = self.unclassified_visible(limit);
let more = self.unclassified_visible(limit + 1).len() > wanted.len();
for entry in wanted {
let mut stated = entry.clone();
stat_and_recall(&mut stated, &self.known);
let probe = look_into_as(&stated, &Default::default());
self.record_measurement(entry.path.clone(), measured_from(&probe, &entry));
}
self.apply_new_measurements();
more
}
pub fn unclassified_visible(&self, limit: usize) -> Vec<Entry> {
self.unclassified_visible_where(limit, |_| true)
}
pub fn unclassified_visible_where(
&self,
limit: usize,
keep: impl Fn(&Entry) -> bool,
) -> Vec<Entry> {
if limit == 0 {
return Vec::new();
}
let view = self.view();
let mut out: Vec<Entry> = Vec::new();
for entry in self.entries_near_cursor(&view, limit) {
if entry.kind != EntryKind::Unknown
|| self.missing.contains(&entry.path)
|| !keep(entry)
{
continue;
}
if self.enriched.contains_key(&entry.path) {
continue;
}
if is_object_store_url(&entry.path) || is_cloud_place(&entry.path) {
continue;
}
if out.iter().any(|e| e.path == entry.path) {
continue;
}
out.push(entry.clone());
if out.len() >= limit {
break;
}
}
out
}
fn entries_on_screen<'a>(&'a self, view: &'a View) -> impl Iterator<Item = &'a Entry> + 'a {
let height = if self.view_height == 0 {
usize::MAX / 4
} else {
self.view_height
};
let rows = view.slots.len();
let top = self.scroll.min(rows);
let reach = height / 2;
let bottom = top.saturating_add(height + reach).min(rows);
let above = top.saturating_sub(reach);
std::iter::once(self.selected)
.chain(top..bottom)
.chain(above..top)
.filter_map(|i| self.entry_of(view.slots.get(i)?))
}
fn entries_near_cursor<'a>(
&'a self,
view: &'a View,
limit: usize,
) -> impl Iterator<Item = &'a Entry> + 'a {
let height = if self.view_height == 0 {
limit
} else {
self.view_height
};
let rows = view.slots.len();
let top = self.scroll.min(rows);
let ahead = top.saturating_add(2 * height).min(rows);
let behind = top.saturating_sub(height);
std::iter::once(self.selected)
.chain(top..ahead)
.chain(behind..top)
.filter_map(|i| self.entry_of(view.slots.get(i)?))
}
fn entry_of(&self, slot: &Slot) -> Option<&Entry> {
match slot {
Slot::Entry { section, index, .. } => Some(&self.sections[*section].rows[*index]),
_ => None,
}
}
pub fn record_measurement(&mut self, path: PathBuf, measured: Measured) {
self.unapplied.insert(path.clone());
let before = self.stated.insert(
path.clone(),
Stated {
size: measured.size,
modified: measured.modified,
epoch: self.stat_epoch,
},
);
if !measured.stat_only {
self.stale.remove(&path);
self.enriched.insert(path, measured);
return;
}
let changed = before.is_some_and(|before| {
before.modified.is_some()
&& (before.size, before.modified) != (measured.size, measured.modified)
});
if changed && self.enriched.remove(&path).is_some() {
self.stale.insert(path);
}
}
pub fn record_size(&mut self, path: PathBuf, measured: Measured) {
match self.enriched.get_mut(&path) {
Some(known) => {
known.size = measured.size;
self.unapplied.insert(path);
}
None => self.record_measurement(path, measured),
}
}
pub fn apply_measurements(&mut self) {
self.unapplied.clear();
for section in &mut self.sections {
for row in section.rows.iter_mut().chain(section.door.iter_mut()) {
fold_known(row, &self.enriched, &self.stated, &self.stale);
}
if let Some(door) = section.door.as_mut() {
door.name = door_name(door, §ion.rows);
}
}
self.changed();
self.land_again();
}
pub fn apply_new_measurements(&mut self) {
if self.unapplied.is_empty() {
return;
}
let unapplied = std::mem::take(&mut self.unapplied);
let names: std::collections::HashSet<&std::ffi::OsStr> = (unapplied.iter())
.map(|path| path.file_name().unwrap_or(path.as_os_str()))
.collect();
let new = |path: &Path| {
path.file_name().is_none_or(|name| names.contains(name)) && unapplied.contains(path)
};
let mut touched: Vec<(usize, usize)> = Vec::new();
let mut any = false;
let mut moved = false;
let sort = self.sort;
for (si, section) in self.sections.iter_mut().enumerate() {
let mut here = false;
for (i, row) in section.rows.iter_mut().enumerate() {
if !new(&row.path) {
continue;
}
let before = Standing::of(row, sort);
if fold_known(row, &self.enriched, &self.stated, &self.stale) {
moved |= Standing::of(row, sort) != before;
touched.push((si, i));
here = true;
}
}
if let Some(door) = section.door.as_mut()
&& new(&door.path)
{
here |= fold_known(door, &self.enriched, &self.stated, &self.stale);
}
if here && let Some(door) = section.door.as_mut() {
door.name = door_name(door, §ion.rows);
}
any |= here;
}
if !any {
return;
}
if moved || sort != SortMode::Natural || !self.hits_hold(&touched) {
self.rows_changed(&touched);
}
self.land_again();
}
fn hits_hold(&self, touched: &[(usize, usize)]) -> bool {
let built = self.rows_cache.built.borrow();
let Some(view) = built.as_ref().filter(|view| view.key.matches(self)) else {
return false;
};
let needle = Needle::new(&self.filter);
touched.iter().all(|&(si, i)| {
let was = view.hits.sections.get(si).and_then(|s| s.as_ref()?.get(i));
was == Some(&needle.hit(&self.sections[si].rows[i]))
})
}
fn land_again(&mut self) {
if self.landing
&& let Some(Row::Door { entry, .. }) = self.row_at(self.selected)
&& !door_lands(entry)
{
self.selected = self.landing_row();
self.follow_selection();
}
}
pub fn select_first_entry(&mut self) {
self.returning = None;
self.landing = true;
if !self.filter.is_empty() {
self.resting = false;
}
self.selected = self.landing_row();
}
fn landing_row(&self) -> usize {
if self.filter.is_empty()
&& let Some(newest) = self.newest_recent.as_ref()
&& let Some(at) = self.position(|r| {
matches!(r, Row::Entry { section, entry, .. }
if entry.path == *newest
&& self.sections[*section].title == Self::RECENT_SECTION)
})
{
return at;
}
let first = self.position(|r| matches!(r, Row::Entry { .. } | Row::Door { .. }));
let first = match first.and_then(|i| self.row_at(i)) {
Some(Row::Door { entry, .. }) if !door_lands(entry) => self
.position(|r| matches!(r, Row::Entry { .. } | Row::Hidden { .. }))
.or(first),
_ => first,
};
first
.or_else(|| self.position(|r| matches!(r, Row::Hidden { .. })))
.unwrap_or(0)
}
pub fn clamp_selection(&mut self) {
let n = self.row_count();
if n == 0 {
self.selected = 0;
} else if self.selected >= n {
self.selected = n - 1;
}
}
pub fn select(&mut self, index: usize) {
if index < self.row_count() {
self.resting = false;
self.returning = None;
self.landing = false;
self.selected = index;
}
}
pub fn move_selection(&mut self, delta: isize) {
self.resting = false;
self.returning = None;
self.landing = false;
let n = self.row_count();
if n == 0 {
return;
}
let cur = self.selected as isize;
let next = (cur + delta).rem_euclid(n as isize);
self.selected = next as usize;
}
pub fn page_selection(&mut self, delta: isize) {
self.resting = false;
self.returning = None;
self.landing = false;
let n = self.row_count();
if n == 0 {
return;
}
let next = (self.selected as isize)
.saturating_add(delta)
.clamp(0, n as isize - 1);
self.selected = next as usize;
}
}
fn source_entry(source: &CloudSource) -> Entry {
Entry {
path: cloud_place(&source.id),
kind: EntryKind::Directory,
name: source.label.clone(),
size: None,
modified: source.listed_at,
rows: None,
cols: None,
cols_sampled: false,
columns: Vec::new(),
cost: Default::default(),
holds: Default::default(),
opens_whole_directory: false,
measured: false,
format_spec: None,
table: None,
}
}
fn bucket_entry(url: &Path) -> Entry {
let mut entry = Entry::directory(url);
let text = url.to_string_lossy();
let (_, plain) = crate::cloud::source::split_source_id(&text);
entry.name = plain
.rsplit('/')
.find(|part| !part.is_empty())
.unwrap_or("")
.to_string();
entry
}
pub fn names_a_file(path: &Path) -> bool {
let named = path.to_string_lossy();
let dotted = !named.ends_with('/')
&& path
.file_name()
.map(|last| last.to_string_lossy())
.is_some_and(|last| last.trim_start_matches('.').contains('.'));
discover::is_data_file(path) || dotted
}
fn entry_for_path(path: &Path, remote: bool) -> Entry {
if !remote && let Some(table) = discover::table_row(path) {
return table;
}
let mut holds = discover::Holds::default();
let kind = if remote {
if names_a_file(path) {
EntryKind::File
} else {
EntryKind::Unknown
}
} else if path.is_dir() {
let (kind, found) = discover::look_at_directory(path);
holds = found;
kind
} else {
EntryKind::File
};
let mut entry = Entry {
path: path.to_path_buf(),
kind,
name: path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned()),
size: None,
modified: None,
rows: None,
cols: None,
cols_sampled: false,
columns: Vec::new(),
cost: Default::default(),
holds,
opens_whole_directory: false,
measured: false,
format_spec: None,
table: None,
};
if !remote && let Ok(meta) = std::fs::metadata(path) {
if meta.is_file() {
entry.size = Some(meta.len());
}
entry.modified = meta.modified().ok();
}
entry
}
pub fn display_path(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(rest) = path.strip_prefix(&home)
{
if rest.as_os_str().is_empty() {
return "~".to_string();
}
return format!("~{}{}", std::path::MAIN_SEPARATOR, rest.display());
}
path.display().to_string()
}
pub fn complete_path(typed: &str) -> (String, usize) {
let expanded = expand_user_path(typed);
let is_separator = |c: char| c == '/' || (cfg!(windows) && c == '\\');
let typed_ends_in_sep = typed.ends_with(is_separator);
let (dir, prefix) = if typed_ends_in_sep {
(expanded.clone(), String::new())
} else {
match (expanded.parent(), expanded.file_name()) {
(Some(parent), Some(name)) => {
(parent.to_path_buf(), name.to_string_lossy().into_owned())
}
_ => (expanded.clone(), String::new()),
}
};
let Ok(entries) = std::fs::read_dir(&dir) else {
return (typed.to_string(), 0);
};
let mut names: Vec<String> = entries
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with('.') && !prefix.starts_with('.') {
return None;
}
name.starts_with(&prefix).then_some(name)
})
.collect();
if names.is_empty() {
return (typed.to_string(), 0);
}
names.sort();
let shared = names
.iter()
.skip(1)
.fold(names[0].clone(), |acc, name| common_prefix(&acc, name));
let mut completed = typed.to_string();
completed.truncate(typed.len() - prefix.len());
completed.push_str(&shared);
if names.len() == 1 && dir.join(&shared).is_dir() && !completed.ends_with(is_separator) {
let separator = typed
.chars()
.rev()
.find(|c| is_separator(*c))
.unwrap_or(std::path::MAIN_SEPARATOR);
completed.push(separator);
}
(completed, names.len())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathName {
pub name: String,
pub dir: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PathListing {
pub dir: String,
pub names: Vec<PathName>,
pub failed: bool,
pub matched: SegmentMatches,
}
#[derive(Debug, Default)]
pub struct SegmentMatches(std::sync::Mutex<Option<(String, std::sync::Arc<[usize]>)>>);
impl Clone for SegmentMatches {
fn clone(&self) -> Self {
Self::default()
}
}
impl PartialEq for SegmentMatches {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for SegmentMatches {}
fn path_matches(names: &[PathName], segment: &str) -> std::sync::Arc<[usize]> {
let mut matched: Vec<(usize, i32)> = (names.iter().enumerate())
.filter(|(_, n)| !n.name.starts_with('.') || segment.starts_with('.'))
.filter_map(|(i, n)| {
if segment.is_empty() {
return Some((i, 0));
}
let prefix = n.name.starts_with(segment) as i32 * 1_000_000;
fuzzy_score(segment, &n.name).map(|score| (i, prefix + score))
})
.collect();
matched
.sort_by(|(a, sa), (b, sb)| sb.cmp(sa).then_with(|| names[*a].name.cmp(&names[*b].name)));
matched.into_iter().map(|(i, _)| i).collect()
}
const PATH_LISTING_MAX: usize = 5_000;
pub fn typed_dir(typed: &str) -> &str {
let is_separator = |c: char| c == '/' || (cfg!(windows) && c == '\\');
let floor = typed.find("://").map_or(0, |at| at + 3);
match typed[floor..].rfind(is_separator) {
Some(at) => &typed[..floor + at + 1],
None => &typed[..floor],
}
}
fn separator_in(dir: &str) -> char {
if typed_dir_is_url(dir) {
return '/';
}
dir.chars()
.rev()
.find(|c| *c == '/' || (cfg!(windows) && *c == '\\'))
.unwrap_or(std::path::MAIN_SEPARATOR)
}
pub fn typed_dir_is_url(dir: &str) -> bool {
dir.contains("://")
}
pub fn list_typed_dir(dir: &str) -> PathListing {
let path = if dir.is_empty() {
PathBuf::from(".")
} else {
expand_user_path(dir)
};
let Ok(entries) = std::fs::read_dir(&path) else {
return PathListing {
dir: dir.to_string(),
names: Vec::new(),
failed: true,
matched: Default::default(),
};
};
let mut names: Vec<PathName> = entries
.flatten()
.take(PATH_LISTING_MAX)
.map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
let dir = e.file_type().is_ok_and(|t| t.is_dir())
|| (e.file_type().is_ok_and(|t| t.is_symlink()) && e.path().is_dir());
PathName { name, dir }
})
.collect();
names.sort_by(|a, b| a.name.cmp(&b.name));
PathListing {
dir: dir.to_string(),
names,
failed: false,
matched: Default::default(),
}
}
pub fn names_under(dir: &str, urls: impl IntoIterator<Item = String>) -> PathListing {
let mut names: Vec<PathName> = Vec::new();
for url in urls {
let forms = match crate::cloud::source::azure_parts(&url) {
Some((_, container, key)) => vec![url.clone(), format!("az://{container}/{key}")],
None => vec![url],
};
for form in forms {
let Some(rest) = form.strip_prefix(dir) else {
continue;
};
let (name, more) = match rest.split_once('/') {
Some((name, more)) => (name, Some(more)),
None => (rest, None),
};
if name.is_empty() {
continue;
}
let is_dir = more.is_some() || !names_a_file(Path::new(&form));
match names.iter_mut().find(|n| n.name == name) {
Some(known) => known.dir |= is_dir,
None => names.push(PathName {
name: name.to_string(),
dir: is_dir,
}),
}
}
}
names.sort_by(|a, b| a.name.cmp(&b.name));
PathListing {
dir: dir.to_string(),
names,
failed: false,
matched: Default::default(),
}
}
fn common_prefix(a: &str, b: &str) -> String {
a.chars()
.zip(b.chars())
.take_while(|(x, y)| x == y)
.map(|(x, _)| x)
.collect()
}
pub fn expand_user_path(raw: &str) -> PathBuf {
crate::config::expand_config_path(raw)
}
#[cfg(test)]
mod holds_flow_tests {
use super::*;
#[test]
fn a_rows_label_is_one_decision_for_the_list_and_the_pane() {
let mut directory = Entry::for_test(Path::new("s3://bucket/warehouse"), "warehouse");
directory.kind = EntryKind::Directory;
let said = |look, place_kind| describe(&directory, place_kind, look, 0, None);
let g = crate::glyphs::get();
let waiting = said(Some(CloudLook::Waiting), None);
assert_eq!(
(waiting.short.as_str(), waiting.words.as_str()),
(g.ellipsis, "")
);
assert_eq!(said(Some(CloudLook::Looking), None).words, "");
assert!(
said(Some(CloudLook::Failed), None)
.words
.contains("listing failed")
);
assert_eq!(said(None, None).words, "directory");
let curated = said(None, Some("dataset"));
assert_eq!(
(curated.short.as_str(), curated.words.as_str()),
("dataset", "dataset")
);
assert!(curated.curated);
directory.holds = crate::home::discover::Holds {
formats: vec![("parquet".to_string(), 12)],
..Default::default()
};
let counted = describe(&directory, None, None, 0, None);
assert_eq!(counted.short, "12 parquet");
assert_eq!(
counted.words, "directory",
"the count is the pane's `contains` line"
);
let curated = describe(&directory, Some("dataset"), None, 0, None);
assert_eq!(
curated.short, "dataset",
"the curated word wins over the count"
);
directory.opens_whole_directory = true;
assert_eq!(
describe(&directory, Some("dataset"), None, 0, None),
RowLabel::default()
);
}
#[cfg(windows)]
#[test]
fn a_windows_home_path_is_shown_and_typed_with_backslashes() {
let home = dirs::home_dir().unwrap();
let path = home.join("data").join("a.csv");
let shown = display_path(&path);
assert_eq!(shown, r"~\data\a.csv");
assert_eq!(expand_user_path(&shown), path);
}
#[cfg(windows)]
#[test]
fn a_dot_above_a_windows_recent_does_not_make_it_a_file() {
let path = Path::new(r"C:\Users\RUNNER~1\AppData\Local\Temp\.tmpAzMMTE\orders");
assert_eq!(entry_for_path(path, true).kind, EntryKind::Unknown);
let file = Path::new(r"C:\Users\RUNNER~1\AppData\Local\Temp\.tmpAzMMTE\a.parquet");
assert_eq!(entry_for_path(file, true).kind, EntryKind::File);
}
fn counted(n: usize) -> crate::home::discover::Holds {
crate::home::discover::Holds {
formats: vec![("parquet".to_string(), n)],
..Default::default()
}
}
fn in_flight() -> (EntryKind, crate::home::discover::Holds) {
(
EntryKind::Directory,
crate::home::discover::Holds::default(),
)
}
#[test]
fn a_claim_staked_before_a_peek_lands_keeps_the_count_a_row_already_has() {
let root = std::path::PathBuf::from("s3://bucket/warehouse");
let path = root.join("orders");
let mut row = Entry::for_test(&path, "orders");
row.kind = EntryKind::Directory;
row.holds = counted(15);
let mut home = HomeState::default();
home.probe_ready(root.clone(), vec![row], false);
home.cloud_kinds.insert(path, in_flight());
home.apply_cloud_kinds(&root);
assert_eq!(
home.probes.listed(&root).unwrap()[0].holds.label(),
"15 parquet",
"the placeholder erased a count the row already had"
);
}
#[test]
fn a_cloud_directory_waits_then_looks_then_answers() {
let path = std::path::PathBuf::from("gs://pitscope/seasons");
let mut row = Entry::for_test(&path, "seasons");
row.kind = EntryKind::Directory;
let mut home = HomeState::default();
assert_eq!(
home.cloud_look(&row),
Some(CloudLook::Waiting),
"not asked yet"
);
home.peeking.insert(path.clone());
assert_eq!(
home.cloud_look(&row),
Some(CloudLook::Looking),
"being looked into"
);
home.peeking.remove(&path);
home.cloud_kinds
.insert(path.clone(), (EntryKind::Directory, counted(12)));
assert_eq!(home.cloud_look(&row), Some(CloudLook::Looking));
home.cloud_kinds
.insert(path.clone(), (EntryKind::Hive, Default::default()));
assert_eq!(home.cloud_look(&row), Some(CloudLook::Looking));
home.cloud_kinds.insert(path.clone(), in_flight());
assert_eq!(home.cloud_look(&row), None, "answered");
let mut home = HomeState {
network_check: |_| true,
..Default::default()
};
let root = std::path::PathBuf::from("gs://pitscope");
home.probe_ready(root.clone(), vec![row.clone()], false);
home.browsing = Some(root);
home.rebuild(&[]);
assert_eq!(
home.cloud_directories_to_peek(4),
std::slice::from_ref(&path)
);
home.peek_failed.insert(path.clone());
assert_eq!(home.cloud_look(&row), Some(CloudLook::Failed));
assert!(home.cloud_directories_to_peek(4).is_empty());
let mut counted_row = Entry::for_test(&path.join("x"), "x");
counted_row.kind = EntryKind::Directory;
counted_row.holds = counted(3);
assert_eq!(home.cloud_look(&counted_row), None);
let mut bucket = Entry::for_test(std::path::Path::new("gs://pitscope"), "pitscope");
bucket.kind = EntryKind::Directory;
assert_eq!(home.cloud_look(&bucket), None);
let mut local = Entry::for_test(std::path::Path::new("/data/seasons"), "seasons");
local.kind = EntryKind::Directory;
assert_eq!(home.cloud_look(&local), None);
}
#[test]
fn a_peeks_answer_replaces_the_count_a_row_had() {
let root = std::path::PathBuf::from("s3://bucket/warehouse");
let path = root.join("orders");
let mut row = Entry::for_test(&path, "orders");
row.kind = EntryKind::Directory;
row.holds = counted(15);
let mut home = HomeState::default();
home.probe_ready(root.clone(), vec![row], false);
home.cloud_kinds
.insert(path, (EntryKind::MultiFile, counted(40)));
home.apply_cloud_kinds(&root);
assert_eq!(
home.probes.listed(&root).unwrap()[0].holds.label(),
"40 parquet"
);
assert_eq!(
home.probes.listed(&root).unwrap()[0].kind,
EntryKind::MultiFile
);
}
#[test]
fn a_peek_answers_only_the_rows_that_asked() {
let root = std::path::PathBuf::from("s3://bucket/warehouse");
let settled = root.join("sales");
let mut row = Entry::for_test(&settled, "sales");
row.kind = EntryKind::Hive;
row.holds = counted(40);
let mut home = HomeState::default();
home.probe_ready(root.clone(), vec![row], false);
home.cloud_kinds
.insert(settled, (EntryKind::Directory, counted(1)));
home.apply_cloud_kinds(&root);
assert_eq!(home.probes.listed(&root).unwrap()[0].kind, EntryKind::Hive);
assert_eq!(
home.probes.listed(&root).unwrap()[0].holds.label(),
"40 parquet"
);
}
#[test]
fn a_peek_goes_to_the_row_the_cursor_is_on_and_is_never_asked_twice() {
let root = std::path::PathBuf::from("s3://bucket/warehouse");
let mut home = HomeState {
network_check: |_| true,
..Default::default()
};
let rows: Vec<Entry> = ["a", "b", "c", "d", "e"]
.iter()
.map(|n| {
let mut row = Entry::for_test(&root.join(n), n);
row.kind = EntryKind::Directory;
row
})
.collect();
home.probe_ready(root.clone(), rows, false);
home.browsing = Some(root.clone());
home.view_height = 10;
home.rebuild(&[]);
home.cloud_kinds
.insert(root.join("b"), (EntryKind::MultiFile, counted(3)));
home.peeking.insert(root.join("c"));
home.selected = home
.visible()
.iter()
.position(|r| matches!(r, Row::Entry { entry, .. } if entry.name == "e"))
.expect("the row is listed");
let asked = home.cloud_directories_to_peek(3);
assert_eq!(
asked.first(),
Some(&root.join("e")),
"the highlighted row is the one about to be acted on"
);
assert_eq!(asked.len(), 3, "the budget is a budget");
assert!(
!asked.contains(&root.join("b")),
"a directory already looked into is not asked again"
);
assert!(
!asked.contains(&root.join("c")),
"nor one with a request already out"
);
}
#[test]
fn a_measurement_that_counted_nothing_keeps_the_count_a_row_already_has() {
let path = std::path::PathBuf::from("/data/warehouse/orders");
let mut row = Entry::for_test(&path, "orders");
row.kind = EntryKind::Directory;
row.holds = counted(15);
let mut home = HomeState::default();
home.sections.push(Section::titled("Here", vec![row]));
home.enriched.insert(
path,
Measured {
kind: Some(EntryKind::Directory),
..Default::default()
},
);
home.apply_measurements();
assert_eq!(
home.sections[0].rows[0].holds.label(),
"15 parquet",
"a measurement with nothing to say erased the label"
);
}
}
#[cfg(test)]
mod look_into_batch_tests {
use super::*;
use polars::prelude::*;
#[test]
fn every_kind_is_sent_before_any_count() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let cache = crate::cache::CacheManager::with_dir(cache_dir.path().to_path_buf());
let mut rows = Vec::new();
for name in ["a", "b"] {
let partition = dir.path().join(name).join("year=2024");
std::fs::create_dir_all(&partition).unwrap();
let mut frame = df!("x" => [1i32, 2, 3]).unwrap();
let file = std::fs::File::create(partition.join("part.parquet")).unwrap();
ParquetWriter::new(file).finish(&mut frame).unwrap();
rows.push(Entry::new(dir.path().join(name), EntryKind::Unknown));
}
let mut sent = Vec::new();
look_into_batch(
rows,
&cache,
&Default::default(),
Reads::Files,
|path, m| {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
sent.push((name, m.kind, m.rows));
},
);
let hive = Some(EntryKind::Hive);
assert_eq!(
sent,
vec![
("a".to_string(), hive, None),
("b".to_string(), hive, None),
("a".to_string(), hive, Some(3)),
("b".to_string(), hive, Some(3)),
]
);
}
}
#[cfg(test)]
mod known_facts_tests {
use super::*;
use crate::cache::DatasetFacts;
#[test]
fn what_a_directory_holds_is_restored_beside_its_kind() {
let holds = crate::home::discover::Holds {
formats: vec![("parquet".to_string(), 15)],
..Default::default()
};
for (path, remote) in [
(
std::path::PathBuf::from("s3://bucket/warehouse/orders"),
true,
),
(std::path::PathBuf::from("/data/warehouse/orders"), false),
] {
let facts = DatasetFacts {
mtime: 0,
size: 4096,
rows: Some(999),
cols: Some(72),
cols_sampled: false,
columns: vec!["lat".to_string()],
kind: Some(EntryKind::Directory),
classified_by: crate::home::discover::CLASSIFIER_VERSION,
holds: holds.clone(),
cost: Default::default(),
};
let mut row = Entry::directory(&path);
row.kind = EntryKind::Unknown;
row.modified = Some(std::time::UNIX_EPOCH);
assert_eq!(row.size, None);
let index = std::collections::HashMap::from([(path.clone(), facts)]);
apply_known_facts(&mut row, &index, remote);
assert_eq!(row.kind, EntryKind::Directory, "{path:?}");
assert_eq!(row.label(), "15 parquet", "{path:?}");
if !remote {
assert_eq!(row.rows, None, "{path:?}");
assert_eq!(row.cols, None, "{path:?}");
assert_eq!(row.size, None, "{path:?}");
assert!(row.columns.is_empty(), "{path:?}");
}
}
}
#[test]
fn a_datasets_counts_are_measured_rather_than_restored() {
let path = std::path::PathBuf::from("/data/warehouse/events");
let facts = DatasetFacts {
mtime: 0,
size: 4096,
rows: Some(1_200_000),
cols: Some(58),
cols_sampled: false,
columns: vec!["ts".to_string()],
kind: Some(EntryKind::MultiFile),
classified_by: crate::home::discover::CLASSIFIER_VERSION,
holds: crate::home::discover::Holds {
formats: vec![("parquet".to_string(), 15)],
..Default::default()
},
cost: Default::default(),
};
let mut row = Entry::directory(&path);
row.kind = EntryKind::Unknown;
row.modified = Some(std::time::UNIX_EPOCH);
let index = std::collections::HashMap::from([(path.clone(), facts)]);
apply_known_facts(&mut row, &index, false);
assert_eq!(row.kind, EntryKind::MultiFile, "the kind comes back");
assert_eq!(row.label(), "15 parquet", "and what it holds");
assert_eq!(
row.rows, None,
"but not the count: the measuring pass skips a row that has one"
);
assert_eq!(row.cols, None);
}
#[test]
fn a_kind_from_an_older_classifier_is_not_restored() {
let remote = std::path::PathBuf::from("s3://bucket/warehouse/orders");
let facts = |classified_by| DatasetFacts {
mtime: 0,
size: 4096,
rows: Some(1_000),
cols: Some(7),
cols_sampled: false,
columns: vec!["id".into(), "amount".into()],
kind: Some(EntryKind::MultiFile),
classified_by,
cost: Default::default(),
holds: Default::default(),
};
let unprobed = || {
let mut row = Entry::directory(&remote);
row.kind = EntryKind::Unknown;
row
};
let index = |classified_by| {
std::collections::HashMap::from([(remote.clone(), facts(classified_by))])
};
let mut row = unprobed();
apply_known_facts(
&mut row,
&index(crate::home::discover::CLASSIFIER_VERSION),
true,
);
assert_eq!(
row.kind,
EntryKind::MultiFile,
"this build's own answer comes back"
);
let mut row = unprobed();
apply_known_facts(&mut row, &index(0), true);
assert_eq!(
row.kind,
EntryKind::Unknown,
"an older build's does not: it may be a lake table this one would recognize"
);
assert_eq!(
row.rows,
Some(1_000),
"but what it measured is still measured"
);
assert_eq!(row.columns, vec!["id".to_string(), "amount".to_string()]);
}
}
#[cfg(test)]
mod build_feature_tests {
use super::*;
#[test]
fn the_builtin_catalog_lists_only_what_this_build_opens() {
let urls: Vec<String> = catalogs(&crate::config::AppConfig::default())
.into_iter()
.filter(|c| c.origin == crate::home::catalog::Origin::Bundled)
.flat_map(|c| c.datasets)
.map(|d| d.location.to_string_lossy().into_owned())
.collect();
let web = urls.iter().filter(|u| u.starts_with("https://")).count();
let stores = urls
.iter()
.filter(|u| is_object_store_url(Path::new(u)))
.count();
assert_eq!(web + stores, urls.len(), "{urls:?}");
assert_eq!(web > 0, cfg!(feature = "http"), "{urls:?}");
assert_eq!(stores > 0, cfg!(feature = "cloud"), "{urls:?}");
}
#[test]
fn an_empty_examples_toml_hides_the_section() {
let mut config = crate::config::AppConfig::default();
assert_eq!(
catalogs(&config)
.iter()
.any(|c| c.origin == crate::home::catalog::Origin::Bundled),
cfg!(any(feature = "http", feature = "cloud"))
);
config.read_catalogs = vec![
crate::home::catalog::parse(
"label = \"Mine\"\n",
crate::home::catalog::EXAMPLES,
crate::home::catalog::Origin::Folder,
None,
)
.unwrap(),
];
assert!(catalogs(&config).is_empty(), "{:?}", catalogs(&config));
}
#[test]
fn a_users_catalog_is_shown_whole() {
let mut config = crate::config::AppConfig::default();
let mine = crate::home::catalog::parse(
r#"
[bucket]
name = "Bucket"
url = "s3://bucket/prefix/"
[web]
name = "Web"
url = "https://example.com/data.csv"
"#,
crate::home::catalog::MINE,
crate::home::catalog::Origin::Mine,
None,
)
.unwrap();
config.read_catalogs = vec![mine];
let shown = catalogs(&config);
let mine = shown.iter().find(|c| c.id == "mine").unwrap();
assert_eq!(mine.datasets.len(), 2);
assert_eq!(mine.label, crate::home::catalog::MINE_LABEL);
}
}
#[cfg(test)]
mod place_tests {
use super::same_place;
use std::path::Path;
#[test]
fn local_paths_are_one_place_however_spelled() {
assert!(same_place(
Path::new("/data/./sales/"),
Path::new("/data/sales")
));
assert!(!same_place(
Path::new("/data/sales"),
Path::new("/data/sale")
));
assert!(same_place(
Path::new("s3://bucket/dir/"),
Path::new("s3://bucket/dir")
));
if cfg!(windows) {
assert!(same_place(
Path::new("c:/data/sales.csv"),
Path::new(r"C:\data\sales.csv")
));
}
}
}