use std::path::PathBuf;
use crossterm::event::KeyCode;
pub enum FilePickerOutcome {
Continue,
Selected(PathBuf),
Cancelled,
}
pub struct FilePickerState {
pub dir: PathBuf,
pub entries: Vec<(String, bool, u64)>,
pub index: usize,
pub filter: String,
pub filtered: Vec<usize>,
pub error: Option<String>,
}
impl Default for FilePickerState {
fn default() -> Self {
Self {
dir: dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")),
entries: Vec::new(),
index: 0,
filter: String::new(),
filtered: Vec::new(),
error: None,
}
}
}
impl FilePickerState {
pub fn open(&mut self) {
self.dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
self.index = 0;
self.filter.clear();
self.error = None;
self.refresh_entries();
}
pub fn refresh_entries(&mut self) {
self.entries.clear();
self.error = None;
match std::fs::read_dir(&self.dir) {
Ok(read_entries) => {
let mut dirs: Vec<(String, bool, u64)> = Vec::new();
let mut files: Vec<(String, bool, u64)> = Vec::new();
for entry in read_entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let meta = entry.metadata();
let is_dir = meta.as_ref().map(|m| m.is_dir()).unwrap_or(false);
let size = meta.as_ref().map(|m| m.len()).unwrap_or(0);
if is_dir {
dirs.push((name, true, 0));
} else {
files.push((name, false, size));
}
}
dirs.sort_by_key(|a| a.0.to_lowercase());
files.sort_by_key(|a| a.0.to_lowercase());
self.entries.extend(dirs);
self.entries.extend(files);
}
Err(e) => {
self.error = Some(format!("Cannot read directory: {e}"));
}
}
self.refresh_filter();
}
pub fn refresh_filter(&mut self) {
let filter_lower = self.filter.to_lowercase();
self.filtered = self
.entries
.iter()
.enumerate()
.filter(|(_, (name, _, _))| {
filter_lower.is_empty() || name.to_lowercase().contains(&filter_lower)
})
.map(|(i, _)| i)
.collect();
if self.filtered.is_empty() {
self.index = 0;
} else if self.index >= self.filtered.len() {
self.index = self.filtered.len() - 1;
}
}
pub fn handle_key(&mut self, code: KeyCode) -> FilePickerOutcome {
match code {
KeyCode::Char('j') | KeyCode::Down
if !self.filtered.is_empty() && self.index < self.filtered.len() - 1 =>
{
self.index += 1;
}
KeyCode::Char('k') | KeyCode::Up => {
self.index = self.index.saturating_sub(1);
}
KeyCode::Enter => {
if let Some(&entry_idx) = self.filtered.get(self.index) {
let (name, is_dir, _) = self.entries[entry_idx].clone();
if is_dir {
self.dir = self.dir.join(&name);
self.index = 0;
self.filter.clear();
self.refresh_entries();
} else {
return FilePickerOutcome::Selected(self.dir.join(&name));
}
}
}
KeyCode::Backspace => {
if !self.filter.is_empty() {
self.filter.pop();
self.refresh_filter();
} else {
self.navigate_up();
}
}
KeyCode::Char('-') => {
self.navigate_up();
}
KeyCode::Esc => return FilePickerOutcome::Cancelled,
KeyCode::Char(c) => {
self.filter.push(c);
self.refresh_filter();
}
_ => {}
}
FilePickerOutcome::Continue
}
fn navigate_up(&mut self) {
if let Some(parent) = self.dir.parent() {
let parent = parent.to_path_buf();
if parent != self.dir {
self.dir = parent;
self.index = 0;
self.filter.clear();
self.refresh_entries();
}
}
}
}