use crate::app::App;
use crate::fs::discovery::FileEntry;
use crate::fs::git_status::GitFileStatus;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
};
use std::collections::{HashMap, 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>,
pub git_status: HashMap<PathBuf, GitFileStatus>,
}
#[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)
.style(Style::default().bg(p.background));
let items: Vec<ListItem> = app
.tree
.flat_items
.iter()
.map(|item| {
let indent = " ".repeat(item.depth);
let (prefix, prefix_color) = if item.is_dir {
let marker = if app.tree.expanded.contains(&item.path) {
"▼ "
} else {
"▶ "
};
(marker, p.accent)
} else {
(" ", p.foreground)
};
let name_color: Color = match app.tree.git_status.get(&item.path) {
Some(GitFileStatus::New) => p.git_new,
Some(GitFileStatus::Modified) => p.git_modified,
None => {
if item.is_dir {
p.accent
} else {
p.foreground
}
}
};
let prefix_style = Style::default()
.fg(prefix_color)
.add_modifier(if item.is_dir {
Modifier::BOLD
} else {
Modifier::empty()
});
let name_style = Style::default()
.fg(name_color)
.add_modifier(if item.is_dir {
Modifier::BOLD
} else {
Modifier::empty()
});
let line = Line::from(vec![
Span::styled(format!("{indent}{prefix}"), prefix_style),
Span::styled(item.name.clone(), name_style),
]);
ListItem::new(line)
})
.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);
}