use crate::platform::Platform; use crate::shared::{calculate_list_bounds, render_list_tip}; use crate::state::refresh_directory; use pretty_bytes::converter::convert as pretty_bytes; use std::collections::HashMap; use std::path::PathBuf; use unicode_width::UnicodeWidthStr; use zellij_tile::prelude::*; #[derive(Debug, Clone)] pub struct FileListView { pub path: PathBuf, pub path_is_dir: bool, pub files: Vec, pub cursor_hist: HashMap, pub platform: Platform, } impl Default for FileListView { fn default() -> Self { FileListView { path_is_dir: true, path: PathBuf::new(), files: Default::default(), cursor_hist: Default::default(), platform: Platform::default(), } } } impl FileListView { pub fn descend_to_previous_path(&mut self) { if Platform::is_root(&self.path, self.platform) { return; } if let Some(parent) = self.path.parent() { self.path = Platform::ensure_drive_root(parent.to_path_buf(), self.platform); } else { self.path = PathBuf::new(); } self.path_is_dir = true; self.files.clear(); self.clear_selected(); refresh_directory(&self.path); } pub fn descend_to_root_path(&mut self, initial_cwd: &PathBuf) { self.path = initial_cwd.clone(); self.path_is_dir = true; self.files.clear(); self.clear_selected(); } pub fn enter_dir(&mut self, entry: &FsEntry) { let is_dir = entry.is_folder(); let path = entry.get_full_pathbuf(); self.path = path; self.path_is_dir = is_dir; self.files.clear(); self.clear_selected(); } pub fn clear_selected(&mut self) { self.cursor_hist.remove(&self.path); } pub fn update_files( &mut self, paths: Vec<(PathBuf, Option)>, hide_hidden_files: bool, ) { let mut files = vec![]; for (entry, entry_metadata) in paths { let entry = Platform::normalize(&entry); let entry = self .path .join(entry.strip_prefix("/host").unwrap_or(&entry)); if entry_metadata.map(|e| e.is_symlink).unwrap_or(false) { continue; } let entry = if entry_metadata.map(|e| e.is_dir).unwrap_or(false) { FsEntry::Dir(entry) } else { let size = entry_metadata.map(|e| e.len).unwrap_or(0); FsEntry::File(entry, size) }; if !entry.is_hidden_file() || !hide_hidden_files { files.push(entry); } } self.files = files; self.files.sort_unstable(); } pub fn get_selected_entry(&self) -> Option { self.selected().and_then(|f| self.files.get(f).cloned()) } pub fn selected_mut(&mut self) -> &mut usize { self.cursor_hist.entry(self.path.clone()).or_default() } pub fn selected(&self) -> Option { self.cursor_hist.get(&self.path).copied() } pub fn move_selection_up(&mut self) { if let Some(selected) = self.selected() { *self.selected_mut() = selected.saturating_sub(1); } } pub fn move_selection_down(&mut self) { if let Some(selected) = self.selected() { let next = selected.saturating_add(1); *self.selected_mut() = std::cmp::min(self.files.len().saturating_sub(1), next); } else { *self.selected_mut() = 0; } } pub fn render(&mut self, rows: usize, cols: usize) { let (start_index, selected_index_in_range, end_index) = calculate_list_bounds(self.files.len(), rows.saturating_sub(1), self.selected()); render_list_tip(3, cols); for i in start_index..end_index { if let Some(entry) = self.files.get(i) { let is_selected = Some(i) == selected_index_in_range; let mut file_or_folder_name = entry.name(); let size = entry .size() .map(|s| pretty_bytes(s as f64)) .unwrap_or("".to_owned()); if entry.is_folder() { file_or_folder_name.push(self.platform.separator()); } let file_or_folder_name_width = file_or_folder_name.width(); let size_width = size.width(); let text = if file_or_folder_name_width + size_width < cols { let padding = " ".repeat( cols.saturating_sub(file_or_folder_name_width) .saturating_sub(size_width), ); format!("{}{}{}", file_or_folder_name, padding, size) } else { let padding = " ".repeat(cols.saturating_sub(file_or_folder_name_width)); format!("{}{}", file_or_folder_name, padding) }; let mut text_element = if is_selected { Text::new(text).selected() } else { Text::new(text) }; if entry.is_folder() { text_element = text_element.color_range(0, ..); } print_text_with_coordinates( text_element, 0, 4 + i.saturating_sub(start_index), Some(cols), None, ); } } } } #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] pub enum FsEntry { Dir(PathBuf), File(PathBuf, u64), } impl FsEntry { pub fn name(&self) -> String { let path = match self { FsEntry::Dir(p) => p, FsEntry::File(p, _) => p, }; path.file_name().unwrap().to_string_lossy().into_owned() } pub fn size(&self) -> Option { match self { FsEntry::Dir(_p) => None, FsEntry::File(_, size) => Some(*size), } } pub fn get_full_pathbuf(&self) -> PathBuf { match self { FsEntry::Dir(p) => p.clone(), FsEntry::File(p, _) => p.clone(), } } pub fn is_hidden_file(&self) -> bool { self.name().starts_with('.') } pub fn is_folder(&self) -> bool { matches!(self, FsEntry::Dir(_)) } }