use ratatui::{
Frame,
layout::Rect,
text::{Line, Span},
widgets::{Block, Borders, List, ListItem},
};
use std::path::Path;
use unicode_width::UnicodeWidthStr;
use crate::app::{App, FileTreeItem, FocusedPanel};
use crate::ui::diff_view::apply_horizontal_scroll;
use crate::ui::styles;
const EXPANDED_GLYPH: &str = "\u{25bc}"; const COLLAPSED_GLYPH: &str = "\u{25b6}"; const REVIEWED_BOX: &str = "\u{25a3}"; const UNREVIEWED_BOX: &str = "\u{25a2}";
pub(super) fn render_file_list(frame: &mut Frame, app: &mut App, area: Rect) {
let focused = app.focused_panel == FocusedPanel::FileList;
let title = format!(
" Files \u{00b7} {}/{} ",
app.reviewed_count(),
app.file_count()
);
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.style(styles::panel_style(&app.theme))
.border_style(styles::border_style(&app.theme, focused));
let inner = block.inner(area);
app.file_list_inner_area = Some(inner);
let visible_items = app.build_visible_items();
let max_content_width = visible_items
.iter()
.map(|item| match item {
FileTreeItem::Directory { path, depth, .. } => {
let dir_name = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path);
depth * 2 + 2 + dir_name.width() + 1
}
FileTreeItem::File { file_idx, depth } => {
let file = &app.diff_files[*file_idx];
let filename = file
.display_path()
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
depth * 2 + 4 + filename.width()
}
})
.max()
.unwrap_or(0);
app.file_list_state.viewport_width = inner.width as usize;
app.file_list_state.viewport_height = inner.height as usize;
app.file_list_state.max_content_width = max_content_width;
let max_scroll_x = max_content_width.saturating_sub(inner.width as usize);
if app.file_list_state.scroll_x > max_scroll_x {
app.file_list_state.scroll_x = max_scroll_x;
}
let scroll_x = app.file_list_state.scroll_x;
if app.focused_panel == FocusedPanel::Diff {
let current_file_idx = app.diff_state.current_file_idx;
for (tree_idx, item) in visible_items.iter().enumerate() {
if let FileTreeItem::File { file_idx, .. } = item
&& *file_idx == current_file_idx
{
if app.file_list_state.selected() != tree_idx {
let current_offset = app.file_list_state.list_state.offset();
app.file_list_state.select(tree_idx);
*app.file_list_state.list_state.offset_mut() = current_offset;
}
break;
}
}
}
let items: Vec<ListItem> = visible_items
.iter()
.map(|item| {
let line = match item {
FileTreeItem::Directory {
path,
depth,
expanded,
} => {
let indent = " ".repeat(*depth);
let icon = if *expanded {
EXPANDED_GLYPH
} else {
COLLAPSED_GLYPH
};
let dir_name = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path);
Line::from(vec![
Span::raw(indent),
Span::styled(format!("{icon} "), styles::dir_icon_style(&app.theme)),
Span::raw(format!("{dir_name}/")),
])
}
FileTreeItem::File { file_idx, depth } => {
let file = &app.diff_files[*file_idx];
let path = file.display_path();
let is_reviewed = app.session.is_file_reviewed(path);
let checkbox = if is_reviewed {
REVIEWED_BOX
} else {
UNREVIEWED_BOX
};
let checkbox_style = if is_reviewed {
styles::reviewed_style(&app.theme)
} else {
styles::pending_style(&app.theme)
};
if file.is_commit_message {
Line::from(vec![
Span::styled(format!("{checkbox} "), checkbox_style),
Span::raw(" Commit Message".to_string()),
])
} else {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let indent = " ".repeat(*depth);
let mut spans = vec![
Span::raw(indent),
Span::styled(format!("{checkbox} "), checkbox_style),
];
if !app.is_pristine_mode {
let status = file.status.as_char();
spans.push(Span::styled(
format!("{status} "),
styles::file_status_style(&app.theme, status),
));
}
spans.push(Span::raw(filename.to_string()));
Line::from(spans)
}
}
};
ListItem::new(apply_horizontal_scroll(line, scroll_x))
})
.collect();
let list = List::new(items)
.style(styles::panel_style(&app.theme))
.highlight_style(styles::selected_style(&app.theme))
.block(block);
frame.render_stateful_widget(list, area, &mut app.file_list_state.list_state);
}