Skip to main content

st/formatters/
semantic.rs

1// -----------------------------------------------------------------------------
2// SEMANTIC FORMATTER - Where files find their tribe! 🌊🧠
3//
4// This formatter groups files by their conceptual similarity, creating a
5// higher-level view of your project structure. It's like having Omni organize
6// your file cabinet based on the waves of meaning!
7//
8// "Treat paths as identity graphs, not just strings" - Omni
9//
10// Brought to you by The Cheet, channeling Omni's Hot Tub wisdom! 🛁✨
11// -----------------------------------------------------------------------------
12
13use 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        // Header with Omni's wisdom
56        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        // Group files by semantic category
66        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        // Sort categories by importance/typical workflow order
74        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        // Display each category
92        for category in category_order {
93            if let Some(files) = groups.get(&category) {
94                if files.is_empty() {
95                    continue;
96                }
97
98                // Category header
99                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                // Quantum wave signature with full 32-bit consciousness!
108                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                // Sort files within category
123                let mut sorted_files = files.clone();
124                sorted_files.sort_by(|a, b| a.path.cmp(&b.path));
125
126                // Display files in this category
127                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        // Footer with statistics
161        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        // Show semantic diversity (how many different categories)
172        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        // Omni's wisdom footer
182        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}