rust_filesearch/output/
pretty.rs1use 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 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
75pub 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 let mut sorted_entries = entries.to_vec();
95 if self.dirs_first {
96 sorted_entries.sort_by(|a, b| {
97 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}