Skip to main content

rgx/ui/
mod.rs

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