1use ratatui::layout::{Constraint, Direction, Layout, Rect};
4use ratatui::style::{Modifier, Style};
5use ratatui::text::{Line, Span};
6use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
7use ratatui::Frame;
8
9use crate::filter::FilterApp;
10use crate::ui::theme;
11
12pub fn render(frame: &mut Frame, app: &FilterApp) {
13 let area = frame.area();
14 let chunks = Layout::default()
15 .direction(Direction::Vertical)
16 .constraints([
17 Constraint::Length(3),
18 Constraint::Min(1),
19 Constraint::Length(1),
20 ])
21 .split(area);
22
23 render_pattern_pane(frame, chunks[0], app);
24 render_match_list(frame, chunks[1], app);
25 render_status(frame, chunks[2], app);
26}
27
28fn render_pattern_pane(frame: &mut Frame, area: Rect, app: &FilterApp) {
29 let content = app.pattern();
30 let style = if app.error.is_some() {
31 Style::default().fg(theme::RED)
32 } else {
33 Style::default().fg(theme::TEXT)
34 };
35 let title = if app.error.is_some() {
36 " Pattern (invalid) "
37 } else {
38 " Pattern "
39 };
40 let block = Block::default()
41 .title(Span::styled(title, Style::default().fg(theme::BLUE)))
42 .borders(Borders::ALL);
43 let paragraph =
44 Paragraph::new(Line::from(Span::styled(content.to_string(), style))).block(block);
45 frame.render_widget(paragraph, area);
46}
47
48fn render_match_list(frame: &mut Frame, area: Rect, app: &FilterApp) {
49 if let Some(err) = app.error.as_deref() {
50 let block = Block::default().borders(Borders::ALL).title(" Matches ");
51 let paragraph = Paragraph::new(Line::from(Span::styled(
52 format!("error: {err}"),
53 Style::default().fg(theme::RED),
54 )))
55 .block(block);
56 frame.render_widget(paragraph, area);
57 return;
58 }
59
60 let inner_height = area.height.saturating_sub(2) as usize;
61 let start = if inner_height == 0 || app.matched.is_empty() {
65 0
66 } else {
67 (app.selected + 1).saturating_sub(inner_height)
68 };
69 let end = (start + inner_height).min(app.matched.len());
70 let visible = if start < end {
71 &app.matched[start..end]
72 } else {
73 &[][..]
74 };
75 let items: Vec<ListItem> = visible
76 .iter()
77 .enumerate()
78 .map(|(visible_idx, &line_idx)| {
79 let absolute = start + visible_idx;
80 let mut style = Style::default().fg(theme::TEXT);
81 if absolute == app.selected {
82 style = style.add_modifier(Modifier::REVERSED);
83 }
84 let content = format!("{:>5} {}", line_idx + 1, app.lines[line_idx]);
85 ListItem::new(Line::from(Span::styled(content, style)))
86 })
87 .collect();
88 let block = Block::default().borders(Borders::ALL).title(Span::styled(
89 format!(" Matches ({}/{}) ", app.matched.len(), app.lines.len()),
90 Style::default().fg(theme::BLUE),
91 ));
92 frame.render_widget(List::new(items).block(block), area);
93}
94
95fn render_status(frame: &mut Frame, area: Rect, app: &FilterApp) {
96 let flags = if app.options.case_insensitive {
97 "i"
98 } else {
99 "-"
100 };
101 let invert = if app.options.invert { "v" } else { "-" };
102 let text = format!(
103 " flags: [{flags}{invert}] Enter: emit Esc: discard Alt+i: case Alt+v: invert Up/Down: browse "
104 );
105 frame.render_widget(
106 Paragraph::new(Line::from(Span::styled(
107 text,
108 Style::default().fg(theme::SUBTEXT),
109 ))),
110 area,
111 );
112}