use crate::core::app::AppContext;
use crate::git::state::GitState;
use crate::github::state::GitHubState;
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
fn page_tab_spans(ctx: &AppContext) -> Vec<Span<'static>> {
let active_style = Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD);
let inactive_style = Style::default().fg(Color::DarkGray);
let mut spans = vec![Span::raw(" ")];
for (i, label) in ctx.page_labels.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
let style = if i == ctx.active_page {
active_style
} else {
inactive_style
};
spans.push(Span::styled(format!(" {}:{} ", i + 1, label), style));
}
spans
}
fn render_header_common(
f: &mut Frame,
ctx: &AppContext,
context_spans: Vec<Span<'static>>,
area: Rect,
) {
let mut spans = vec![
Span::styled(
" vig ",
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
];
spans.extend(context_spans);
spans.extend(page_tab_spans(ctx));
spans.push(Span::raw(" "));
spans.push(Span::styled("? help", Style::default().fg(Color::DarkGray)));
f.render_widget(Paragraph::new(Line::from(spans)), area);
}
pub fn render_header(f: &mut Frame, ctx: &AppContext, git: &GitState, area: Rect) {
let base_label = match &git.diff_base_ref {
Some(base) => format!(" vs {base} "),
None => " vs HEAD ".to_string(),
};
render_header_common(
f,
ctx,
vec![
Span::styled(
format!(" {} ", git.diff_meta.branch_name),
Style::default().fg(Color::Black).bg(Color::Magenta),
),
Span::raw(" "),
Span::styled(
base_label,
Style::default().fg(Color::Black).bg(Color::Yellow),
),
],
area,
);
}
pub fn render_gh_header(f: &mut Frame, ctx: &AppContext, area: Rect) {
render_header_common(
f,
ctx,
vec![Span::styled(
" GitHub ",
Style::default().fg(Color::Black).bg(Color::Rgb(36, 41, 47)),
)],
area,
);
}
fn render_search_prompt(f: &mut Frame, input: &str, area: Rect) {
let prompt = format!("/{}\u{2588}", input);
let line = Line::from(Span::styled(
format!(" {prompt}"),
Style::default().fg(Color::White),
));
f.render_widget(Paragraph::new(line), area);
}
pub fn render_status_bar(f: &mut Frame, ctx: &AppContext, git: &GitState, area: Rect) {
if git.pane.search.active {
render_search_prompt(f, &git.pane.search.input, area);
return;
}
let file_count = git.diff_meta.file_count;
let adds = git.diff_meta.stats.additions;
let dels = git.diff_meta.stats.deletions;
let status = if let Some(ref msg) = ctx.status_message {
Line::from(Span::styled(
format!(" {msg}"),
Style::default().fg(Color::Yellow),
))
} else if file_count == 0 {
Line::from(Span::styled(
" Working tree clean",
Style::default().fg(Color::Green),
))
} else {
Line::from(vec![
Span::styled(
format!(
" {file_count} file{}",
if file_count == 1 { "" } else { "s" }
),
Style::default().fg(Color::White),
),
Span::raw(" "),
Span::styled(format!("+{adds}"), Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled(format!("-{dels}"), Style::default().fg(Color::Red)),
])
};
f.render_widget(Paragraph::new(status), area);
}
pub fn render_gh_status_bar(f: &mut Frame, ctx: &AppContext, gh: &GitHubState, area: Rect) {
if gh.pane.search.active {
render_search_prompt(f, &gh.pane.search.input, area);
return;
}
if let Some(ref err) = gh.gh_error {
let line = Line::from(Span::styled(
format!(" {err}"),
Style::default().fg(Color::Red),
));
f.render_widget(Paragraph::new(line), area);
return;
}
let issue_count = gh.panes.issue_tab.list.item_count();
let pr_count = gh.panes.pr_tab.list.item_count();
let loading = gh.panes.issue_tab.list.is_loading() || gh.panes.pr_tab.list.is_loading();
let has_data = issue_count > 0 || pr_count > 0;
let mut spans = Vec::new();
if loading && !has_data {
spans.push(Span::styled(
" Loading...",
Style::default().fg(Color::DarkGray),
));
} else {
spans.push(Span::styled(
format!(
" {} issue{}",
issue_count,
if issue_count == 1 { "" } else { "s" }
),
Style::default().fg(Color::White),
));
spans.push(Span::raw(" "));
spans.push(Span::styled(
format!("{} PR{}", pr_count, if pr_count == 1 { "" } else { "s" }),
Style::default().fg(Color::White),
));
if loading {
spans.push(Span::raw(" "));
spans.push(Span::styled(
"↻ Updating...",
Style::default().fg(Color::DarkGray),
));
}
}
if let Some(ws) = gh.panes.pr_tab.detail.watch_status() {
spans.push(Span::raw(" "));
if let Some(ref err) = ws.error {
spans.push(Span::styled(
format!("\u{23f1} Watch (err: {err})"),
Style::default().fg(Color::Red),
));
} else {
spans.push(Span::styled(
format!("\u{23f1} Watch (last: {})", ws.last_update_time),
Style::default().fg(Color::Yellow),
));
}
}
if let Some(ref msg) = ctx.status_message {
spans.push(Span::raw(" "));
spans.push(Span::styled(
msg.clone(),
Style::default().fg(Color::Yellow),
));
}
let line = Line::from(spans);
f.render_widget(Paragraph::new(line), area);
}
pub fn render_help_overlay(f: &mut Frame, area: Rect, keybindings: &[(String, String)]) {
use ratatui::widgets::{Block, Borders, Clear};
let key_w = keybindings
.iter()
.map(|(k, _)| k.chars().count())
.max()
.unwrap_or(0)
.max(12);
let desc_w = keybindings
.iter()
.map(|(_, d)| d.chars().count())
.max()
.unwrap_or(0);
let help_width = ((key_w + desc_w + 6) as u16)
.max(50)
.min(area.width.saturating_sub(4));
let help_height = ((keybindings.len() as u16) + 2).min(area.height.saturating_sub(4));
let x = (area.width.saturating_sub(help_width)) / 2;
let y = (area.height.saturating_sub(help_height)) / 2;
let help_area = Rect::new(x, y, help_width, help_height);
f.render_widget(Clear, help_area);
let lines: Vec<Line> = keybindings
.iter()
.map(|(key, desc)| {
Line::from(vec![
Span::styled(
format!(" {key:<key_w$} "),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(desc.as_str()),
])
})
.collect();
let block = Block::default()
.title(" Keybindings ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let para = Paragraph::new(lines).block(block);
f.render_widget(para, help_area);
}