| 1 | use crate::types::{FileEntry, FileStatus, SelectableItem, TreeNode}; |
| 2 | use std::path::{Path, PathBuf}; |
| 3 | |
| 4 | /// Build a tree from a list of file entries |
| 5 | pub fn build_tree(files: &[FileEntry], hide_dotfiles: bool) -> TreeNode { |
| 6 | let mut root = TreeNode::root(); |
| 7 | |
| 8 | for file in files { |
| 9 | // Skip dotfiles if requested |
| 10 | if hide_dotfiles && is_dotfile(&file.path) { |
| 11 | continue; |
| 12 | } |
| 13 | |
| 14 | add_to_tree(&mut root, &file.path, &file.status); |
| 15 | } |
| 16 | |
| 17 | root.sort_children(); |
| 18 | root |
| 19 | } |
| 20 | |
| 21 | /// Check if a path is a dotfile/dotdir |
| 22 | fn is_dotfile(path: &Path) -> bool { |
| 23 | path.components().any(|c| { |
| 24 | c.as_os_str() |
| 25 | .to_str() |
| 26 | .map(|s| s.starts_with('.')) |
| 27 | .unwrap_or(false) |
| 28 | }) |
| 29 | } |
| 30 | |
| 31 | /// Add a file path to the tree, creating intermediate directories |
| 32 | fn add_to_tree(node: &mut TreeNode, path: &Path, status: &FileStatus) { |
| 33 | let components: Vec<_> = path.components().collect(); |
| 34 | add_to_tree_recursive(node, &components, 0, status, path.to_path_buf()); |
| 35 | } |
| 36 | |
| 37 | fn add_to_tree_recursive( |
| 38 | node: &mut TreeNode, |
| 39 | components: &[std::path::Component], |
| 40 | depth: usize, |
| 41 | status: &FileStatus, |
| 42 | full_path: PathBuf, |
| 43 | ) { |
| 44 | if depth >= components.len() { |
| 45 | return; |
| 46 | } |
| 47 | |
| 48 | let name = components[depth] |
| 49 | .as_os_str() |
| 50 | .to_str() |
| 51 | .unwrap_or("") |
| 52 | .to_string(); |
| 53 | |
| 54 | let is_last_component = depth == components.len() - 1; |
| 55 | |
| 56 | // Build path up to this component |
| 57 | let component_path: PathBuf = components[..=depth] |
| 58 | .iter() |
| 59 | .map(|c| c.as_os_str()) |
| 60 | .collect(); |
| 61 | |
| 62 | // Find or create child node |
| 63 | let child_idx = node.children.iter().position(|c| c.name == name); |
| 64 | |
| 65 | match child_idx { |
| 66 | Some(idx) => { |
| 67 | // Node exists, merge status if it's the final component |
| 68 | if is_last_component { |
| 69 | node.children[idx].status.merge(status); |
| 70 | } else { |
| 71 | // Continue recursing |
| 72 | add_to_tree_recursive( |
| 73 | &mut node.children[idx], |
| 74 | components, |
| 75 | depth + 1, |
| 76 | status, |
| 77 | full_path, |
| 78 | ); |
| 79 | } |
| 80 | } |
| 81 | None => { |
| 82 | // Create new node |
| 83 | let new_node = if is_last_component { |
| 84 | TreeNode::new_file(name, full_path, status.clone()) |
| 85 | } else { |
| 86 | let mut dir = TreeNode::new_directory(name, component_path.clone()); |
| 87 | add_to_tree_recursive(&mut dir, components, depth + 1, status, full_path); |
| 88 | dir |
| 89 | }; |
| 90 | node.children.push(new_node); |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | /// Flatten tree into a list of selectable items (respecting expanded state) |
| 96 | pub fn flatten_tree(root: &TreeNode, hide_dotfiles: bool) -> Vec<SelectableItem> { |
| 97 | let mut items = Vec::new(); |
| 98 | flatten_node(root, 0, true, Vec::new(), &mut items, hide_dotfiles); |
| 99 | items |
| 100 | } |
| 101 | |
| 102 | fn flatten_node( |
| 103 | node: &TreeNode, |
| 104 | depth: usize, |
| 105 | is_last: bool, |
| 106 | ancestors: Vec<bool>, |
| 107 | items: &mut Vec<SelectableItem>, |
| 108 | hide_dotfiles: bool, |
| 109 | ) { |
| 110 | // Skip root node itself, but process its children |
| 111 | if node.name != "." { |
| 112 | // Skip dotfiles if requested |
| 113 | if hide_dotfiles && node.name.starts_with('.') { |
| 114 | return; |
| 115 | } |
| 116 | |
| 117 | items.push(SelectableItem::from_node(node, depth, is_last, ancestors.clone())); |
| 118 | } |
| 119 | |
| 120 | // Only process children if expanded (or if this is root) |
| 121 | if node.is_expanded || node.name == "." { |
| 122 | let child_count = node.children.len(); |
| 123 | for (i, child) in node.children.iter().enumerate() { |
| 124 | let child_is_last = i == child_count - 1; |
| 125 | let mut child_ancestors = ancestors.clone(); |
| 126 | if node.name != "." { |
| 127 | child_ancestors.push(is_last); |
| 128 | } |
| 129 | flatten_node( |
| 130 | child, |
| 131 | if node.name == "." { depth } else { depth + 1 }, |
| 132 | child_is_last, |
| 133 | child_ancestors, |
| 134 | items, |
| 135 | hide_dotfiles, |
| 136 | ); |
| 137 | } |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | /// Find a node in the tree by path |
| 142 | pub fn find_node_mut<'a>(root: &'a mut TreeNode, path: &Path) -> Option<&'a mut TreeNode> { |
| 143 | if root.full_path == path { |
| 144 | return Some(root); |
| 145 | } |
| 146 | |
| 147 | for child in &mut root.children { |
| 148 | if let Some(found) = find_node_mut(child, path) { |
| 149 | return Some(found); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | None |
| 154 | } |
| 155 | |
| 156 | /// Toggle expanded state for a node at the given path |
| 157 | pub fn toggle_expanded(root: &mut TreeNode, path: &Path) -> bool { |
| 158 | if let Some(node) = find_node_mut(root, path) { |
| 159 | if !node.is_file { |
| 160 | node.toggle_expanded(); |
| 161 | return true; |
| 162 | } |
| 163 | } |
| 164 | false |
| 165 | } |
| 166 |