use std::path::{Path, PathBuf};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::StatefulWidget,
};
use crate::popups::choose_popup::{ChoosePopup, ChoosePopupState};
use crate::popup::PopupSize;
use crate::{BorderType, DEFAULT_HIGHLIGHT, POPUP_WIDTH};
#[derive(Clone)]
pub struct FileEntry {
pub name: String,
pub is_dir: bool,
pub path: PathBuf,
}
const DEFAULT_DIR_ICON: &str = "📁 ";
const DEFAULT_FILE_ICON: &str = "📄 ";
impl Default for FileBrowserState {
fn default() -> Self {
Self {
entries: Vec::new(),
items: Vec::new(),
cwd: PathBuf::new(),
choose_popup_state: ChoosePopupState::default(),
show_hidden: false,
}
}
}
#[derive(Clone)]
pub struct FileBrowserState {
pub entries: Vec<FileEntry>,
pub items: Vec<(String, Style)>,
pub cwd: PathBuf,
pub choose_popup_state: ChoosePopupState,
pub show_hidden: bool,
}
impl FileBrowserState {
pub fn navigate_to(&mut self, path: &Path) {
if let Ok(read_dir) = std::fs::read_dir(path) {
let mut entries: Vec<FileEntry> = read_dir
.filter_map(|e| e.ok())
.filter(|e| {
if self.show_hidden {
true
} else {
let name = e.file_name();
let name = name.to_string_lossy();
!name.starts_with('.')
}
})
.map(|e| {
let path = e.path();
let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
FileEntry {
name: e.file_name().to_string_lossy().to_string(),
is_dir,
path,
}
})
.collect();
entries.sort_by(|a, b| {
if a.is_dir != b.is_dir {
b.is_dir.cmp(&a.is_dir)
} else {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
}
});
if let Some(parent) = path.parent() {
entries.insert(
0,
FileEntry {
name: "..".to_string(),
is_dir: true,
path: parent.to_path_buf(),
},
);
}
self.items = entries
.iter()
.map(|e| {
let text = if e.is_dir {
if e.name == ".." {
"↑ ..".to_string()
} else {
format!("{}{}", DEFAULT_DIR_ICON, e.name)
}
} else {
format!("{}{}", DEFAULT_FILE_ICON, e.name)
};
let style = if e.is_dir {
Style::new().fg(Color::Cyan)
} else {
Style::new().fg(Color::White)
};
(text, style)
})
.collect();
self.entries = entries;
self.cwd = path.to_path_buf();
self.choose_popup_state = ChoosePopupState::default();
self.choose_popup_state.scroll_list_state.follow = false;
self.choose_popup_state.scroll_list_state.select(Some(0));
}
}
pub fn go_up(&mut self) {
let parent = self.cwd.parent().map(|p| p.to_path_buf());
if let Some(p) = parent {
self.navigate_to(&p);
}
}
pub fn enter_directory(&mut self) {
let target = self.selected_entry().map(|e| (e.is_dir, e.path.clone()));
if let Some((true, path)) = target {
self.navigate_to(&path);
}
}
pub fn selected_entry(&self) -> Option<&FileEntry> {
if self.choose_popup_state.show_filter && !self.choose_popup_state.text_input.content.is_empty() {
self.filtered_indices().get(self.choose_popup_state.cursor).and_then(|&i| self.entries.get(i))
} else {
self.entries.get(self.choose_popup_state.cursor)
}
}
pub fn current_path(&self) -> Option<PathBuf> {
self.selected_entry().map(|e| e.path.clone())
}
pub fn select(&mut self, index: usize) {
let count = self.visible_count();
self.choose_popup_state.cursor = index.min(count.saturating_sub(1));
self.choose_popup_state.scroll_list_state.select(Some(self.choose_popup_state.cursor));
}
pub fn next(&mut self) {
let count = self.visible_count();
self.choose_popup_state.next(count);
}
pub fn previous(&mut self) {
self.choose_popup_state.previous();
}
pub fn first(&mut self) {
self.choose_popup_state.first();
}
pub fn last(&mut self) {
let count = self.visible_count();
self.choose_popup_state.last(count);
}
pub fn toggle(&mut self, idx: usize) {
self.choose_popup_state.toggle(idx);
}
pub fn toggle_cursor(&mut self) {
self.choose_popup_state.toggle_cursor();
}
pub fn chosen_indices(&self) -> &std::collections::HashSet<usize> {
&self.choose_popup_state.chosen_indices
}
pub fn chosen_paths(&self) -> Vec<PathBuf> {
self.choose_popup_state
.chosen_indices
.iter()
.filter_map(|&i| self.entries.get(i).map(|e| e.path.clone()))
.collect()
}
pub fn show_filter(&self) -> bool {
self.choose_popup_state.show_filter
}
pub fn set_show_filter(&mut self, show: bool) {
self.choose_popup_state.show_filter = show;
}
pub fn insert_filter_char(&mut self, c: char) {
self.choose_popup_state.insert_filter_char(c);
}
pub fn delete_before_filter(&mut self) {
self.choose_popup_state.delete_before_filter();
}
pub fn delete_at_filter(&mut self) {
self.choose_popup_state.delete_at_filter();
}
pub fn filter_cursor_left(&mut self) {
self.choose_popup_state.filter_cursor_left();
}
pub fn filter_cursor_right(&mut self) {
self.choose_popup_state.filter_cursor_right();
}
pub fn filter_cursor_home(&mut self) {
self.choose_popup_state.filter_cursor_home();
}
pub fn filter_cursor_end(&mut self) {
self.choose_popup_state.filter_cursor_end();
}
pub fn filtered_indices(&self) -> Vec<usize> {
self.choose_popup_state.filtered_indices(&self.items)
}
pub fn visible_count(&self) -> usize {
self.choose_popup_state.visible_count(&self.items)
}
pub fn original_index(&self) -> Option<usize> {
self.choose_popup_state.original_index(&self.items)
}
pub fn filter_content(&self) -> &str {
&self.choose_popup_state.text_input.content
}
pub fn max_selected(&self) -> Option<usize> {
self.choose_popup_state.max_selected
}
pub fn set_max_selected(&mut self, max: Option<usize>) {
self.choose_popup_state.max_selected = max;
}
pub fn show_hidden(&self) -> bool {
self.show_hidden
}
pub fn set_show_hidden(&mut self, show: bool) {
self.show_hidden = show;
}
}
#[derive(Clone)]
pub struct FileBrowserPopup {
dir_style: Style,
file_style: Style,
highlight_style: Style,
width: PopupSize,
dir_icon: String,
file_icon: String,
border_color: Option<Color>,
border_type: BorderType,
padding: u16,
header: bool,
max_selected: Option<usize>,
}
impl FileBrowserPopup {
pub fn new() -> Self {
Self {
dir_style: Style::new().fg(Color::Cyan),
file_style: Style::new().fg(Color::White),
highlight_style: DEFAULT_HIGHLIGHT,
width: POPUP_WIDTH,
dir_icon: "📁 ".to_string(),
file_icon: "📄 ".to_string(),
border_color: None,
border_type: BorderType::Rounded,
padding: 0,
header: false,
max_selected: None,
}
}
pub fn dir_style(mut self, style: Style) -> Self {
self.dir_style = style;
self
}
pub fn file_style(mut self, style: Style) -> Self {
self.file_style = style;
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn width(mut self, w: PopupSize) -> Self {
self.width = w;
self
}
pub fn dir_icon(mut self, icon: &str) -> Self {
self.dir_icon = icon.to_string();
self
}
pub fn file_icon(mut self, icon: &str) -> Self {
self.file_icon = icon.to_string();
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn border_type(mut self, bt: BorderType) -> Self {
self.border_type = bt;
self
}
pub fn padding(mut self, p: u16) -> Self {
self.padding = p;
self
}
pub fn header(mut self) -> Self {
self.header = true;
self
}
pub fn max_selected(mut self, max: usize) -> Self {
self.max_selected = Some(max);
self
}
pub fn no_max_selected(mut self) -> Self {
self.max_selected = None;
self
}
}
impl StatefulWidget for FileBrowserPopup {
type State = FileBrowserState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let path_str = state.cwd.to_string_lossy().to_string();
let max_visible = area.height.saturating_sub(7).min(20).max(3) as usize;
let visible = state.entries.len().min(max_visible);
let total_height = visible as u16 + 5;
let items: Vec<(String, Style)> = state
.items
.iter()
.enumerate()
.map(|(i, (text, _))| {
let is_dir = state.entries.get(i).map(|e| e.is_dir).unwrap_or(false);
let style = if is_dir { self.dir_style } else { self.file_style };
(text.clone(), style)
})
.collect();
let mut choose_popup = ChoosePopup::new(items)
.title(&path_str)
.width(self.width)
.border_type(self.border_type)
.padding(self.padding)
.height(PopupSize::Fixed(total_height))
.highlight_style(self.highlight_style);
if let Some(color) = self.border_color {
choose_popup = choose_popup.border_color(color);
}
if self.header {
choose_popup = choose_popup.header();
}
if let Some(max) = self.max_selected {
choose_popup = choose_popup.max_selected(max);
}
choose_popup.render(area, buf, &mut state.choose_popup_state);
}
}