Skip to main content

plot/lib/logic/path/
paths_table.rs

1use std::{
2	collections::BTreeMap,
3	fs,
4	io::Read,
5	path::{Path, PathBuf},
6};
7
8use chrono::{DateTime, Local};
9
10use crate::lib::logic::{
11	AnchoredPathsDatum,
12	MatchLabel,
13	Partition,
14	TabColumn,
15	TabPathStructure,
16	TabSortBy,
17	TabSortOrder,
18	TabSpec,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
22pub enum FileKind {
23	Dir,
24	Text,
25	Binary,
26	Other,
27}
28
29/// Idiomatyczny wiersz danych
30#[derive(Debug, Clone)]
31pub struct TableRow {
32	pub path: String,
33	pub size: u64,
34	pub modified: DateTime<Local>,
35	pub kind: FileKind,
36}
37
38/// Idiomatyczny kontener zebranych danych
39#[derive(Debug, Clone)]
40pub struct TableData {
41	pub rows: Vec<TableRow>,
42	pub structure: TabPathStructure,
43}
44
45/// Ostateczny wynik materializacji
46pub struct TableOutput {
47	pub data: TableData,
48	pub columns: Vec<TabColumn>,
49	pub trim_size: Option<usize>,
50	pub trim_page: usize,
51	pub more_icons: bool,
52}
53
54fn is_binary(path: &std::path::Path) -> std::io::Result<bool> {
55	let mut file = fs::File::open(path)?;
56	let mut buffer = [0u8; 1024];
57	let n = file.read(&mut buffer)?;
58	Ok(buffer[..n].contains(&0))
59}
60
61fn get_dir_size(path: &std::path::Path) -> u64 {
62	walkdir::WalkDir::new(path)
63		.into_iter()
64		.filter_map(|e| e.ok())
65		.filter(|e| e.file_type().is_file())
66		.filter_map(|e| e.metadata().ok())
67		.map(|m| m.len())
68		.sum()
69}
70
71impl TableData {
72	pub fn gather<L: MatchLabel>(list: &Partition<L>) -> Self {
73		let rows = list.paths.iter().filter_map(|p| Self::inspect(p, &list.entry).ok()).collect();
74		Self { rows, structure: TabPathStructure::Tree }
75	}
76
77	fn inspect(rel_path: &str, relation: &AnchoredPathsDatum) -> anyhow::Result<TableRow> {
78		let clean_rel = rel_path.strip_prefix("./").unwrap_or(rel_path);
79		let absolute_path = relation.select_dir.buf.join(clean_rel);
80
81		let metadata = fs::metadata(&absolute_path)?;
82		let modified = DateTime::from(metadata.modified()?);
83
84		let (kind, size) = if metadata.is_dir() {
85			(FileKind::Dir, get_dir_size(&absolute_path))
86		} else {
87			let k = if is_binary(&absolute_path)? { FileKind::Binary } else { FileKind::Text };
88			(k, metadata.len())
89		};
90
91		Ok(TableRow { path: rel_path.to_string(), size, modified, kind })
92	}
93
94	pub fn sort(mut self, by: TabSortBy, order: TabSortOrder, structure: TabPathStructure) -> Self {
95		self.structure = structure;
96
97		fn get_merge_key(path: &str) -> &str {
98			let trimmed = path.trim_end_matches('/');
99			if let Some(idx) = trimmed.rfind('.')
100				&& idx > 0 && trimmed.as_bytes()[idx - 1] != b'/'
101			{
102				return &trimmed[..idx];
103			}
104			trimmed
105		}
106
107		let compare = |a: &TableRow, b: &TableRow| {
108			let a_is_dir = a.kind == FileKind::Dir;
109			let b_is_dir = b.kind == FileKind::Dir;
110			let a_merge = get_merge_key(&a.path);
111			let b_merge = get_merge_key(&b.path);
112
113			let mut cmp = match by {
114				TabSortBy::Name => a.path.cmp(&b.path),
115				TabSortBy::Size => a.size.cmp(&b.size),
116				TabSortBy::Date => a.modified.cmp(&b.modified),
117				TabSortBy::Kind => (a.kind.clone() as u8).cmp(&(b.kind.clone() as u8)),
118				TabSortBy::FileFirst => (a_is_dir, &a.path).cmp(&(b_is_dir, &b.path)),
119				TabSortBy::DirFirst => (!a_is_dir, &a.path).cmp(&(!b_is_dir, &b.path)),
120				TabSortBy::FileFirstMerge => (a_merge, a_is_dir, &a.path).cmp(&(b_merge, b_is_dir, &b.path)),
121				TabSortBy::DirFirstMerge => (a_merge, !a_is_dir, &a.path).cmp(&(b_merge, !b_is_dir, &b.path)),
122			};
123			if matches!(order, TabSortOrder::Desc) {
124				cmp = cmp.reverse();
125			}
126			cmp
127		};
128
129		match structure {
130			TabPathStructure::Tree => {
131				let clean_paths: Vec<PathBuf> =
132					self.rows.iter().map(|r| PathBuf::from(r.path.trim_end_matches('/'))).collect();
133				let mut tree_map: BTreeMap<PathBuf, Vec<usize>> = BTreeMap::new();
134
135				for (i, p) in clean_paths.iter().enumerate() {
136					let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf);
137					tree_map.entry(parent).or_default().push(i);
138				}
139
140				for indices in tree_map.values_mut() {
141					indices.sort_by(|&a, &b| compare(&self.rows[a], &self.rows[b]));
142				}
143
144				let mut root_indices = Vec::new();
145				for (i, p) in clean_paths.iter().enumerate() {
146					let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf);
147					let is_root = parent == Path::new(".") || parent == Path::new("") || !clean_paths.contains(&parent);
148					if is_root {
149						root_indices.push(i);
150					}
151				}
152				root_indices.sort_by(|&a, &b| compare(&self.rows[a], &self.rows[b]));
153
154				let mut flat_indices = Vec::with_capacity(self.rows.len());
155				fn flatten(
156					indices: &[usize],
157					tree_map: &BTreeMap<PathBuf, Vec<usize>>,
158					clean_paths: &[PathBuf],
159					out: &mut Vec<usize>,
160				) {
161					for &idx in indices {
162						out.push(idx);
163						if let Some(children) = tree_map.get(&clean_paths[idx]) {
164							flatten(children, tree_map, clean_paths, out);
165						}
166					}
167				}
168
169				flatten(&root_indices, &tree_map, &clean_paths, &mut flat_indices);
170
171				let old_rows = std::mem::take(&mut self.rows);
172				let mut temp_rows: Vec<Option<TableRow>> = old_rows.into_iter().map(Some).collect();
173				let mut new_rows = Vec::with_capacity(temp_rows.len());
174
175				for idx in flat_indices {
176					new_rows.push(temp_rows[idx].take().unwrap());
177				}
178				self.rows = new_rows;
179			}
180
181			TabPathStructure::List => {
182				// ⚡ LISTA: Czysty sort bez cudowania z mapami
183				self.rows.sort_by(compare);
184			}
185		}
186
187		self
188	}
189
190	pub fn into_output(self, spec: &TabSpec) -> TableOutput {
191		TableOutput {
192			data: self,
193			columns: spec.columns.clone(),
194			trim_size: spec.trim_size,
195			trim_page: spec.trim_page,
196			more_icons: spec.more_icons,
197		}
198	}
199}