Skip to main content

rgx/ui/
status_bar.rs

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::ui::theme;
13
14fn format_duration(d: Duration) -> String {
15    let micros = d.as_micros();
16    if micros < 1000 {
17        format!("{micros}\u{03bc}s")
18    } else {
19        format!("{:.1}ms", micros as f64 / 1000.0)
20    }
21}
22
23pub struct StatusBar {
24    pub engine: EngineKind,
25    pub match_count: usize,
26    pub flags: EngineFlags,
27    pub show_whitespace: bool,
28    pub compile_time: Option<Duration>,
29    pub match_time: Option<Duration>,
30}
31
32impl Widget for StatusBar {
33    fn render(self, area: Rect, buf: &mut Buffer) {
34        let mut spans = vec![
35            Span::styled(
36                format!(" {} ", self.engine),
37                Style::default()
38                    .fg(theme::BASE)
39                    .bg(theme::BLUE)
40                    .add_modifier(Modifier::BOLD),
41            ),
42            Span::styled(" ", Style::default().bg(theme::SURFACE0)),
43            Span::styled(
44                format!(
45                    " {} match{} ",
46                    self.match_count,
47                    if self.match_count == 1 { "" } else { "es" }
48                ),
49                Style::default().fg(theme::TEXT).bg(theme::SURFACE0),
50            ),
51            Span::styled(" ", Style::default().bg(theme::SURFACE0)),
52        ];
53
54        // Timing info
55        if self.compile_time.is_some() || self.match_time.is_some() {
56            let mut parts = Vec::new();
57            if let Some(ct) = self.compile_time {
58                parts.push(format!("compile: {}", format_duration(ct)));
59            }
60            if let Some(mt) = self.match_time {
61                parts.push(format!("match: {}", format_duration(mt)));
62            }
63            spans.push(Span::styled(
64                format!("{} ", parts.join(" | ")),
65                Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
66            ));
67        }
68
69        // Flag indicators
70        let flags = [
71            ("i", self.flags.case_insensitive),
72            ("m", self.flags.multi_line),
73            ("s", self.flags.dot_matches_newline),
74            ("u", self.flags.unicode),
75            ("x", self.flags.extended),
76        ];
77
78        for (name, active) in &flags {
79            let style = if *active {
80                Style::default()
81                    .fg(theme::BASE)
82                    .bg(theme::GREEN)
83                    .add_modifier(Modifier::BOLD)
84            } else {
85                Style::default().fg(theme::OVERLAY).bg(theme::SURFACE0)
86            };
87            spans.push(Span::styled(format!(" {name} "), style));
88        }
89
90        if self.show_whitespace {
91            spans.push(Span::styled(
92                " \u{00b7} ",
93                Style::default()
94                    .fg(theme::BASE)
95                    .bg(theme::TEAL)
96                    .add_modifier(Modifier::BOLD),
97            ));
98        }
99
100        spans.push(Span::styled(
101            " | Tab: switch | Ctrl+E: engine | Ctrl+W: ws | F1: help ",
102            Style::default().fg(theme::SUBTEXT).bg(theme::SURFACE0),
103        ));
104
105        let line = Line::from(spans);
106        let paragraph = Paragraph::new(line).style(Style::default().bg(theme::SURFACE0));
107        paragraph.render(area, buf);
108    }
109}