use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
};
use crate::app::App;
use crate::ui::styles;
pub fn filter_templates(templates: &[(String, String)], filter: &str) -> Vec<usize> {
if filter.is_empty() {
return (0..templates.len()).collect();
}
let lower = filter.to_ascii_lowercase();
templates
.iter()
.enumerate()
.filter(|(_, (name, body))| {
name.to_ascii_lowercase().contains(&lower) || body.to_ascii_lowercase().contains(&lower)
})
.map(|(i, _)| i)
.collect()
}
pub fn render_comment_template_picker(frame: &mut Frame, app: &App) {
let theme = &app.theme;
let area = centered_rect(60, 60, frame.area());
frame.render_widget(Clear, area);
let block = Block::default()
.title(" Comment Templates ")
.borders(Borders::ALL)
.style(styles::popup_style(theme))
.border_style(styles::border_style(theme, true));
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), ])
.split(inner);
let filter_text = format!("/{}", app.ui_layout.template_filter());
let filter_line = Paragraph::new(Line::from(Span::styled(
filter_text,
Style::default().fg(theme.fg_primary),
)));
frame.render_widget(filter_line, chunks[0]);
let sep = Paragraph::new(Line::from(Span::styled(
"\u{2500}".repeat(chunks[1].width as usize),
Style::default().fg(theme.border_unfocused),
)));
frame.render_widget(sep, chunks[1]);
let filtered = filter_templates(&app.comment_templates, app.ui_layout.template_filter());
let list_height = chunks[2].height as usize;
let cursor = app
.ui_layout
.template_cursor()
.min(filtered.len().saturating_sub(1));
let scroll_offset = if cursor >= list_height {
cursor - list_height + 1
} else {
0
};
let items: Vec<ListItem> = filtered
.iter()
.skip(scroll_offset)
.take(list_height)
.enumerate()
.map(|(display_idx, &entry_idx)| {
let (name, body) = &app.comment_templates[entry_idx];
let is_selected = (display_idx + scroll_offset) == cursor;
let preview: String = body
.chars()
.map(|c| match c {
'\n' => '\u{21b5}',
_ => c,
})
.collect();
let name_span = Span::styled(
format!("{name:<16}"),
Style::default()
.fg(theme.diff_hunk_header)
.add_modifier(Modifier::BOLD),
);
let body_span = Span::styled(preview, Style::default().fg(theme.fg_secondary));
let line = Line::from(vec![
Span::raw(if is_selected { "> " } else { " " }),
name_span,
body_span,
]);
if is_selected {
ListItem::new(line).style(styles::selected_style(theme))
} else {
ListItem::new(line)
}
})
.collect();
let list = List::new(items);
frame.render_widget(list, chunks[2]);
let footer = Paragraph::new(Line::from(vec![
Span::styled(
" Enter",
Style::default()
.fg(theme.fg_primary)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" \u{2192} insert ",
Style::default().fg(theme.fg_secondary),
),
Span::styled(
"Esc",
Style::default()
.fg(theme.fg_primary)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" \u{2192} cancel ",
Style::default().fg(theme.fg_secondary),
),
Span::styled(
"\u{2191}\u{2193}",
Style::default()
.fg(theme.fg_primary)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" \u{2192} navigate",
Style::default().fg(theme.fg_secondary),
),
]));
frame.render_widget(footer, chunks[3]);
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}
#[cfg(test)]
mod tests {
use super::*;
fn make_templates(entries: &[(&str, &str)]) -> Vec<(String, String)> {
entries
.iter()
.map(|(n, b)| ((*n).to_string(), (*b).to_string()))
.collect()
}
#[test]
fn empty_filter_returns_every_entry() {
let templates = make_templates(&[("nit", "nit: "), ("q", "Q: "), ("style", "Style: ")]);
let filtered = filter_templates(&templates, "");
assert_eq!(filtered, vec![0, 1, 2]);
}
#[test]
fn filter_matches_name_case_insensitive() {
let templates = make_templates(&[("nit", "nit: "), ("Q", "Question: ")]);
let filtered = filter_templates(&templates, "ni");
assert_eq!(filtered, vec![0]);
let filtered = filter_templates(&templates, "q");
assert_eq!(filtered, vec![1]);
}
#[test]
fn filter_matches_body_contents() {
let templates = make_templates(&[("nit", "nit: "), ("q", "Question: ")]);
let filtered = filter_templates(&templates, "question");
assert_eq!(filtered, vec![1]);
}
}