1use colored::*;
2use serde::Serialize;
3use std::io::{self, Write};
4
5use crate::format::{format_size, format_time};
6use crate::tree::{TreeEntry, TreeStats};
7
8#[derive(Debug, Clone, Default)]
10pub enum OutputFormat {
11 #[default]
12 Text,
13 Json,
14 Toon,
15}
16
17#[derive(Debug, Clone)]
19pub struct PrintConfig {
20 pub colorize: bool,
21 pub show_permissions: bool,
22 pub show_size: bool,
23 pub human_readable: bool,
24 pub si_units: bool,
25 pub show_date: bool,
26 pub time_format: Option<String>,
27 pub show_type_indicator: bool,
28 pub no_indent: bool,
29 pub full_path: bool,
30 pub no_report: bool,
31 pub output_format: OutputFormat,
32}
33
34impl Default for PrintConfig {
35 fn default() -> Self {
36 Self {
37 colorize: true,
38 show_permissions: false,
39 show_size: false,
40 human_readable: false,
41 si_units: false,
42 show_date: false,
43 time_format: None,
44 show_type_indicator: false,
45 no_indent: false,
46 full_path: false,
47 no_report: false,
48 output_format: OutputFormat::Text,
49 }
50 }
51}
52
53const BRANCH: &str = "├── ";
55const LAST_BRANCH: &str = "└── ";
56const VERTICAL: &str = "│ ";
57const EMPTY: &str = " ";
58
59pub fn print_tree<W: Write>(
61 writer: &mut W,
62 entry: &TreeEntry,
63 config: &PrintConfig,
64 stats: &TreeStats,
65) -> io::Result<()> {
66 match config.output_format {
67 OutputFormat::Text => print_tree_text(writer, entry, config, stats),
68 OutputFormat::Json => print_tree_json(writer, entry),
69 OutputFormat::Toon => print_tree_toon(writer, entry),
70 }
71}
72
73fn print_tree_text<W: Write>(
75 writer: &mut W,
76 entry: &TreeEntry,
77 config: &PrintConfig,
78 stats: &TreeStats,
79) -> io::Result<()> {
80 let root_name = format_entry_name(entry, config, true);
82 writeln!(writer, "{}", root_name)?;
83
84 print_children(writer, entry, config, "")?;
86
87 if !config.no_report {
89 writeln!(writer)?;
90 let dir_word = if stats.directories == 1 {
91 "directory"
92 } else {
93 "directories"
94 };
95 let file_word = if stats.files == 1 { "file" } else { "files" };
96 writeln!(
97 writer,
98 "{} {}, {} {}",
99 stats.directories, dir_word, stats.files, file_word
100 )?;
101 }
102
103 Ok(())
104}
105
106fn print_children<W: Write>(
107 writer: &mut W,
108 entry: &TreeEntry,
109 config: &PrintConfig,
110 prefix: &str,
111) -> io::Result<()> {
112 let children = &entry.children;
113 let count = children.len();
114
115 for (idx, child) in children.iter().enumerate() {
116 let is_last = idx == count - 1;
117
118 let (branch, child_prefix) = if config.no_indent {
120 ("", "".to_string())
121 } else if is_last {
122 (LAST_BRANCH, format!("{}{}", prefix, EMPTY))
123 } else {
124 (BRANCH, format!("{}{}", prefix, VERTICAL))
125 };
126
127 let mut line = String::new();
129
130 if config.show_permissions {
132 line.push_str(&child.permissions_string());
133 line.push(' ');
134 }
135
136 if config.show_size {
137 let size_str = if config.human_readable {
138 format_size(child.size(), config.si_units)
139 } else {
140 format!("{:>10}", child.size())
141 };
142 line.push_str(&size_str);
143 line.push(' ');
144 }
145
146 if config.show_date {
147 if let Some(time) = child.modified() {
148 let time_str = format_time(time, config.time_format.as_deref());
149 line.push_str(&time_str);
150 line.push(' ');
151 }
152 }
153
154 let name = format_entry_name(child, config, false);
156
157 if config.no_indent {
159 writeln!(writer, "{}{}", line, name)?;
160 } else {
161 writeln!(writer, "{}{}{}{}", prefix, branch, line, name)?;
162 }
163
164 if let Some(ref error) = child.error {
166 let error_prefix = if config.no_indent {
167 ""
168 } else {
169 &child_prefix
170 };
171 writeln!(writer, "{}{}", error_prefix, error.red())?;
172 }
173
174 if !child.children.is_empty() {
176 print_children(writer, child, config, &child_prefix)?;
177 }
178 }
179
180 Ok(())
181}
182
183fn format_entry_name(entry: &TreeEntry, config: &PrintConfig, is_root: bool) -> String {
184 let name = if config.full_path && !is_root {
185 entry.path.to_string_lossy().to_string()
186 } else {
187 entry.name.clone()
188 };
189
190 let mut display_name = if config.colorize {
191 if entry.is_dir {
192 name.bold().blue().to_string()
193 } else if entry.is_symlink {
194 name.cyan().to_string()
195 } else if entry.is_executable() {
196 name.bold().green().to_string()
197 } else {
198 name
199 }
200 } else {
201 name
202 };
203
204 if config.show_type_indicator {
206 display_name.push_str(entry.type_indicator());
207 }
208
209 if entry.is_symlink {
211 if let Some(ref target) = entry.symlink_target {
212 let target_str = target.to_string_lossy();
213 if config.colorize {
214 display_name = format!("{} -> {}", display_name, target_str.cyan());
215 } else {
216 display_name = format!("{} -> {}", display_name, target_str);
217 }
218 }
219 }
220
221 display_name
222}
223
224#[derive(Serialize)]
226struct TreeNode {
227 #[serde(rename = "type")]
228 node_type: String,
229 name: String,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 contents: Option<Vec<TreeNode>>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 target: Option<String>,
234}
235
236impl From<&TreeEntry> for TreeNode {
237 fn from(entry: &TreeEntry) -> Self {
238 let node_type = if entry.is_dir {
239 "directory"
240 } else if entry.is_symlink {
241 "link"
242 } else {
243 "file"
244 };
245
246 let contents = if entry.is_dir && !entry.children.is_empty() {
247 Some(entry.children.iter().map(TreeNode::from).collect())
248 } else {
249 None
250 };
251
252 let target = entry
253 .symlink_target
254 .as_ref()
255 .map(|p| p.to_string_lossy().to_string());
256
257 TreeNode {
258 node_type: node_type.to_string(),
259 name: entry.name.clone(),
260 contents,
261 target,
262 }
263 }
264}
265
266fn print_tree_json<W: Write>(writer: &mut W, entry: &TreeEntry) -> io::Result<()> {
267 let tree_node = TreeNode::from(entry);
268 let json = serde_json::to_string_pretty(&[tree_node]).map_err(|e| {
269 io::Error::new(io::ErrorKind::Other, e)
270 })?;
271 writeln!(writer, "{}", json)?;
272 Ok(())
273}
274
275fn print_tree_toon<W: Write>(writer: &mut W, entry: &TreeEntry) -> io::Result<()> {
278 writeln!(writer, "# TOON - Tree Output")?;
279 print_toon_entry(writer, entry, 0)?;
280 Ok(())
281}
282
283fn print_toon_entry<W: Write>(writer: &mut W, entry: &TreeEntry, depth: usize) -> io::Result<()> {
284 let indent = " ".repeat(depth);
285 let node_type = if entry.is_dir {
286 "d"
287 } else if entry.is_symlink {
288 "l"
289 } else {
290 "f"
291 };
292
293 if let Some(ref target) = entry.symlink_target {
295 writeln!(writer, "{}{}:{} -> {}", indent, node_type, entry.name, target.display())?;
296 } else {
297 writeln!(writer, "{}{}:{}", indent, node_type, entry.name)?;
298 }
299
300 if entry.is_dir && !entry.children.is_empty() {
302 for child in &entry.children {
303 print_toon_entry(writer, child, depth + 1)?;
304 }
305 }
306
307 Ok(())
308}