feat(tui): add git status colors to file tree UI

- Map git badges and cleanliness states to specific `Color` values and modifiers.
- Apply these colors to file icons, filenames, and markers in the UI.
- Propagate the most relevant dirty badge from child nodes up to parent directories.
- Extend the help overlay with a “GIT COLORS” section describing the new color legend.
This commit is contained in:
2025-10-13 22:32:32 +02:00
parent 825dfc0722
commit ba9d083088
2 changed files with 60 additions and 13 deletions

View File

@@ -585,22 +585,31 @@ fn propagate_directory_git_state(nodes: &mut [FileNode]) {
continue;
}
let mut has_dirty = false;
let mut dirty_badge: Option<char> = None;
let mut has_staged = false;
for child in nodes[idx].children.clone() {
match nodes.get(child).map(|n| n.git.cleanliness) {
Some('●') => {
has_dirty = true;
break;
if let Some(child_node) = nodes.get(child) {
match child_node.git.cleanliness {
'●' => {
has_dirty = true;
let candidate = child_node.git.badge.unwrap_or('M');
dirty_badge = Some(match (dirty_badge, candidate) {
(Some('D'), _) | (_, 'D') => 'D',
(Some('U'), _) | (_, 'U') => 'U',
(Some(existing), _) => existing,
(None, new_badge) => new_badge,
});
}
'○' => {
has_staged = true;
}
_ => {}
}
Some('○') => {
has_staged = true;
}
_ => {}
}
}
nodes[idx].git = if has_dirty {
GitDecoration::dirty(None)
GitDecoration::dirty(dirty_badge)
} else if has_staged {
GitDecoration::staged(None)
} else {

View File

@@ -1,7 +1,7 @@
use pathdiff::diff_paths;
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
use serde_json;
@@ -689,11 +689,37 @@ fn render_file_tree(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
};
spans.push(Span::styled(toggle_symbol.to_string(), guide_style));
let mut git_color: Option<Color> = match node.git.badge {
Some('D') => Some(Color::LightRed),
Some('A') => Some(Color::LightGreen),
Some('R') | Some('C') => Some(Color::Yellow),
Some('U') => Some(Color::Magenta),
Some('M') => Some(Color::Yellow),
_ => None,
};
let mut git_modifiers = Modifier::empty();
if let Some('D') = node.git.badge {
git_modifiers |= Modifier::ITALIC;
}
if let Some('U') = node.git.badge {
git_modifiers |= Modifier::BOLD;
}
if git_color.is_none() {
git_color = match node.git.cleanliness {
'○' => Some(Color::LightYellow),
'●' => Some(Color::Yellow),
_ => None,
};
}
let mut icon_style = if node.is_dir {
Style::default().fg(theme.info)
} else {
Style::default().fg(theme.text)
};
if let Some(color) = git_color {
icon_style = icon_style.fg(color);
}
if !has_focus && !is_selected {
icon_style = icon_style.add_modifier(Modifier::DIM);
}
@@ -705,6 +731,9 @@ fn render_file_tree(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
let is_unsaved = !node.is_dir && unsaved_paths.contains(&node.path);
let mut name_style = Style::default().fg(theme.text);
if let Some(color) = git_color {
name_style = name_style.fg(color);
}
if node.is_dir {
name_style = name_style.add_modifier(Modifier::BOLD);
}
@@ -714,12 +743,16 @@ fn render_file_tree(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
if is_unsaved {
name_style = name_style.add_modifier(Modifier::ITALIC);
}
if !git_modifiers.is_empty() {
name_style = name_style.add_modifier(git_modifiers);
}
spans.push(Span::styled(node.name.clone(), name_style));
let mut marker_spans: Vec<Span<'static>> = Vec::new();
let marker_color = git_color.unwrap_or(theme.info);
if node.git.cleanliness != '✓' {
marker_spans.push(Span::styled("*", Style::default().fg(theme.info)));
marker_spans.push(Span::styled("*", Style::default().fg(marker_color)));
}
if is_unsaved {
marker_spans.push(Span::styled(
@@ -740,7 +773,7 @@ fn render_file_tree(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
if let Some(badge) = node.git.badge {
marker_spans.push(Span::styled(
badge.to_string(),
Style::default().fg(theme.info),
Style::default().fg(marker_color),
));
}
@@ -758,7 +791,7 @@ fn render_file_tree(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
if is_selected {
line_style = line_style.bg(theme.selection_bg).fg(theme.selection_fg);
} else if !has_focus {
line_style = line_style.fg(theme.text).add_modifier(Modifier::DIM);
line_style = line_style.add_modifier(Modifier::DIM);
}
items.push(ListItem::new(Line::from(spans)).style(line_style));
@@ -3071,6 +3104,11 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
Line::from(" :files, :explorer → toggle files panel"),
Line::from(" Ctrl+←/→ → resize files panel"),
Line::from(" Ctrl+↑/↓ → resize chat/thinking split"),
Line::from(vec![Span::styled(
"GIT COLORS",
Style::default().add_modifier(Modifier::BOLD).fg(theme.info),
)]),
Line::from(" Green → added · Yellow → modified/staged · Red → deleted/conflict"),
],
1 => vec![
// Editing