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