tree_create/
lib.rs

1use std::fs;
2use std::io::{self, BufRead};
3use std::path::Path;
4
5#[cfg(test)]
6mod tests;
7
8#[derive(Debug, PartialEq)]
9struct TreeNode {
10    name: String,
11    indent_level: usize,
12}
13
14#[derive(Debug)]
15struct TreeStructure {
16    nodes: Vec<TreeNode>,
17    #[allow(dead_code)] // This is used implicitly in Debug
18    indent_width: usize,
19}
20
21impl TreeStructure {
22    /// Parse a string in either ASCII tree format or indented format into our internal representation
23    pub fn from_string(input: &str) -> io::Result<Self> {
24        if input.is_empty() {
25            return Err(io::Error::new(io::ErrorKind::InvalidData, "Input is empty"));
26        }
27
28        let first_line = input.lines().next().unwrap_or("");
29        let leading_spaces = first_line.chars().take_while(|c| c.is_whitespace()).count();
30
31        if leading_spaces > 0 {
32            return Err(io::Error::new(
33                io::ErrorKind::InvalidData,
34                "Root directory (line 1) should not be indented",
35            ));
36        }
37
38        if is_ascii_tree(input) {
39            Self::from_ascii_tree(input)
40        } else {
41            Self::from_indented(input)
42        }
43    }
44
45    /// Convert ASCII tree format ("├── file.txt") to our internal representation
46    fn from_ascii_tree(input: &str) -> io::Result<Self> {
47        let mut nodes: Vec<TreeNode> = Vec::new();
48
49        for line in input.lines() {
50            if line.trim().is_empty() {
51                continue;
52            }
53
54            // Count the indent level based on the tree characters
55            let prefixes = line
56                .chars()
57                .take_while(|&c| c == ' ' || c == '│' || c == '├' || c == '└')
58                .count();
59
60            // Each level consists of either "│   " (4 chars) or "├── " (4 chars)
61            let indent_level = if prefixes == 0 { 0 } else { (prefixes + 3) / 4 };
62
63            // Extract the name by trimming tree characters
64            let name = line
65                .trim_start_matches(|c: char| {
66                    c.is_whitespace() || c == '│' || c == '├' || c == '└' || c == '─'
67                })
68                .to_string();
69
70            nodes.push(TreeNode { name, indent_level });
71        }
72
73        if nodes.is_empty() {
74            return Err(io::Error::new(
75                io::ErrorKind::InvalidData,
76                "No valid nodes found",
77            ));
78        }
79
80        Ok(Self {
81            nodes,
82            indent_width: 2, // Default indent width for output
83        })
84    }
85
86    /// Parse simple indented format into our internal representation
87    fn from_indented(input: &str) -> io::Result<Self> {
88        let mut nodes: Vec<TreeNode> = Vec::new();
89        let mut indent_width = None;
90
91        for (line_num, line) in input.lines().enumerate() {
92            if line.trim().is_empty() {
93                continue;
94            }
95
96            let spaces = line.chars().take_while(|c| c.is_whitespace()).count();
97            let name = line.trim().to_string();
98
99            // If this is the first indented line, use it to determine indent width
100            if spaces > 0 && indent_width.is_none() {
101                indent_width = Some(spaces);
102            }
103
104            let indent_width = indent_width.unwrap_or(2);
105
106            // Validate indentation is consistent
107            if spaces % indent_width != 0 {
108                return Err(io::Error::new(
109                    io::ErrorKind::InvalidData,
110                    format!(
111                        "Inconsistent indentation at line {}. Expected a multiple of {} spaces (found {} spaces)",
112                        line_num + 1, indent_width, spaces
113                    )
114                ));
115            }
116
117            let indent_level = spaces / indent_width;
118
119            // Validate indent level doesn't skip levels
120            if let Some(prev_node) = nodes.last() {
121                if indent_level > prev_node.indent_level + 1 {
122                    return Err(io::Error::new(
123                        io::ErrorKind::InvalidData,
124                        format!(
125                            "Invalid indentation at line {}. Indentation can only increase by one level at a time",
126                            line_num + 1
127                        )
128                    ));
129                }
130            }
131
132            nodes.push(TreeNode { name, indent_level });
133        }
134
135        if nodes.is_empty() {
136            return Err(io::Error::new(
137                io::ErrorKind::InvalidData,
138                "No valid nodes found",
139            ));
140        }
141
142        Ok(Self {
143            nodes,
144            indent_width: indent_width.unwrap_or(2),
145        })
146    }
147
148    /// Convert the internal representation to ASCII tree format
149    fn to_ascii_tree(&self) -> String {
150        let mut result = Vec::new();
151
152        // First pass: determine which levels have subsequent siblings
153        let mut level_has_next = [false; 32]; // 32 levels should be enough
154
155        for (i, node) in self.nodes.iter().enumerate() {
156            let current_level = node.indent_level;
157
158            // Look ahead to see if there are any more nodes at this level
159            if let Some(next_node) = self.nodes.get(i + 1) {
160                if next_node.indent_level == current_level {
161                    level_has_next[current_level] = true;
162                }
163            }
164        }
165
166        // Second pass: generate the tree
167        for node in self.nodes.iter() {
168            let mut prefix = String::new();
169
170            // Add vertical lines for previous levels that have subsequent nodes
171            prefix.extend(
172                level_has_next
173                    .iter()
174                    .take(node.indent_level)
175                    .skip(1)
176                    .map(|&has_next| if has_next { "│   " } else { "    " }),
177            );
178
179            // Add the appropriate branch character
180            if node.indent_level > 0 {
181                prefix.push_str(if level_has_next[node.indent_level] {
182                    "├── "
183                } else {
184                    "└── "
185                });
186            }
187
188            result.push(format!("{}{}", prefix, node.name));
189        }
190
191        result.join("\n")
192    }
193}
194
195/// Check if input is using ASCII tree format
196fn is_ascii_tree(input: &str) -> bool {
197    input.contains("├──") || input.contains("└──") || input.contains("│")
198}
199
200pub fn create_tree(input: &str, base_path: &Path, force: bool) -> io::Result<()> {
201    // Convert the input to our internal representation
202    let tree = TreeStructure::from_string(input)?;
203
204    // Then convert to ASCII tree format for processing
205    let ascii_tree = tree.to_ascii_tree();
206
207    let reader = io::BufReader::new(ascii_tree.as_bytes());
208    let mut lines = reader.lines().peekable();
209
210    // Get the root directory name from the first line
211    let root_name = if let Some(Ok(first_line)) = lines.next() {
212        first_line.trim_end_matches('/').to_string()
213    } else {
214        return Err(io::Error::new(io::ErrorKind::InvalidData, "Input is empty"));
215    };
216
217    let base_path = base_path.join(&root_name);
218    if base_path.exists() {
219        if force {
220            if base_path.is_file() {
221                fs::remove_file(&base_path)?;
222            } else {
223                fs::remove_dir_all(&base_path)?;
224            }
225            fs::create_dir_all(&base_path)?;
226            println!("Overwrote existing directory: {:?}", base_path);
227        } else {
228            println!("Directory already exists: {:?}", base_path);
229        }
230    } else {
231        fs::create_dir_all(&base_path)?;
232        println!("Created root directory: {:?}", base_path);
233    }
234
235    let mut current_depth = 0;
236    let mut path_stack = vec![base_path.clone()];
237
238    for line in lines {
239        let line = line?;
240        let depth = line
241            .chars()
242            .take_while(|&c| c == ' ' || c == '│' || c == '└' || c == '├')
243            .count()
244            / 4;
245        let name = line
246            .trim_start_matches(|c: char| {
247                c.is_whitespace() || c == '│' || c == '└' || c == '├' || c == '─'
248            })
249            .to_string();
250
251        // Adjust the path stack based on the new depth
252        while depth < current_depth && !path_stack.is_empty() {
253            path_stack.pop();
254            current_depth -= 1;
255        }
256        current_depth = depth;
257
258        let mut full_path = path_stack
259            .last()
260            .cloned()
261            .unwrap_or_else(|| base_path.clone());
262        full_path.push(&name);
263
264        if name.ends_with('/') {
265            if full_path.exists() {
266                if force {
267                    if full_path.is_file() {
268                        fs::remove_file(&full_path)?;
269                        fs::create_dir_all(&full_path)?;
270                        println!("Overwrote file with directory: {:?}", full_path);
271                    } else {
272                        // Directory exists, but we don't remove it as it might contain other files
273                        println!("Using existing directory: {:?}", full_path);
274                    }
275                } else {
276                    println!("Directory already exists: {:?}", full_path);
277                }
278            } else {
279                fs::create_dir_all(&full_path)?;
280                println!("Created directory: {:?}", full_path);
281            }
282            path_stack.push(full_path);
283        } else {
284            if let Some(parent) = full_path.parent() {
285                fs::create_dir_all(parent)?;
286            }
287            if full_path.exists() {
288                if force {
289                    if full_path.is_dir() {
290                        fs::remove_dir_all(&full_path)?;
291                    }
292                    fs::write(&full_path, "")?;
293                    println!("Overwrote existing file: {:?}", full_path);
294                } else {
295                    println!("File already exists: {:?}", full_path);
296                }
297            } else {
298                fs::File::create(&full_path)?;
299                println!("Created file: {:?}", full_path);
300            }
301        }
302    }
303
304    Ok(())
305}