Skip to main content

plot/lib/logic/
specification.rs

1// use super::{ConfigJob, ScanRawJobNew, ConfigLayout, ConfigTrimming,
2// ConfigExport, ConfigSpec};
3use crate::lib::logic::config_model::*;
4// ============================================================================
5// ENUMY Z PARSERAMI (SINGLE SOURCE OF TRUTH)
6// ============================================================================
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TabColumn {
10	TreeList, // Symbole drzewa (  ├──• ) // Wcięcia wg głębokości ( │  │  ├──• )
11	Number,   // Numeracja pozycji ( 1. )
12	Icon,     // Ikona typu ( 📂 / 📝 )
13	Size,     // Rozmiar w nawiasach ( [ 1.20 kB] )
14	Date,     // Data: | 2026 W14 Sun |
15	Time,     // Czas: | 11:08:06.298 PM |
16	Path,     // Ścieżka pliku
17}
18
19impl TabColumn {
20	pub fn parse(s: &str) -> Result<Self, String> {
21		match s.trim().to_lowercase().as_str() {
22			"treelist" | "tree" | "list" => Ok(TabColumn::TreeList),
23			"number" => Ok(TabColumn::Number),
24			"icon" => Ok(TabColumn::Icon),
25			"size" => Ok(TabColumn::Size),
26			"date" => Ok(TabColumn::Date),
27			"time" => Ok(TabColumn::Time),
28			"path" => Ok(TabColumn::Path),
29			_ => Err(format!(
30				"Nieprawidłowa nazwa kolumny: '{}'. Dostępne: list, tree, number, icon, size, date, time, path",
31				s.trim()
32			)),
33		}
34	}
35}
36
37//==================================================================================================
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum TabSortBy {
41	Name,
42	Size,
43	Date,
44	Kind,
45	FileFirst,
46	DirFirst,
47	FileFirstMerge,
48	DirFirstMerge,
49}
50
51impl TabSortBy {
52	pub fn parse(s: &str) -> Result<Self, String> {
53		match s.trim().to_lowercase().as_str() {
54			"name" => Ok(TabSortBy::Name),
55			"size" => Ok(TabSortBy::Size),
56			"date" => Ok(TabSortBy::Date),
57			"kind" => Ok(TabSortBy::Kind),
58			"file-first" => Ok(TabSortBy::FileFirst),
59			"dir-first" => Ok(TabSortBy::DirFirst),
60			"file-merge" => Ok(TabSortBy::FileFirstMerge),
61			"dir-merge" => Ok(TabSortBy::DirFirstMerge),
62			_ => Err(format!(
63				"Nieprawidłowa wartość sortowania: '{}'. Dostępne: name, size, date, kind, file-first, dir-first, file-merge, dir-merge",
64				s.trim()
65			)),
66		}
67	}
68}
69
70//==================================================================================================
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum TabSortOrder {
74	Asc,
75	Desc,
76}
77
78impl TabSortOrder {
79	pub fn parse(s: &str) -> Result<Self, String> {
80		match s.trim().to_lowercase().as_str() {
81			"az" | "asc" | "ascending" => Ok(TabSortOrder::Asc),
82			"za" | "desc" | "descending" => Ok(TabSortOrder::Desc),
83			_ => Err(format!("Nieprawidłowy kierunek sortowania: '{}'. Dostępne: asc, desc", s)),
84		}
85	}
86}
87//==================================================================================================
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum TabPathStructure {
91	List,
92	Tree,
93}
94
95impl TabPathStructure {
96	pub fn parse(s: &str) -> Result<Self, String> {
97		match s.trim().to_lowercase().as_str() {
98			"tree" | "t" | "T" => Ok(TabPathStructure::Tree),
99			"list" | "l" | "L" => Ok(TabPathStructure::List),
100			_ => Err(format!("Nieprawidłowa struktura: '{}'. Dostępne: tree, list", s)),
101		}
102	}
103}
104
105//==================================================================================================
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum JobMode {
109	Matched,
110	Mismatched,
111}
112
113impl JobMode {
114	pub fn parse(s: &str) -> Result<Self, String> {
115		match s.trim().to_lowercase().as_str() {
116			"m" | "M" | "matched" => Ok(JobMode::Matched),
117			"x" | "X" | "mismatched" => Ok(JobMode::Mismatched),
118			_ => Err(format!("Nieprawidłowy tryb zadania (mode): '{}'. Dostępne: m, x", s)),
119		}
120	}
121}
122
123//==================================================================================================
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct TabSpec {
127	pub sort_by: TabSortBy,
128	pub sort_order: TabSortOrder,
129	pub structure: TabPathStructure,
130	pub columns: Vec<TabColumn>,
131	pub trim_size: Option<usize>,
132	pub trim_page: usize, // Domyślnie 1, działa tylko gdy trim_size jest Some
133	pub more_icons: bool,
134}
135
136impl Default for TabSpec {
137	fn default() -> Self {
138		Self {
139			sort_by: TabSortBy::Name,
140			sort_order: TabSortOrder::Asc,
141			structure: TabPathStructure::Tree,
142			columns: vec![
143				TabColumn::Date,
144				TabColumn::Time,
145				TabColumn::Size,
146				TabColumn::TreeList,
147				TabColumn::Icon,
148				TabColumn::Number,
149				TabColumn::Path,
150			],
151			trim_size: None,
152			trim_page: 1,
153			more_icons: false,
154		}
155	}
156}
157
158impl TabSpec {
159	pub fn from_config(cfg: &ConfigLayout, spec: &ConfigSpec) -> Result<Self, String> {
160		let mut columns = Vec::new();
161		for col_str in &cfg.columns {
162			columns.push(TabColumn::parse(col_str)?);
163		}
164
165		let mut tab = Self::default()
166			.sort(
167				TabSortBy::parse(&cfg.sort_by)?,       // ⚡ BYŁO: cfg.sort
168				TabSortOrder::parse(&cfg.sort_order)?, // ⚡ BYŁO: if cfg.reverse...
169				TabPathStructure::parse(&cfg.structure)?,
170			)
171			.columns(&columns)
172			.more_icons(cfg.more_icons);
173
174		// Zostawiamy wsparcie dla flagi reverse, jeśli ktoś woli jej używać
175		if cfg.reverse {
176			tab.sort_order = TabSortOrder::Desc;
177		}
178
179		// ⚡ Pobieramy domyślną stronę z głównej sekcji
180		tab.trim_page = cfg.trim_page;
181
182		if let Some(trim) = &spec.trimming
183			&& let Some(size) = trim.page_size
184		{
185			tab = tab.trim(size, trim.show_page);
186		}
187		Ok(tab)
188	}
189
190	pub fn sort(mut self, by: TabSortBy, order: TabSortOrder, structure: TabPathStructure) -> Self {
191		self.sort_by = by;
192		self.sort_order = order;
193		self.structure = structure;
194		self
195	}
196
197	pub fn columns(mut self, cols: &[TabColumn]) -> Self {
198		self.columns = cols.to_vec();
199		self
200	}
201
202	pub fn trim(mut self, size: usize, page: Option<usize>) -> Self {
203		self.trim_size = Some(size);
204		if let Some(p) = page {
205			self.trim_page = p;
206		}
207		self
208	}
209
210	pub fn more_icons(mut self, enabled: bool) -> Self {
211		self.more_icons = enabled;
212		self
213	}
214}
215
216//==================================================================================================
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct ScanSpec {
220	pub work_path: String,
221	pub patterns: Vec<String>,
222	pub ignore_case: bool,
223}
224
225impl Default for ScanSpec {
226	fn default() -> Self {
227		Self {
228			work_path: ".".to_string(),
229			patterns: vec![
230				"./{.rustfmt,Cargo,rust-toolchain,Makefile}.toml&/".to_string(),
231				"./**/*.rs&/".to_string(),
232				"!./target/**".to_string(),
233				"!./.git/**".to_string(),
234				"./.{gitattributes,gitignore}".to_string(),
235				"./.github/workflows/*.yml&/".to_string(),
236				"./.vscode/settings.json&/".to_string(),
237				"./{API,ARCHITECTURE,AUTHORS,CHANGELOG,README,ROADMAP,TODO}.md".to_string(),
238				"./dist/**".to_string(),
239			],
240			ignore_case: false,
241		}
242	}
243}
244
245impl ScanSpec {
246	pub fn new(work_path: impl Into<String>) -> Self { Self { work_path: work_path.into(), ..Default::default() } }
247
248	pub fn from_config(cfg: &ScanRawJobNew) -> Self {
249		Self::new(&cfg.work_path)
250			.patterns(cfg.patterns.iter().map(|s| s.as_str()).collect())
251			.ignore_case(cfg.ignore_case)
252	}
253
254	pub fn patterns(mut self, pats: Vec<&str>) -> Self {
255		self.patterns = pats.into_iter().map(|s| s.to_string()).collect();
256		self
257	}
258
259	pub fn ignore_case(mut self, ignore: bool) -> Self {
260		self.ignore_case = ignore;
261		self
262	}
263}
264
265//==================================================================================================
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct JobSpec {
269	pub id: String,
270	pub name: Option<String>,
271	pub description: Option<String>,
272	//------------------------------
273	pub mode: JobMode,
274	pub scan: ScanSpec,
275	//------------------------------
276	pub save_sotc_at: Option<String>,
277	pub title_sotc: Option<String>,
278	pub save_cots_at: Option<String>,
279	pub title_cots: Option<String>,
280	//------------------------------
281	pub quiet_work: bool,
282	pub hide_stats: bool,
283	pub hide_promo: bool,
284	//------------------------------
285	pub table: TabSpec,
286}
287
288impl Default for JobSpec {
289	fn default() -> Self {
290		Self {
291			id: "default_job".to_string(),
292			name: None,
293			description: None,
294			//-----------
295			mode: JobMode::Matched,
296			scan: ScanSpec::default(),
297			save_sotc_at: None,
298			title_sotc: None,
299			save_cots_at: None,
300			title_cots: None,
301			quiet_work: false,
302			hide_stats: false,
303			hide_promo: false,
304			table: TabSpec::default(), // Domyślna tabela (z pełnymi kolumnami i drzewem)
305		}
306	}
307}
308
309impl JobSpec {
310	pub fn new(id: impl Into<String>) -> Self { Self { id: id.into(), ..Default::default() } }
311
312	pub fn from_config(cfg: &ConfigJob) -> Result<Self, String> {
313		let scan = ScanSpec::from_config(&cfg.scan);
314		let table = TabSpec::from_config(&cfg.table_sotc, &cfg.spec)?;
315
316		let mut job = Self::new(&cfg.id)
317			.scan(scan)
318			.table(table)
319			.mode(JobMode::parse(&cfg.spec.mode)?)
320			.quiet(cfg.quiet_work)
321			.hide_stats(cfg.spec.hide_stats)
322			.hide_promo(cfg.spec.hide_promo);
323
324		job.name = cfg.name.clone();
325		job.description = cfg.description.clone();
326
327		if let Some(export) = &cfg.export {
328			job.save_sotc_at = export.save_sotc_at.clone();
329			job.title_sotc = export.title_sotc.clone();
330			job.save_cots_at = export.save_cots_at.clone();
331			job.title_cots = export.title_cots.clone();
332		}
333		Ok(job)
334	}
335
336	pub fn work_path(mut self, path: impl Into<String>) -> Self {
337		self.scan.work_path = path.into();
338		self
339	}
340
341	pub fn patterns(mut self, pats: Vec<&str>) -> Self {
342		self.scan.patterns = pats.into_iter().map(|s| s.to_string()).collect();
343		self
344	}
345
346	pub fn ignore_case(mut self, ignore: bool) -> Self {
347		self.scan.ignore_case = ignore;
348		self
349	}
350
351	pub fn scan(mut self, scan_spec: ScanSpec) -> Self {
352		self.scan = scan_spec;
353		self
354	}
355
356	pub fn mode(mut self, mode: JobMode) -> Self {
357		self.mode = mode;
358		self
359	}
360
361	pub fn quiet(mut self, quiet: bool) -> Self {
362		self.quiet_work = quiet;
363		self
364	}
365
366	pub fn hide_stats(mut self, hide: bool) -> Self {
367		self.hide_stats = hide;
368		self
369	}
370
371	pub fn hide_promo(mut self, hide: bool) -> Self {
372		self.hide_promo = hide;
373		self
374	}
375
376	//  Delegujemy trimming prosto do zagnieżdżonego TabSpec
377	pub fn trim(mut self, size: usize, page: Option<usize>) -> Self {
378		self.table = self.table.trim(size, page);
379		self
380	}
381
382	// Pozwala wstrzyknąć gotową specyfikację tabeli
383	pub fn table(mut self, tab_spec: TabSpec) -> Self {
384		self.table = tab_spec;
385		self
386	}
387}
388
389//==================================================================================================