1use colored::Colorize;
2
3use super::{Color, DrawTree, Icon};
4use crate::lib::logic::{FileKind, TabColumn, TabPathStructure, TableData, TableOutput, TableRow};
5
6fn format_size(bytes: u64) -> String {
7 if bytes < 1024 {
8 format!("{} B", bytes)
9 } else if bytes < 1024 * 1024 {
10 format!("{:.2} kB", bytes as f64 / 1024.0)
11 } else {
12 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
13 }
14}
15
16impl std::fmt::Display for TableData {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 let total = self.rows.len();
19 if total == 0 {
20 return write!(f, "{} {} Pusta tabela", DrawTree::ITEM_LAST, Icon::EMPTY);
21 }
22
23 let mut lines = Vec::new();
24
25 for (i, row) in self.rows.iter().enumerate() {
26 let prefix = DrawTree::list(i, total);
27
28 let kind_icon = match row.kind {
29 FileKind::Dir => Icon::FOLDER,
30 FileKind::Binary => "๐พ",
31 FileKind::Text => Icon::FILE,
32 FileKind::Other => "โ",
33 };
34
35 lines.push(format!(
36 "{} {} [{:>10}] | {} | {}",
37 prefix,
38 kind_icon,
39 format_size(row.size),
40 row.modified.format("%Y-%m-%d %H:%M"),
41 row.path
42 ));
43 }
44
45 write!(f, "{}", lines.join("\n"))
46 }
47}
48
49impl std::fmt::Display for TableOutput {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 let total_rows = self.data.rows.len();
52
53 let (rows_to_show, index_offset) = if let Some(size) = self.trim_size {
54 let start = self.trim_page.saturating_sub(1) * size;
55 (self.data.rows.iter().skip(start).take(size).collect::<Vec<_>>(), start)
56 } else {
57 (self.data.rows.iter().collect::<Vec<_>>(), 0)
58 };
59
60 let current_view_count = rows_to_show.len();
61 let num_width = total_rows.to_string().len();
62
63 let mut prefix_map = std::collections::HashMap::new();
64
65 if self.data.structure == TabPathStructure::Tree {
66 use std::{
67 collections::BTreeMap,
68 path::{Path, PathBuf},
69 };
70
71 let clean_paths: Vec<PathBuf> =
72 self.data.rows.iter().map(|r| PathBuf::from(r.path.trim_end_matches('/'))).collect();
73
74 let mut tree_map: BTreeMap<PathBuf, Vec<&TableRow>> = BTreeMap::new();
75
76 for (i, p) in clean_paths.iter().enumerate() {
77 let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf);
78 tree_map.entry(parent).or_default().push(&self.data.rows[i]);
79 }
80
81 let mut roots = Vec::new();
82 for (i, p) in clean_paths.iter().enumerate() {
83 let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf);
84 let is_root = parent == Path::new(".") || parent == Path::new("") || !clean_paths.contains(&parent);
85 if is_root {
86 roots.push(&self.data.rows[i]);
87 }
88 }
89
90 fn build_prefixes(
91 nodes: &[&TableRow],
92 tree_map: &BTreeMap<PathBuf, Vec<&TableRow>>,
93 indent: &str,
94 out_map: &mut std::collections::HashMap<String, String>,
95 ) {
96 for (i, node) in nodes.iter().enumerate() {
97 let clean_p = PathBuf::from(node.path.trim_end_matches('/'));
98 let is_last = i == nodes.len() - 1;
99 let children = tree_map.get(&clean_p);
100 let has_children = children.is_some() && !children.unwrap().is_empty();
101 let is_dir = node.kind == FileKind::Dir;
102
103 let (branch, next_indent) = DrawTree::tree(is_dir, is_last, has_children);
104
105 let prefix = format!("{}{}", indent, branch);
106 out_map.insert(node.path.clone(), prefix);
107
108 if let Some(child_nodes) = children {
109 let new_indent = format!("{}{}", indent, next_indent);
110 build_prefixes(child_nodes, tree_map, &new_indent, out_map);
111 }
112 }
113 }
114
115 build_prefixes(&roots, &tree_map, "", &mut prefix_map);
116 }
117
118 let mut output_lines = Vec::new();
119
120 for (i, row) in rows_to_show.iter().enumerate() {
121 let actual_idx = index_offset + i + 1;
122
123 let tree_list_prefix_str = if self.data.structure == TabPathStructure::Tree {
124 prefix_map.get(&row.path).map(|s| s.as_str()).unwrap_or("")
125 } else {
126 DrawTree::list(i, current_view_count)
127 };
128 let tree_list_prefix = Color::tree(tree_list_prefix_str);
129
130 let icon_str = if self.more_icons {
131 let is_dir = row.kind == FileKind::Dir;
132 let file_name = row.path.trim_end_matches('/').split('/').next_back().unwrap_or("");
133 let is_hidden = file_name.starts_with('.');
134
135 if is_dir {
136 if is_hidden { Icon::FOLDER2_HIDDEN } else { Icon::FOLDER2 }
137 } else if is_hidden {
138 Icon::FILE2_HIDDEN
139 } else {
140 let path_obj = std::path::Path::new(&row.path);
141 if let Some(ext) = path_obj.extension().and_then(|e| e.to_str()) {
142 match ext.to_lowercase().as_str() {
143 "rs" => Icon::LANG_RUST,
144 "toml" => "โ๏ธ",
145 "slint" => "๐จ",
146 "md" => "๐",
147 "json" => "๐ฃ",
148 "yaml" | "yml" => "๐ ๏ธ",
149 "html" => "๐",
150 "css" => "๐๏ธ",
151 "js" => "๐",
152 "ts" => "๐",
153 _ => {
154 if row.kind == FileKind::Binary {
155 "๐พ"
156 } else {
157 Icon::FILE2
158 }
159 }
160 }
161 } else {
162 if row.kind == FileKind::Binary { "๐พ" } else { Icon::FILE2 }
163 }
164 }
165 } else {
166 match row.kind {
167 FileKind::Dir => Icon::FOLDER,
168 FileKind::Binary => "๐พ",
169 FileKind::Text => Icon::FILE,
170 FileKind::Other => "โ",
171 }
172 };
173
174 let path_styled = match row.kind {
175 FileKind::Dir => Color::folder(&row.path),
176 FileKind::Binary => Color::binary(&row.path),
177 FileKind::Text => Color::file(&row.path),
178 FileKind::Other => row.path.dimmed(),
179 };
180
181 let icon = match row.kind {
182 FileKind::Dir => icon_str.yellow(),
183 FileKind::Binary => icon_str.magenta(),
184 FileKind::Text => icon_str.normal(),
185 FileKind::Other => icon_str.red(),
186 };
187
188 let size_str = Color::size(&format!("{:>10}", format_size(row.size)));
189 let pos_num = Color::num(&format!("{:>width$}.", actual_idx, width = num_width));
190 let date_str = Color::date(&row.modified.format("%Y W%V %a").to_string());
191 let time_str = Color::time(&row.modified.format("%H:%M:%S.%3f").to_string());
192
193 let mut line_parts = Vec::new();
194 for (col_idx, col) in self.columns.iter().enumerate() {
195 let part = match col {
196 TabColumn::TreeList => tree_list_prefix.to_string(),
197 TabColumn::Number => pos_num.to_string(),
198 TabColumn::Icon => icon.to_string(),
199 TabColumn::Size => {
200 format!("{}{}{}", Color::border("[ "), size_str, Color::border(" ]"))
201 }
202 TabColumn::Date => {
203 let pipe = Color::border("|");
204 let next_is_time = matches!(self.columns.get(col_idx + 1), Some(TabColumn::Time));
205 let prev_is_time = col_idx > 0 && self.columns[col_idx - 1] == TabColumn::Time;
206 match (prev_is_time, next_is_time) {
207 (false, true) => format!("{} {}", pipe, date_str),
208 (true, false) => format!("{} {}", date_str, pipe),
209 (true, true) => format!("{}", date_str),
210 (false, false) => format!("{} {} {}", pipe, date_str, pipe),
211 }
212 }
213 TabColumn::Time => {
214 let pipe = Color::border("|");
215 let next_is_date = matches!(self.columns.get(col_idx + 1), Some(TabColumn::Date));
216 let prev_is_date = col_idx > 0 && self.columns[col_idx - 1] == TabColumn::Date;
217 match (prev_is_date, next_is_date) {
218 (false, true) => format!("{} {}", pipe, time_str),
219 (true, false) => format!("{} {}", time_str, pipe),
220 (true, true) => format!("{}", time_str),
221 (false, false) => format!("{} {} {}", pipe, time_str, pipe),
222 }
223 }
224 TabColumn::Path => path_styled.to_string(),
225 };
226 line_parts.push(part);
227 }
228
229 output_lines.push(line_parts.join(" "));
230 }
231
232 if let Some(size) = self.trim_size {
233 let total_pages = total_rows.saturating_add(size - 1) / size;
234 if total_pages > 1 {
235 output_lines.push(format!(
236 " {}",
237 format!("... Strona {} z {} (ลฤ
cznie {} pozycji)", self.trim_page, total_pages, total_rows)
238 .italic()
239 .dimmed()
240 ));
241 } else if total_rows > size {
242 output_lines.push(format!(
243 " {}",
244 format!("... i {} innych pozycji", total_rows - size).italic().dimmed()
245 ));
246 }
247 }
248
249 write!(f, "{}", output_lines.join("\n"))
250 }
251}