use super::app::{SplitApp, SplitMode};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
Frame,
};
pub fn render(f: &mut Frame, app: &SplitApp) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(f.area());
render_commits(f, app, chunks[0]);
render_preview(f, app, chunks[1]);
match &app.mode {
SplitMode::Naming => render_naming_dialog(f, app),
SplitMode::Confirm => render_confirm_dialog(f, app),
SplitMode::Help => render_help_dialog(f),
SplitMode::Normal => {}
}
}
fn render_commits(f: &mut Frame, app: &SplitApp, area: Rect) {
let mut items: Vec<ListItem> = Vec::new();
for (i, commit) in app.commits.iter().enumerate() {
let is_selected = i == app.selected_index;
let has_split = app.split_points.iter().any(|sp| sp.after_commit_index == i);
let mut spans = vec![];
if is_selected {
spans.push(Span::styled("► ", Style::default().fg(Color::Yellow)));
} else {
spans.push(Span::raw(" "));
}
spans.push(Span::styled(
format!("{} ", commit.short_sha),
Style::default().fg(Color::Cyan),
));
let msg_style = if is_selected {
Style::default().add_modifier(Modifier::BOLD)
} else {
Style::default()
};
spans.push(Span::styled(&commit.message, msg_style));
let mut item = ListItem::new(Line::from(spans));
if is_selected {
item = item.style(Style::default().bg(Color::DarkGray));
}
items.push(item);
if has_split {
if let Some(sp) = app
.split_points
.iter()
.find(|sp| sp.after_commit_index == i)
{
let split_line = Line::from(vec![
Span::raw(" "),
Span::styled("──── ", Style::default().fg(Color::Green)),
Span::styled(
format!("split: {} ", sp.branch_name),
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::styled("────", Style::default().fg(Color::Green)),
]);
items.push(ListItem::new(split_line));
}
}
}
let title = format!(
" Commits on '{}' ({} total) ",
app.current_branch,
app.commits.len()
);
let help_text = if let Some(msg) = &app.status_message {
msg.clone()
} else {
"s: split | d: remove | Enter: apply | ?: help | q: quit".to_string()
};
let block = Block::default()
.title(title)
.title_bottom(Line::from(help_text).centered())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue));
let list = List::new(items).block(block);
f.render_widget(list, area);
}
fn render_preview(f: &mut Frame, app: &SplitApp, area: Rect) {
let preview = app.build_preview();
let mut items: Vec<ListItem> = Vec::new();
items.push(ListItem::new(Line::from(vec![Span::styled(
&app.parent_branch,
Style::default().fg(Color::DarkGray),
)])));
for (i, branch) in preview.iter().enumerate() {
let indent = " ".repeat(i + 1);
let connector = if i == preview.len() - 1 {
"└─"
} else {
"├─"
};
let is_current = branch.name == app.current_branch;
let name_style = if is_current {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Green)
};
let line = Line::from(vec![
Span::raw(format!("{}{} ", indent, connector)),
Span::styled(&branch.name, name_style),
Span::styled(
format!(
" ({} commit{})",
branch.commit_count,
if branch.commit_count == 1 { "" } else { "s" }
),
Style::default().fg(Color::DarkGray),
),
]);
items.push(ListItem::new(line));
}
if preview.is_empty() {
items.push(ListItem::new(Line::from(vec![Span::styled(
" No split points defined",
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
)])));
}
let block = Block::default()
.title(" Preview ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta));
let list = List::new(items).block(block);
f.render_widget(list, area);
}
fn render_naming_dialog(f: &mut Frame, app: &SplitApp) {
let area = centered_rect(50, 20, f.area());
f.render_widget(Clear, area);
let block = Block::default()
.title(" Enter branch name ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow));
let inner = block.inner(area);
f.render_widget(block, area);
let input_area = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(inner);
let input_text =
Paragraph::new(app.input_buffer.as_str()).style(Style::default().fg(Color::White));
f.render_widget(input_text, input_area[0]);
f.set_cursor_position((input_area[0].x + app.input_cursor as u16, input_area[0].y));
let hint = Paragraph::new("Enter to confirm, Esc to cancel")
.style(Style::default().fg(Color::DarkGray));
f.render_widget(hint, input_area[1]);
}
fn render_confirm_dialog(f: &mut Frame, app: &SplitApp) {
let preview = app.build_preview();
let height = (preview.len() + 6).min(15) as u16;
let area = centered_rect(60, height, f.area());
f.render_widget(Clear, area);
let block = Block::default()
.title(" Confirm Split ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow));
let inner = block.inner(area);
f.render_widget(block, area);
let mut lines = vec![
Line::from(Span::styled(
format!("Create {} new branch(es):", app.split_points.len()),
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
for sp in &app.split_points {
lines.push(Line::from(vec![
Span::raw(" • "),
Span::styled(&sp.branch_name, Style::default().fg(Color::Green)),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("Proceed? ", Style::default()),
Span::styled("(y/n)", Style::default().fg(Color::Yellow)),
]));
let text = Paragraph::new(lines);
f.render_widget(text, inner);
}
fn render_help_dialog(f: &mut Frame) {
let area = centered_rect(50, 60, f.area());
f.render_widget(Clear, area);
let block = Block::default()
.title(" Help ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let inner = block.inner(area);
f.render_widget(block, area);
let help_text = vec![
Line::from(Span::styled(
"Navigation",
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(" j/↓ Move down"),
Line::from(" k/↑ Move up"),
Line::from(""),
Line::from(Span::styled(
"Actions",
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(" s Mark split point at cursor"),
Line::from(" d Remove split point at cursor"),
Line::from(" S-J/K Move split point down/up"),
Line::from(" Enter Execute split"),
Line::from(""),
Line::from(Span::styled(
"Other",
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(" ? Toggle help"),
Line::from(" q/Esc Quit"),
Line::from(""),
Line::from(Span::styled(
"Press any key to close",
Style::default().fg(Color::DarkGray),
)),
];
let text = Paragraph::new(help_text);
f.render_widget(text, inner);
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}