1pub mod explanation;
2pub mod grex_overlay;
3pub mod match_display;
4pub mod regex_input;
5pub mod replace_input;
6pub mod status_bar;
7pub mod syntax_highlight;
8pub mod test_input;
9pub mod theme;
10
11#[cfg(feature = "pcre2-engine")]
12pub mod debugger;
13
14use std::collections::HashMap;
15
16use ratatui::{
17 layout::{Constraint, Direction, Layout, Rect},
18 style::{Modifier, Style},
19 text::{Line, Span},
20 widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
21 Frame,
22};
23
24use crate::app::{App, BenchmarkResult};
25use crate::codegen;
26use crate::engine::EngineKind;
27use crate::recipe::RECIPES;
28use explanation::ExplanationPanel;
29use match_display::MatchDisplay;
30use regex_input::RegexInput;
31use replace_input::ReplaceInput;
32use status_bar::StatusBar;
33use test_input::TestInput;
34
35pub(crate) const fn border_type(rounded: bool) -> BorderType {
38 if rounded {
39 BorderType::Rounded
40 } else {
41 BorderType::Plain
42 }
43}
44
45pub struct PanelLayout {
47 pub regex_input: Rect,
48 pub test_input: Rect,
49 pub replace_input: Rect,
50 pub match_display: Rect,
51 pub explanation: Rect,
52 pub status_bar: Rect,
53 pub quickref: Option<Rect>,
56}
57
58pub const QUICKREF_PANEL_WIDTH: u16 = 38;
60
61pub const QUICKREF_MIN_RESULTS_WIDTH: u16 = 60;
64
65pub fn compute_layout(size: Rect, show_quickref: bool) -> PanelLayout {
66 let outer = Layout::default()
71 .direction(Direction::Vertical)
72 .constraints([Constraint::Min(0), Constraint::Length(1)])
73 .split(size);
74 let main_top = outer[0];
75 let status_bar = outer[1];
76
77 let quickref_fits =
81 show_quickref && main_top.width >= QUICKREF_PANEL_WIDTH + QUICKREF_MIN_RESULTS_WIDTH;
82 let (left_area, quickref) = if quickref_fits {
83 let chunks = Layout::default()
84 .direction(Direction::Horizontal)
85 .constraints([Constraint::Min(0), Constraint::Length(QUICKREF_PANEL_WIDTH)])
86 .split(main_top);
87 (chunks[0], Some(chunks[1]))
88 } else {
89 (main_top, None)
90 };
91
92 let main_chunks = Layout::default()
94 .direction(Direction::Vertical)
95 .constraints([
96 Constraint::Length(3), Constraint::Length(8), Constraint::Length(3), Constraint::Min(5), ])
101 .split(left_area);
102 let results_area = main_chunks[3];
103
104 let results_chunks = if results_area.width > 80 {
105 Layout::default()
106 .direction(Direction::Horizontal)
107 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
108 .split(results_area)
109 } else {
110 Layout::default()
111 .direction(Direction::Vertical)
112 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
113 .split(results_area)
114 };
115
116 PanelLayout {
117 regex_input: main_chunks[0],
118 test_input: main_chunks[1],
119 replace_input: main_chunks[2],
120 match_display: results_chunks[0],
121 explanation: results_chunks[1],
122 status_bar,
123 quickref,
124 }
125}
126
127pub fn render(frame: &mut Frame, app: &App) {
128 let size = frame.area();
129 let layout = compute_layout(size, app.show_quickref);
130
131 let bt = border_type(app.rounded_borders);
132
133 if app.overlay.help {
135 render_help_overlay(
136 frame,
137 size,
138 app.engine_kind,
139 app.overlay.help_page,
140 bt,
141 app.help_scroll_offset,
142 );
143 return;
144 }
145 if app.overlay.recipes {
146 render_recipe_overlay(frame, size, app.overlay.recipe_index, bt);
147 return;
148 }
149 if app.overlay.benchmark {
150 render_benchmark_overlay(frame, size, &app.benchmark_results, bt);
151 return;
152 }
153 if app.overlay.codegen {
154 render_codegen_overlay(
155 frame,
156 size,
157 app.overlay.codegen_language_index,
158 app.regex_editor.content(),
159 app.flags,
160 bt,
161 );
162 return;
163 }
164 if let Some(grex_state) = app.overlay.grex.as_ref() {
165 grex_overlay::render_with_border(frame, size, grex_state, bt);
166 return;
167 }
168
169 #[cfg(feature = "pcre2-engine")]
170 if let Some(ref session) = app.debug_session {
171 debugger::render_debugger(frame, size, session, bt);
172 return;
173 }
174
175 let error_str = app.error.as_deref();
176
177 frame.render_widget(
179 RegexInput {
180 editor: &app.regex_editor,
181 focused: app.focused_panel == 0,
182 error: error_str,
183 error_offset: app.error_offset,
184 border_type: bt,
185 syntax_tokens: &app.syntax_tokens,
186 },
187 layout.regex_input,
188 );
189
190 frame.render_widget(
192 TestInput {
193 editor: &app.test_editor,
194 focused: app.focused_panel == 1,
195 matches: &app.matches,
196 show_whitespace: app.show_whitespace,
197 border_type: bt,
198 },
199 layout.test_input,
200 );
201
202 frame.render_widget(
204 ReplaceInput {
205 editor: &app.replace_editor,
206 focused: app.focused_panel == 2,
207 border_type: bt,
208 },
209 layout.replace_input,
210 );
211
212 frame.render_widget(
214 MatchDisplay {
215 matches: &app.matches,
216 replace_result: app.replace_result.as_ref(),
217 scroll: app.scroll.match_scroll,
218 focused: app.focused_panel == 3,
219 selected_match: app.selection.match_index,
220 selected_capture: app.selection.capture_index,
221 clipboard_status: app.status.text.as_deref(),
222 border_type: bt,
223 },
224 layout.match_display,
225 );
226
227 frame.render_widget(
229 ExplanationPanel {
230 nodes: &app.explanation,
231 error: error_str,
232 scroll: app.scroll.explain_scroll,
233 focused: app.focused_panel == 4,
234 border_type: bt,
235 },
236 layout.explanation,
237 );
238
239 if let Some(quickref_area) = layout.quickref {
242 let pages = build_help_pages(app.engine_kind);
243 let mut lines: Vec<Line<'static>> = vec![Line::from("")];
244 lines.extend(pages[1].1.iter().cloned());
245 let max_scroll =
249 (lines.len() as u16).saturating_sub(quickref_area.height.saturating_sub(2));
250 let scroll = app.quickref_scroll.min(max_scroll);
251 let block = Block::default()
252 .borders(Borders::ALL)
253 .border_type(bt)
254 .border_style(Style::default().fg(theme::BLUE))
255 .title(Span::styled(
256 " Quick Reference (F3) ",
257 Style::default().fg(theme::TEXT),
258 ));
259 frame.render_widget(
260 Paragraph::new(lines)
261 .block(block)
262 .wrap(Wrap { trim: false })
263 .scroll((scroll, 0)),
264 quickref_area,
265 );
266 }
267
268 #[cfg(feature = "pcre2-engine")]
270 let engine_warning: Option<&'static str> =
271 if app.engine_kind == EngineKind::Pcre2 && crate::engine::pcre2::is_pcre2_10_45() {
272 Some("CVE-2025-58050: PCRE2 10.45 linked — upgrade to >= 10.46.")
273 } else {
274 None
275 };
276 #[cfg(not(feature = "pcre2-engine"))]
277 let engine_warning: Option<&'static str> = None;
278
279 frame.render_widget(
280 StatusBar {
281 engine: app.engine_kind,
282 match_count: app.matches.len(),
283 flags: app.flags,
284 show_whitespace: app.show_whitespace,
285 compile_time: app.compile_time,
286 match_time: app.match_time,
287 vim_mode: if app.vim_mode {
288 Some(app.vim_state.mode)
289 } else {
290 None
291 },
292 engine_warning,
293 },
294 layout.status_bar,
295 );
296
297 if let Some(msg) = app.status.text.as_deref() {
303 let label = format!(" {msg} ");
304 let label_width = label.chars().count() as u16;
305 let bar = layout.status_bar;
306 let width = label_width.min(bar.width);
307 let overlay_rect = Rect {
308 x: bar.x + bar.width.saturating_sub(width),
309 y: bar.y,
310 width,
311 height: 1,
312 };
313 frame.render_widget(Clear, overlay_rect);
314 frame.render_widget(
315 Paragraph::new(Span::styled(
316 label,
317 Style::default()
318 .fg(theme::BASE)
319 .bg(theme::GREEN)
320 .add_modifier(Modifier::BOLD),
321 )),
322 overlay_rect,
323 );
324 }
325}
326
327pub const HELP_PAGE_COUNT: usize = 3;
328
329pub const HELP_PAGE_PADDING: u16 = 4;
335
336pub const HELP_PAGE_COL_0_WIDTH: usize = 16;
337fn build_help_pages(engine: EngineKind) -> Vec<(String, Vec<Line<'static>>)> {
338 let shortcut = |key: &'static str, desc: &'static str| -> Line<'static> {
339 Line::from(vec![
340 Span::styled(
341 format!("{key:<HELP_PAGE_COL_0_WIDTH$}"),
342 Style::default().fg(theme::GREEN),
343 ),
344 Span::styled(desc, Style::default().fg(theme::TEXT)),
345 ])
346 };
347
348 let page0 = vec![
350 shortcut("Tab/Shift+Tab", "Cycle focus forward/backward"),
351 shortcut("Up/Down", "Scroll panel / move cursor / select match"),
352 shortcut("Enter", "Insert newline (test string)"),
353 shortcut("Ctrl+E", "Cycle regex engine"),
354 shortcut("Ctrl+Z", "Undo"),
355 shortcut("Ctrl+Shift+Z", "Redo"),
356 shortcut(
357 "Ctrl+Y",
358 "Copy pattern (regex panel) or match (matches panel)",
359 ),
360 shortcut("Ctrl+O", "Output results to stdout and quit"),
361 shortcut("Ctrl+S", "Save workspace"),
362 shortcut("Ctrl+R", "Open regex recipe library"),
363 shortcut("Ctrl+B", "Benchmark pattern across all engines"),
364 shortcut("Ctrl+U", "Copy regex101.com URL to clipboard"),
365 shortcut("Ctrl+D", "Step-through regex debugger"),
366 shortcut("Ctrl+G", "Generate code for pattern"),
367 shortcut("Ctrl+X", "Generate regex from examples (grex)"),
368 shortcut("Ctrl+W", "Toggle whitespace visualization"),
369 shortcut("Ctrl+Left/Right", "Move cursor by word"),
370 shortcut("Alt+Up/Down", "Browse pattern history"),
371 shortcut("Alt+i", "Toggle case-insensitive"),
372 shortcut("Alt+m", "Toggle multi-line"),
373 shortcut("Alt+s", "Toggle dot-matches-newline"),
374 shortcut("Alt+u", "Toggle unicode mode"),
375 shortcut("Alt+x", "Toggle extended mode"),
376 shortcut(
377 "F1",
378 "Show/hide help (Left(h)/Right(l) to page, Up(k)/Down(j) to scroll)",
379 ),
380 shortcut("F3", "Toggle Quick Reference side panel"),
381 shortcut("PgUp/PgDn", "Scroll Quick Reference side panel"),
382 shortcut("Esc", "Quit"),
383 Line::from(""),
384 Line::from(Span::styled(
385 "Vim: --vim flag | Normal: hjkl wb e 0$^ gg/G x dd cc o/O u p",
386 Style::default().fg(theme::SUBTEXT),
387 )),
388 Line::from(Span::styled(
389 "Mouse: click to focus/position, scroll to navigate",
390 Style::default().fg(theme::SUBTEXT),
391 )),
392 ];
393
394 let header = |text: &'static str| -> Line<'static> {
395 Line::from(Span::styled(text, Style::default().fg(theme::OVERLAY)))
396 };
397
398 let page1 = vec![
400 header("── Sequences ─────────────────────────────────────"),
401 shortcut(".", "Any character (except newline by default)"),
402 shortcut("\\d \\D", "Digit / non-digit"),
403 shortcut("\\w \\W", "Word char / non-word char"),
404 shortcut("\\s \\S", "Whitespace / non-whitespace"),
405 shortcut("\\t \\n \\r", "Tab / newline / carriage return"),
406 shortcut("\\b \\B", "Word boundary / non-boundary"),
407 shortcut("^ $", "Start / end of line"),
408 header("── Classes & Groups ──────────────────────────────"),
409 shortcut("[abc]", "Character class"),
410 shortcut("[^abc]", "Negated character class"),
411 shortcut("[a-z]", "Character range"),
412 shortcut("(group)", "Capturing group"),
413 shortcut("(?:group)", "Non-capturing group"),
414 shortcut("(?P<n>...)", "Named capturing group"),
415 shortcut("(?=...) (?!...)", "Lookahead pos/neg (fancy/PCRE2)"),
416 shortcut("a|b", "Alternation (a or b)"),
417 header("── Quantifiers ───────────────────────────────────"),
418 shortcut("* + ?", "0+, 1+, 0 or 1 (greedy)"),
419 shortcut("*? +? ??", "Lazy variants"),
420 shortcut("{n} {n,m}", "Exact / range repetition"),
421 Line::from(Span::styled(
422 "Replacement: $1, ${name}, $0/$&, $$ for literal $",
423 Style::default().fg(theme::SUBTEXT),
424 )),
425 ];
426
427 let engine_name = format!("{engine}");
429 let page2 = match engine {
430 EngineKind::RustRegex => vec![
431 Line::from(Span::styled(
432 "Rust regex engine — linear time guarantee",
433 Style::default().fg(theme::BLUE),
434 )),
435 Line::from(""),
436 shortcut("Unicode", "Full Unicode support by default"),
437 shortcut("No lookbehind", "Use fancy-regex or PCRE2 for lookaround"),
438 shortcut("No backrefs", "Use fancy-regex or PCRE2 for backrefs"),
439 shortcut("\\p{Letter}", "Unicode category"),
440 shortcut("(?i)", "Inline case-insensitive flag"),
441 shortcut("(?m)", "Inline multi-line flag"),
442 shortcut("(?s)", "Inline dot-matches-newline flag"),
443 shortcut("(?x)", "Inline extended/verbose flag"),
444 ],
445 EngineKind::FancyRegex => vec![
446 Line::from(Span::styled(
447 "fancy-regex engine — lookaround + backreferences",
448 Style::default().fg(theme::BLUE),
449 )),
450 Line::from(""),
451 shortcut("(?=...)", "Positive lookahead"),
452 shortcut("(?!...)", "Negative lookahead"),
453 shortcut("(?<=...)", "Positive lookbehind"),
454 shortcut("(?<!...)", "Negative lookbehind"),
455 shortcut("\\1 \\2", "Backreferences"),
456 shortcut("(?>...)", "Atomic group"),
457 Line::from(""),
458 Line::from(Span::styled(
459 "Delegates to Rust regex for non-fancy patterns",
460 Style::default().fg(theme::SUBTEXT),
461 )),
462 ],
463 #[cfg(feature = "pcre2-engine")]
464 EngineKind::Pcre2 => vec![
465 Line::from(Span::styled(
466 "PCRE2 engine — full-featured",
467 Style::default().fg(theme::BLUE),
468 )),
469 Line::from(""),
470 shortcut("(?=...)(?!...)", "Lookahead"),
471 shortcut("(?<=...)(?<!..)", "Lookbehind"),
472 shortcut("\\1 \\2", "Backreferences"),
473 shortcut("(?>...)", "Atomic group"),
474 shortcut("(*SKIP)(*FAIL)", "Backtracking control verbs"),
475 shortcut("(?R) (?1)", "Recursion / subroutine calls"),
476 shortcut("(?(cond)y|n)", "Conditional patterns"),
477 shortcut("\\K", "Reset match start"),
478 shortcut("(*UTF)", "Force UTF-8 mode"),
479 ],
480 };
481
482 vec![
483 ("Keyboard Shortcuts".to_string(), page0),
484 ("Quick Reference".to_string(), page1),
485 (format!("Engine: {engine_name}"), page2),
486 ]
487}
488
489pub fn build_lengths_of_help_pages() -> HashMap<EngineKind, Vec<u16>> {
491 let mut map: HashMap<EngineKind, Vec<u16>> = HashMap::new();
492 let engines = EngineKind::all();
493 for engine in engines {
494 let pages_len = (0..HELP_PAGE_COUNT)
495 .map(|page| {
496 let (lines, _) = generate_help_page_content(engine, page);
497 let counts: Vec<u16> = lines
498 .iter()
499 .map(|x| {
500 let width = (x.width() + 2) as u16;
502 width.div_ceil(HELP_PAGE_MAX_WIDTH)
503 })
504 .collect();
505 counts.iter().sum::<u16>() + HELP_PAGE_PADDING
506 })
507 .collect();
508 map.insert(engine, pages_len);
509 }
510 map
511}
512
513pub(crate) fn centered_overlay(
514 frame: &mut Frame,
515 area: Rect,
516 max_width: u16,
517 content_height: u16,
518) -> Rect {
519 let w = max_width.min(area.width.saturating_sub(4));
520 let h = content_height.min(area.height.saturating_sub(4));
521 let x = (area.width.saturating_sub(w)) / 2;
522 let y = (area.height.saturating_sub(h)) / 2;
523 let rect = Rect::new(x, y, w, h);
524 frame.render_widget(Clear, rect);
525 rect
526}
527
528pub const HELP_PAGE_HEIGHT: u16 = 28;
529pub const HELP_PAGE_MAX_WIDTH: u16 = 64;
530
531fn generate_help_page_content(
532 engine: EngineKind,
533 page: usize,
534) -> (std::vec::Vec<ratatui::prelude::Line<'static>>, usize) {
535 let pages = build_help_pages(engine);
536 let current = page.min(pages.len() - 1);
537 let (title, content) = &pages[current];
538
539 let mut lines: Vec<Line<'static>> = vec![
540 Line::from(Span::styled(
541 title.clone(),
542 Style::default()
543 .fg(theme::BLUE)
544 .add_modifier(Modifier::BOLD),
545 )),
546 Line::from(""),
547 ];
548 lines.extend(content.iter().cloned());
549 (lines, current)
550}
551
552fn render_help_overlay(
553 frame: &mut Frame,
554 area: Rect,
555 engine: EngineKind,
556 page: usize,
557 bt: BorderType,
558 scroll_offset: u16,
559) {
560 let help_area = centered_overlay(frame, area, HELP_PAGE_MAX_WIDTH, HELP_PAGE_HEIGHT);
561
562 let chunks = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(help_area);
563
564 let (lines, current) = generate_help_page_content(engine, page);
565
566 let block = Block::default()
567 .borders(Borders::ALL)
568 .border_type(bt)
569 .border_style(Style::default().fg(theme::BLUE))
570 .title(Span::styled(" Help ", Style::default().fg(theme::TEXT)))
571 .title(
572 Line::styled(
573 format!(" Page {}/{} ", current + 1, HELP_PAGE_COUNT),
574 Style::default().fg(theme::BASE).bg(theme::BLUE),
575 )
576 .right_aligned(),
577 )
578 .style(Style::default().bg(theme::BASE));
579
580 let paragraph = Paragraph::new(lines)
581 .block(block)
582 .wrap(Wrap { trim: false })
583 .scroll((scroll_offset, 0));
584 let nav_ui = Paragraph::new(Line::styled(
585 " Up(k)/Down(j): Scroll | Left(h)/Right(l): Page | Any other key: Close ",
586 Style::default().fg(theme::TEXT),
587 ))
588 .right_aligned()
589 .style(Style::default().bg(theme::BASE));
590
591 frame.render_widget(paragraph, chunks[0]);
592 frame.render_widget(nav_ui, chunks[1]);
593}
594
595fn render_recipe_overlay(frame: &mut Frame, area: Rect, selected: usize, bt: BorderType) {
596 let overlay_area = centered_overlay(frame, area, 70, RECIPES.len() as u16 + 6);
597
598 let mut lines: Vec<Line<'static>> = vec![
599 Line::from(Span::styled(
600 "Select a recipe to load",
601 Style::default()
602 .fg(theme::BLUE)
603 .add_modifier(Modifier::BOLD),
604 )),
605 Line::from(""),
606 ];
607
608 for (i, recipe) in RECIPES.iter().enumerate() {
609 let is_selected = i == selected;
610 let marker = if is_selected { ">" } else { " " };
611 let style = if is_selected {
612 Style::default().fg(theme::BASE).bg(theme::BLUE)
613 } else {
614 Style::default().fg(theme::TEXT)
615 };
616 lines.push(Line::from(Span::styled(
617 format!("{marker} {:<24} {}", recipe.name, recipe.description),
618 style,
619 )));
620 }
621
622 lines.push(Line::from(""));
623 lines.push(Line::from(Span::styled(
624 " Up/Down: select | Enter: load | Esc: cancel ",
625 Style::default().fg(theme::SUBTEXT),
626 )));
627
628 let block = Block::default()
629 .borders(Borders::ALL)
630 .border_type(bt)
631 .border_style(Style::default().fg(theme::GREEN))
632 .title(Span::styled(
633 " Recipes (Ctrl+R) ",
634 Style::default().fg(theme::TEXT),
635 ))
636 .style(Style::default().bg(theme::BASE));
637
638 let paragraph = Paragraph::new(lines)
639 .block(block)
640 .wrap(Wrap { trim: false });
641
642 frame.render_widget(paragraph, overlay_area);
643}
644
645fn render_benchmark_overlay(
646 frame: &mut Frame,
647 area: Rect,
648 results: &[BenchmarkResult],
649 bt: BorderType,
650) {
651 let overlay_area = centered_overlay(frame, area, 70, results.len() as u16 + 8);
652
653 let fastest_idx = results
654 .iter()
655 .enumerate()
656 .filter(|(_, r)| r.error.is_none())
657 .min_by_key(|(_, r)| r.compile_time + r.match_time)
658 .map(|(i, _)| i);
659
660 let mut lines: Vec<Line<'static>> = vec![
661 Line::from(Span::styled(
662 "Performance Comparison",
663 Style::default()
664 .fg(theme::BLUE)
665 .add_modifier(Modifier::BOLD),
666 )),
667 Line::from(""),
668 Line::from(vec![Span::styled(
669 format!(
670 "{:<16} {:>10} {:>10} {:>10} {:>8}",
671 "Engine", "Compile", "Match", "Total", "Matches"
672 ),
673 Style::default()
674 .fg(theme::SUBTEXT)
675 .add_modifier(Modifier::BOLD),
676 )]),
677 ];
678
679 for (i, result) in results.iter().enumerate() {
680 let is_fastest = fastest_idx == Some(i);
681 if let Some(ref err) = result.error {
682 let line_text = format!("{:<16} {}", result.engine, err);
683 lines.push(Line::from(Span::styled(
684 line_text,
685 Style::default().fg(theme::RED),
686 )));
687 } else {
688 let total = result.compile_time + result.match_time;
689 let line_text = format!(
690 "{:<16} {:>10} {:>10} {:>10} {:>8}",
691 result.engine,
692 status_bar::format_duration(result.compile_time),
693 status_bar::format_duration(result.match_time),
694 status_bar::format_duration(total),
695 result.match_count,
696 );
697 let style = if is_fastest {
698 Style::default()
699 .fg(theme::GREEN)
700 .add_modifier(Modifier::BOLD)
701 } else {
702 Style::default().fg(theme::TEXT)
703 };
704 let mut spans = vec![Span::styled(line_text, style)];
705 if is_fastest {
706 spans.push(Span::styled(" *", Style::default().fg(theme::GREEN)));
707 }
708 lines.push(Line::from(spans));
709 }
710 }
711
712 lines.push(Line::from(""));
713 if fastest_idx.is_some() {
714 lines.push(Line::from(Span::styled(
715 "* = fastest",
716 Style::default().fg(theme::GREEN),
717 )));
718 }
719 lines.push(Line::from(Span::styled(
720 " Any key: close ",
721 Style::default().fg(theme::SUBTEXT),
722 )));
723
724 let block = Block::default()
725 .borders(Borders::ALL)
726 .border_type(bt)
727 .border_style(Style::default().fg(theme::PEACH))
728 .title(Span::styled(
729 " Benchmark (Ctrl+B) ",
730 Style::default().fg(theme::TEXT),
731 ))
732 .style(Style::default().bg(theme::BASE));
733
734 let paragraph = Paragraph::new(lines)
735 .block(block)
736 .wrap(Wrap { trim: false });
737
738 frame.render_widget(paragraph, overlay_area);
739}
740
741fn render_codegen_overlay(
742 frame: &mut Frame,
743 area: Rect,
744 selected: usize,
745 pattern: &str,
746 flags: crate::engine::EngineFlags,
747 bt: BorderType,
748) {
749 let langs = codegen::Language::all();
750 let preview = if pattern.is_empty() {
751 String::from("(no pattern)")
752 } else {
753 let lang = &langs[selected.min(langs.len() - 1)];
754 codegen::generate_code(lang, pattern, &flags)
755 };
756
757 let preview_lines: Vec<&str> = preview.lines().collect();
758 let preview_height = preview_lines.len() as u16;
759 let content_height = langs.len() as u16 + preview_height + 7;
761 let overlay_area = centered_overlay(frame, area, 74, content_height);
762
763 let mut lines: Vec<Line<'static>> = vec![
764 Line::from(Span::styled(
765 "Select a language to generate code",
766 Style::default()
767 .fg(theme::MAUVE)
768 .add_modifier(Modifier::BOLD),
769 )),
770 Line::from(""),
771 ];
772
773 for (i, lang) in langs.iter().enumerate() {
774 let is_selected = i == selected;
775 let marker = if is_selected { ">" } else { " " };
776 let style = if is_selected {
777 Style::default().fg(theme::BASE).bg(theme::MAUVE)
778 } else {
779 Style::default().fg(theme::TEXT)
780 };
781 lines.push(Line::from(Span::styled(format!("{marker} {lang}"), style)));
782 }
783
784 lines.push(Line::from(""));
785 lines.push(Line::from(Span::styled(
786 "Preview:",
787 Style::default()
788 .fg(theme::SUBTEXT)
789 .add_modifier(Modifier::BOLD),
790 )));
791 for pl in preview_lines {
792 lines.push(Line::from(Span::styled(
793 pl.to_string(),
794 Style::default().fg(theme::GREEN),
795 )));
796 }
797
798 lines.push(Line::from(""));
799 lines.push(Line::from(Span::styled(
800 " Up/Down: select | Enter: copy to clipboard | Esc: cancel ",
801 Style::default().fg(theme::SUBTEXT),
802 )));
803
804 let block = Block::default()
805 .borders(Borders::ALL)
806 .border_type(bt)
807 .border_style(Style::default().fg(theme::MAUVE))
808 .title(Span::styled(
809 " Code Generation (Ctrl+G) ",
810 Style::default().fg(theme::TEXT),
811 ))
812 .style(Style::default().bg(theme::BASE));
813
814 let paragraph = Paragraph::new(lines)
815 .block(block)
816 .wrap(Wrap { trim: false });
817
818 frame.render_widget(paragraph, overlay_area);
819}