plot/lib/pipelines/
step2.rs1use crate::lib::{
2 logic::ScanNodeIs,
3 schema::{
4 PipelineJobRow,
5 PipelineJobTab,
6 ReadyJob,
7 SharedJobAttributeToSelect,
8 SharedJobOptForAttrItem,
9 SharedJobOptForAttrSize,
10 SharedJobRunMode,
11 },
12};
13
14#[derive(Debug, Clone)]
19pub struct FormattedRow {
20 pub raw: PipelineJobRow,
21 pub cells: Vec<String>,
22}
23
24pub fn engine_step2_data_formater(
29 job: &ReadyJob,
30 tab: &PipelineJobTab, ) -> Vec<FormattedRow> {
32 let attr = job.attributes();
33 let select = attr.select();
34 let opts = attr.for_item();
35
36 let is_inspection = job
38 .run_modes()
39 .iter()
40 .any(|m| matches!(m, SharedJobRunMode::SaveWithInspection | SharedJobRunMode::PrintWithInspection));
41
42 let total_rows = tab.rows.len();
49 let num_width = total_rows.to_string().len();
50 let mut formatted_table = Vec::with_capacity(total_rows);
51
52 for (idx, row) in tab.rows.iter().enumerate() {
53 let mut cells = Vec::with_capacity(select.len() + 1);
54
55 for col in select {
56 let cell_str = match col {
57 SharedJobAttributeToSelect::Date => row.dt_modified.format(attr.for_date()).to_string(),
58 SharedJobAttributeToSelect::Time => row.dt_modified.format(attr.for_time()).to_string(),
59 SharedJobAttributeToSelect::Size => format_size(&attr.for_size(), row.size_real),
60 SharedJobAttributeToSelect::Item => format_item(idx, num_width, tab, opts, row),
61 SharedJobAttributeToSelect::Path => {
68 let mut p_str = row.node.path.str.clone();
69 if !opts.contains(&SharedJobOptForAttrItem::AlignHide) {
71 let padding = tab.path_len_max.saturating_sub(p_str.chars().count());
72 p_str.push_str(&" ".repeat(padding));
73 }
74 p_str
75 }
76 };
77 cells.push(cell_str);
78 }
79
80 if is_inspection {
81 let type_node = match row.node.node {
82 ScanNodeIs::Dir => "📂",
83 ScanNodeIs::File => "📃",
84 };
85
86 let debug_info = format!("🆔{}[{:03}] 🔙📂{:?}", type_node, row.node.id_self, row.node.id_path);
88 cells.push(debug_info);
89 }
90
91 formatted_table.push(FormattedRow { raw: row.clone(), cells });
92 }
93
94 formatted_table
95}
96
97fn format_item(
102 index: usize,
103 num_width: usize,
104 tab: &PipelineJobTab,
105 opts: &[SharedJobOptForAttrItem],
106 row: &PipelineJobRow,
107) -> String {
108 let mut parts = Vec::new();
109 let num_str = format!("{:>width$}.", index + 1, width = num_width);
110
111 if opts.contains(&SharedJobOptForAttrItem::NumListBef) {
113 parts.push(num_str.clone());
114 }
115
116 let is_tree = opts.contains(&SharedJobOptForAttrItem::ListTree);
118 let is_flat = opts.contains(&SharedJobOptForAttrItem::ListFlat);
119 if is_tree || is_flat {
120 parts.push(draw_list(is_tree, index, &tab.rows));
121 }
122
123 let hide_icons = opts.contains(&SharedJobOptForAttrItem::IconsHide);
125 if !hide_icons {
126 let is_more = opts.contains(&SharedJobOptForAttrItem::IconsMore);
127 parts.push(draw_icon(is_more, &row.node.node, &row.name_with_ext).to_string());
128 }
129
130 if opts.contains(&SharedJobOptForAttrItem::NumListAft) {
132 parts.push(num_str);
133 }
134
135 if !opts.contains(&SharedJobOptForAttrItem::NameHide) {
137 parts.push(row.name_with_ext.clone());
138 }
139
140 let mut base = parts.join(" ");
141
142 if !opts.contains(&SharedJobOptForAttrItem::AlignHide) {
144 let p_tier = (tab.tier_max.saturating_sub(row.node.tier)) * 3;
145 let p_name = tab.name_len_max.saturating_sub(row.node.name.chars().count());
146 base.push_str(&" ".repeat(p_tier + p_name));
147 }
148
149 base
150}
151
152struct TreeLast;
157impl TreeLast {
158 const DIR_NO_CHILDREN: &'static str = "└───";
159 const DIR_WITH_CHILDREN: &'static str = "└──┬";
160 const FILE: &'static str = "└──•";
161 const INDENT: &'static str = " ";
162 const BRIDGE_START: &'static str = "└──•";
163 }
166
167struct TreeMid;
168impl TreeMid {
169 const DIR_NO_CHILDREN: &'static str = "├───";
170 const DIR_WITH_CHILDREN: &'static str = "├──┬";
171 const FILE: &'static str = "├──•";
172 const INDENT: &'static str = "│ ";
173 const BRIDGE_START: &'static str = "├──•";
174 const BRIDGE_NEXT: &'static str = "──•";
175 }
177
178struct DrawTree;
179impl DrawTree {
180 const ITEM_BETWEEN: &'static str = " ├──•";
181 const ITEM_FIRST: &'static str = " ┌──•";
182 const ITEM_LAST: &'static str = " └──•";
183 const ITEM_ONEFOLD: &'static str = " ──•";
184
185 fn list(index: usize, total: usize) -> &'static str {
186 if total <= 1 {
187 Self::ITEM_ONEFOLD
188 } else if index == 0 {
189 Self::ITEM_FIRST
190 } else if index == total - 1 {
191 Self::ITEM_LAST
192 } else {
193 Self::ITEM_BETWEEN
194 }
195 }
196}
197
198fn draw_list(is_tree: bool, index: usize, rows: &[PipelineJobRow]) -> String {
199 if !is_tree {
200 return DrawTree::list(index, rows.len()).to_string();
201 }
202
203 let node = &rows[index].node;
204 let t = node.tier;
205
206 let is_dir = node.node == ScanNodeIs::Dir;
208 let has_children = if is_dir {
209 if index + 1 < rows.len() { rows[index + 1].node.tier > t } else { false }
210 } else {
211 false
212 };
213
214 let mut is_last = vec![true; t + 1];
216 let mut active_levels = t;
217
218 for j in (index + 1)..rows.len() {
219 let next_tier = rows[j].node.tier;
220 if next_tier <= active_levels {
221 is_last[next_tier] = false;
222 active_levels = next_tier - 1;
223 }
224 if active_levels == 0 {
225 break;
226 }
227 }
228
229 let mut vis_parent_tier = 0;
231 for i_prev in (0..index).rev() {
232 if node.path.str.starts_with(&rows[i_prev].node.path.str) && rows[i_prev].node.node == ScanNodeIs::Dir {
233 vis_parent_tier = rows[i_prev].node.tier;
234 break;
235 }
236 }
237
238 let mut result = String::new();
239 let has_bridge = t > vis_parent_tier + 1;
240
241 for l in 1..t {
243 if has_bridge && l > vis_parent_tier {
244 if l == vis_parent_tier + 1 {
245 result.push_str(if is_last[l] { TreeLast::BRIDGE_START } else { TreeMid::BRIDGE_START });
246 } else {
247 result.push_str(TreeMid::BRIDGE_NEXT);
248 }
249 } else if is_last[l] {
250 result.push_str(TreeLast::INDENT);
251 } else {
252 result.push_str(TreeMid::INDENT);
253 }
254 }
255
256 let am_i_last = is_last[t];
258 let branch = if is_dir {
259 match (am_i_last, has_children) {
260 (true, true) => TreeLast::DIR_WITH_CHILDREN,
261 (false, true) => TreeMid::DIR_WITH_CHILDREN,
262 (true, false) => TreeLast::DIR_NO_CHILDREN,
263 (false, false) => TreeMid::DIR_NO_CHILDREN,
264 }
265 } else if am_i_last {
266 TreeLast::FILE
267 } else {
268 TreeMid::FILE
269 };
270
271 if has_bridge {
273 let tail: String = branch.chars().skip(1).collect();
274 result.push_str("");
275 result.push_str(&tail);
276 } else {
277 result.push_str(branch);
278 }
279
280 result
281}
282
283fn draw_icon(is_more: bool, node_type: &ScanNodeIs, name: &str) -> &'static str {
288 if !is_more {
289 if *node_type == ScanNodeIs::Dir { "📂" } else { "📝" }
290 } else if *node_type == ScanNodeIs::Dir {
291 "📁"
292 } else if name.ends_with(".rs") {
293 "🦀"
294 } else if name.ends_with(".toml") {
295 "⚙️"
296 } else if name.ends_with(".md") {
297 "📖"
298 } else {
299 "📄"
300 }
301}
302
303fn format_size(mode: &SharedJobOptForAttrSize, bytes: u64) -> String {
304 let base = match mode {
305 SharedJobOptForAttrSize::Decimal => 1000.0,
306 SharedJobOptForAttrSize::Binary => 1024.0,
307 };
308
309 let suffix = match mode {
310 SharedJobOptForAttrSize::Decimal => ["B ", "kB", "MB", "GB"],
311 SharedJobOptForAttrSize::Binary => ["B ", "KiB", "MiB", "GiB"],
312 };
313
314 let bytes_f = bytes as f64;
315
316 if bytes_f < base {
317 format!("{:>6} {}", bytes, suffix[0])
318 } else if bytes_f < base.powi(2) {
319 format!("{:>6.2} {}", bytes_f / base, suffix[1])
320 } else if bytes_f < base.powi(3) {
321 format!("{:>6.2} {}", bytes_f / base.powi(2), suffix[2])
322 } else {
323 format!("{:>6.2} {}", bytes_f / base.powi(3), suffix[3])
324 }
325}
326
327pub fn render_table(rows: &[FormattedRow], select: &[SharedJobAttributeToSelect]) -> String {
332 let mut multiline = String::new();
333
334 for row in rows {
335 let mut line = String::new();
336 let cells_len = row.cells.len();
337
338 for (col_idx, cell) in row.cells.iter().enumerate() {
339 line.push_str(cell);
340
341 if col_idx + 1 < cells_len {
342 let current = select.get(col_idx);
343 let next = select.get(col_idx + 1);
344
345 let is_date_time_pair = matches!(
346 (current, next),
347 (Some(SharedJobAttributeToSelect::Date), Some(SharedJobAttributeToSelect::Time))
348 | (Some(SharedJobAttributeToSelect::Time), Some(SharedJobAttributeToSelect::Date))
349 );
350
351 if is_date_time_pair {
352 line.push(' ');
353 } else {
354 line.push_str(" | ");
355 }
356 }
357 }
358 multiline.push_str(&line);
359 multiline.push('\n');
360 }
361
362 multiline
363}