Skip to main content

plot/lib/job/
config.rs

1use std::{path::PathBuf, process};
2
3use chrono;
4
5use crate::lib::{
6	job::{self},
7	logic::{self},
8};
9
10pub const IS_DEBUG: bool = true; // ⚡ GLOBALNA FLAGA DEBUGOWANIA
11// ============================================================================
12// OSTATECZNY WIERSZ DANYCH (Zhydratowany)
13// ============================================================================
14
15/// Reprezentuje finalny wiersz gotowy do tabeli, zawierający wszystkie
16/// zgromadzone metadane z dysku i z etapu skanowania.
17#[derive(Debug, Clone)]
18pub struct ValidResultMainRow {
19	pub dt_modified: chrono::DateTime<chrono::Local>,
20	pub name_with_ext: String,
21	pub size_real: u64,
22	pub node: logic::ScannedNode,
23}
24
25#[derive(Debug, Clone)]
26pub struct ValidResultMainTab {
27	pub rows: Vec<ValidResultMainRow>,
28	pub tier_max: usize,
29	pub name_len_max: usize,
30	pub path_len_max: usize,
31}
32// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
33/*
34 * 🧠 KAŻDY `Valid<NAZWA>Parse` posiada parę `Valid<NAZWA>Config` i odwrotnie.
35 *
36 * 🧠 KAŻDY `Valid<NAZWA>Parse` ma metodę `parse_vec_as_config` - automatyczne wywołanie bliźniaka
37 *
38 * 🧠 KAŻDY `Valid<NAZWA>Config` ma metodę `get` - pobiera znormalizowaną konfiguracje
39 *
40 * 🧠 DODATKI DLA `Valid<NAZWA><Config|Parse>` mają nazwę `<ZASTOSOWANIE>ForValid<NAZWA>`
41 *
42 * 🧠 CELEM NINIESZJEGO PLIKU JEST ZAPEWNIANIE PRAWIDŁOWEJ KONFIGURACJI,
43 *          CIĘŻKA LOGIKA POWINNA BYĆ W `Crate::lib::logic::*`;
44 * 	        W ETAPACH KOLEJNYCH W `Crate::lib::job::engine_step*` W GŁÓWNEJ FUNKCJI - ARGUMENTY SĄ
45 *          ZAWSZE TYPU `&job::Valid<NAZWA>Config`
46 *  */
47// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
48// CONFIG VALID: TABLE-PART
49// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
50
51pub enum ValidTablePartParse {
52	MD,
53	MF,
54	XD,
55	XF,
56}
57
58#[derive(Debug, Clone)]
59pub struct ValidTablePartConfig {
60	pub md: bool,
61	pub mf: bool,
62	pub xd: bool,
63	pub xf: bool,
64}
65
66impl ValidTablePartParse {
67	pub fn parse(s: &str) -> Result<Self, String> {
68		match s.trim().to_lowercase().as_str() {
69			"MD" | "md" | "match-dirs" => Ok(ValidTablePartParse::MD),
70			"MF" | "mf" | "match-files" => Ok(ValidTablePartParse::MF),
71			"XD" | "xd" | "mismatch-dirs" => Ok(ValidTablePartParse::XD),
72			"XF" | "xf" | "mismatch-files" => Ok(ValidTablePartParse::XF),
73			_ => Err(format!("Nieprawidłowa nazwa: '{}'. Dostępne: md, mf, xd, xf", s.trim())),
74		}
75	}
76
77	pub fn parses<'a, I>(inputs: I) -> Result<Vec<Self>, String>
78	where I: IntoIterator<Item = &'a str> {
79		inputs.into_iter().map(Self::parse).collect()
80	}
81
82	pub fn parse_vec<I>(inputs: I) -> Vec<Self>
83	where
84		I: IntoIterator,
85		I::Item: AsRef<str>, {
86		inputs.into_iter().map(|s| Self::parse(s.as_ref()).expect("invalid ValidTablePartParse input")).collect()
87	}
88	pub fn parse_vec_as_config<I>(inputs: I) -> ValidTablePartConfig
89	where
90		I: IntoIterator,
91		I::Item: AsRef<str>, {
92		let mut md = false;
93		let mut mf = false;
94		let mut xd = false;
95		let mut xf = false;
96
97		let mut has_any_valid = false;
98
99		for item in inputs {
100			// Cicho ignorujemy błędy parsowania (zgodnie z założeniem o niepoprawnych elementach)
101			if let Ok(parsed) = Self::parse(item.as_ref()) {
102				has_any_valid = true;
103				match parsed {
104					ValidTablePartParse::MD => md = true,
105					ValidTablePartParse::MF => mf = true,
106					ValidTablePartParse::XD => xd = true,
107					ValidTablePartParse::XF => xf = true,
108				}
109			}
110		}
111
112		// Jeśli tablica była pusta lub wszystko było śmieciami, ładujemy domyślne ustawienia
113		if !has_any_valid {
114			md = true;
115			mf = true;
116		}
117
118		ValidTablePartConfig { md, mf, xd, xf }
119	}
120}
121
122// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
123// CONFIG VALID: COLUMN-ITEM
124// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
125
126pub enum ValidColumnItemParse {
127	ListNone,
128	ListTree,
129	ListFlat,
130	IconsNone,
131	IconsLite,
132	IconsMore,
133
134	NumPrefix,
135	NumSuffix,
136	NameNone,
137	NameShow,
138	WhitespaceTrailNone,
139	WhitespaceTrailShow,
140}
141
142#[derive(Debug)]
143pub struct ValidColumnItemConfig {
144	pub list: ModeListForValidColumnItem,
145	pub icons: ModeIconsForValidColumnItem,
146	pub name: bool,
147	pub align_end: bool,
148	pub num_is_first: bool,
149}
150
151#[derive(Debug, PartialEq, Eq)]
152pub enum ModeListForValidColumnItem {
153	None,
154	Flat,
155	Tree,
156}
157
158#[derive(Debug, PartialEq, Eq)]
159pub enum ModeIconsForValidColumnItem {
160	Lite,
161	More,
162	None,
163}
164
165impl ValidColumnItemParse {
166	pub fn parse(s: &str) -> Result<Self, String> {
167		match s.trim().to_lowercase().as_str() {
168			"list-none" => Ok(ValidColumnItemParse::ListNone),
169			"list-flat" => Ok(ValidColumnItemParse::ListFlat),
170			"list-tree" => Ok(ValidColumnItemParse::ListTree),
171
172			"icons-lite" => Ok(ValidColumnItemParse::IconsLite),
173			"icons-more" => Ok(ValidColumnItemParse::IconsMore),
174			"icons-none" => Ok(ValidColumnItemParse::IconsNone),
175
176			"num-prefix" => Ok(ValidColumnItemParse::NumPrefix),
177			"num-suffix" => Ok(ValidColumnItemParse::NumSuffix),
178
179			"name-none" => Ok(ValidColumnItemParse::NameNone),
180			"name-show" => Ok(ValidColumnItemParse::NameShow),
181
182			"ws-none" | "whitespace-none" => Ok(ValidColumnItemParse::WhitespaceTrailNone),
183			"ws-show" | "whitespace-show" => Ok(ValidColumnItemParse::WhitespaceTrailShow),
184
185			_ => Err(format!("Nieznany ValidColumnItemParse: '{}'", s.trim())),
186		}
187	}
188
189	pub fn parse_vec<I>(inputs: I) -> Result<Vec<Self>, String>
190	where
191		I: IntoIterator,
192		I::Item: AsRef<str>, {
193		inputs.into_iter().map(|s| Self::parse(s.as_ref())).collect()
194	}
195
196	pub fn parse_vec_as_config<I>(inputs: I) -> Result<ValidColumnItemConfig, String>
197	where
198		I: IntoIterator,
199		I::Item: AsRef<str>, {
200		let items = Self::parse_vec(inputs)?;
201		Ok(ValidColumnItemConfig::get(items))
202	}
203}
204
205impl ValidColumnItemConfig {
206	pub fn get(items: Vec<ValidColumnItemParse>) -> Self {
207		let mut list: Option<ModeListForValidColumnItem> = None;
208		let mut icons: Option<ModeIconsForValidColumnItem> = None;
209		let mut num_is_first: Option<bool> = None;
210		let mut name: Option<bool> = None;
211		let mut align_end: Option<bool> = None;
212
213		let mut list_count = 0usize;
214		let mut icons_count = 0usize;
215		let mut num_count = 0usize;
216		let mut name_count = 0usize;
217		let mut ws_count = 0usize;
218
219		for item in items {
220			match item {
221				// LIST
222				ValidColumnItemParse::ListNone => {
223					list = Some(ModeListForValidColumnItem::None);
224					list_count += 1;
225				}
226				ValidColumnItemParse::ListFlat => {
227					list = Some(ModeListForValidColumnItem::Flat);
228					list_count += 1;
229				}
230				ValidColumnItemParse::ListTree => {
231					list = Some(ModeListForValidColumnItem::Tree);
232					list_count += 1;
233				}
234
235				// NUM
236				ValidColumnItemParse::NumPrefix => {
237					num_is_first = Some(true);
238					num_count += 1;
239				}
240				ValidColumnItemParse::NumSuffix => {
241					num_is_first = Some(false);
242					num_count += 1;
243				}
244
245				// ICONS
246				ValidColumnItemParse::IconsLite => {
247					icons = Some(ModeIconsForValidColumnItem::Lite);
248					icons_count += 1;
249				}
250				ValidColumnItemParse::IconsMore => {
251					icons = Some(ModeIconsForValidColumnItem::More);
252					icons_count += 1;
253				}
254				ValidColumnItemParse::IconsNone => {
255					icons = Some(ModeIconsForValidColumnItem::None);
256					icons_count += 1;
257				}
258
259				// NAME
260				ValidColumnItemParse::NameNone => {
261					name = Some(false);
262					name_count += 1;
263				}
264				ValidColumnItemParse::NameShow => {
265					name = Some(true);
266					name_count += 1;
267				}
268
269				// WHITESPACE (ALIGN_END)
270				ValidColumnItemParse::WhitespaceTrailNone => {
271					align_end = Some(false);
272					ws_count += 1;
273				}
274				ValidColumnItemParse::WhitespaceTrailShow => {
275					align_end = Some(true);
276					ws_count += 1;
277				}
278			}
279		}
280
281		Self {
282			list: if list_count == 1 { list.unwrap() } else { ModeListForValidColumnItem::Tree },
283
284			icons: if icons_count == 1 { icons.unwrap() } else { ModeIconsForValidColumnItem::Lite },
285
286			num_is_first: if num_count == 1 { num_is_first.unwrap() } else { false },
287
288			name: if name_count == 1 { name.unwrap() } else { false },
289
290			align_end: if ws_count == 1 { align_end.unwrap() } else { false },
291		}
292	}
293
294	/// Wygodny builder: parsuje kolekcję stringów i od razu zwraca gotowy config.
295	/// Zwraca Result, ponieważ stringi wejściowe mogą być nieprawidłowe.
296	pub fn from_strings<I>(inputs: I) -> Result<Self, String>
297	where
298		I: IntoIterator,
299		I::Item: AsRef<str>, {
300		// 1. Parsujemy stringi na wektor enumów ValidColumnItemParse
301		let parsed_items = ValidColumnItemParse::parse_vec(inputs)?;
302
303		// 2. Budujemy i zwracamy ostateczny config
304		Ok(Self::get(parsed_items))
305	}
306
307	pub fn format_item(
308		&self,
309		u: (usize, usize, usize, usize),
310		row: &job::ValidResultMainRow,
311		tab: &[job::ValidResultMainRow],
312	) -> String {
313		let (index, num_width, tier_max, name_len_max) = u;
314		let mut parts = Vec::new();
315		let num_str = format!("{:>width$}.", index + 1, width = num_width);
316
317		// 1. Jeśli num_is_first = true, numer idzie na początek
318		if self.num_is_first {
319			parts.push(num_str.clone());
320		}
321
322		// 2. Struktura Listy
323		if self.list != ModeListForValidColumnItem::None {
324			let l = job::gens::item_list::draw_list(&self.list, index, tab, tier_max);
325			parts.push(l);
326		}
327
328		// 3. Ikony
329		if self.icons != ModeIconsForValidColumnItem::None {
330			let ic = job::gens::draw_icon(&self.icons, &row.node.node, &row.name_with_ext);
331			parts.push(ic.to_string());
332		}
333
334		// 4. Jeśli num_is_first = false, numer idzie po liście i ikonach (ale przed nazwą)
335		if !self.num_is_first {
336			parts.push(num_str);
337		}
338
339		// 5. Nazwa pliku
340		if self.name {
341			parts.push(row.name_with_ext.clone());
342		}
343
344		let mut base = parts.join(" ");
345
346		// 6. Align End (Trailing space) - dodaje pusty string, co przy join(" ") da spację na końcu
347		if self.align_end {
348			let p_tier = (tier_max - row.node.tier) * 3;
349			let p_name = name_len_max - row.node.name.chars().count();
350			base.push_str(&" ".repeat(p_tier + p_name));
351		}
352
353		base
354	}
355}
356
357// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
358// CONFIG VALID: TABLE-COLUMNS
359// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
360
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub enum ValidTableColumnsParse {
363	Date,
364	Time,
365	Size,
366	Item,
367	Path,
368}
369
370#[derive(Debug, Clone)]
371pub struct ValidTableColumnsConfig {
372	pub columns: Vec<ValidTableColumnsParse>,
373}
374
375impl ValidTableColumnsParse {
376	pub fn parse(s: &str) -> Result<Self, String> {
377		match s.trim().to_lowercase().as_str() {
378			"date" => Ok(ValidTableColumnsParse::Date),
379			"time" => Ok(ValidTableColumnsParse::Time),
380			"size" => Ok(ValidTableColumnsParse::Size),
381			"item" => Ok(ValidTableColumnsParse::Item),
382			"path" => Ok(ValidTableColumnsParse::Path),
383			_ => Err(format!("Nieprawidłowa nazwa kolumny: '{}'. Dostępne: date, time, size, item, path", s.trim())),
384		}
385	}
386
387	pub fn parse_vec<I>(inputs: I) -> Result<Vec<Self>, String>
388	where
389		I: IntoIterator,
390		I::Item: AsRef<str>, {
391		inputs.into_iter().map(|s| Self::parse(s.as_ref())).collect()
392	}
393
394	pub fn parse_vec_as_config<I>(inputs: I) -> ValidTableColumnsConfig
395	where
396		I: IntoIterator,
397		I::Item: AsRef<str>, {
398		let mut columns = Vec::new();
399
400		let mut has_date = false;
401		let mut has_time = false;
402		let mut has_size = false;
403		let mut has_item = false;
404		let mut has_path = false;
405
406		let mut has_any_valid = false;
407
408		for item in inputs {
409			if let Ok(parsed) = Self::parse(item.as_ref()) {
410				has_any_valid = true;
411				match parsed {
412					ValidTableColumnsParse::Date => {
413						if !has_date {
414							columns.push(ValidTableColumnsParse::Date);
415							has_date = true;
416						}
417					}
418					ValidTableColumnsParse::Time => {
419						if !has_time {
420							columns.push(ValidTableColumnsParse::Time);
421							has_time = true;
422						}
423					}
424					ValidTableColumnsParse::Size => {
425						if !has_size {
426							columns.push(ValidTableColumnsParse::Size);
427							has_size = true;
428						}
429					}
430					ValidTableColumnsParse::Item => {
431						if !has_item {
432							columns.push(ValidTableColumnsParse::Item);
433							has_item = true;
434						}
435					}
436					ValidTableColumnsParse::Path => {
437						if !has_path {
438							columns.push(ValidTableColumnsParse::Path);
439							has_path = true;
440						}
441					}
442				}
443			}
444		}
445
446		// Jeśli wejście było puste lub same błędy -> wczytujemy konfigurację domyślną
447		if !has_any_valid {
448			return ValidTableColumnsConfig {
449				columns: vec![
450					ValidTableColumnsParse::Date,
451					ValidTableColumnsParse::Time,
452					ValidTableColumnsParse::Size,
453					ValidTableColumnsParse::Item,
454					ValidTableColumnsParse::Path,
455				],
456			};
457		}
458
459		// Jeśli podano cokolwiek poprawnego, ale brakuje 'Item', doklejamy na koniec
460		if !has_item {
461			columns.push(ValidTableColumnsParse::Item);
462		}
463
464		ValidTableColumnsConfig { columns }
465	}
466}
467
468// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
469// CONFIG VALID: COLUMN-SIZE
470// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473pub enum ValidColumnSizeParse {
474	Decimal, // System SI (podstawa 1000)
475	Binary,  // System IEC (podstawa 1024)
476}
477
478#[derive(Debug, Clone)]
479pub struct ValidColumnSizeConfig {
480	pub mode: ValidColumnSizeParse,
481}
482
483impl ValidColumnSizeParse {
484	pub fn parse(s: &str) -> Result<Self, String> {
485		match s.trim().to_lowercase().as_str() {
486			"dec" | "decimal" => Ok(ValidColumnSizeParse::Decimal),
487			"bin" | "binary" => Ok(ValidColumnSizeParse::Binary),
488			_ => Err(format!("Nieznany system miar: '{}'. Dostępne: dec, bin", s.trim())),
489		}
490	}
491
492	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnSizeConfig
493	where
494		I: IntoIterator,
495		I::Item: AsRef<str>, {
496		let mut mode = ValidColumnSizeParse::Decimal; // Domyślnie Dec
497
498		for item in inputs {
499			if let Ok(parsed) = Self::parse(item.as_ref()) {
500				mode = parsed;
501				break; // Przyjmujemy pierwszy poprawny wpis
502			}
503		}
504
505		ValidColumnSizeConfig { mode }
506	}
507}
508
509impl ValidColumnSizeConfig {
510	/// Metoda formatująca rozmiar w zależności od wybranego trybu (Dec/Bin)
511	pub fn format_size(&self, bytes: u64) -> String {
512		let base = match self.mode {
513			ValidColumnSizeParse::Decimal => 1000.0,
514			ValidColumnSizeParse::Binary => 1024.0,
515		};
516
517		let suffix = match self.mode {
518			ValidColumnSizeParse::Decimal => ["B ", "kB", "MB", "GB"],
519			ValidColumnSizeParse::Binary => ["B ", "KiB", "MiB", "GiB"], // Lub kB/MB/GB wg preferencji
520		};
521
522		let bytes_f = bytes as f64;
523
524		if bytes_f < base {
525			format!("{:>6} {}", bytes, suffix[0])
526		} else if bytes_f < base.powi(2) {
527			format!("{:>6.2} {}", bytes_f / base, suffix[1])
528		} else if bytes_f < base.powi(3) {
529			format!("{:>6.2} {}", bytes_f / base.powi(2), suffix[2])
530		} else {
531			format!("{:>6.2} {}", bytes_f / base.powi(3), suffix[3])
532		}
533	}
534}
535// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
536// CONFIG VALID: COLUMN-DATE
537// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum ValidColumnDateParse {
541	Default,
542}
543
544#[derive(Debug, Clone)]
545pub struct ValidColumnDateConfig {
546	pub format: String,
547}
548
549impl ValidColumnDateParse {
550	pub fn parse(s: &str) -> Result<Self, String> {
551		match s.trim().to_lowercase().as_str() {
552			"date-default" | "default" => Ok(ValidColumnDateParse::Default),
553			_ => Err(format!("Nieznany format daty: '{}'", s.trim())),
554		}
555	}
556
557	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnDateConfig
558	where
559		I: IntoIterator,
560		I::Item: AsRef<str>, {
561		let format = "%Y W%V %u-%a".to_string(); // Domyślny format: Rok Tydzień Dzień(1-7) SkrótDnia
562
563		for item in inputs {
564			if let Ok(_) = Self::parse(item.as_ref()) {
565				// Tutaj można dodać logikę wyboru formatu, jeśli pojawi się więcej wariantów w enum
566				break;
567			}
568		}
569
570		ValidColumnDateConfig { format }
571	}
572}
573
574impl ValidColumnDateConfig {
575	pub fn format_date(&self, dt: chrono::DateTime<chrono::Local>) -> String { dt.format(&self.format).to_string() }
576}
577
578// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
579// CONFIG VALID: COLUMN-TIME
580// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub enum ValidColumnTimeParse {
584	Default,
585}
586
587#[derive(Debug, Clone)]
588pub struct ValidColumnTimeConfig {
589	pub format: String,
590}
591
592impl ValidColumnTimeParse {
593	pub fn parse(s: &str) -> Result<Self, String> {
594		match s.trim().to_lowercase().as_str() {
595			"time-default" | "default" => Ok(ValidColumnTimeParse::Default),
596			_ => Err(format!("Nieznany format czasu: '{}'", s.trim())),
597		}
598	}
599
600	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnTimeConfig
601	where
602		I: IntoIterator,
603		I::Item: AsRef<str>, {
604		let format = "%H:%M:%S.%3f".to_string(); // Domyślny format: Godz:Min:Sek.Milisekundy
605
606		for item in inputs {
607			if let Ok(_) = Self::parse(item.as_ref()) {
608				break;
609			}
610		}
611
612		ValidColumnTimeConfig { format }
613	}
614}
615
616impl ValidColumnTimeConfig {
617	pub fn format_time(&self, dt: chrono::DateTime<chrono::Local>) -> String { dt.format(&self.format).to_string() }
618}
619// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
620// CONFIG VALID: PATTERN
621// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
622
623#[derive(Debug, Clone)]
624pub struct ValidPatternConfig {
625	pub patterns: Vec<String>,
626	pub ignore_case: bool,
627}
628
629impl ValidPatternConfig {
630	/// Zwraca gotowy obiekt logic::PatternsQueries używany przez skaner
631	pub fn get(&self) -> logic::PatternsQueries {
632		// Musimy jawnie stworzyć wektor referencji, aby Rust nie miał wątpliwości co do typu
633		let patterns_ref: Vec<&str> = self.patterns.iter().map(|s| s.as_str()).collect();
634		logic::PatternsQueries::new(patterns_ref, self.ignore_case)
635	}
636}
637
638pub struct ValidPatternParse;
639
640impl ValidPatternParse {
641	pub fn parse_vec_as_config<I>(inputs: I, ignore_case_sensitive: Option<bool>) -> ValidPatternConfig
642	where
643		I: IntoIterator,
644		I::Item: AsRef<str>, {
645		let patterns: Vec<String> = inputs.into_iter().map(|s| s.as_ref().to_string()).collect();
646
647		// Jeśli lista jest pusta po zebraniu, ładujemy domyślne
648		if patterns.is_empty() {
649			return ValidPatternConfig {
650				patterns: vec![
651					"./{.rustfmt,Cargo,rust-toolchain,Makefile}.toml&/".to_string(),
652					"./**/*.rs&/".to_string(),
653					"!./target/**".to_string(),
654					"!./.git/**".to_string(),
655					"./.{gitattributes,gitignore}".to_string(),
656					"./.github/workflows/*.yml&/".to_string(),
657					"./.vscode/settings.json&/".to_string(),
658					"./{API,ARCHITECTURE,AUTHORS,CHANGELOG,README,ROADMAP,TODO}.md".to_string(),
659					"./dist/{**/*,*}.{bat,exe}&/".to_string(),
660				],
661				ignore_case: ignore_case_sensitive.unwrap_or(false),
662			};
663		}
664
665		ValidPatternConfig { patterns, ignore_case: ignore_case_sensitive.unwrap_or(false) }
666	}
667}
668
669// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
670// CONFIG VALID: WORKSPACE
671// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
672
673#[derive(Debug, Clone)]
674pub struct ValidWorkspaceConfig {
675	pub workspace_dir: String,
676}
677
678impl ValidWorkspaceConfig {
679	/// Normalizuje ścieżkę i zwraca logic::AnchoredPathsDatum.
680	/// W przypadku błędu (np. brak folderu) przerywa program z komunikatem.
681	pub fn get(&self) -> logic::AnchoredPathsDatum {
682		// 1. Zamiana na PathBuf dla natywnej obsługi OS
683		let path = PathBuf::from(&self.workspace_dir);
684
685		// 2. Kanonizacja (rozwiązuje .., ścieżki relatywne i symlinki)
686		// Jeśli ścieżka nie istnieje, fs::canonicalize zwróci błąd
687		let work_path = match path.canonicalize() {
688			Ok(p) => p.to_string_lossy().to_string(),
689			Err(_) => {
690				// Jeśli canonicalize zawiedzie (np. brak folderu),
691				// próbujemy chociaż zwrócić czytelną ścieżkę absolutną
692				match std::env::current_dir() {
693					Ok(current) => current.join(&path).to_string_lossy().to_string(),
694					Err(_) => self.workspace_dir.clone(),
695				}
696			}
697		};
698
699		// 3. Inicjalizacja AnchoredPathsDatum z obsługą błędu
700		logic::AnchoredPathsDatum::new(&work_path).unwrap_or_else(|x| {
701			eprintln!("❌ Błąd lokalizacji workspace: {}", x);
702			process::exit(1);
703		})
704	}
705}
706
707pub struct ValidWorkspaceParse;
708
709impl ValidWorkspaceParse {
710	pub fn parse_vec_as_config<I>(inputs: I) -> ValidWorkspaceConfig
711	where
712		I: IntoIterator,
713		I::Item: AsRef<str>, {
714		let mut workspace_dir = ".".to_string();
715
716		// Bierzemy pierwszy niepusty element jako ścieżkę workspace
717		for item in inputs {
718			let s = item.as_ref().trim();
719			if !s.is_empty() {
720				workspace_dir = s.to_string();
721				break;
722			}
723		}
724
725		ValidWorkspaceConfig { workspace_dir }
726	}
727}
728// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
729// CONFIG VALID: SORT-BY
730// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
731
732/* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
733 * dodajemy `ValidSortByParse`
734 *
735 * - "Date" -  `(Date:DateTime, Reverse:bool)`
736 * - "Size" -  `(Size:Num, Reverse:bool)`
737 * - "Name" -  `(Name:ModeSortName, Reverse:bool, FileGroup:ModeGroup)`
738 * - "Path" -  `(Name:ModeSortName, Reverse:bool, DirSplit:bool, FileGroup:ModeGroup)`
739 *
740 *
741 * `ModeSortName`
742 * - "[Spec][Num][AZaz]"
743 * - "[Spec][Num][azAZ]"
744 * - "[Spec][Num][aAzZ]"
745 * - "[Spec][Num][AaZz]"
746 * - "[Num][Spec][AZaz]"
747 * - "[Num][Spec][azAZ]"
748 * - "[Num][Spec][aAzZ]"
749 * - "[Num][Spec][AaZz]"
750 * - "[Spec][AZaz][Num]"
751 * - "[Spec][azAZ][Num]"
752 * - "[Spec][aAzZ][Num]"
753 * - "[Spec][AaZz][Num]"
754 * - "[Num][AZaz][Spec]"
755 * - "[Num][azAZ][Spec]"
756 * - "[Num][aAzZ][Spec]"
757 * - "[Num][AaZz][Spec]"
758 * - "[AZaz][Num][Spec]"
759 * - "[azAZ][Num][Spec]"
760 * - "[aAzZ][Num][Spec]"
761 * - "[AaZz][Num][Spec]"
762 * - "[AZaz][Spec][Num]"
763 * - "[azAZ][Spec][Num]"
764 * - "[aAzZ][Spec][Num]"
765 * - "[AaZz][Spec][Num]"
766 *
767 * `ModeGroup`
768 * - "Name"
769 * - "Exte"
770 * - "None"
771 *
772 * `Priority`
773 * - "FirstFile"
774 * - "FirstDir"
775 * - "None"
776 *
777 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
778 * oraz dodajemy `ValidSortByConfig`
779 *
780 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
781 * Tu kluczowe na metode sortowania będzie miał wpływ tryb `ValidColumnItemParse`
782 * - ValidColumnItemParse::ListTree
783 * - ValidColumnItemParse::ListFlat = ValidColumnItemParse::ListNone
784 *
785 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
786 * */
787#[derive(Debug, Clone, PartialEq, Eq)]
788pub enum ModeFileGroupForValidSortBy {
789	Name,
790	Exte,
791	None,
792}
793
794#[derive(Debug, Clone)]
795pub enum StrategyForValidSortBy {
796	Date { reverse: bool },
797	Size { reverse: bool },
798	Name { mode: String, reverse: bool, file_group: ModeFileGroupForValidSortBy },
799	Path { mode: String, reverse: bool, dir_split: bool, file_group: ModeFileGroupForValidSortBy },
800}
801
802#[derive(Debug, Clone)]
803pub struct ValidSortByConfig {
804	pub strategy: StrategyForValidSortBy,
805}
806
807impl ValidSortByConfig {
808	/// Zwraca gotowy obiekt z warstwy logic:: (np. logic::SortQueries),
809	/// gdzie dopiero tam te proste stringi i bool'e zostaną przetworzone
810	/// w potężny algorytm (wspomniane 24 warianty).
811	pub fn get(&self) -> logic::SortQueries { logic::SortQueries::new(self.clone()) }
812}
813
814pub struct ValidSortByParse;
815
816impl ValidSortByParse {
817	pub fn parse_vec_as_config<I>(inputs: I) -> ValidSortByConfig
818	where
819		I: IntoIterator,
820		I::Item: AsRef<str>, {
821		// 1. Ustawienia domyślne
822		let mut strategy_name = "name";
823		let mut reverse = false;
824		let mut dir_split = true;
825		let mut file_group = ModeFileGroupForValidSortBy::None;
826		let mut mode_sort_name = "[Spec][Num][AZaz]".to_string();
827
828		// 2. Płaskie parsowanie CLI/TOML (bez wchodzenia w detale algorytmów)
829		for item in inputs {
830			let s = item.as_ref().trim();
831			let lower = s.to_lowercase();
832
833			match lower.as_str() {
834				"date" => strategy_name = "date",
835				"size" => strategy_name = "size",
836				"name" => strategy_name = "name",
837				"path" => strategy_name = "path",
838
839				"rev" | "reverse" => reverse = true,
840
841				"group-name" => file_group = ModeFileGroupForValidSortBy::Name,
842				"group-exte" => file_group = ModeFileGroupForValidSortBy::Exte,
843				"group-none" => file_group = ModeFileGroupForValidSortBy::None,
844
845				"dir-split-true" => dir_split = true,
846				"dir-split-false" => dir_split = false,
847
848				_ => {
849					// Czyste przekazanie definicji maski (np. "[Num][Spec][azAZ]") do logiki
850					if s.starts_with('[') && s.ends_with(']') {
851						mode_sort_name = s.to_string();
852					}
853				}
854			}
855		}
856
857		// 3. Budowa wariantu konfiguracji
858		let strategy = match strategy_name {
859			"date" => StrategyForValidSortBy::Date { reverse },
860			"size" => StrategyForValidSortBy::Size { reverse },
861			"path" => StrategyForValidSortBy::Path { mode: mode_sort_name, reverse, dir_split, file_group },
862			_ => StrategyForValidSortBy::Name { mode: mode_sort_name, reverse, file_group },
863		};
864
865		ValidSortByConfig { strategy }
866	}
867}
868
869// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
870
871/*
872 * dodać ubsługe wyśfietlania ukrywania pustych folderów
873 * dodać ubsługe wyśfietlania ukrywania pustych plików
874 *
875 *
876 *  */