rust_filesearch/output/
pretty.rs

1use crate::errors::Result;
2use crate::models::{Column, Entry, EntryKind};
3use crate::output::format::OutputSink;
4use crate::util::{format_size_human, is_tty};
5use nu_ansi_term::Color;
6use std::io::Write;
7
8pub struct PrettyFormatter {
9    writer: Box<dyn Write>,
10    columns: Vec<Column>,
11    use_color: bool,
12}
13
14impl PrettyFormatter {
15    pub fn new(writer: Box<dyn Write>, columns: Vec<Column>, no_color: bool) -> Self {
16        let use_color = is_tty() && !no_color;
17        Self {
18            writer,
19            columns,
20            use_color,
21        }
22    }
23
24    fn format_entry(&self, entry: &Entry) -> String {
25        let mut parts = Vec::new();
26
27        for column in &self.columns {
28            let value = match column {
29                Column::Path => self.colorize_path(&entry.path.display().to_string(), entry.kind),
30                Column::Name => self.colorize_path(&entry.name, entry.kind),
31                Column::Size => format_size_human(entry.size),
32                Column::Mtime => entry.mtime.format("%Y-%m-%d %H:%M:%S").to_string(),
33                Column::Kind => format!("{:?}", entry.kind).to_lowercase(),
34                Column::Perms => entry.perms.clone().unwrap_or_default(),
35                Column::Owner => entry.owner.clone().unwrap_or_default(),
36            };
37            parts.push(value);
38        }
39
40        parts.join("  ")
41    }
42
43    fn colorize_path(&self, path: &str, kind: EntryKind) -> String {
44        if !self.use_color {
45            return path.to_string();
46        }
47
48        match kind {
49            EntryKind::Dir => Color::Blue.bold().paint(path).to_string(),
50            EntryKind::Symlink => Color::Cyan.paint(path).to_string(),
51            EntryKind::File => {
52                // Color executables differently if possible
53                if path.ends_with(".exe") || path.ends_with(".sh") {
54                    Color::Green.bold().paint(path).to_string()
55                } else {
56                    path.to_string()
57                }
58            }
59        }
60    }
61}
62
63impl OutputSink for PrettyFormatter {
64    fn write(&mut self, entry: &Entry) -> Result<()> {
65        writeln!(self.writer, "{}", self.format_entry(entry))?;
66        Ok(())
67    }
68
69    fn finish(&mut self) -> Result<()> {
70        self.writer.flush()?;
71        Ok(())
72    }
73}
74
75/// Tree view formatter for hierarchical display
76pub struct TreeFormatter {
77    writer: Box<dyn Write>,
78    use_color: bool,
79    dirs_first: bool,
80}
81
82impl TreeFormatter {
83    pub fn new(writer: Box<dyn Write>, no_color: bool, dirs_first: bool) -> Self {
84        let use_color = is_tty() && !no_color;
85        Self {
86            writer,
87            use_color,
88            dirs_first,
89        }
90    }
91
92    pub fn write_tree(&mut self, entries: &[Entry]) -> Result<()> {
93        // Sort entries if dirs_first is enabled
94        let mut sorted_entries = entries.to_vec();
95        if self.dirs_first {
96            sorted_entries.sort_by(|a, b| {
97                // First by kind (dirs before files), then by name
98                match (a.kind, b.kind) {
99                    (EntryKind::Dir, EntryKind::File) => std::cmp::Ordering::Less,
100                    (EntryKind::File, EntryKind::Dir) => std::cmp::Ordering::Greater,
101                    _ => a.name.cmp(&b.name),
102                }
103            });
104        }
105
106        for entry in &sorted_entries {
107            self.write_tree_entry(entry)?;
108        }
109
110        self.writer.flush()?;
111        Ok(())
112    }
113
114    fn write_tree_entry(&mut self, entry: &Entry) -> Result<()> {
115        let indent = "  ".repeat(entry.depth);
116        let prefix = if entry.depth > 0 { "├── " } else { "" };
117
118        let name = self.colorize_name(&entry.name, entry.kind);
119        writeln!(self.writer, "{}{}{}", indent, prefix, name)?;
120        Ok(())
121    }
122
123    fn colorize_name(&self, name: &str, kind: EntryKind) -> String {
124        if !self.use_color {
125            return name.to_string();
126        }
127
128        match kind {
129            EntryKind::Dir => Color::Blue.bold().paint(format!("{}/", name)).to_string(),
130            EntryKind::Symlink => Color::Cyan.paint(format!("{} @", name)).to_string(),
131            EntryKind::File => name.to_string(),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::path::PathBuf;
140
141    fn make_test_entry(name: &str, kind: EntryKind) -> Entry {
142        use chrono::Utc;
143
144        Entry {
145            path: PathBuf::from(name),
146            name: name.to_string(),
147            size: 1024,
148            kind,
149            mtime: Utc::now(),
150            perms: Some("rw-r--r--".to_string()),
151            owner: Some("1000".to_string()),
152            depth: 0,
153        }
154    }
155
156    #[test]
157    fn test_pretty_formatter() {
158        use std::io::Cursor;
159
160        let output = Cursor::new(Vec::new());
161        let mut formatter =
162            PrettyFormatter::new(Box::new(output), vec![Column::Name, Column::Size], true);
163
164        let entry = make_test_entry("test.txt", EntryKind::File);
165        formatter.write(&entry).unwrap();
166        formatter.finish().unwrap();
167    }
168
169    #[test]
170    fn test_tree_formatter() {
171        use std::io::Cursor;
172
173        let output = Cursor::new(Vec::new());
174        let mut formatter = TreeFormatter::new(Box::new(output), true, false);
175
176        let entries = vec![
177            make_test_entry("root", EntryKind::Dir),
178            Entry {
179                depth: 1,
180                ..make_test_entry("file.txt", EntryKind::File)
181            },
182        ];
183
184        formatter.write_tree(&entries).unwrap();
185    }
186}