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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user