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#[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#[derive(Debug, Clone)]
40pub struct TableData {
41 pub rows: Vec<TableRow>,
42 pub structure: TabPathStructure,
43}
44
45pub 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 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}