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 pub engine_warning: Option<&'static str>,
34}
35
36impl Widget for StatusBar {
37 fn render(self, area: Rect, buf: &mut Buffer) {
38 let mut spans = Vec::new();
39
40 if let Some(mode) = self.vim_mode {
41 let (mode_text, mode_bg) = match mode {
42 VimMode::Insert => (" INSERT ", theme::GREEN),
43 VimMode::Normal => (" NORMAL ", theme::BLUE),
44 };
45 spans.push(Span::styled(
46 mode_text,
47 Style::default()
48 .fg(theme::BASE)
49 .bg(mode_bg)
50 .add_modifier(Modifier::BOLD),
51 ));
52 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
53 }
54
55 spans.push(Span::styled(
56 format!(" {} ", self.engine),
57 Style::default()
58 .fg(theme::BASE)
59 .bg(theme::BLUE)
60 .add_modifier(Modifier::BOLD),
61 ));
62 if let Some(warning) = self.engine_warning {
63 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
64 spans.push(Span::styled(
65 format!(" \u{26a0} {} ", warning),
66 Style::default()
67 .fg(theme::BASE)
68 .bg(theme::RED)
69 .add_modifier(Modifier::BOLD),
70 ));
71 }
72 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
73 spans.push(Span::styled(
74 format!(
75 " {} match{} ",
76 self.match_count,
77 if self.match_count == 1 { "" } else { "es" }
78 ),
79 Style::default().fg(theme::TEXT).bg(theme::SURFACE0),
80 ));
81 spans.push(Span::styled(" ", Style::default().bg(theme::SURFACE0)));
82
83 if self.compile_time.is_some() || self.match_time.is_some() {
85 let mut parts = Vec::new();
86 if let Some(ct) = self.compile_time {
87 parts.push(format!("compile: {}", format_duration(ct)));
88 }
89 if let Some(mt) = self.match_time {
90 parts.push(format!("match: {}", format_duration(mt)));
91 }
92 spans.push(Span::styled(
93 format!("{} ", parts.join(" | ")),
94 Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
95 ));
96 }
97
98 let flags = [
100 ("i", self.flags.case_insensitive),
101 ("m", self.flags.multi_line),
102 ("s", self.flags.dot_matches_newline),
103 ("u", self.flags.unicode),
104 ("x", self.flags.extended),
105 ];
106
107 for (name, active) in &flags {
108 let style = if *active {
109 Style::default()
110 .fg(theme::BASE)
111 .bg(theme::GREEN)
112 .add_modifier(Modifier::BOLD)
113 } else {
114 Style::default().fg(theme::OVERLAY).bg(theme::SURFACE0)
115 };
116 spans.push(Span::styled(format!(" {name} "), style));
117 }
118
119 if self.show_whitespace {
120 spans.push(Span::styled(
121 " \u{00b7} ",
122 Style::default()
123 .fg(theme::BASE)
124 .bg(theme::TEAL)
125 .add_modifier(Modifier::BOLD),
126 ));
127 }
128
129 spans.push(Span::styled(
130 " | Tab: switch | Ctrl+E: engine | Ctrl+W: ws | F1: help ",
131 Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
132 ));
133
134 let line = Line::from(spans);
135 let paragraph = Paragraph::new(line).style(Style::default().bg(theme::SURFACE0));
136 paragraph.render(area, buf);
137 }
138}