Skip to main content

tree_rust/
printer.rs

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/// Output format options
9#[derive(Debug, Clone, Default)]
10pub enum OutputFormat {
11    #[default]
12    Text,
13    Json,
14    Toon,
15}
16
17/// Configuration for tree printing
18#[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
53// Tree drawing characters
54const BRANCH: &str = "├── ";
55const LAST_BRANCH: &str = "└── ";
56const VERTICAL: &str = "│   ";
57const EMPTY: &str = "    ";
58
59/// Print the tree structure
60pub 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, config),
70    }
71}
72
73/// Print tree in text format
74fn print_tree_text<W: Write>(
75    writer: &mut W,
76    entry: &TreeEntry,
77    config: &PrintConfig,
78    stats: &TreeStats,
79) -> io::Result<()> {
80    // Print root directory
81    let root_name = format_entry_name(entry, config, true);
82    writeln!(writer, "{}", root_name)?;
83
84    // Print children
85    print_children(writer, entry, config, "")?;
86
87    // Print statistics
88    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        // Build the line prefix
119        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        // Format the entry info
128        let mut line = String::new();
129
130        // Add metadata before the name if needed
131        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        // Format name with color
155        let name = format_entry_name(child, config, false);
156
157        // Print the line
158        if config.no_indent {
159            writeln!(writer, "{}{}", line, name)?;
160        } else {
161            writeln!(writer, "{}{}{}{}", prefix, branch, line, name)?;
162        }
163
164        // Handle errors
165        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        // Recursively print children
175        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    // Add type indicator
205    if config.show_type_indicator {
206        display_name.push_str(entry.type_indicator());
207    }
208
209    // Add symlink target
210    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// JSON/TOML serialization structures
225#[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
275/// Print tree in TOON (Token-Oriented Object Notation) format
276/// TOON is optimized for LLMs with minimal token usage
277fn print_tree_toon<W: Write>(writer: &mut W, entry: &TreeEntry, config: &PrintConfig) -> io::Result<()> {
278    writeln!(writer, "# TOON - Tree Output")?;
279    print_toon_entry(writer, entry, 0, config)?;
280    Ok(())
281}
282
283fn print_toon_entry<W: Write>(writer: &mut W, entry: &TreeEntry, depth: usize, config: &PrintConfig) -> 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    // Build metadata parts
294    let mut parts: Vec<String> = vec![node_type.to_string()];
295
296    if config.show_permissions {
297        parts.push(entry.permissions_string());
298    }
299
300    if config.show_size {
301        let size_str = if config.human_readable {
302            format_size(entry.size(), config.si_units)
303        } else {
304            entry.size().to_string()
305        };
306        parts.push(size_str);
307    }
308
309    if config.show_date {
310        if let Some(time) = entry.modified() {
311            let time_str = format_time(time, config.time_format.as_deref());
312            parts.push(time_str);
313        }
314    }
315
316    // Add name as last part
317    parts.push(entry.name.clone());
318
319    // Output entry: type:perm:size:date:name or type:name
320    let line = parts.join(":");
321    if let Some(ref target) = entry.symlink_target {
322        writeln!(writer, "{}{} -> {}", indent, line, target.display())?;
323    } else {
324        writeln!(writer, "{}{}", indent, line)?;
325    }
326
327    // Output children count if directory has children
328    if entry.is_dir && !entry.children.is_empty() {
329        for child in &entry.children {
330            print_toon_entry(writer, child, depth + 1, config)?;
331        }
332    }
333
334    Ok(())
335}