1use crate::preview::doc::format_unix_timestamp;
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::text::{Line, Span, Text};
4use std::fs;
5use std::path::Path;
6use std::time::UNIX_EPOCH;
7
8pub fn generate_preview(path: &Path) -> Text<'static> {
9 let mut lines: Vec<Line<'static>> = Vec::new();
10
11 let abs_path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
12 lines.push(Line::from(vec![Span::styled(
13 format!("Directory: {}", abs_path.display()),
14 Style::default()
15 .fg(Color::Cyan)
16 .add_modifier(Modifier::BOLD),
17 )]));
18 lines.push(Line::from(""));
19
20 let entries = match collect_entries(path) {
21 Ok(e) => e,
22 Err(err) => {
23 lines.push(Line::from(vec![Span::styled(
24 format!("Error reading directory: {}", err),
25 Style::default().fg(Color::Red),
26 )]));
27 return Text::from(lines);
28 }
29 };
30
31 let dir_count = entries.iter().filter(|e| e.kind == EntryKind::Dir).count();
32 let file_count = entries.iter().filter(|e| e.kind == EntryKind::File).count();
33 let link_count = entries
34 .iter()
35 .filter(|e| e.kind == EntryKind::Symlink)
36 .count();
37
38 let summary = build_summary(dir_count, file_count, link_count);
39 lines.push(Line::from(vec![Span::styled(
40 summary,
41 Style::default().fg(Color::DarkGray),
42 )]));
43 lines.push(Line::from(""));
44
45 lines.push(Line::from(vec![Span::styled(
46 format!(
47 " {:<10} {:>8} {:<16} {}",
48 "Permissions", "Size", "Modified", "Name"
49 ),
50 Style::default()
51 .fg(Color::DarkGray)
52 .add_modifier(Modifier::BOLD),
53 )]));
54 lines.push(Line::from(vec![Span::styled(
55 format!(" {}", "─".repeat(60)),
56 Style::default().fg(Color::DarkGray),
57 )]));
58
59 for entry in &entries {
60 lines.push(render_entry(entry));
61 }
62
63 Text::from(lines)
64}
65
66fn build_summary(dirs: usize, files: usize, links: usize) -> String {
67 let mut parts = Vec::new();
68 if dirs > 0 {
69 parts.push(format!(
70 "{} {}",
71 dirs,
72 if dirs == 1 {
73 "directory"
74 } else {
75 "directories"
76 }
77 ));
78 }
79 if files > 0 {
80 parts.push(format!(
81 "{} {}",
82 files,
83 if files == 1 { "file" } else { "files" }
84 ));
85 }
86 if links > 0 {
87 parts.push(format!(
88 "{} {}",
89 links,
90 if links == 1 { "symlink" } else { "symlinks" }
91 ));
92 }
93 if parts.is_empty() {
94 " empty".to_string()
95 } else {
96 format!(" {}", parts.join(", "))
97 }
98}
99
100#[derive(PartialEq, Eq)]
101enum EntryKind {
102 Dir,
103 File,
104 Symlink,
105}
106
107struct DirEntry {
108 name: String,
109 kind: EntryKind,
110 size: Option<u64>,
111 mtime: Option<u64>,
112 permissions: String,
113 link_target: Option<String>,
114}
115
116fn collect_entries(path: &Path) -> std::io::Result<Vec<DirEntry>> {
117 let mut entries: Vec<DirEntry> = Vec::new();
118
119 for result in fs::read_dir(path)? {
120 let dir_entry = result?;
121 let name = dir_entry.file_name().to_string_lossy().into_owned();
122 let meta = dir_entry.path().symlink_metadata()?;
123 let file_type = meta.file_type();
124
125 let kind = if file_type.is_symlink() {
126 EntryKind::Symlink
127 } else if file_type.is_dir() {
128 EntryKind::Dir
129 } else {
130 EntryKind::File
131 };
132
133 let size = if file_type.is_dir() {
134 None
135 } else {
136 Some(meta.len())
137 };
138
139 let mtime = meta
140 .modified()
141 .ok()
142 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
143 .map(|d| d.as_secs());
144
145 let permissions = format_permissions(&meta);
146
147 let link_target = if file_type.is_symlink() {
148 fs::read_link(dir_entry.path())
149 .ok()
150 .map(|t| t.to_string_lossy().into_owned())
151 } else {
152 None
153 };
154
155 entries.push(DirEntry {
156 name,
157 kind,
158 size,
159 mtime,
160 permissions,
161 link_target,
162 });
163 }
164
165 entries.sort_by(|a, b| {
166 let order = |k: &EntryKind| match k {
167 EntryKind::Dir => 0,
168 EntryKind::Symlink => 1,
169 EntryKind::File => 2,
170 };
171 order(&a.kind)
172 .cmp(&order(&b.kind))
173 .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
174 });
175
176 Ok(entries)
177}
178
179fn render_entry(entry: &DirEntry) -> Line<'static> {
180 let perm_str = format!(" {:<10} ", entry.permissions);
181 let size_str = match entry.size {
182 Some(s) => format!("{:>8} ", format_size_short(s)),
183 None => format!("{:>8} ", "-"),
184 };
185 let mtime_str = match entry.mtime {
186 Some(t) => format!("{:<16} ", format_unix_timestamp(t)),
187 None => format!("{:<16} ", "Unknown"),
188 };
189 let name_str = match (&entry.kind, &entry.link_target) {
190 (EntryKind::Dir, _) => format!("{}/", entry.name),
191 (EntryKind::Symlink, Some(target)) => format!("{} -> {}", entry.name, target),
192 (EntryKind::Symlink, None) => format!("{}@", entry.name),
193 (EntryKind::File, _) => entry.name.clone(),
194 };
195
196 let name_color = match entry.kind {
197 EntryKind::Dir => Color::Cyan,
198 EntryKind::Symlink => Color::Magenta,
199 EntryKind::File => name_color_for_file(&entry.name, &entry.permissions),
200 };
201
202 Line::from(vec![
203 Span::styled(perm_str, Style::default().fg(Color::DarkGray)),
204 Span::styled(size_str, Style::default().fg(Color::Yellow)),
205 Span::styled(mtime_str, Style::default().fg(Color::DarkGray)),
206 Span::styled(name_str, Style::default().fg(name_color)),
207 ])
208}
209
210fn format_size_short(size: u64) -> String {
211 const KB: u64 = 1024;
212 const MB: u64 = KB * 1024;
213 const GB: u64 = MB * 1024;
214 if size >= GB {
215 format!("{:.1}G", size as f64 / GB as f64)
216 } else if size >= MB {
217 format!("{:.1}M", size as f64 / MB as f64)
218 } else if size >= KB {
219 format!("{:.1}K", size as f64 / KB as f64)
220 } else {
221 format!("{}", size)
222 }
223}
224
225#[cfg(unix)]
226fn format_permissions(meta: &fs::Metadata) -> String {
227 use std::os::unix::fs::PermissionsExt;
228 let mode = meta.permissions().mode();
229 let ft = meta.file_type();
230 let type_char = if ft.is_symlink() {
231 'l'
232 } else if ft.is_dir() {
233 'd'
234 } else {
235 '-'
236 };
237 let bits = [
238 (0o400, 'r'),
239 (0o200, 'w'),
240 (0o100, 'x'),
241 (0o040, 'r'),
242 (0o020, 'w'),
243 (0o010, 'x'),
244 (0o004, 'r'),
245 (0o002, 'w'),
246 (0o001, 'x'),
247 ];
248 let mut s = String::with_capacity(10);
249 s.push(type_char);
250 for &(bit, ch) in &bits {
251 s.push(if mode & bit != 0 { ch } else { '-' });
252 }
253 s
254}
255
256#[cfg(not(unix))]
257fn format_permissions(meta: &fs::Metadata) -> String {
258 let ft = meta.file_type();
259 let type_char = if ft.is_symlink() {
260 'l'
261 } else if ft.is_dir() {
262 'd'
263 } else {
264 '-'
265 };
266 let rw = if meta.permissions().readonly() {
268 "r--r--r--"
269 } else {
270 "rw-rw-rw-"
271 };
272 format!("{}{}", type_char, rw)
273}
274
275fn name_color_for_file(name: &str, permissions: &str) -> Color {
277 let _ = name;
278 #[cfg(unix)]
279 {
280 if permissions.as_bytes().get(3).copied() == Some(b'x') {
282 return Color::Green;
283 }
284 }
285 let _ = permissions;
286 Color::White
287}