1use ratatui::Frame;
11use ratatui::layout::Rect;
12use ratatui::style::{Color, Style};
13use ratatui::text::{Line, Span};
14use ratatui::widgets::Paragraph;
15
16use crate::search::{StructuredFilter, number_query};
17
18#[derive(Debug, Clone, Default)]
20pub struct FilterRow {
21 pub query: Option<String>,
23 pub structured: StructuredFilter,
25}
26
27impl FilterRow {
28 pub fn describe(&self) -> Option<String> {
33 let mut segments = Vec::new();
34 if let Some(q) = self.query.as_deref().filter(|q| !q.is_empty()) {
35 segments.push(match number_query(q) {
36 Some(n) => format!("#{n}"),
37 None => format!("`{q}`"),
38 });
39 }
40 let criteria = self.structured.to_string();
41 if criteria != "(no active criteria)" {
42 segments.push(format!("({criteria})"));
43 }
44 if segments.is_empty() {
45 None
46 } else {
47 Some(format!("Filters: {}", segments.join(" ")))
48 }
49 }
50
51 pub fn render(&self, f: &mut Frame, area: Rect) {
52 if area.height == 0 || area.width == 0 {
53 return;
54 }
55 let text = self
56 .describe()
57 .unwrap_or_else(|| "Filters: (none)".to_string());
58 let para = Paragraph::new(Line::from(Span::styled(
59 text,
60 Style::default().fg(Color::Cyan),
61 )));
62 f.render_widget(para, area);
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use ratatui::Terminal;
70 use ratatui::backend::TestBackend;
71
72 fn render_row(row: &FilterRow, w: u16, h: u16) -> String {
73 let backend = TestBackend::new(w, h);
74 let mut terminal = Terminal::new(backend).unwrap();
75 terminal.draw(|f| row.render(f, f.area())).unwrap();
76 let buffer = terminal.backend().buffer();
77 buffer.content.iter().map(|c| c.symbol()).collect()
78 }
79
80 #[test]
81 fn describe_query_only() {
82 let row = FilterRow {
83 query: Some("target".into()),
84 structured: StructuredFilter::default(),
85 };
86 assert_eq!(row.describe().as_deref(), Some("Filters: `target`"));
87 }
88
89 #[test]
90 fn describe_numeric_query_renders_issue_reference() {
91 let row = FilterRow {
92 query: Some("55".into()),
93 structured: StructuredFilter::default(),
94 };
95 assert_eq!(row.describe().as_deref(), Some("Filters: #55"));
96 }
97
98 #[test]
99 fn describe_criteria_only_shows_criteria() {
100 let row = FilterRow {
101 query: None,
102 structured: StructuredFilter {
103 status: Some("todo".to_string()),
104 ..Default::default()
105 },
106 };
107 assert_eq!(row.describe().as_deref(), Some("Filters: (status:todo)"));
108 }
109
110 #[test]
111 fn describe_query_and_criteria_both() {
112 let row = FilterRow {
113 query: Some("55".into()),
114 structured: StructuredFilter {
115 status: Some("todo".to_string()),
116 ..Default::default()
117 },
118 };
119 assert_eq!(
120 row.describe().as_deref(),
121 Some("Filters: #55 (status:todo)")
122 );
123 }
124
125 #[test]
126 fn describe_empty_layers_is_none() {
127 let row = FilterRow {
128 query: None,
129 structured: StructuredFilter::default(),
130 };
131 assert_eq!(row.describe(), None);
132 }
133
134 #[test]
135 fn render_shows_the_described_filters() {
136 let row = FilterRow {
137 query: Some("target".into()),
138 structured: StructuredFilter::default(),
139 };
140 let content = render_row(&row, 80, 1);
141 assert!(content.contains("`target`"), "got {content:?}");
142 }
143
144 #[test]
145 fn render_smoke_tiny_and_tall() {
146 let _ = render_row(
148 &FilterRow {
149 query: Some("55".into()),
150 structured: StructuredFilter::default(),
151 },
152 4,
153 1,
154 );
155 let row = FilterRow::default();
156 let content = render_row(&row, 80, 1);
157 assert!(content.starts_with("Filters:"), "got {content:?}");
158 }
159}