use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType};
use crate::core::command::{CM_OK, CM_CANCEL, CM_FILE_FOCUSED, CommandId};
use crate::terminal::Terminal;
use super::dialog::Dialog;
use super::input_line::InputLine;
use super::listbox::ListBox;
use super::button::Button;
use super::label::Label;
use super::View;
use std::path::PathBuf;
use std::fs;
use std::rc::Rc;
use std::cell::RefCell;
const CMD_FILE_SELECTED: u16 = 1000;
const CHILD_LISTBOX: usize = 4; const CHILD_OK_BUTTON: usize = 5;
pub struct FileDialog {
dialog: Dialog,
current_path: PathBuf,
wildcard: String,
file_name_data: Rc<RefCell<String>>,
files: Vec<String>,
selected_file_index: usize, }
impl FileDialog {
pub fn new(bounds: Rect, title: &str, wildcard: &str, initial_dir: Option<PathBuf>) -> Self {
let dialog = Dialog::new(bounds, title);
let current_path = initial_dir.unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
});
let file_name_data = Rc::new(RefCell::new(String::new()));
Self {
dialog,
current_path,
wildcard: wildcard.to_string(),
file_name_data,
files: Vec::new(),
selected_file_index: 0,
}
}
pub fn build(mut self) -> Self {
let bounds = self.dialog.bounds();
let dialog_width = bounds.width();
let name_label = Label::new(Rect::new(2, 1, 12, 2), "~N~ame:");
self.dialog.add(Box::new(name_label));
let file_input = InputLine::new(
Rect::new(12, 1, dialog_width - 4, 2),
255,
self.file_name_data.clone()
);
self.dialog.add(Box::new(file_input));
let path_str = format!(" {}", self.current_path.display());
let path_label = Label::new(Rect::new(2, 3, dialog_width - 4, 4), &path_str);
self.dialog.add(Box::new(path_label));
let files_label = Label::new(Rect::new(2, 5, 12, 6), "~F~iles:");
self.dialog.add(Box::new(files_label));
let mut file_list = ListBox::new(
Rect::new(2, 6, dialog_width - 4, bounds.height() - 6),
CMD_FILE_SELECTED,
);
self.read_directory();
file_list.set_items(self.files.clone());
self.dialog.add(Box::new(file_list));
let button_y = bounds.height() - 4;
let button_spacing = 14;
let mut button_x = 2;
let open_button = Button::new(
Rect::new(button_x, button_y, button_x + 12, button_y + 2),
" ~O~pen ",
CM_OK,
true,
);
self.dialog.add(Box::new(open_button));
button_x += button_spacing;
let cancel_button = Button::new(
Rect::new(button_x, button_y, button_x + 12, button_y + 2),
" ~C~ancel ",
CM_CANCEL,
false,
);
self.dialog.add(Box::new(cancel_button));
self.dialog.set_initial_focus();
self
}
pub fn execute(&mut self, app: &mut crate::app::Application) -> Option<PathBuf> {
loop {
self.update_ok_button_state();
app.desktop.draw(&mut app.terminal);
self.dialog.draw(&mut app.terminal);
self.dialog.update_cursor(&mut app.terminal);
let _ = app.terminal.flush();
if let Some(mut event) = app.terminal.poll_event(std::time::Duration::from_millis(50)).ok().flatten() {
if event.what == EventType::Keyboard && event.key_code == crate::core::event::KB_ESC_ESC {
return None;
}
self.dialog.handle_event(&mut event);
self.sync_inputline_with_listbox();
if event.what == EventType::Command {
match event.command {
CM_OK => {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
if self.contains_wildcards(&file_name) {
self.wildcard = file_name.clone();
self.read_directory();
self.rebuild_and_redraw(&mut app.terminal);
continue;
}
if let Some(path) = self.handle_selection(&file_name, &mut app.terminal) {
return Some(path);
}
}
}
CM_CANCEL | crate::core::command::CM_CLOSE => {
return None;
}
CMD_FILE_SELECTED => {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
if let Some(path) = self.handle_selection(&file_name, &mut app.terminal) {
return Some(path);
}
}
}
_ => {}
}
}
}
}
}
fn sync_inputline_with_listbox(&mut self) {
if CHILD_LISTBOX >= self.dialog.child_count() {
return;
}
let listbox = self.dialog.child_at(CHILD_LISTBOX);
let new_selection = listbox.get_list_selection();
if new_selection != self.selected_file_index {
self.selected_file_index = new_selection;
if self.selected_file_index < self.files.len() {
let selected = self.files[self.selected_file_index].clone();
let display_text = if selected.starts_with('[') && selected.ends_with(']') {
let dir_name = &selected[1..selected.len() - 1];
format!("{}/{}", dir_name, self.wildcard)
} else if selected == ".." {
selected.clone()
} else {
selected.clone()
};
*self.file_name_data.borrow_mut() = display_text;
let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
self.dialog.handle_event(&mut broadcast);
}
}
}
fn handle_selection(&mut self, file_name: &str, terminal: &mut Terminal) -> Option<PathBuf> {
if let Some(slash_pos) = file_name.rfind('/') {
let dir_part = &file_name[..slash_pos];
let file_part = &file_name[slash_pos + 1..];
if !dir_part.is_empty() {
self.current_path.push(dir_part);
}
if self.contains_wildcards(file_part) {
self.wildcard = file_part.to_string();
}
self.rebuild_and_redraw(terminal);
return None;
}
if file_name == ".." {
if let Some(parent) = self.current_path.parent() {
self.current_path = parent.to_path_buf();
self.rebuild_and_redraw(terminal);
}
None
} else if file_name.starts_with('[') && file_name.ends_with(']') {
let dir_name = &file_name[1..file_name.len() - 1];
self.current_path.push(dir_name);
self.rebuild_and_redraw(terminal);
None
} else {
*self.file_name_data.borrow_mut() = file_name.to_string();
Some(self.current_path.join(file_name))
}
}
fn update_ok_button_state(&mut self) {
use crate::core::state::SF_DISABLED;
let is_empty = self.file_name_data.borrow().is_empty();
if CHILD_OK_BUTTON < self.dialog.child_count() {
let ok_button = self.dialog.child_at_mut(CHILD_OK_BUTTON);
ok_button.set_state_flag(SF_DISABLED, is_empty);
}
}
fn rebuild_and_redraw(&mut self, _terminal: &mut Terminal) {
let old_bounds = self.dialog.bounds();
let old_title = "Open File";
*self = Self::new(old_bounds, old_title, &self.wildcard.clone(), Some(self.current_path.clone())).build();
if CHILD_LISTBOX < self.dialog.child_count() {
self.dialog.set_focus_to_child(CHILD_LISTBOX);
self.dialog.child_at_mut(CHILD_LISTBOX).set_list_selection(0);
}
self.selected_file_index = 0;
if !self.files.is_empty() {
let first_item = self.files[0].clone();
let display_text = if first_item.starts_with('[') && first_item.ends_with(']') {
let dir_name = &first_item[1..first_item.len() - 1];
format!("{}/{}", dir_name, self.wildcard)
} else if first_item == ".." {
first_item.clone()
} else {
first_item.clone()
};
*self.file_name_data.borrow_mut() = display_text;
let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
self.dialog.handle_event(&mut broadcast);
} else {
*self.file_name_data.borrow_mut() = String::new();
}
}
fn read_directory(&mut self) {
self.files.clear();
if self.current_path.parent().is_some() {
self.files.push("..".to_string());
}
if let Ok(entries) = fs::read_dir(&self.current_path) {
let mut dirs = Vec::new();
let mut regular_files = Vec::new();
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
let name = entry.file_name().to_string_lossy().to_string();
if metadata.is_dir() {
dirs.push(format!("[{}]", name));
} else if self.matches_wildcard(&name) {
regular_files.push(name);
}
}
}
dirs.sort();
regular_files.sort();
self.files.extend(dirs);
self.files.extend(regular_files);
}
}
fn contains_wildcards(&self, name: &str) -> bool {
name.contains('*') || name.contains('?')
}
fn matches_wildcard(&self, name: &str) -> bool {
if self.wildcard == "*" || self.wildcard.is_empty() {
return true;
}
if let Some(ext) = self.wildcard.strip_prefix("*.") {
name.ends_with(&format!(".{}", ext))
} else {
name.contains(&self.wildcard)
}
}
pub fn get_selected_file(&self) -> Option<PathBuf> {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
Some(self.current_path.join(file_name))
} else {
None
}
}
pub fn get_current_directory(&self) -> PathBuf {
self.current_path.clone()
}
pub fn get_end_state(&self) -> CommandId {
self.dialog.get_end_state()
}
}
impl View for FileDialog {
fn bounds(&self) -> Rect {
self.dialog.bounds()
}
fn set_bounds(&mut self, bounds: Rect) {
self.dialog.set_bounds(bounds);
}
fn draw(&mut self, terminal: &mut Terminal) {
self.dialog.draw(terminal);
}
fn handle_event(&mut self, event: &mut Event) {
self.dialog.handle_event(event);
}
}