Skip to main content

binocular/ui/search/
bar.rs

1use crate::app::{InputMode, Mode};
2use crate::ui::indicators::mode_indicator;
3use crate::ui::shortcuts::{render_hints_line, search_bar_hints};
4use ratatui::{
5    layout::Rect,
6    style::{Color, Modifier, Style},
7    text::{Line, Span},
8    widgets::{Block, BorderType, Borders, Paragraph},
9    Frame,
10};
11
12use super::search_border_style;
13
14pub struct SearchBarView<'a> {
15    pub app_mode: Mode,
16    pub search_mode: InputMode,
17    pub preview_search_active: bool,
18    pub preview_search_query: &'a str,
19    pub preview_search_cursor: usize,
20    pub query_text: &'a str,
21    pub query_cursor: usize,
22    pub search_label: &'a str,
23    pub match_mode_label: &'static str,
24}
25
26pub fn render_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
27    if view.app_mode == Mode::Preview && view.preview_search_active {
28        render_preview_search_bar(f, view, area);
29        return;
30    }
31
32    render_main_search_bar(f, view, area);
33}
34
35fn render_preview_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
36    let input_text = format!("/{}", view.preview_search_query);
37    let input = Paragraph::new(input_text.as_str())
38        .style(Style::default().fg(Color::Blue))
39        .block(
40            Block::default()
41                .borders(Borders::ALL)
42                .border_type(BorderType::Rounded)
43                .title(" Search Preview "),
44        );
45    f.render_widget(input, area);
46    f.set_cursor_position((
47        area.x + 1 + view.preview_search_cursor as u16 + 1,
48        area.y + 1,
49    ));
50}
51
52fn render_main_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
53    let is_insert_mode = view.search_mode == InputMode::Insert;
54    let input_line = build_query_line(view.query_text, view.query_cursor, is_insert_mode);
55    let search_mode = if is_insert_mode {
56        InputMode::Insert
57    } else {
58        InputMode::Normal
59    };
60
61    let center_title = Line::from(vec![
62        Span::raw(" "),
63        Span::styled(
64            view.search_label,
65            Style::default().add_modifier(Modifier::BOLD),
66        ),
67        Span::raw(" "),
68    ])
69    .centered();
70
71    let mode_title = Line::from(vec![Span::raw(" "), mode_indicator(&search_mode, None)]);
72
73    let hints = render_hints_line(search_bar_hints(view.app_mode, view.search_mode));
74    let mut block = Block::default()
75        .borders(Borders::ALL)
76        .border_type(BorderType::Rounded)
77        .title(center_title)
78        .title(mode_title)
79        .title_bottom(hints.right_aligned())
80        .border_style(search_border_style(view.app_mode, view.search_mode));
81    block = block.title(
82        Line::from(vec![Span::styled(
83            view.match_mode_label,
84            Style::default().add_modifier(Modifier::BOLD),
85        )])
86        .right_aligned(),
87    );
88    let input = Paragraph::new(input_line)
89        .style(Style::default().fg(Color::White))
90        .block(block);
91
92    f.render_widget(input, area);
93}
94
95fn build_query_line(query: &str, cursor: usize, is_insert_mode: bool) -> Line<'static> {
96    let query_chars: Vec<char> = query.chars().collect();
97    let cursor_pos = cursor.min(query_chars.len());
98
99    if query_chars.is_empty() {
100        return Line::from(vec![empty_cursor_span(is_insert_mode)]);
101    }
102
103    let mut spans = Vec::new();
104
105    if cursor_pos > 0 {
106        let before: String = query_chars[..cursor_pos].iter().collect();
107        spans.push(Span::raw(before));
108    }
109
110    let cursor_char: String = if cursor_pos < query_chars.len() {
111        query_chars[cursor_pos..=cursor_pos].iter().collect()
112    } else {
113        " ".to_string()
114    };
115    spans.push(cursor_span(cursor_char, is_insert_mode));
116
117    if cursor_pos + 1 < query_chars.len() {
118        let after: String = query_chars[cursor_pos + 1..].iter().collect();
119        spans.push(Span::raw(after));
120    }
121
122    Line::from(spans)
123}
124
125fn empty_cursor_span(is_insert_mode: bool) -> Span<'static> {
126    if is_insert_mode {
127        Span::styled(" ", Style::default().add_modifier(Modifier::UNDERLINED))
128    } else {
129        Span::styled(" ", Style::default().bg(Color::White).fg(Color::Black))
130    }
131}
132
133fn cursor_span(content: String, is_insert_mode: bool) -> Span<'static> {
134    if is_insert_mode {
135        Span::styled(
136            content,
137            Style::default()
138                .add_modifier(Modifier::UNDERLINED)
139                .fg(Color::LightGreen),
140        )
141    } else {
142        Span::styled(content, Style::default().bg(Color::White).fg(Color::Black))
143    }
144}