use std::path::{Path, PathBuf};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::StatefulWidget,
};
use crate::badge::BadgeStack;
use crate::popup::{Popup, PopupSize};
use crate::popups::auto_sized::AutoSized;
use crate::popups::choose_popup::ChoosePopupState;
use crate::popups::file_browser::FileBrowser;
use crate::{BorderType, DotGridConfig, DotPattern};
#[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 {
file_browser: FileBrowser,
width: PopupSize,
height: PopupSize,
border_color: Option<Color>,
border_type: BorderType,
padding: u16,
header: bool,
position: Option<(u16, u16)>,
origin: Option<(u16, u16)>,
bg_color: Option<Color>,
no_dot_grid: bool,
dot_grid: Option<DotGridConfig>,
badges: Option<BadgeStack<'static>>,
}
impl FileBrowserPopup {
pub fn new() -> Self {
Self {
file_browser: FileBrowser::new(),
width: PopupSize::Auto,
height: PopupSize::Auto,
border_color: None,
border_type: BorderType::Rounded,
padding: 0,
header: false,
position: None,
origin: None,
bg_color: None,
no_dot_grid: false,
dot_grid: None,
badges: None,
}
}
pub fn dir_style(mut self, style: Style) -> Self {
self.file_browser = self.file_browser.dir_style(style);
self
}
pub fn file_style(mut self, style: Style) -> Self {
self.file_browser = self.file_browser.file_style(style);
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.file_browser = self.file_browser.highlight_style(style);
self
}
pub fn dir_icon(mut self, icon: &str) -> Self {
self.file_browser = self.file_browser.dir_icon(icon);
self
}
pub fn file_icon(mut self, icon: &str) -> Self {
self.file_browser = self.file_browser.file_icon(icon);
self
}
pub fn max_selected(mut self, max: usize) -> Self {
self.file_browser = self.file_browser.max_selected(max);
self
}
pub fn no_max_selected(mut self) -> Self {
self.file_browser = self.file_browser.no_max_selected();
self
}
pub fn width(mut self, w: PopupSize) -> Self {
self.width = w;
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 bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
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 height(mut self, h: PopupSize) -> Self {
self.height = h;
self
}
pub fn position(mut self, x: u16, y: u16) -> Self {
self.position = Some((x, y));
self
}
pub fn origin(mut self, x: u16, y: u16) -> Self {
self.origin = Some((x, y));
self
}
pub fn no_background(mut self) -> Self {
self.no_dot_grid = true;
self
}
pub fn background_dots(mut self, color: Color, symbol: &str, density: u16) -> Self {
self.dot_grid = Some(DotGridConfig { color, symbol: symbol.to_string(), density_x: density, density_y: density, pattern: DotPattern::default() });
self
}
pub fn background_pattern(mut self, pattern: DotPattern) -> Self {
if let Some(ref mut dg) = self.dot_grid {
dg.pattern = pattern;
} else {
self.no_dot_grid = false;
self.dot_grid = Some(DotGridConfig { pattern, ..DotGridConfig::default() });
}
self
}
pub fn background_spacing(mut self, density_x: u16, density_y: u16) -> Self {
if let Some(ref mut dg) = self.dot_grid {
dg.density_x = density_x;
dg.density_y = density_y;
} else {
self.no_dot_grid = false;
self.dot_grid = Some(DotGridConfig {
density_x,
density_y,
..DotGridConfig::default()
});
}
self
}
pub fn badges(mut self, badges: BadgeStack<'static>) -> Self {
self.badges = Some(badges);
self
}
}
impl Default for FileBrowserPopup {
fn default() -> Self {
Self::new()
}
}
impl AutoSized for FileBrowserPopup {
type State = FileBrowserState;
fn auto_height(&self, state: &Self::State, area: Rect) -> u16 {
let max_visible = area.height.saturating_sub(7).min(20).max(3) as usize;
let visible = state.entries.len().min(max_visible);
visible as u16 + 5
}
}
impl FileBrowserPopup {
pub fn resolve_rect(&self, area: Rect, state: &FileBrowserState) -> Rect {
let border_color = self.border_color.unwrap_or(Color::White);
let mut popup = Popup::new(border_color)
.padding(self.padding)
.width(self.width)
.border_type(self.border_type);
if self.height == PopupSize::Auto {
popup = popup.height(PopupSize::Fixed(self.auto_height(state, area)));
} else {
popup = popup.height(self.height);
}
if let Some((x, y)) = self.position {
popup = popup.position(x, y);
}
if let Some((x, y)) = self.origin {
popup = popup.origin(x, y);
}
popup.resolve_rect(area)
}
}
impl StatefulWidget for FileBrowserPopup {
type State = FileBrowserState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let auto_h = if self.height == PopupSize::Auto {
Some(self.auto_height(state, area))
} else {
None
};
let path_str = state.cwd.to_string_lossy().to_string();
let border_color = self.border_color.unwrap_or(Color::White);
let mut popup = Popup::new(border_color)
.padding(self.padding)
.width(self.width)
.border_type(self.border_type);
if let Some(h) = auto_h {
popup = popup.height(PopupSize::Fixed(h));
}
if !path_str.is_empty() {
popup = popup.title(&path_str);
}
if let Some(bg) = self.bg_color {
popup = popup.bg_color(bg);
}
if self.header {
popup = popup.header();
}
if let Some((x, y)) = self.position {
popup = popup.position(x, y);
}
if let Some((x, y)) = self.origin {
popup = popup.origin(x, y);
}
if self.no_dot_grid {
popup = popup.no_background();
}
if let Some(ref dg) = self.dot_grid {
popup = popup
.background_dots(dg.color, &dg.symbol, dg.density_x)
.background_spacing(dg.density_x, dg.density_y);
}
if self.height != PopupSize::Auto {
popup = popup.height(self.height);
}
if let Some(ref badges) = self.badges {
badges.render_all(area, buf);
}
let inner = popup.render_inner(area, buf);
self.file_browser.render(inner, buf, state);
}
}
#[cfg(test)]
mod tests;