trail/preview/
directory.rs1use std::fs;
10use std::path::PathBuf;
11
12use tokio::sync::mpsc;
13
14use crate::app::state::{Entry, EntryKind};
15use crate::preview::provider::{PreviewContent, PreviewCtx, PreviewOutcome, PreviewProvider};
16use crate::workers::WorkerMsg;
17
18const MAX_PREVIEW_ENTRIES: usize = 64;
20
21pub struct DirectoryProvider;
27
28impl PreviewProvider for DirectoryProvider {
29 fn can_handle(&self, entry: &Entry) -> bool {
30 entry.kind == EntryKind::Dir
31 }
32
33 fn preview(&self, entry: &Entry, ctx: &PreviewCtx) -> PreviewOutcome {
34 spawn_directory_preview(
35 entry.path.clone(),
36 ctx.show_hidden,
37 ctx.generation,
38 ctx.worker_tx.clone(),
39 );
40 PreviewOutcome::Deferred
41 }
42}
43
44fn spawn_directory_preview(
50 path: PathBuf,
51 show_hidden: bool,
52 generation: u64,
53 tx: mpsc::Sender<WorkerMsg>,
54) {
55 tokio::spawn(async move {
56 let path_for_msg = path.clone();
57 let content =
58 tokio::task::spawn_blocking(move || build_directory_preview(&path, show_hidden))
59 .await
60 .unwrap_or(PreviewContent::Empty);
61
62 let msg = WorkerMsg::Preview {
63 generation,
64 path: path_for_msg,
65 content,
66 truncated: false,
69 is_text: None,
71 };
72 let _ = tx.send(msg).await;
74 });
75}
76
77pub fn build_directory_preview(path: &std::path::Path, show_hidden: bool) -> PreviewContent {
81 let read = match fs::read_dir(path) {
82 Ok(r) => r,
83 Err(_) => return PreviewContent::Empty,
84 };
85
86 let mut file_count = 0usize;
87 let mut dir_count = 0usize;
88 let mut hidden_count = 0usize;
89 let mut names: Vec<String> = Vec::new();
90
91 for res in read {
92 let de = match res {
93 Ok(d) => d,
94 Err(_) => continue,
95 };
96
97 let name = match de.file_name().to_str() {
101 Some(n) => crate::preview::provider::sanitize(n),
102 None => continue,
103 };
104
105 let is_hidden = name.starts_with('.');
106 if is_hidden {
107 hidden_count += 1;
108 }
109
110 let metadata = de.metadata().ok();
111 let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
112
113 if is_dir {
114 dir_count += 1;
115 } else {
116 file_count += 1;
117 }
118
119 if (!is_hidden || show_hidden) && names.len() < MAX_PREVIEW_ENTRIES {
120 if is_dir {
122 names.push(format!("{name}/"));
123 } else {
124 names.push(name);
125 }
126 }
127 }
128
129 names.sort_by(|a, b| {
131 let a_dir = a.ends_with('/');
132 let b_dir = b.ends_with('/');
133 match (a_dir, b_dir) {
134 (true, false) => std::cmp::Ordering::Less,
135 (false, true) => std::cmp::Ordering::Greater,
136 _ => a.to_lowercase().cmp(&b.to_lowercase()),
137 }
138 });
139
140 PreviewContent::Directory {
141 file_count,
142 dir_count,
143 hidden_count,
144 entries: names,
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::fs as sfs;
152 use tempfile::tempdir;
153
154 #[test]
155 fn directory_preview_counts() {
156 let dir = tempdir().unwrap();
157 sfs::create_dir(dir.path().join("subdir")).unwrap();
158 sfs::write(dir.path().join("file.txt"), b"").unwrap();
159 sfs::write(dir.path().join(".hidden"), b"").unwrap();
160
161 let content = build_directory_preview(dir.path(), false);
162
163 if let PreviewContent::Directory {
164 file_count,
165 dir_count,
166 hidden_count,
167 entries,
168 } = content
169 {
170 assert_eq!(dir_count, 1);
171 assert_eq!(file_count, 2); assert_eq!(hidden_count, 1);
173 assert!(!entries.iter().any(|e| e.contains("hidden")));
175 } else {
176 panic!("expected Directory variant");
177 }
178 }
179
180 #[test]
181 fn directory_preview_shows_hidden_when_requested() {
182 let dir = tempdir().unwrap();
183 sfs::write(dir.path().join(".hidden"), b"").unwrap();
184
185 let content = build_directory_preview(dir.path(), true);
186
187 if let PreviewContent::Directory { entries, .. } = content {
188 assert!(entries.iter().any(|e| e.contains("hidden")));
189 } else {
190 panic!("expected Directory variant");
191 }
192 }
193}