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::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    /// Non-None when the active engine has a known security advisory.
33    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        // Timing info
84        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        // Flag indicators
99        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}