1use std::time::Duration;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Modifier, Style},
7 text::{Line, Span},
8 widgets::{Paragraph, Widget},
9};
10
11use crate::engine::{EngineFlags, EngineKind};
12use crate::input::vim::VimMode;
13use crate::ui::theme;
14
15pub fn format_duration(d: Duration) -> String {
16 let micros = d.as_micros();
17 if micros < 1000 {
18 format!("{micros}\u{03bc}s")
19 } else {
20 format!("{:.1}ms", micros as f64 / 1000.0)
21 }
22}
23
24pub struct StatusBar {
25 pub engine: EngineKind,
26 pub match_count: usize,
27 pub flags: EngineFlags,
28 pub show_whitespace: bool,
29 pub compile_time: Option<Duration>,
30 pub match_time: Option<Duration>,
31 pub vim_mode: Option<VimMode>,
32}
33
34impl Widget for StatusBar {
35 fn render(self, area: Rect, buf: &mut Buffer) {
36 let mut spans = Vec::new();
37
38 if let Some(mode) = self.vim_mode {
39 let (mode_text, mode_bg) = match mode {
40 VimMode::Insert => (" INSERT ", theme::GREEN),
41 VimMode::Normal => (" NORMAL ", theme::BLUE),
42 };
43 spans.push(Span::styled(
44 mode_text,
45 Style::default()
46 .fg(theme::BASE)
47 .bg(mode_bg)
48 .add_modifier(Modifier::BOLD),
49 ));
50 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
51 }
52
53 spans.push(Span::styled(
54 format!(" {} ", self.engine),
55 Style::default()
56 .fg(theme::BASE)
57 .bg(theme::BLUE)
58 .add_modifier(Modifier::BOLD),
59 ));
60 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
61 spans.push(Span::styled(
62 format!(
63 " {} match{} ",
64 self.match_count,
65 if self.match_count == 1 { "" } else { "es" }
66 ),
67 Style::default().fg(theme::TEXT).bg(theme::SURFACE0),
68 ));
69 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
70
71 if self.compile_time.is_some() || self.match_time.is_some() {
73 let mut parts = Vec::new();
74 if let Some(ct) = self.compile_time {
75 parts.push(format!("compile: {}", format_duration(ct)));
76 }
77 if let Some(mt) = self.match_time {
78 parts.push(format!("match: {}", format_duration(mt)));
79 }
80 spans.push(Span::styled(
81 format!("{} ", parts.join(" | ")),
82 Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
83 ));
84 }
85
86 let flags = [
88 ("i", self.flags.case_insensitive),
89 ("m", self.flags.multi_line),
90 ("s", self.flags.dot_matches_newline),
91 ("u", self.flags.unicode),
92 ("x", self.flags.extended),
93 ];
94
95 for (name, active) in &flags {
96 let style = if *active {
97 Style::default()
98 .fg(theme::BASE)
99 .bg(theme::GREEN)
100 .add_modifier(Modifier::BOLD)
101 } else {
102 Style::default().fg(theme::OVERLAY).bg(theme::SURFACE0)
103 };
104 spans.push(Span::styled(format!(" {name} "), style));
105 }
106
107 if self.show_whitespace {
108 spans.push(Span::styled(
109 " \u{00b7} ",
110 Style::default()
111 .fg(theme::BASE)
112 .bg(theme::TEAL)
113 .add_modifier(Modifier::BOLD),
114 ));
115 }
116
117 spans.push(Span::styled(
118 " | Tab: switch | Ctrl+E: engine | Ctrl+W: ws | F1: help ",
119 Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
120 ));
121
122 let line = Line::from(spans);
123 let paragraph = Paragraph::new(line).style(Style::default().bg(theme::SURFACE0));
124 paragraph.render(area, buf);
125 }
126}