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;
|
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;
|
has_dirty = true;
|
||||||
break;
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Some('○') => {
|
'○' => {
|
||||||
has_staged = true;
|
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user