use crate::app::App;
use crate::fs::discovery::FileEntry;
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::Line,
widgets::{Block, Borders, List, ListItem, ListState},
};
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug, Default)]
pub struct FileTreeState {
pub entries: Vec<FileEntry>,
pub flat_items: Vec<FlatItem>,
pub list_state: ListState,
pub expanded: HashSet<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct FlatItem {
pub path: PathBuf,
pub name: String,
pub is_dir: bool,
pub depth: usize,
}
impl FileTreeState {
pub fn rebuild(&mut self, entries: Vec<FileEntry>) {
self.entries = entries;
self.flatten_visible();
if !self.flat_items.is_empty() && self.list_state.selected().is_none() {
self.list_state.select(Some(0));
}
}
pub fn flatten_visible(&mut self) {
self.flat_items.clear();
let entries = std::mem::take(&mut self.entries);
flatten_entries(&entries, &self.expanded, 0, &mut self.flat_items);
self.entries = entries;
}
pub fn selected_path(&self) -> Option<&std::path::Path> {
let idx = self.list_state.selected()?;
self.flat_items.get(idx).map(|item| item.path.as_path())
}
pub fn selected_item(&self) -> Option<&FlatItem> {
let idx = self.list_state.selected()?;
self.flat_items.get(idx)
}
pub fn move_up(&mut self) {
if self.flat_items.is_empty() {
return;
}
let i = match self.list_state.selected() {
Some(i) if i > 0 => i - 1,
_ => 0,
};
self.list_state.select(Some(i));
}
pub fn move_down(&mut self) {
if self.flat_items.is_empty() {
return;
}
let i = match self.list_state.selected() {
Some(i) if i < self.flat_items.len() - 1 => i + 1,
Some(i) => i,
None => 0,
};
self.list_state.select(Some(i));
}
pub fn toggle_expand(&mut self) {
if let Some(item) = self.selected_item().cloned()
&& item.is_dir
{
if self.expanded.contains(&item.path) {
self.expanded.remove(&item.path);
} else {
self.expanded.insert(item.path);
}
self.flatten_visible();
}
}
pub fn go_first(&mut self) {
if !self.flat_items.is_empty() {
self.list_state.select(Some(0));
}
}
pub fn go_last(&mut self) {
if !self.flat_items.is_empty() {
self.list_state.select(Some(self.flat_items.len() - 1));
}
}
}
fn flatten_entries(
entries: &[FileEntry],
expanded: &HashSet<PathBuf>,
depth: usize,
out: &mut Vec<FlatItem>,
) {
for entry in entries {
out.push(FlatItem {
path: entry.path.clone(),
name: entry.name.clone(),
is_dir: entry.is_dir,
depth,
});
if entry.is_dir && expanded.contains(&entry.path) {
flatten_entries(&entry.children, expanded, depth + 1, out);
}
}
}
pub fn draw(f: &mut Frame, app: &mut App, area: Rect, focused: bool) {
let p = &app.palette;
let border_style = if focused {
p.border_focused_style()
} else {
p.border_style()
};
let block = Block::default()
.title(" Files ")
.title_style(p.title_style())
.borders(Borders::ALL)
.border_style(border_style);
let items: Vec<ListItem> = app
.tree
.flat_items
.iter()
.map(|item| {
let indent = " ".repeat(item.depth);
let prefix = if item.is_dir {
if app.tree.expanded.contains(&item.path) {
"▼ "
} else {
"▶ "
}
} else {
" "
};
let style = if item.is_dir {
Style::default().fg(p.accent).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(p.foreground)
};
ListItem::new(Line::styled(
format!("{indent}{prefix}{}", item.name),
style,
))
})
.collect();
let list = List::new(items)
.block(block)
.highlight_style(p.selected_style())
.highlight_symbol("│ ");
f.render_stateful_widget(list, area, &mut app.tree.list_state);
}