Skip to main content

plot/lib/pipelines/
step2.rs

1use 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// ============================================================================
15// STRUKTURA WYJŚCIOWA (Transport do kolejnych etapów)
16// ============================================================================
17
18#[derive(Debug, Clone)]
19pub struct FormattedRow {
20	pub raw: PipelineJobRow,
21	pub cells: Vec<String>,
22}
23
24// ============================================================================
25// FORMATTER (Step 2) - GŁÓWNA PĘTLA
26// ============================================================================
27
28pub fn engine_step2_data_formater(
29	job: &ReadyJob,
30	tab: &PipelineJobTab, // ⚡ Zmiana typu
31) -> Vec<FormattedRow> {
32	let attr = job.attributes();
33	let select = attr.select();
34	let opts = attr.for_item();
35
36	// Sprawdzamy, czy użytkownik zażyczył sobie inspekcji (debugowania)
37	let is_inspection = job
38		.run_modes()
39		.iter()
40		.any(|m| matches!(m, SharedJobRunMode::SaveWithInspection | SharedJobRunMode::PrintWithInspection));
41
42	// ? let item_cfg = &j.item;
43	// ? let col_cfg = &j.cols;
44	// ? let date_cfg = &j.date;
45	// ? let time_cfg = &j.time;
46	// ? let size_cfg = &j.size;
47
48	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				// Tu w przyszłości wejdzie zaawansowana logika ikon i drzewa
62				// ?? let u_params = (i, num_width, tab.tier_max, tab.name_len_max);
63				// ?? item_cfg.format_item(u_params, row, &tab.rows)
64				// row.name_with_ext.clone()
65				//format_item_column(idx, num_width, item_opts, row, &tab.rows)
66				//}
67				SharedJobAttributeToSelect::Path => {
68					let mut p_str = row.node.path.str.clone();
69					// Zgodnie z Twoją logiką: align jeśli nie ma AlignHide
70					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			// Formatujemy debugowy string z ID węzła i ścieżką ID rodziców
87			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
97// ============================================================================
98// FORMATOWANIE: ITEM (Ikony + Drzewo + Nazwa + Wyrównanie)
99// ============================================================================
100
101fn 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	// 1. Jeśli NumListBef (num_is_first)
112	if opts.contains(&SharedJobOptForAttrItem::NumListBef) {
113		parts.push(num_str.clone());
114	}
115
116	// 2. Struktura Listy (Zaktualizowane draw_list)
117	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	// 3. Ikony
124	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	// 4. Jeśli NumListAft (num_is_after)
131	if opts.contains(&SharedJobOptForAttrItem::NumListAft) {
132		parts.push(num_str);
133	}
134
135	// 5. Nazwa pliku
136	if !opts.contains(&SharedJobOptForAttrItem::NameHide) {
137		parts.push(row.name_with_ext.clone());
138	}
139
140	let mut base = parts.join(" ");
141
142	// 6. Align End (Trailing space)
143	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
152// ============================================================================
153// FORMATOWANIE: DRZEWO (Twoja zoptymalizowana logika mostków)
154// ============================================================================
155
156struct 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	// const BRIDGE_NEXT: &'static str = "──•";
164	// const BRIDGE_LAST: &'static str = "───";
165}
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	// const BRIDGE_LAST: &'static str = "───";
176}
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	// 1. Logika Lookahead O(1) - dzieci w tabeli
207	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	// 2. Logika Lookahead O(n) - obliczanie is_last
215	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	// --- DETEKCJA NAJBLIŻSZEGO WIDOCZNEGO RODZICA (Dla mostka) ---
230	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	// 3. Generujemy wcięcia LUB segmenty mostka (poziomy 1 do T-1)
242	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	// 4. Dobieramy odpowiedni symbol gałęzi końcowej
257	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	// 5. Łączymy gałąź z ewentualnym mostkiem
272	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
283// ============================================================================
284// FORMATOWANIE: IKONY ORAZ ROZMIAR
285// ============================================================================
286
287fn 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
327// ============================================================================
328// MOCK RENDERER (Renderowanie do podglądu konsolowego)
329// ============================================================================
330
331pub 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}