Skip to main content

rgx/ui/
mod.rs

1pub mod explanation;
2pub mod match_display;
3pub mod regex_input;
4pub mod status_bar;
5pub mod test_input;
6pub mod theme;
7
8use ratatui::{
9    layout::{Constraint, Direction, Layout, Rect},
10    Frame,
11};
12
13use crate::app::App;
14use explanation::ExplanationPanel;
15use match_display::MatchDisplay;
16use regex_input::RegexInput;
17use status_bar::StatusBar;
18use test_input::TestInput;
19
20pub fn render(frame: &mut Frame, app: &App) {
21    let size = frame.area();
22
23    // Main layout: inputs on top, results on bottom, status bar at very bottom
24    let main_chunks = Layout::default()
25        .direction(Direction::Vertical)
26        .constraints([
27            Constraint::Length(3), // regex input
28            Constraint::Length(3), // test string input
29            Constraint::Min(5),    // results area
30            Constraint::Length(1), // status bar
31        ])
32        .split(size);
33
34    // Results area: split horizontally between matches and explanation
35    let results_chunks = if main_chunks[2].width > 80 {
36        Layout::default()
37            .direction(Direction::Horizontal)
38            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
39            .split(main_chunks[2])
40    } else {
41        // Stack vertically on narrow terminals
42        Layout::default()
43            .direction(Direction::Vertical)
44            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
45            .split(main_chunks[2])
46    };
47
48    // Help overlay
49    if app.show_help {
50        render_help_overlay(frame, size);
51        return;
52    }
53
54    let error_str = app.error.as_deref();
55
56    // Regex input
57    frame.render_widget(
58        RegexInput {
59            editor: &app.regex_editor,
60            focused: app.focused_panel == 0,
61            error: error_str,
62        },
63        main_chunks[0],
64    );
65
66    // Test string input
67    frame.render_widget(
68        TestInput {
69            editor: &app.test_editor,
70            focused: app.focused_panel == 1,
71            matches: &app.matches,
72        },
73        main_chunks[1],
74    );
75
76    // Match display
77    frame.render_widget(
78        MatchDisplay {
79            matches: &app.matches,
80        },
81        results_chunks[0],
82    );
83
84    // Explanation panel
85    frame.render_widget(
86        ExplanationPanel {
87            nodes: &app.explanation,
88            error: error_str,
89        },
90        results_chunks[1],
91    );
92
93    // Status bar
94    frame.render_widget(
95        StatusBar {
96            engine: app.engine_kind,
97            match_count: app.matches.len(),
98            flags: app.flags.clone(),
99        },
100        main_chunks[3],
101    );
102}
103
104fn render_help_overlay(frame: &mut Frame, area: Rect) {
105    use ratatui::{
106        style::Style,
107        text::{Line, Span},
108        widgets::{Block, Borders, Clear, Paragraph, Wrap},
109    };
110
111    let help_width = 60.min(area.width.saturating_sub(4));
112    let help_height = 18.min(area.height.saturating_sub(4));
113    let x = (area.width.saturating_sub(help_width)) / 2;
114    let y = (area.height.saturating_sub(help_height)) / 2;
115    let help_area = Rect::new(x, y, help_width, help_height);
116
117    frame.render_widget(Clear, help_area);
118
119    let lines = vec![
120        Line::from(Span::styled(
121            "rgx - Keyboard Shortcuts",
122            Style::default()
123                .fg(theme::BLUE)
124                .add_modifier(ratatui::style::Modifier::BOLD),
125        )),
126        Line::from(""),
127        Line::from(vec![
128            Span::styled("Tab       ", Style::default().fg(theme::GREEN)),
129            Span::styled(
130                "Switch between pattern/test string",
131                Style::default().fg(theme::TEXT),
132            ),
133        ]),
134        Line::from(vec![
135            Span::styled("Ctrl+E    ", Style::default().fg(theme::GREEN)),
136            Span::styled("Cycle regex engine", Style::default().fg(theme::TEXT)),
137        ]),
138        Line::from(vec![
139            Span::styled("Alt+i     ", Style::default().fg(theme::GREEN)),
140            Span::styled("Toggle case-insensitive", Style::default().fg(theme::TEXT)),
141        ]),
142        Line::from(vec![
143            Span::styled("Alt+m     ", Style::default().fg(theme::GREEN)),
144            Span::styled("Toggle multi-line", Style::default().fg(theme::TEXT)),
145        ]),
146        Line::from(vec![
147            Span::styled("Alt+s     ", Style::default().fg(theme::GREEN)),
148            Span::styled(
149                "Toggle dot-matches-newline",
150                Style::default().fg(theme::TEXT),
151            ),
152        ]),
153        Line::from(vec![
154            Span::styled("Alt+u     ", Style::default().fg(theme::GREEN)),
155            Span::styled("Toggle unicode mode", Style::default().fg(theme::TEXT)),
156        ]),
157        Line::from(vec![
158            Span::styled("Alt+x     ", Style::default().fg(theme::GREEN)),
159            Span::styled("Toggle extended mode", Style::default().fg(theme::TEXT)),
160        ]),
161        Line::from(vec![
162            Span::styled("?         ", Style::default().fg(theme::GREEN)),
163            Span::styled("Show/hide this help", Style::default().fg(theme::TEXT)),
164        ]),
165        Line::from(vec![
166            Span::styled("Esc       ", Style::default().fg(theme::GREEN)),
167            Span::styled("Quit", Style::default().fg(theme::TEXT)),
168        ]),
169        Line::from(""),
170        Line::from(Span::styled(
171            "Press any key to close",
172            Style::default().fg(theme::SUBTEXT),
173        )),
174    ];
175
176    let block = Block::default()
177        .borders(Borders::ALL)
178        .border_style(Style::default().fg(theme::BLUE))
179        .title(Span::styled(" Help ", Style::default().fg(theme::TEXT)))
180        .style(Style::default().bg(theme::BASE));
181
182    let paragraph = Paragraph::new(lines)
183        .block(block)
184        .wrap(Wrap { trim: false });
185
186    frame.render_widget(paragraph, help_area);
187}