use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use super::icons;
use crate::app::App;
use crate::config::Config;
use crate::git::{DiffStat, GitInfo};
use crate::tree::Node;
pub fn render(app: &App, frame: &mut Frame, area: Rect) {
let height = usize::from(area.height);
let width = usize::from(area.width);
let lines: Vec<Line> = app
.tree
.rows()
.iter()
.enumerate()
.skip(app.scroll)
.take(height)
.map(|(index, row)| {
line(
app.tree.node(row.id),
Layout {
depth: row.depth,
open: app.tree.is_open(row.id),
selected: app.is_selected(index),
width,
},
&app.config,
&app.git,
)
})
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
struct Layout {
depth: usize,
open: bool,
selected: bool,
width: usize,
}
fn line(node: &Node, layout: Layout, config: &Config, git: &GitInfo) -> Line<'static> {
let (marker, glyph) = icons::prefix(node, layout.open, config.icons);
let mut left = vec![
Span::raw(" ".repeat(layout.depth * config.indent + 1)),
Span::raw(marker),
Span::raw(glyph),
Span::styled(node.name.clone(), name_style(node, config, git)),
];
if layout.open && !node.is_loaded() {
left.push(Span::styled(
" …",
Style::default().fg(config.colors.ui_muted_fg.0),
));
}
let right = decorations(node, layout.open, config, git);
let used: usize = left.iter().chain(right.iter()).map(Span::width).sum();
let mut spans = left;
spans.push(Span::raw(" ".repeat(layout.width.saturating_sub(used))));
spans.extend(right);
let line = Line::from(spans);
match layout.selected {
true => line.style(Style::default().bg(config.colors.ui_selection_bg.0)),
false => line,
}
}
fn name_style(node: &Node, config: &Config, git: &GitInfo) -> Style {
let colors = &config.colors;
let color = match git.file(&node.path) {
Some(status) if status.conflicted => colors.file_conflicted.0,
Some(status) if status.untracked => colors.file_untracked.0,
Some(status) if status.unstaged.is_some() => colors.file_modified.0,
Some(_) => colors.file_staged.0,
None if node.kind.is_dir() && git.directory(&node.path).is_some() => colors.dir_changed.0,
None if node.ignored == Some(true) => colors.file_ignored.0,
None if node.kind.is_dir() => colors.dir.0,
None => colors.file.0,
};
let style = Style::default().fg(color);
match node.symlink {
true => style.add_modifier(Modifier::ITALIC),
false => style,
}
}
fn decorations(node: &Node, open: bool, config: &Config, git: &GitInfo) -> Vec<Span<'static>> {
let status = git.file(&node.path);
let stat = match status {
Some(status) => status.stat(),
None if open => DiffStat::default(),
None => git.directory(&node.path).unwrap_or_default(),
};
let mut spans = Vec::new();
if config.show_diffstat && !stat.is_empty() {
spans.push(Span::styled(
format!(" +{}", stat.added),
Style::default().fg(config.colors.diff_added.0),
));
spans.push(Span::styled(
format!(" -{}", stat.removed),
Style::default().fg(config.colors.diff_removed.0),
));
}
let code = status.map_or_else(|| " ".to_owned(), |status| status.code());
spans.push(Span::styled(
format!(" {code} "),
Style::default().fg(code_color(node, config, git)),
));
spans
}
fn code_color(node: &Node, config: &Config, git: &GitInfo) -> Color {
match git.file(&node.path) {
Some(status) if status.conflicted => config.colors.file_conflicted.0,
Some(status) if status.untracked => config.colors.file_untracked.0,
Some(status) if status.unstaged.is_some() => config.colors.file_modified.0,
Some(_) => config.colors.file_staged.0,
None => config.colors.file.0,
}
}