use std::fs;
use std::path::PathBuf;
use tokio::sync::mpsc;
use crate::app::state::{Entry, EntryKind};
use crate::preview::provider::{PreviewContent, PreviewCtx, PreviewOutcome, PreviewProvider};
use crate::workers::WorkerMsg;
const MAX_PREVIEW_ENTRIES: usize = 64;
pub struct DirectoryProvider;
impl PreviewProvider for DirectoryProvider {
fn can_handle(&self, entry: &Entry) -> bool {
entry.kind == EntryKind::Dir
}
fn preview(&self, entry: &Entry, ctx: &PreviewCtx) -> PreviewOutcome {
spawn_directory_preview(
entry.path.clone(),
ctx.show_hidden,
ctx.generation,
ctx.worker_tx.clone(),
);
PreviewOutcome::Deferred
}
}
fn spawn_directory_preview(
path: PathBuf,
show_hidden: bool,
generation: u64,
tx: mpsc::Sender<WorkerMsg>,
) {
tokio::spawn(async move {
let path_for_msg = path.clone();
let content =
tokio::task::spawn_blocking(move || build_directory_preview(&path, show_hidden))
.await
.unwrap_or(PreviewContent::Empty);
let msg = WorkerMsg::Preview {
generation,
path: path_for_msg,
content,
truncated: false,
is_text: None,
};
let _ = tx.send(msg).await;
});
}
pub fn build_directory_preview(path: &std::path::Path, show_hidden: bool) -> PreviewContent {
let read = match fs::read_dir(path) {
Ok(r) => r,
Err(_) => return PreviewContent::Empty,
};
let mut file_count = 0usize;
let mut dir_count = 0usize;
let mut hidden_count = 0usize;
let mut names: Vec<String> = Vec::new();
for res in read {
let de = match res {
Ok(d) => d,
Err(_) => continue,
};
let name = match de.file_name().to_str() {
Some(n) => crate::preview::provider::sanitize(n),
None => continue,
};
let is_hidden = name.starts_with('.');
if is_hidden {
hidden_count += 1;
}
let metadata = de.metadata().ok();
let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
if is_dir {
dir_count += 1;
} else {
file_count += 1;
}
if (!is_hidden || show_hidden) && names.len() < MAX_PREVIEW_ENTRIES {
if is_dir {
names.push(format!("{name}/"));
} else {
names.push(name);
}
}
}
names.sort_by(|a, b| {
let a_dir = a.ends_with('/');
let b_dir = b.ends_with('/');
match (a_dir, b_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.to_lowercase().cmp(&b.to_lowercase()),
}
});
PreviewContent::Directory {
file_count,
dir_count,
hidden_count,
entries: names,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs as sfs;
use tempfile::tempdir;
#[test]
fn directory_preview_counts() {
let dir = tempdir().unwrap();
sfs::create_dir(dir.path().join("subdir")).unwrap();
sfs::write(dir.path().join("file.txt"), b"").unwrap();
sfs::write(dir.path().join(".hidden"), b"").unwrap();
let content = build_directory_preview(dir.path(), false);
if let PreviewContent::Directory {
file_count,
dir_count,
hidden_count,
entries,
} = content
{
assert_eq!(dir_count, 1);
assert_eq!(file_count, 2); assert_eq!(hidden_count, 1);
assert!(!entries.iter().any(|e| e.contains("hidden")));
} else {
panic!("expected Directory variant");
}
}
#[test]
fn directory_preview_shows_hidden_when_requested() {
let dir = tempdir().unwrap();
sfs::write(dir.path().join(".hidden"), b"").unwrap();
let content = build_directory_preview(dir.path(), true);
if let PreviewContent::Directory { entries, .. } = content {
assert!(entries.iter().any(|e| e.contains("hidden")));
} else {
panic!("expected Directory variant");
}
}
}