cargo_plot/core/path_view/
grid.rs1use 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 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 let final_roots = if let Some(r_name) = root_name {
122 let root_bytes = if weight_cfg.dir_sum_included {
125 top_nodes.iter().map(|n| n.weight_bytes).sum()
127 } else {
128 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}