Skip to main content

cargo_plot/core/path_view/
grid.rs

1use colored::Colorize;
2use std::collections::BTreeMap;
3use std::path::{Path, PathBuf};
4
5use super::node::FileNode;
6use crate::core::file_stats::weight::{self, WeightConfig};
7use crate::core::path_matcher::SortStrategy;
8use crate::theme::for_path_tree::{DIR_ICON, TreeStyle, get_file_type};
9
10pub struct PathGrid {
11    roots: Vec<FileNode>,
12    style: TreeStyle,
13}
14
15impl PathGrid {
16    #[must_use]
17    pub fn build(
18        paths_strings: &[String],
19        base_dir: &str,
20        sort_strategy: SortStrategy,
21        weight_cfg: &WeightConfig,
22        root_name: Option<&str>,
23        no_emoji: bool,
24    ) -> Self {
25        // Dokładnie taka sama logika budowania struktury węzłów jak w PathTree::build
26        let base_path_obj = Path::new(base_dir);
27        let paths: Vec<PathBuf> = paths_strings.iter().map(PathBuf::from).collect();
28        let mut tree_map: BTreeMap<PathBuf, Vec<PathBuf>> = BTreeMap::new();
29
30        for p in &paths {
31            let parent = p
32                .parent()
33                .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
34            tree_map.entry(parent).or_default().push(p.clone());
35        }
36
37        fn build_node(
38            path: &PathBuf,
39            paths_map: &BTreeMap<PathBuf, Vec<PathBuf>>,
40            base_path: &Path,
41            sort_strategy: SortStrategy,
42            weight_cfg: &WeightConfig,
43            no_emoji: bool,
44        ) -> FileNode {
45            let name = path
46                .file_name()
47                .map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string());
48            let is_dir = path.is_dir() || path.to_string_lossy().ends_with('/');
49            let icon = if no_emoji {
50                String::new()
51            } else if is_dir {
52                DIR_ICON.to_string()
53            } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
54                get_file_type(ext).icon.to_string()
55            } else {
56                "📄".to_string()
57            };
58
59            let absolute_path = base_path.join(path);
60            let mut weight_bytes =
61                weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included);
62            let mut children = vec![];
63
64            if let Some(child_paths) = paths_map.get(path) {
65                let mut child_nodes: Vec<FileNode> = child_paths
66                    .iter()
67                    .map(|c| {
68                        build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji)
69                    })
70                    .collect();
71
72                FileNode::sort_slice(&mut child_nodes, sort_strategy);
73
74                if is_dir && weight_cfg.dir_sum_included {
75                    weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum();
76                }
77                children = child_nodes;
78            }
79
80            let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg);
81            FileNode {
82                name,
83                path: path.clone(),
84                is_dir,
85                icon,
86                weight_str,
87                weight_bytes,
88                children,
89            }
90        }
91
92        let roots_paths: Vec<PathBuf> = paths
93            .iter()
94            .filter(|p| {
95                let parent = p.parent();
96                parent.is_none()
97                    || parent.unwrap() == Path::new("")
98                    || !paths.contains(&parent.unwrap().to_path_buf())
99            })
100            .cloned()
101            .collect();
102
103        let mut top_nodes: Vec<FileNode> = roots_paths
104            .into_iter()
105            .map(|r| {
106                build_node(
107                    &r,
108                    &tree_map,
109                    base_path_obj,
110                    sort_strategy,
111                    weight_cfg,
112                    no_emoji,
113                )
114            })
115            .collect();
116
117        FileNode::sort_slice(&mut top_nodes, sort_strategy);
118
119        // [ENG]: Logic for creating the final root node with proper weight calculation.
120        // [POL]: Logika tworzenia końcowego węzła głównego z poprawnym obliczeniem wagi.
121        let final_roots = if let Some(r_name) = root_name {
122            // [ENG]: Calculate total weight for the root node.
123            // [POL]: Obliczenie całkowitej wagi dla węzła głównego.
124            let root_bytes = if weight_cfg.dir_sum_included {
125                // [POL]: Suma wag bezpośrednich dzieci (dopasowanych elementów).
126                top_nodes.iter().map(|n| n.weight_bytes).sum()
127            } else {
128                // [POL]: Fizyczna waga folderu wejściowego z dysku.
129                weight::get_path_weight(base_path_obj, false)
130            };
131
132            let root_weight_str = weight::format_weight(root_bytes, true, weight_cfg);
133
134            vec![FileNode {
135                name: r_name.to_string(),
136                path: PathBuf::from(r_name),
137                is_dir: true,
138                icon: if no_emoji {
139                    String::new()
140                } else {
141                    DIR_ICON.to_string()
142                },
143                weight_str: root_weight_str,
144                weight_bytes: root_bytes,
145                children: top_nodes,
146            }]
147        } else {
148            top_nodes
149        };
150
151        Self {
152            roots: final_roots,
153            style: TreeStyle::default(),
154        }
155    }
156
157    #[must_use]
158    pub fn render_cli(&self) -> String {
159        let max_width = self.calc_max_width(&self.roots, 0);
160        self.plot(&self.roots, "", true, max_width)
161    }
162
163    #[must_use]
164    pub fn render_txt(&self) -> String {
165        let max_width = self.calc_max_width(&self.roots, 0);
166        self.plot(&self.roots, "", false, max_width)
167    }
168
169    fn calc_max_width(&self, nodes: &[FileNode], indent_len: usize) -> usize {
170        let mut max = 0;
171        for (i, node) in nodes.iter().enumerate() {
172            let is_last = i == nodes.len() - 1;
173            let has_children = !node.children.is_empty();
174            let branch = if node.is_dir {
175                match (is_last, has_children) {
176                    (true, true) => &self.style.dir_last_with_children,
177                    (false, true) => &self.style.dir_mid_with_children,
178                    (true, false) => &self.style.dir_last_no_children,
179                    (false, false) => &self.style.dir_mid_no_children,
180                }
181            } else if is_last {
182                &self.style.file_last
183            } else {
184                &self.style.file_mid
185            };
186
187            let current_len = node.weight_str.chars().count()
188                + indent_len
189                + branch.chars().count()
190                + 1
191                + node.icon.chars().count()
192                + 1
193                + node.name.chars().count();
194            if current_len > max {
195                max = current_len;
196            }
197
198            if has_children {
199                let next_indent = indent_len
200                    + if is_last {
201                        self.style.indent_last.chars().count()
202                    } else {
203                        self.style.indent_mid.chars().count()
204                    };
205                let child_max = self.calc_max_width(&node.children, next_indent);
206                if child_max > max {
207                    max = child_max;
208                }
209            }
210        }
211        max
212    }
213
214    fn plot(&self, nodes: &[FileNode], indent: &str, use_color: bool, max_width: usize) -> String {
215        let mut result = String::new();
216        for (i, node) in nodes.iter().enumerate() {
217            let is_last = i == nodes.len() - 1;
218            let has_children = !node.children.is_empty();
219            let branch = if node.is_dir {
220                match (is_last, has_children) {
221                    (true, true) => &self.style.dir_last_with_children,
222                    (false, true) => &self.style.dir_mid_with_children,
223                    (true, false) => &self.style.dir_last_no_children,
224                    (false, false) => &self.style.dir_mid_no_children,
225                }
226            } else if is_last {
227                &self.style.file_last
228            } else {
229                &self.style.file_mid
230            };
231
232            let weight_prefix = if node.weight_str.is_empty() {
233                String::new()
234            } else if use_color {
235                node.weight_str.truecolor(120, 120, 120).to_string()
236            } else {
237                node.weight_str.clone()
238            };
239
240            let raw_left_len = node.weight_str.chars().count()
241                + indent.chars().count()
242                + branch.chars().count()
243                + 1
244                + node.icon.chars().count()
245                + 1
246                + node.name.chars().count();
247            let pad_len = max_width.saturating_sub(raw_left_len) + 4;
248            let padding = " ".repeat(pad_len);
249
250            let rel_path_str = node.path.to_string_lossy().replace('\\', "/");
251            let display_path = if node.is_dir && !rel_path_str.ends_with('/') {
252                format!("./{}/", rel_path_str)
253            } else if !rel_path_str.starts_with("./") && !rel_path_str.starts_with('.') {
254                format!("./{}", rel_path_str)
255            } else {
256                rel_path_str
257            };
258
259            let right_colored = if use_color {
260                if node.is_dir {
261                    display_path.truecolor(200, 200, 50).to_string()
262                } else {
263                    display_path.white().to_string()
264                }
265            } else {
266                display_path
267            };
268
269            let left_colored = if use_color {
270                if node.is_dir {
271                    format!(
272                        "{}{}{} {}{}",
273                        weight_prefix,
274                        indent.green(),
275                        branch.green(),
276                        node.icon,
277                        node.name.truecolor(200, 200, 50)
278                    )
279                } else {
280                    format!(
281                        "{}{}{} {}{}",
282                        weight_prefix,
283                        indent.green(),
284                        branch.green(),
285                        node.icon,
286                        node.name.white()
287                    )
288                }
289            } else {
290                format!(
291                    "{}{}{} {} {}",
292                    weight_prefix, indent, branch, node.icon, node.name
293                )
294            };
295
296            result.push_str(&format!("{}{}{}\n", left_colored, padding, right_colored));
297
298            if has_children {
299                let new_indent = if is_last {
300                    format!("{}{}", indent, self.style.indent_last)
301                } else {
302                    format!("{}{}", indent, self.style.indent_mid)
303                };
304                result.push_str(&self.plot(&node.children, &new_indent, use_color, max_width));
305            }
306        }
307        result
308    }
309}