1use console::Style;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
8pub struct TreeNode {
9 pub name: String,
10 pub node_type: NodeType,
11 pub size: Option<u64>,
12 pub children: Vec<TreeNode>,
13 pub metadata: HashMap<String, String>,
14 pub external_refs: Vec<ExternalRef>,
15}
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum NodeType {
20 Root,
21 Header,
22 #[allow(dead_code)]
23 Chunk,
24 #[allow(dead_code)]
25 Table,
26 File,
27 Directory,
28 #[allow(dead_code)] Reference,
30 #[allow(dead_code)]
31 Property,
32 #[allow(dead_code)]
33 Data,
34}
35
36#[derive(Debug, Clone)]
38pub struct ExternalRef {
39 pub path: String,
40 pub ref_type: RefType,
41 pub exists: Option<bool>,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum RefType {
47 Texture,
48 Model,
49 Animation,
50 Map,
51 Database,
52 Sound,
53 Script,
54 Unknown,
55}
56
57#[derive(Debug, Clone)]
59pub struct TreeOptions {
60 pub max_depth: Option<usize>,
61 pub show_external_refs: bool,
62 pub no_color: bool,
63 pub show_metadata: bool,
64 pub compact: bool,
65}
66
67impl Default for TreeOptions {
68 fn default() -> Self {
69 Self {
70 max_depth: None,
71 show_external_refs: true,
72 no_color: false,
73 show_metadata: true,
74 compact: false,
75 }
76 }
77}
78
79impl TreeNode {
80 pub fn new(name: String, node_type: NodeType) -> Self {
82 Self {
83 name,
84 node_type,
85 size: None,
86 children: Vec::new(),
87 metadata: HashMap::new(),
88 external_refs: Vec::new(),
89 }
90 }
91
92 pub fn add_child(mut self, child: TreeNode) -> Self {
94 self.children.push(child);
95 self
96 }
97
98 pub fn with_size(mut self, size: u64) -> Self {
100 self.size = Some(size);
101 self
102 }
103
104 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
106 self.metadata.insert(key.to_string(), value.to_string());
107 self
108 }
109
110 pub fn with_external_ref(mut self, path: &str, ref_type: RefType) -> Self {
112 self.external_refs.push(ExternalRef {
113 path: path.to_string(),
114 ref_type,
115 exists: None,
116 });
117 self
118 }
119}
120
121impl ExternalRef {
122 pub fn icon(&self) -> &'static str {
124 match self.ref_type {
125 RefType::Texture => "πΌοΈ",
126 RefType::Model => "ποΈ",
127 RefType::Animation => "π½οΈ",
128 RefType::Map => "πΊοΈ",
129 RefType::Database => "π",
130 RefType::Sound => "π",
131 RefType::Script => "π",
132 RefType::Unknown => "π",
133 }
134 }
135
136 pub fn style(&self, no_color: bool) -> Style {
138 if no_color {
139 Style::new()
140 } else {
141 match self.exists {
142 Some(true) => Style::new().green(),
143 Some(false) => Style::new().red(),
144 None => Style::new().yellow(),
145 }
146 }
147 }
148}
149
150impl NodeType {
151 pub fn icon(&self) -> &'static str {
153 match self {
154 NodeType::Root => "π",
155 NodeType::Header => "π",
156 NodeType::Chunk => "π¦",
157 NodeType::Table => "π",
158 NodeType::File => "π",
159 NodeType::Directory => "π",
160 NodeType::Reference => "π",
161 NodeType::Property => "π·οΈ",
162 NodeType::Data => "πΎ",
163 }
164 }
165
166 pub fn style(&self, no_color: bool) -> Style {
168 if no_color {
169 Style::new()
170 } else {
171 match self {
172 NodeType::Root => Style::new().bold().cyan(),
173 NodeType::Header => Style::new().bold().yellow(),
174 NodeType::Chunk => Style::new().blue(),
175 NodeType::Table => Style::new().magenta(),
176 NodeType::File => Style::new().green(),
177 NodeType::Directory => Style::new().cyan(),
178 NodeType::Reference => Style::new().yellow(),
179 NodeType::Property => Style::new().dim(),
180 NodeType::Data => Style::new().white(),
181 }
182 }
183 }
184}
185
186pub fn render_tree(root: &TreeNode, options: &TreeOptions) -> String {
188 let mut output = String::new();
189 render_node(root, &mut output, "", true, 0, options);
190 output
191}
192
193fn render_node(
195 node: &TreeNode,
196 output: &mut String,
197 prefix: &str,
198 is_last: bool,
199 depth: usize,
200 options: &TreeOptions,
201) {
202 if let Some(max_depth) = options.max_depth {
204 if depth > max_depth {
205 return;
206 }
207 }
208
209 let icon = node.node_type.icon();
211 let style = node.node_type.style(options.no_color);
212 let connector = if depth == 0 {
213 ""
214 } else if is_last {
215 "βββ "
216 } else {
217 "βββ "
218 };
219
220 let mut line = format!(
221 "{}{}{} {}",
222 prefix,
223 connector,
224 icon,
225 style.apply_to(&node.name)
226 );
227
228 if let Some(size) = node.size {
230 line.push_str(&format!(" ({})", format_bytes(size)));
231 }
232
233 if options.show_metadata && !node.metadata.is_empty() && options.compact {
235 let mut meta_parts = Vec::new();
237 for (key, value) in &node.metadata {
238 if ["version", "count", "flags", "type"].contains(&key.as_str()) {
239 meta_parts.push(format!("{key}:{value}"));
240 }
241 }
242 if !meta_parts.is_empty() {
243 line.push_str(&format!(" [{}]", meta_parts.join(", ")));
244 }
245 }
246
247 output.push_str(&line);
248 output.push('\n');
249
250 if options.show_metadata && !options.compact && !node.metadata.is_empty() {
252 let child_prefix = if depth == 0 {
253 ""
254 } else if is_last {
255 " "
256 } else {
257 "β "
258 };
259 let meta_prefix = format!("{prefix}{child_prefix} ");
260
261 for (key, value) in &node.metadata {
262 let meta_style = Style::new().dim();
263 output.push_str(&format!(
264 "{}π·οΈ {}: {}\n",
265 meta_prefix,
266 meta_style.apply_to(key),
267 value
268 ));
269 }
270 }
271
272 if options.show_external_refs && !node.external_refs.is_empty() {
274 let child_prefix = if depth == 0 {
275 ""
276 } else if is_last {
277 " "
278 } else {
279 "β "
280 };
281 let ref_prefix = format!("{prefix}{child_prefix} ");
282
283 for ext_ref in &node.external_refs {
284 let icon = ext_ref.icon();
285 let style = ext_ref.style(options.no_color);
286 output.push_str(&format!(
287 "{}βββ {} {}\n",
288 ref_prefix,
289 icon,
290 style.apply_to(&ext_ref.path)
291 ));
292 }
293 }
294
295 if !node.children.is_empty() {
297 let new_prefix = if depth == 0 {
298 String::new()
299 } else {
300 format!("{}{}", prefix, if is_last { " " } else { "β " })
301 };
302
303 for (i, child) in node.children.iter().enumerate() {
304 let is_last_child = i == node.children.len() - 1;
305 render_node(
306 child,
307 output,
308 &new_prefix,
309 is_last_child,
310 depth + 1,
311 options,
312 );
313 }
314 }
315}
316
317fn format_bytes(bytes: u64) -> String {
319 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
320 let mut size = bytes as f64;
321 let mut unit_index = 0;
322
323 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
324 size /= 1024.0;
325 unit_index += 1;
326 }
327
328 if unit_index == 0 {
329 format!("{} {}", bytes, UNITS[unit_index])
330 } else {
331 format!("{:.1} {}", size, UNITS[unit_index])
332 }
333}
334
335pub fn detect_ref_type(path: &str) -> RefType {
337 let path_lower = path.to_lowercase();
338
339 if path_lower.ends_with(".blp") {
340 RefType::Texture
341 } else if path_lower.ends_with(".m2") || path_lower.ends_with(".mdx") {
342 RefType::Model
343 } else if path_lower.ends_with(".anim") || path_lower.ends_with(".bone") {
344 RefType::Animation
345 } else if path_lower.ends_with(".wdt")
346 || path_lower.ends_with(".adt")
347 || path_lower.ends_with(".wdl")
348 {
349 RefType::Map
350 } else if path_lower.ends_with(".dbc") || path_lower.ends_with(".db2") {
351 RefType::Database
352 } else if path_lower.ends_with(".wav") || path_lower.ends_with(".mp3") {
353 RefType::Sound
354 } else if path_lower.ends_with(".lua") || path_lower.ends_with(".xml") {
355 RefType::Script
356 } else {
357 RefType::Unknown
358 }
359}