1use super::{Formatter, PathDisplayMode};
14use crate::scanner::{FileNode, TreeStats};
15use crate::semantic::{SemanticAnalyzer, SemanticCategory};
16use anyhow::Result;
17use colored::Colorize;
18use std::collections::HashMap;
19use std::io::Write;
20
21pub struct SemanticFormatter {
22 path_mode: PathDisplayMode,
23 analyzer: SemanticAnalyzer,
24}
25
26impl SemanticFormatter {
27 pub fn new(path_mode: PathDisplayMode, _no_emoji: bool) -> Self {
28 Self {
29 path_mode,
30 analyzer: SemanticAnalyzer::new(),
31 }
32 }
33
34 fn format_size(size: u64) -> String {
35 if size < 1024 {
36 format!("{} B", size)
37 } else if size < 1024 * 1024 {
38 format!("{:.1} KB", size as f64 / 1024.0)
39 } else if size < 1024 * 1024 * 1024 {
40 format!("{:.1} MB", size as f64 / 1024.0 / 1024.0)
41 } else {
42 format!("{:.1} GB", size as f64 / 1024.0 / 1024.0 / 1024.0)
43 }
44 }
45}
46
47impl Formatter for SemanticFormatter {
48 fn format(
49 &self,
50 writer: &mut dyn Write,
51 nodes: &[FileNode],
52 stats: &TreeStats,
53 _root_path: &std::path::Path,
54 ) -> Result<()> {
55 writeln!(writer, "{}", "🌊 SEMANTIC WAVE ANALYSIS 🌊".cyan().bold())?;
57 writeln!(
58 writer,
59 "{}",
60 "Grouping files by conceptual similarity...".dimmed()
61 )?;
62 writeln!(writer, "{}", "━".repeat(60).dimmed())?;
63 writeln!(writer)?;
64
65 let mut groups: HashMap<SemanticCategory, Vec<FileNode>> = HashMap::new();
67
68 for node in nodes {
69 let category = self.analyzer.categorize(&node.path);
70 groups.entry(category).or_default().push(node.clone());
71 }
72
73 let category_order = vec![
75 SemanticCategory::ProjectRoot,
76 SemanticCategory::Documentation,
77 SemanticCategory::SourceCode,
78 SemanticCategory::Tests,
79 SemanticCategory::Configuration,
80 SemanticCategory::BuildSystem,
81 SemanticCategory::Scripts,
82 SemanticCategory::Assets,
83 SemanticCategory::Data,
84 SemanticCategory::Dependencies,
85 SemanticCategory::Generated,
86 SemanticCategory::Development,
87 SemanticCategory::Deployment,
88 SemanticCategory::Unknown,
89 ];
90
91 for category in category_order {
93 if let Some(files) = groups.get(&category) {
94 if files.is_empty() {
95 continue;
96 }
97
98 writeln!(writer, "{}", category.display_name().bold())?;
100 writeln!(
101 writer,
102 " {} files | Total size: {}",
103 files.len().to_string().green(),
104 Self::format_size(files.iter().map(|f| f.size).sum()).yellow()
105 )?;
106
107 let sig = crate::quantum_wave_signature::QuantumWaveSignature::from_raw(
109 category.wave_signature(),
110 );
111 writeln!(
112 writer,
113 " Wave: {} ({}Hz ∠{}° {}% τ{})",
114 format!("0x{:08X}", category.wave_signature()).cyan(),
115 sig.to_hz() as u32,
116 (sig.to_radians() * 180.0 / std::f32::consts::PI) as u32,
117 sig.amplitude_percent() as u32,
118 sig.torsion()
119 )?;
120 writeln!(writer)?;
121
122 let mut sorted_files = files.clone();
124 sorted_files.sort_by(|a, b| a.path.cmp(&b.path));
125
126 for (idx, node) in sorted_files.iter().enumerate() {
128 let prefix = if idx == sorted_files.len() - 1 {
129 " └── "
130 } else {
131 " ├── "
132 };
133
134 let name = match self.path_mode {
135 PathDisplayMode::Off => node
136 .path
137 .file_name()
138 .and_then(|n| n.to_str())
139 .unwrap_or("?")
140 .to_string(),
141 PathDisplayMode::Relative => node.path.display().to_string(),
142 PathDisplayMode::Full => node.path.display().to_string(),
143 };
144
145 let size_str = if node.is_dir {
146 "[DIR]".dimmed().to_string()
147 } else {
148 format!("({})", Self::format_size(node.size))
149 .dimmed()
150 .to_string()
151 };
152
153 writeln!(writer, "{}{} {}", prefix.dimmed(), name, size_str)?;
154 }
155
156 writeln!(writer)?;
157 }
158 }
159
160 writeln!(writer, "{}", "━".repeat(60).dimmed())?;
162 writeln!(writer, "{}", "WAVE FIELD STATISTICS".cyan().bold())?;
163 writeln!(
164 writer,
165 "Total files: {} | Total directories: {} | Total size: {}",
166 stats.total_files.to_string().green(),
167 stats.total_dirs.to_string().blue(),
168 Self::format_size(stats.total_size).yellow()
169 )?;
170
171 let category_count = groups.len();
173 let diversity_score = (category_count as f32 / 14.0 * 100.0).round();
174 writeln!(
175 writer,
176 "Semantic diversity: {} categories ({:.0}% coverage)",
177 category_count.to_string().magenta(),
178 diversity_score
179 )?;
180
181 writeln!(writer)?;
183 writeln!(
184 writer,
185 "{}",
186 "✨ \"Every file carries waves of meaning\" - Omni ✨"
187 .italic()
188 .dimmed()
189 )?;
190
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::scanner::{FileCategory, FileType, FilesystemType};
199 use std::path::PathBuf;
200 use std::time::SystemTime;
201
202 #[test]
203 fn test_semantic_formatter() {
204 let formatter = SemanticFormatter::new(PathDisplayMode::Off, false);
205
206 let nodes = vec![
207 FileNode {
208 path: PathBuf::from("README.md"),
209 is_dir: false,
210 size: 1024,
211 permissions: 0o644,
212 uid: 1000,
213 gid: 1000,
214 modified: SystemTime::now(),
215 is_symlink: false,
216 is_ignored: false,
217 search_matches: None,
218 is_hidden: false,
219 permission_denied: false,
220 depth: 1,
221 file_type: FileType::RegularFile,
222 category: FileCategory::Markdown,
223 filesystem_type: FilesystemType::Unknown,
224 git_branch: None,
225 traversal_context: None,
226 interest: None,
227 security_findings: Vec::new(),
228 change_status: None,
229 content_hash: None,
230 },
231 FileNode {
232 path: PathBuf::from("src/main.rs"),
233 is_dir: false,
234 size: 2048,
235 permissions: 0o644,
236 uid: 1000,
237 gid: 1000,
238 modified: SystemTime::now(),
239 is_symlink: false,
240 is_ignored: false,
241 search_matches: None,
242 is_hidden: false,
243 permission_denied: false,
244 depth: 2,
245 file_type: FileType::RegularFile,
246 category: FileCategory::Rust,
247 filesystem_type: FilesystemType::Unknown,
248 git_branch: None,
249 traversal_context: None,
250 interest: None,
251 security_findings: Vec::new(),
252 change_status: None,
253 content_hash: None,
254 },
255 FileNode {
256 path: PathBuf::from("tests/test_main.rs"),
257 is_dir: false,
258 size: 512,
259 permissions: 0o644,
260 uid: 1000,
261 gid: 1000,
262 modified: SystemTime::now(),
263 is_symlink: false,
264 is_ignored: false,
265 search_matches: None,
266 is_hidden: false,
267 permission_denied: false,
268 depth: 2,
269 file_type: FileType::RegularFile,
270 category: FileCategory::Rust,
271 filesystem_type: FilesystemType::Unknown,
272 git_branch: None,
273 traversal_context: None,
274 interest: None,
275 security_findings: Vec::new(),
276 change_status: None,
277 content_hash: None,
278 },
279 ];
280
281 let mut stats = TreeStats::default();
282 for node in &nodes {
283 stats.update_file(node);
284 }
285
286 let mut output = Vec::new();
287 let result = formatter.format(&mut output, &nodes, &stats, &PathBuf::from("."));
288 assert!(result.is_ok());
289
290 let output_str = String::from_utf8(output).unwrap();
291 assert!(output_str.contains("Documentation"));
292 assert!(output_str.contains("Source Code"));
293 assert!(output_str.contains("Tests"));
294 assert!(output_str.contains("Wave:"));
295 }
296}