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// ===================================================================
10// CONFIG VALID: TABLE-PART
11// ===================================================================
12
13pub enum ValidTablePart {
14	MD,
15	MF,
16	XD,
17	XF,
18}
19
20#[derive(Debug, Clone)]
21pub struct ValidTablePartConfig {
22	pub md: bool,
23	pub mf: bool,
24	pub xd: bool,
25	pub xf: bool,
26}
27
28impl ValidTablePart {
29	pub fn parse(s: &str) -> Result<Self, String> {
30		match s.trim().to_lowercase().as_str() {
31			"MD" | "md" | "match-dirs" => Ok(ValidTablePart::MD),
32			"MF" | "mf" | "match-files" => Ok(ValidTablePart::MF),
33			"XD" | "xd" | "mismatch-dirs" => Ok(ValidTablePart::XD),
34			"XF" | "xf" | "mismatch-files" => Ok(ValidTablePart::XF),
35			_ => Err(format!("Nieprawidłowa nazwa: '{}'. Dostępne: md, mf, xd, xf", s.trim())),
36		}
37	}
38
39	pub fn parses<'a, I>(inputs: I) -> Result<Vec<Self>, String>
40	where I: IntoIterator<Item = &'a str> {
41		inputs.into_iter().map(Self::parse).collect()
42	}
43
44	pub fn parse_vec<I>(inputs: I) -> Vec<Self>
45	where
46		I: IntoIterator,
47		I::Item: AsRef<str>, {
48		inputs.into_iter().map(|s| Self::parse(s.as_ref()).expect("invalid ValidTablePart input")).collect()
49	}
50	pub fn parse_vec_as_config<I>(inputs: I) -> ValidTablePartConfig
51	where
52		I: IntoIterator,
53		I::Item: AsRef<str>, {
54		let mut md = false;
55		let mut mf = false;
56		let mut xd = false;
57		let mut xf = false;
58
59		let mut has_any_valid = false;
60
61		for item in inputs {
62			// Cicho ignorujemy błędy parsowania (zgodnie z założeniem o niepoprawnych elementach)
63			if let Ok(parsed) = Self::parse(item.as_ref()) {
64				has_any_valid = true;
65				match parsed {
66					ValidTablePart::MD => md = true,
67					ValidTablePart::MF => mf = true,
68					ValidTablePart::XD => xd = true,
69					ValidTablePart::XF => xf = true,
70				}
71			}
72		}
73
74		// Jeśli tablica była pusta lub wszystko było śmieciami, ładujemy domyślne ustawienia
75		if !has_any_valid {
76			md = true;
77			mf = true;
78		}
79
80		ValidTablePartConfig { md, mf, xd, xf }
81	}
82}
83
84// ===================================================================
85// CONFIG VALID: COLUMN-ITEM
86// ===================================================================
87
88pub enum ValidColumnItem {
89	ListNone,
90	ListTree,
91	ListFlat,
92	IconsNone,
93	IconsLite,
94	IconsMore,
95
96	NumPrefix,
97	NumSuffix,
98	NameNone,
99	NameShow,
100	WhitespaceTrailNone,
101	WhitespaceTrailShow,
102}
103
104#[derive(Debug)]
105pub struct ValidColumnItemConfig {
106	pub list: ValidModeItemList,
107	pub icons: ValidModeItemIcons,
108	pub name: bool,
109	pub align_end: bool,
110	pub num_is_first: bool,
111}
112
113#[derive(Debug, PartialEq, Eq)]
114pub enum ValidModeItemList {
115	None,
116	Flat,
117	Tree,
118}
119
120#[derive(Debug, PartialEq, Eq)]
121pub enum ValidModeItemIcons {
122	Lite,
123	More,
124	None,
125}
126
127impl ValidColumnItem {
128	pub fn parse(s: &str) -> Result<Self, String> {
129		match s.trim().to_lowercase().as_str() {
130			"list-none" => Ok(ValidColumnItem::ListNone),
131			"list-flat" => Ok(ValidColumnItem::ListFlat),
132			"list-tree" => Ok(ValidColumnItem::ListTree),
133
134			"icons-lite" => Ok(ValidColumnItem::IconsLite),
135			"icons-more" => Ok(ValidColumnItem::IconsMore),
136			"icons-none" => Ok(ValidColumnItem::IconsNone),
137
138			"num-prefix" => Ok(ValidColumnItem::NumPrefix),
139			"num-suffix" => Ok(ValidColumnItem::NumSuffix),
140
141			"name-none" => Ok(ValidColumnItem::NameNone),
142			"name-show" => Ok(ValidColumnItem::NameShow),
143
144			"ws-none" | "whitespace-none" => Ok(ValidColumnItem::WhitespaceTrailNone),
145			"ws-show" | "whitespace-show" => Ok(ValidColumnItem::WhitespaceTrailShow),
146
147			_ => Err(format!("Nieznany ValidColumnItem: '{}'", s.trim())),
148		}
149	}
150
151	pub fn parse_vec<I>(inputs: I) -> Result<Vec<Self>, String>
152	where
153		I: IntoIterator,
154		I::Item: AsRef<str>, {
155		inputs.into_iter().map(|s| Self::parse(s.as_ref())).collect()
156	}
157
158	pub fn parse_vec_as_config<I>(inputs: I) -> Result<ValidColumnItemConfig, String>
159	where
160		I: IntoIterator,
161		I::Item: AsRef<str>, {
162		let items = Self::parse_vec(inputs)?;
163		Ok(ValidColumnItemConfig::get(items))
164	}
165}
166
167impl ValidColumnItemConfig {
168	pub fn get(items: Vec<ValidColumnItem>) -> Self {
169		let mut list: Option<ValidModeItemList> = None;
170		let mut icons: Option<ValidModeItemIcons> = None;
171		let mut num_is_first: Option<bool> = None;
172		let mut name: Option<bool> = None;
173		let mut align_end: Option<bool> = None;
174
175		let mut list_count = 0usize;
176		let mut icons_count = 0usize;
177		let mut num_count = 0usize;
178		let mut name_count = 0usize;
179		let mut ws_count = 0usize;
180
181		for item in items {
182			match item {
183				// LIST
184				ValidColumnItem::ListNone => {
185					list = Some(ValidModeItemList::None);
186					list_count += 1;
187				}
188				ValidColumnItem::ListFlat => {
189					list = Some(ValidModeItemList::Flat);
190					list_count += 1;
191				}
192				ValidColumnItem::ListTree => {
193					list = Some(ValidModeItemList::Tree);
194					list_count += 1;
195				}
196
197				// NUM
198				ValidColumnItem::NumPrefix => {
199					num_is_first = Some(true);
200					num_count += 1;
201				}
202				ValidColumnItem::NumSuffix => {
203					num_is_first = Some(false);
204					num_count += 1;
205				}
206
207				// ICONS
208				ValidColumnItem::IconsLite => {
209					icons = Some(ValidModeItemIcons::Lite);
210					icons_count += 1;
211				}
212				ValidColumnItem::IconsMore => {
213					icons = Some(ValidModeItemIcons::More);
214					icons_count += 1;
215				}
216				ValidColumnItem::IconsNone => {
217					icons = Some(ValidModeItemIcons::None);
218					icons_count += 1;
219				}
220
221				// NAME
222				ValidColumnItem::NameNone => {
223					name = Some(false);
224					name_count += 1;
225				}
226				ValidColumnItem::NameShow => {
227					name = Some(true);
228					name_count += 1;
229				}
230
231				// WHITESPACE (ALIGN_END)
232				ValidColumnItem::WhitespaceTrailNone => {
233					align_end = Some(false);
234					ws_count += 1;
235				}
236				ValidColumnItem::WhitespaceTrailShow => {
237					align_end = Some(true);
238					ws_count += 1;
239				}
240			}
241		}
242
243		Self {
244			list: if list_count == 1 { list.unwrap() } else { ValidModeItemList::Tree },
245
246			icons: if icons_count == 1 { icons.unwrap() } else { ValidModeItemIcons::Lite },
247
248			num_is_first: if num_count == 1 { num_is_first.unwrap() } else { false },
249
250			name: if name_count == 1 { name.unwrap() } else { false },
251
252			align_end: if ws_count == 1 { align_end.unwrap() } else { false },
253		}
254	}
255
256	/// Wygodny builder: parsuje kolekcję stringów i od razu zwraca gotowy config.
257	/// Zwraca Result, ponieważ stringi wejściowe mogą być nieprawidłowe.
258	pub fn from_strings<I>(inputs: I) -> Result<Self, String>
259	where
260		I: IntoIterator,
261		I::Item: AsRef<str>, {
262		// 1. Parsujemy stringi na wektor enumów ValidColumnItem
263		let parsed_items = ValidColumnItem::parse_vec(inputs)?;
264
265		// 2. Budujemy i zwracamy ostateczny config
266		Ok(Self::get(parsed_items))
267	}
268
269	pub fn format_item(&self, u: (usize, usize, usize, usize), row: &job::RawRow, tab: &[job::RawRow]) -> String {
270		let (index, num_width, tier_max, name_len_max) = u;
271		let mut parts = Vec::new();
272		let num_str = format!("{:>width$}.", index + 1, width = num_width);
273
274		// 1. Jeśli num_is_first = true, numer idzie na początek
275		if self.num_is_first {
276			parts.push(num_str.clone());
277		}
278
279		// 2. Struktura Listy
280		if self.list != ValidModeItemList::None {
281			let l = job::gens::item_list::draw_list(&self.list, index, tab, tier_max);
282			parts.push(l);
283		}
284
285		// 3. Ikony
286		if self.icons != ValidModeItemIcons::None {
287			let ic = job::gens::draw_icon(&self.icons, &row.node.node, &row.name_with_ext);
288			parts.push(ic.to_string());
289		}
290
291		// 4. Jeśli num_is_first = false, numer idzie po liście i ikonach (ale przed nazwą)
292		if !self.num_is_first {
293			parts.push(num_str);
294		}
295
296		// 5. Nazwa pliku
297		if self.name {
298			parts.push(row.name_with_ext.clone());
299		}
300
301		let mut base = parts.join(" ");
302
303		// 6. Align End (Trailing space) - dodaje pusty string, co przy join(" ") da spację na końcu
304		if self.align_end {
305			let p_tier = (tier_max - row.node.tier) * 3;
306			let p_name = name_len_max - row.node.name.chars().count();
307			base.push_str(&" ".repeat(p_tier + p_name));
308		}
309
310		base
311	}
312}
313
314// ===================================================================
315// CONFIG VALID: TABLE-COLUMNS
316// ===================================================================
317
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub enum ValidTableColumns {
320	Date,
321	Time,
322	Size,
323	Item,
324	Path,
325}
326
327#[derive(Debug, Clone)]
328pub struct ValidTableColumnsConfig {
329	pub columns: Vec<ValidTableColumns>,
330}
331
332impl ValidTableColumns {
333	pub fn parse(s: &str) -> Result<Self, String> {
334		match s.trim().to_lowercase().as_str() {
335			"date" => Ok(ValidTableColumns::Date),
336			"time" => Ok(ValidTableColumns::Time),
337			"size" => Ok(ValidTableColumns::Size),
338			"item" => Ok(ValidTableColumns::Item),
339			"path" => Ok(ValidTableColumns::Path),
340			_ => Err(format!("Nieprawidłowa nazwa kolumny: '{}'. Dostępne: date, time, size, item, path", s.trim())),
341		}
342	}
343
344	pub fn parse_vec<I>(inputs: I) -> Result<Vec<Self>, String>
345	where
346		I: IntoIterator,
347		I::Item: AsRef<str>, {
348		inputs.into_iter().map(|s| Self::parse(s.as_ref())).collect()
349	}
350
351	pub fn parse_vec_as_config<I>(inputs: I) -> ValidTableColumnsConfig
352	where
353		I: IntoIterator,
354		I::Item: AsRef<str>, {
355		let mut columns = Vec::new();
356
357		let mut has_date = false;
358		let mut has_time = false;
359		let mut has_size = false;
360		let mut has_item = false;
361		let mut has_path = false;
362
363		let mut has_any_valid = false;
364
365		for item in inputs {
366			if let Ok(parsed) = Self::parse(item.as_ref()) {
367				has_any_valid = true;
368				match parsed {
369					ValidTableColumns::Date => {
370						if !has_date {
371							columns.push(ValidTableColumns::Date);
372							has_date = true;
373						}
374					}
375					ValidTableColumns::Time => {
376						if !has_time {
377							columns.push(ValidTableColumns::Time);
378							has_time = true;
379						}
380					}
381					ValidTableColumns::Size => {
382						if !has_size {
383							columns.push(ValidTableColumns::Size);
384							has_size = true;
385						}
386					}
387					ValidTableColumns::Item => {
388						if !has_item {
389							columns.push(ValidTableColumns::Item);
390							has_item = true;
391						}
392					}
393					ValidTableColumns::Path => {
394						if !has_path {
395							columns.push(ValidTableColumns::Path);
396							has_path = true;
397						}
398					}
399				}
400			}
401		}
402
403		// Jeśli wejście było puste lub same błędy -> wczytujemy konfigurację domyślną
404		if !has_any_valid {
405			return ValidTableColumnsConfig {
406				columns: vec![
407					ValidTableColumns::Date,
408					ValidTableColumns::Time,
409					ValidTableColumns::Size,
410					ValidTableColumns::Item,
411					ValidTableColumns::Path,
412				],
413			};
414		}
415
416		// Jeśli podano cokolwiek poprawnego, ale brakuje 'Item', doklejamy na koniec
417		if !has_item {
418			columns.push(ValidTableColumns::Item);
419		}
420
421		ValidTableColumnsConfig { columns }
422	}
423}
424
425// ===================================================================
426// CONFIG VALID: COLUMN-SIZE
427// ===================================================================
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum ValidColumnSize {
431	Decimal, // System SI (podstawa 1000)
432	Binary,  // System IEC (podstawa 1024)
433}
434
435#[derive(Debug, Clone)]
436pub struct ValidColumnSizeConfig {
437	pub mode: ValidColumnSize,
438}
439
440impl ValidColumnSize {
441	pub fn parse(s: &str) -> Result<Self, String> {
442		match s.trim().to_lowercase().as_str() {
443			"dec" | "decimal" => Ok(ValidColumnSize::Decimal),
444			"bin" | "binary" => Ok(ValidColumnSize::Binary),
445			_ => Err(format!("Nieznany system miar: '{}'. Dostępne: dec, bin", s.trim())),
446		}
447	}
448
449	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnSizeConfig
450	where
451		I: IntoIterator,
452		I::Item: AsRef<str>, {
453		let mut mode = ValidColumnSize::Decimal; // Domyślnie Dec
454
455		for item in inputs {
456			if let Ok(parsed) = Self::parse(item.as_ref()) {
457				mode = parsed;
458				break; // Przyjmujemy pierwszy poprawny wpis
459			}
460		}
461
462		ValidColumnSizeConfig { mode }
463	}
464}
465
466impl ValidColumnSizeConfig {
467	/// Metoda formatująca rozmiar w zależności od wybranego trybu (Dec/Bin)
468	pub fn format_size(&self, bytes: u64) -> String {
469		let base = match self.mode {
470			ValidColumnSize::Decimal => 1000.0,
471			ValidColumnSize::Binary => 1024.0,
472		};
473
474		let suffix = match self.mode {
475			ValidColumnSize::Decimal => ["B ", "kB", "MB", "GB"],
476			ValidColumnSize::Binary => ["B ", "KiB", "MiB", "GiB"], // Lub kB/MB/GB wg preferencji
477		};
478
479		let bytes_f = bytes as f64;
480
481		if bytes_f < base {
482			format!("{:>6} {}", bytes, suffix[0])
483		} else if bytes_f < base.powi(2) {
484			format!("{:>6.2} {}", bytes_f / base, suffix[1])
485		} else if bytes_f < base.powi(3) {
486			format!("{:>6.2} {}", bytes_f / base.powi(2), suffix[2])
487		} else {
488			format!("{:>6.2} {}", bytes_f / base.powi(3), suffix[3])
489		}
490	}
491}
492// ===================================================================
493// CONFIG VALID: COLUMN-DATE
494// ===================================================================
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub enum ValidColumnDate {
498	Default,
499}
500
501#[derive(Debug, Clone)]
502pub struct ValidColumnDateConfig {
503	pub format: String,
504}
505
506impl ValidColumnDate {
507	pub fn parse(s: &str) -> Result<Self, String> {
508		match s.trim().to_lowercase().as_str() {
509			"date-default" | "default" => Ok(ValidColumnDate::Default),
510			_ => Err(format!("Nieznany format daty: '{}'", s.trim())),
511		}
512	}
513
514	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnDateConfig
515	where
516		I: IntoIterator,
517		I::Item: AsRef<str>, {
518		let format = "%Y W%V %u-%a".to_string(); // Domyślny format: Rok Tydzień Dzień(1-7) SkrótDnia
519
520		for item in inputs {
521			if let Ok(_) = Self::parse(item.as_ref()) {
522				// Tutaj można dodać logikę wyboru formatu, jeśli pojawi się więcej wariantów w enum
523				break;
524			}
525		}
526
527		ValidColumnDateConfig { format }
528	}
529}
530
531impl ValidColumnDateConfig {
532	pub fn format_date(&self, dt: chrono::DateTime<chrono::Local>) -> String { dt.format(&self.format).to_string() }
533}
534
535// ===================================================================
536// CONFIG VALID: COLUMN-TIME
537// ===================================================================
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum ValidColumnTime {
541	Default,
542}
543
544#[derive(Debug, Clone)]
545pub struct ValidColumnTimeConfig {
546	pub format: String,
547}
548
549impl ValidColumnTime {
550	pub fn parse(s: &str) -> Result<Self, String> {
551		match s.trim().to_lowercase().as_str() {
552			"time-default" | "default" => Ok(ValidColumnTime::Default),
553			_ => Err(format!("Nieznany format czasu: '{}'", s.trim())),
554		}
555	}
556
557	pub fn parse_vec_as_config<I>(inputs: I) -> ValidColumnTimeConfig
558	where
559		I: IntoIterator,
560		I::Item: AsRef<str>, {
561		let format = "%H:%M:%S.%3f".to_string(); // Domyślny format: Godz:Min:Sek.Milisekundy
562
563		for item in inputs {
564			if let Ok(_) = Self::parse(item.as_ref()) {
565				break;
566			}
567		}
568
569		ValidColumnTimeConfig { format }
570	}
571}
572
573impl ValidColumnTimeConfig {
574	pub fn format_time(&self, dt: chrono::DateTime<chrono::Local>) -> String { dt.format(&self.format).to_string() }
575}
576// ===================================================================
577// CONFIG VALID: PATTERN
578// ===================================================================
579
580#[derive(Debug, Clone)]
581pub struct ValidPatternConfig {
582	pub patterns: Vec<String>,
583	pub ignore_case: bool,
584}
585
586impl ValidPatternConfig {
587	/// Zwraca gotowy obiekt logic::PatternsQueries używany przez skaner
588	pub fn get(&self) -> logic::PatternsQueries {
589		// Musimy jawnie stworzyć wektor referencji, aby Rust nie miał wątpliwości co do typu
590		let patterns_ref: Vec<&str> = self.patterns.iter().map(|s| s.as_str()).collect();
591		logic::PatternsQueries::new(patterns_ref, self.ignore_case)
592	}
593}
594
595pub struct ValidPattern;
596
597impl ValidPattern {
598	pub fn parse_vec_as_config<I>(inputs: I, ignore_case_sensitive: Option<bool>) -> ValidPatternConfig
599	where
600		I: IntoIterator,
601		I::Item: AsRef<str>, {
602		let patterns: Vec<String> = inputs.into_iter().map(|s| s.as_ref().to_string()).collect();
603
604		// Jeśli lista jest pusta po zebraniu, ładujemy domyślne
605		if patterns.is_empty() {
606			return ValidPatternConfig {
607				patterns: vec![
608					"./{.rustfmt,Cargo,rust-toolchain,Makefile}.toml&/".to_string(),
609					"./**/*.rs&/".to_string(),
610					"!./target/**".to_string(),
611					"!./.git/**".to_string(),
612					"./.{gitattributes,gitignore}".to_string(),
613					"./.github/workflows/*.yml&/".to_string(),
614					"./.vscode/settings.json&/".to_string(),
615					"./{API,ARCHITECTURE,AUTHORS,CHANGELOG,README,ROADMAP,TODO}.md".to_string(),
616					"./dist/**".to_string(),
617				],
618				ignore_case: ignore_case_sensitive.unwrap_or(false),
619			};
620		}
621
622		ValidPatternConfig { patterns, ignore_case: ignore_case_sensitive.unwrap_or(false) }
623	}
624}
625
626// ===================================================================
627// CONFIG VALID: WORKSPACE
628// ===================================================================
629
630#[derive(Debug, Clone)]
631pub struct ValidWorkspaceConfig {
632	pub workspace_dir: String,
633}
634
635impl ValidWorkspaceConfig {
636	/// Normalizuje ścieżkę i zwraca logic::AnchoredPathsDatum.
637	/// W przypadku błędu (np. brak folderu) przerywa program z komunikatem.
638	pub fn get(&self) -> logic::AnchoredPathsDatum {
639		// 1. Zamiana na PathBuf dla natywnej obsługi OS
640		let path = PathBuf::from(&self.workspace_dir);
641
642		// 2. Kanonizacja (rozwiązuje .., ścieżki relatywne i symlinki)
643		// Jeśli ścieżka nie istnieje, fs::canonicalize zwróci błąd
644		let work_path = match path.canonicalize() {
645			Ok(p) => p.to_string_lossy().to_string(),
646			Err(_) => {
647				// Jeśli canonicalize zawiedzie (np. brak folderu),
648				// próbujemy chociaż zwrócić czytelną ścieżkę absolutną
649				match std::env::current_dir() {
650					Ok(current) => current.join(&path).to_string_lossy().to_string(),
651					Err(_) => self.workspace_dir.clone(),
652				}
653			}
654		};
655
656		// 3. Inicjalizacja AnchoredPathsDatum z obsługą błędu
657		logic::AnchoredPathsDatum::new(&work_path).unwrap_or_else(|x| {
658			eprintln!("❌ Błąd lokalizacji workspace: {}", x);
659			process::exit(1);
660		})
661	}
662}
663
664pub struct ValidWorkspace;
665
666impl ValidWorkspace {
667	pub fn parse_vec_as_config<I>(inputs: I) -> ValidWorkspaceConfig
668	where
669		I: IntoIterator,
670		I::Item: AsRef<str>, {
671		let mut workspace_dir = ".".to_string();
672
673		// Bierzemy pierwszy niepusty element jako ścieżkę workspace
674		for item in inputs {
675			let s = item.as_ref().trim();
676			if !s.is_empty() {
677				workspace_dir = s.to_string();
678				break;
679			}
680		}
681
682		ValidWorkspaceConfig { workspace_dir }
683	}
684}
685// ===================================================================
686// ===================================================================