use std::path::Path;
use std::collections::HashSet;
use std::path::PathBuf;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::ListItem;
use crate::vcs::{StatusEntry, FileChange, FileChangeStatus};
fn group_by_parent<E, F>(entries: &[E], get_path: F) -> Vec<(Option<String>, Vec<usize>)>
where
F: Fn(&E) -> &Path,
{
let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
let mut current_parent: Option<String> = None;
for (i, e) in entries.iter().enumerate() {
let parent = get_path(e)
.parent()
.filter(|p| *p != Path::new(""))
.map(|p| p.to_string_lossy().replace('\\', "/"));
if parent != current_parent {
groups.push((parent.clone(), vec![i]));
current_parent = parent;
} else if let Some(last) = groups.last_mut() {
last.1.push(i);
} else {
groups.push((parent.clone(), vec![i]));
}
}
groups
}
pub fn build_tree_list_items_with_map_from_status(
entries: &[StatusEntry],
cursor: usize,
active: bool,
partial_paths: &HashSet<&PathBuf>,
) -> (Vec<ListItem<'static>>, Vec<Option<usize>>) {
let groups = group_by_parent(entries, |e| &e.path);
let mut items: Vec<ListItem<'static>> = Vec::new();
let mut mapping: Vec<Option<usize>> = Vec::new();
for (parent_opt, indices) in groups {
if let Some(dir) = parent_opt.as_ref() {
items.push(ListItem::new(Span::styled(
format!("▾ {}/", dir),
Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
)));
mapping.push(None);
}
for &i in &indices {
let e = &entries[i];
let is_cursor = i == cursor && active;
let indent = if parent_opt.is_some() { " " } else { "" };
let name = e.path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let is_partial = partial_paths.contains(&e.path);
let indicator = if e.staged && is_partial {
" ● (partial)"
} else if e.staged {
" ●"
} else {
""
};
let label = format!("{}{}{}", indent, name, indicator);
let style = if is_cursor {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::REVERSED | Modifier::BOLD)
} else if e.staged {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
items.push(ListItem::new(Span::styled(label, style)));
mapping.push(Some(i));
}
}
if items.is_empty() {
let placeholder = if active { "(nothing staged)" } else { "(no changes)" };
items.push(ListItem::new(Span::styled(
placeholder,
Style::default().fg(Color::DarkGray),
)));
mapping.push(None);
}
(items, mapping)
}
pub fn build_tree_list_items_with_map_from_filechanges(
entries: &[FileChange],
cursor: usize,
active: bool,
) -> (Vec<ListItem<'static>>, Vec<Option<usize>>) {
let groups = group_by_parent(entries, |e| &e.path);
let mut items: Vec<ListItem<'static>> = Vec::new();
let mut mapping: Vec<Option<usize>> = Vec::new();
for (parent_opt, indices) in groups {
if let Some(dir) = parent_opt.as_ref() {
items.push(ListItem::new(Span::styled(
format!("▾ {}/", dir),
Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
)));
mapping.push(None);
}
for &i in &indices {
let e = &entries[i];
let is_cursor = i == cursor && active;
let indent = if parent_opt.is_some() { " " } else { "" };
let name = e.path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let indicator_color = match e.status {
FileChangeStatus::Added => Color::Green,
FileChangeStatus::Deleted => Color::Red,
FileChangeStatus::Modified => Color::Yellow,
FileChangeStatus::Renamed => Color::Cyan,
};
let mut spans = Vec::new();
spans.push(Span::styled(
format!(" {} ", e.status.indicator()),
Style::default().fg(indicator_color),
));
let file_style = if is_cursor {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
spans.push(Span::styled(format!("{}{}", indent, name), file_style));
items.push(ListItem::new(Line::from(spans)));
mapping.push(Some(i));
}
}
if items.is_empty() {
items.push(ListItem::new(Span::styled(
"(no files)",
Style::default().fg(Color::DarkGray),
)));
mapping.push(None);
}
(items, mapping)
}
#[allow(dead_code)]
pub fn build_tree_list_items_from_status(
entries: &[StatusEntry],
cursor: usize,
active: bool,
partial_paths: &HashSet<&PathBuf>,
) -> Vec<ListItem<'static>> {
build_tree_list_items_with_map_from_status(entries, cursor, active, partial_paths).0
}
pub fn build_tree_list_items_from_filechanges(
entries: &[FileChange],
cursor: usize,
active: bool,
) -> Vec<ListItem<'static>> {
build_tree_list_items_with_map_from_filechanges(entries, cursor, active).0
}
pub fn build_tree_list_items_with_map_from_paths_staged(
entries: &[(PathBuf, bool)],
cursor: usize,
active: bool,
sel_bg: Color,
sel_fg: Color,
accent: Color,
fg_dim: Color,
) -> (Vec<ListItem<'static>>, Vec<Option<usize>>) {
let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
let mut current_parent: Option<String> = None;
for (i, e) in entries.iter().enumerate() {
let parent = e.0
.parent()
.filter(|p| *p != Path::new(""))
.map(|p| p.to_string_lossy().replace('\\', "/"));
if parent != current_parent {
groups.push((parent.clone(), vec![i]));
current_parent = parent;
} else if let Some(last) = groups.last_mut() {
last.1.push(i);
} else {
groups.push((parent.clone(), vec![i]));
}
}
let mut items: Vec<ListItem<'static>> = Vec::new();
let mut mapping: Vec<Option<usize>> = Vec::new();
for (parent_opt, indices) in groups {
if let Some(dir) = parent_opt.as_ref() {
items.push(ListItem::new(Span::styled(
format!("▾ {}/", dir),
Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
)));
mapping.push(None);
}
for &i in &indices {
let (path, staged) = &entries[i];
let is_cursor = i == cursor && active;
let indent = if parent_opt.is_some() { " " } else { "" };
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let label = if *staged {
format!("{} ● {}", indent, name)
} else {
format!("{} {}", indent, name)
};
let style = if is_cursor {
if active {
Style::default().fg(sel_fg).bg(sel_bg).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(accent).add_modifier(Modifier::BOLD)
}
} else if *staged {
Style::default().fg(accent)
} else {
Style::default().fg(fg_dim)
};
items.push(ListItem::new(Span::styled(label, style)));
mapping.push(Some(i));
}
}
if items.is_empty() {
items.push(ListItem::new(Span::styled(
"(no changes)",
Style::default().fg(Color::DarkGray),
)));
mapping.push(None);
}
(items, mapping)
}