use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use crate::app::App;
use crate::ui::styles;
pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
if app.remote().is_none() {
let theme = &app.theme;
let paragraph = Paragraph::new(vec![Line::from(Span::styled(
" No commits",
styles::dim_style(theme),
))])
.style(styles::panel_style(theme));
frame.render_widget(paragraph, area);
return;
}
let panel_style = styles::panel_style(&app.theme);
let dim_style = styles::dim_style(&app.theme);
let selected_style = styles::selected_style(&app.theme);
let fg_secondary = app.theme.fg_secondary;
let fg_dim = app.theme.fg_dim;
let r = app.remote_mut().expect("remote mode checked above");
let lines = if r.pr_commits.is_empty() {
vec![Line::from(Span::styled(" No commits", dim_style))]
} else {
if r.pr_commits_cursor >= r.pr_commits.len() {
r.pr_commits_cursor = r.pr_commits.len().saturating_sub(1);
}
let viewport_height = area.height.saturating_sub(2) as usize; let scroll_offset = if r.pr_commits_cursor >= viewport_height {
r.pr_commits_cursor - viewport_height + 1
} else {
0
};
r.pr_commits
.iter()
.enumerate()
.skip(scroll_offset)
.take(viewport_height.max(1))
.map(|(i, commit)| {
let is_selected = i == r.pr_commits_cursor;
let marker = if is_selected { "\u{25b6} " } else { " " };
let date = commit.time.format("%Y-%m-%d");
let style = if is_selected {
selected_style
} else {
Style::default()
};
Line::from(vec![
Span::styled(marker.to_string(), style),
Span::styled(
format!("{} ", commit.short_id),
Style::default()
.fg(fg_secondary)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{:<50} ", truncate(&commit.summary, 50)), style),
Span::styled(
format!("{:<12} ", truncate(&commit.author, 12)),
Style::default().fg(fg_dim),
),
Span::styled(date.to_string(), Style::default().fg(fg_dim)),
])
})
.collect()
};
let paragraph = Paragraph::new(lines).style(panel_style);
frame.render_widget(paragraph, area);
}
fn truncate(s: &str, max_len: usize) -> String {
if s.chars().count() <= max_len {
s.to_string()
} else {
let truncated: String = s.chars().take(max_len.saturating_sub(1)).collect();
format!("{truncated}\u{2026}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_short_string_unchanged() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn truncate_exact_length_unchanged() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn truncate_long_string() {
assert_eq!(truncate("hello world", 5), "hell\u{2026}");
}
#[test]
fn truncate_handles_multibyte_chars() {
assert_eq!(truncate("こんにちは世界", 5), "こんにち\u{2026}");
}
}