1use crate::prelude::*;
2use crate::ui::dir_panel::FileEntry::*;
3use crate::ui::prelude::*;
4use buffer_graphics_lib::prelude::Positioning::*;
5use buffer_graphics_lib::prelude::*;
6use std::cmp::Ordering;
7use std::fs::{read_dir, ReadDir};
8use std::path::PathBuf;
9
10const ENTRY_FORMAT: TextFormat = TextFormat::new(
11 WrappingStrategy::Ellipsis(35),
12 PixelFont::Standard4x5,
13 BLACK,
14 LeftTop,
15);
16const ERROR_FORMAT: TextFormat = TextFormat::new(
17 WrappingStrategy::SpaceBeforeCol(20),
18 PixelFont::Standard6x7,
19 RED,
20 Center,
21);
22
23#[derive(Debug, PartialEq, Clone, Eq)]
24enum FileEntry {
25 ParentDir(String),
26 File(FileInfo),
27 Dir(String, String),
28}
29
30impl FileEntry {
31 pub fn to_result(&self) -> DirResult {
32 match self {
33 ParentDir(path) => DirResult::new(path.clone(), false),
34 File(info) => DirResult::new(info.path.clone(), true),
35 Dir(path, _) => DirResult::new(path.clone(), false),
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct DirResult {
42 pub path: String,
43 pub is_file: bool,
44}
45
46impl DirResult {
47 pub fn new(path: String, is_file: bool) -> Self {
48 Self { path, is_file }
49 }
50}
51
52impl PartialOrd for FileEntry {
53 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
54 Some(self.cmp(other))
55 }
56}
57
58impl Ord for FileEntry {
59 fn cmp(&self, other: &Self) -> Ordering {
60 if let ParentDir(_) = self {
61 Ordering::Less
62 } else if let ParentDir(_) = other {
63 Ordering::Greater
64 } else {
65 match (self, other) {
66 (File(info), Dir(_, name)) => info.filename.cmp(name),
67 (Dir(_, name), File(info)) => name.cmp(&info.filename),
68 (Dir(_, lhs), Dir(_, rhs)) => lhs.cmp(rhs),
69 (File(lhs), File(rhs)) => lhs.filename.cmp(&rhs.filename),
70 (_, _) => Ordering::Equal,
71 }
72 }
73 }
74}
75
76#[derive(Debug, PartialEq, Eq, Clone)]
77struct FileInfo {
78 pub path: String,
79 pub filename: String,
80 pub size: String,
81}
82
83#[derive(Debug)]
85pub struct DirPanel {
86 current_dir: String,
87 files: Vec<FileEntry>,
88 first_visible_file_index: usize,
89 entry_visible_count: usize,
90 background: ShapeCollection,
91 bounds: Rect,
92 error: Option<String>,
93 highlight: Option<usize>,
94 allowed_ext: Option<String>,
95 state: ViewState,
96}
97
98impl DirPanel {
99 pub fn new(current_dir: &str, bounds: Rect, allowed_ext: Option<&str>) -> Self {
100 let (background, entry_visible_count) = Self::layout(&bounds);
101 let mut panel = Self {
102 error: None,
103 current_dir: current_dir.to_string(),
104 bounds,
105 files: vec![],
106 entry_visible_count,
107 first_visible_file_index: 0,
108 background,
109 highlight: None,
110 allowed_ext: allowed_ext.map(|s| s.to_string()),
111 state: ViewState::Normal,
112 };
113 panel.set_dir(current_dir);
114 panel
115 }
116
117 fn layout(bounds: &Rect) -> (ShapeCollection, usize) {
118 let mut background = ShapeCollection::default();
119 InsertShape::insert_above(&mut background, bounds.clone(), fill(WHITE));
120 InsertShape::insert_above(&mut background, bounds.clone(), stroke(DARK_GRAY));
121 let entry_visible_count =
122 bounds.height() / (PixelFont::Standard4x5.size().1 + PixelFont::Standard4x5.spacing());
123 (background, entry_visible_count)
124 }
125}
126
127fn fs_size(bytes: u64) -> String {
128 if bytes < 1024 {
129 format!("{bytes}B")
130 } else if bytes < 1024 * 1024 {
131 format!("{}KB", bytes / 1024)
132 } else if bytes < 1024 * 1024 * 1024 {
133 format!("{}MB", bytes / 1024 / 1024)
134 } else {
135 format!("{}GB", bytes / 1024 / 1024 / 1024)
136 }
137}
138
139fn get_files(path: &str, dir: ReadDir, allowed_ext: &Option<String>) -> Vec<FileEntry> {
140 let path = PathBuf::from(path);
141 let mut results = vec![];
142 if let Some(parent) = path.parent() {
143 results.push(ParentDir(parent.to_string_lossy().to_string()));
144 }
145 for file in dir.flatten() {
146 if let Ok(file_type) = file.file_type() {
147 if file_type.is_file() {
148 let include = if let Some(allowed) = allowed_ext {
149 &file
150 .path()
151 .extension()
152 .unwrap_or_default()
153 .to_string_lossy()
154 .to_string()
155 == allowed
156 } else {
157 true
158 };
159 if include {
160 results.push(File(FileInfo {
161 path: file.path().to_string_lossy().to_string(),
162 filename: file.file_name().to_string_lossy().to_string(),
163 size: fs_size(file.metadata().unwrap().len()),
164 }))
165 }
166 } else if file_type.is_dir() {
167 results.push(Dir(
168 file.path().to_string_lossy().to_string(),
169 file.file_name().to_string_lossy().to_string(),
170 ))
171 }
172 }
173 }
174 results
175}
176
177impl DirPanel {
178 pub fn set_dir(&mut self, path: &str) {
179 self.error = None;
180 self.first_visible_file_index = 0;
181 match read_dir(path) {
182 Ok(dir) => {
183 let mut files = get_files(path, dir, &self.allowed_ext);
184 files.sort();
185 self.files = files;
186 }
187 Err(err) => self.error = Some(err.to_string()),
188 }
189 }
190
191 #[must_use]
192 pub fn highlighted(&self) -> Option<DirResult> {
193 if let Some(i) = self.highlight {
194 self.files.get(i).map(|e| e.to_result())
195 } else {
196 None
197 }
198 }
199
200 pub fn set_highlight(&mut self, path: &str) {
201 for (i, entry) in self.files.iter().enumerate() {
202 let entry_path = match entry {
203 ParentDir(path) => path,
204 File(info) => &info.path,
205 Dir(path, _) => path,
206 };
207 if path == entry_path {
208 self.highlight = Some(i);
209 break;
210 }
211 }
212 }
213
214 #[inline]
215 #[must_use]
216 pub fn current_dir(&self) -> &str {
217 &self.current_dir
218 }
219
220 pub fn on_scroll(&mut self, xy: Coord, diff: isize) {
221 if self.bounds.contains(xy) {
222 let factor = diff.abs() % 5;
223 let up = diff < 0;
224 if up && self.first_visible_file_index > 0 {
225 self.first_visible_file_index = self
226 .first_visible_file_index
227 .saturating_sub(factor.unsigned_abs());
228 }
229 if !up && (self.first_visible_file_index + self.entry_visible_count < self.files.len())
230 {
231 self.first_visible_file_index = (self.first_visible_file_index
232 + factor.unsigned_abs())
233 .min(self.files.len() - self.entry_visible_count);
234 }
235 }
236 }
237
238 fn bounds_for_row(&self, row: usize) -> Rect {
239 let xy = self.bounds.top_left()
240 + (
241 2,
242 row * (PixelFont::Standard4x5.spacing() + PixelFont::Standard4x5.size().1)
243 + PixelFont::Standard4x5.spacing() * 2,
244 );
245 Rect::new(
246 xy,
247 (
248 self.bounds.right() - 2,
249 xy.y + (PixelFont::Standard4x5.size().1) as isize,
250 ),
251 )
252 }
253
254 pub fn on_mouse_click(&mut self, down: Coord, up: Coord) -> Option<DirResult> {
255 if self.state == ViewState::Disabled {
256 return None;
257 }
258 if self.bounds.contains(down) && self.bounds.contains(up) {
259 for i in 0..self.entry_visible_count {
260 if self.bounds_for_row(i).contains(up) {
261 return self
262 .files
263 .get(i + self.first_visible_file_index)
264 .map(|e| e.to_result());
265 }
266 }
267 }
268 None
269 }
270}
271
272impl PixelView for DirPanel {
273 fn set_position(&mut self, top_left: Coord) {
274 self.bounds = self.bounds.move_to(top_left);
275 let (background, entry_visible_count) = Self::layout(&self.bounds);
276 self.background = background;
277 self.entry_visible_count = entry_visible_count;
278 }
279
280 #[inline]
281 fn bounds(&self) -> &Rect {
282 &self.bounds
283 }
284
285 fn render(&self, graphics: &mut Graphics, mouse: &MouseData) {
286 graphics.draw(&self.background);
287
288 if let Some(txt) = &self.error {
289 graphics.draw_text(txt, TextPos::px(self.bounds.center()), ERROR_FORMAT);
290 } else {
291 let mut row = 0;
292 for i in self.first_visible_file_index
293 ..self.first_visible_file_index + self.entry_visible_count
294 {
295 let highlighted = self.highlight.map(|r| r == i).unwrap_or_default();
296 if i < self.files.len() {
297 let back = self.bounds_for_row(row);
298 if back.contains(mouse.xy) || highlighted {
299 graphics.draw_rect(
300 back.clone(),
301 fill(if highlighted { CYAN } else { LIGHT_GRAY }),
302 );
303 }
304 match &self.files[i] {
305 ParentDir(_) => {
306 graphics.draw_text("..", TextPos::px(back.top_left()), ENTRY_FORMAT)
307 }
308 File(info) => graphics.draw_text(
309 &info.filename,
310 TextPos::px(back.top_left()),
311 ENTRY_FORMAT,
312 ),
313 Dir(_, name) => {
314 graphics.draw_text(name, TextPos::px(back.top_left()), ENTRY_FORMAT)
315 }
316 }
317 row += 1;
318 }
319 }
320 }
321 }
322
323 fn update(&mut self, _: &Timing) {}
324
325 #[inline]
326 fn set_state(&mut self, new_state: ViewState) {
327 self.state = new_state;
328 }
329
330 #[inline]
331 fn get_state(&self) -> ViewState {
332 self.state
333 }
334}
335
336impl LayoutView for DirPanel {
337 fn set_bounds(&mut self, bounds: Rect) {
338 self.bounds = bounds.clone();
339 self.set_position(bounds.top_left());
340 }
341}