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; continue;
} }
let mut has_dirty = false; let mut has_dirty = false;
let mut dirty_badge: Option<char> = None;
let mut has_staged = false; let mut has_staged = false;
for child in nodes[idx].children.clone() { for child in nodes[idx].children.clone() {
match nodes.get(child).map(|n| n.git.cleanliness) { if let Some(child_node) = nodes.get(child) {
Some('●') => { match child_node.git.cleanliness {
has_dirty = true; '●' => {
break; 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 { nodes[idx].git = if has_dirty {
GitDecoration::dirty(None) GitDecoration::dirty(dirty_badge)
} else if has_staged { } else if has_staged {
GitDecoration::staged(None) GitDecoration::staged(None)
} else { } else {

View File

@@ -1,7 +1,7 @@
use pathdiff::diff_paths; use pathdiff::diff_paths;
use ratatui::Frame; use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 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::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
use serde_json; 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)); 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 { let mut icon_style = if node.is_dir {
Style::default().fg(theme.info) Style::default().fg(theme.info)
} else { } else {
Style::default().fg(theme.text) Style::default().fg(theme.text)
}; };
if let Some(color) = git_color {
icon_style = icon_style.fg(color);
}
if !has_focus && !is_selected { if !has_focus && !is_selected {
icon_style = icon_style.add_modifier(Modifier::DIM); 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 is_unsaved = !node.is_dir && unsaved_paths.contains(&node.path);
let mut name_style = Style::default().fg(theme.text); 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 { if node.is_dir {
name_style = name_style.add_modifier(Modifier::BOLD); 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 { if is_unsaved {
name_style = name_style.add_modifier(Modifier::ITALIC); 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)); spans.push(Span::styled(node.name.clone(), name_style));
let mut marker_spans: Vec<Span<'static>> = Vec::new(); let mut marker_spans: Vec<Span<'static>> = Vec::new();
let marker_color = git_color.unwrap_or(theme.info);
if node.git.cleanliness != '✓' { 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 { if is_unsaved {
marker_spans.push(Span::styled( 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 { if let Some(badge) = node.git.badge {
marker_spans.push(Span::styled( marker_spans.push(Span::styled(
badge.to_string(), 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 { if is_selected {
line_style = line_style.bg(theme.selection_bg).fg(theme.selection_fg); line_style = line_style.bg(theme.selection_bg).fg(theme.selection_fg);
} else if !has_focus { } 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)); 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(" :files, :explorer → toggle files panel"),
Line::from(" Ctrl+←/→ → resize files panel"), Line::from(" Ctrl+←/→ → resize files panel"),
Line::from(" Ctrl+↑/↓ → resize chat/thinking split"), 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![ 1 => vec![
// Editing // Editing