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 let main_chunks = Layout::default()
28 .direction(Direction::Vertical)
29 .constraints([
30 Constraint::Length(3), Constraint::Length(8), Constraint::Length(3), Constraint::Min(5), Constraint::Length(1), ])
36 .split(size);
37
38 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 Layout::default()
47 .direction(Direction::Vertical)
48 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
49 .split(main_chunks[3])
50 };
51
52 if app.show_help {
54 render_help_overlay(frame, size);
55 return;
56 }
57
58 let error_str = app.error.as_deref();
59
60 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 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 frame.render_widget(
82 ReplaceInput {
83 editor: &app.replace_editor,
84 focused: app.focused_panel == 2,
85 },
86 main_chunks[2],
87 );
88
89 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 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 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}