Skip to main content

rgx/ui/
grex_overlay.rs

1//! grex overlay widget — lets users enter example strings and load a generated regex into the main editor.
2
3use std::time::Instant;
4
5use ratatui::layout::Rect;
6use ratatui::style::{Modifier, Style};
7use ratatui::text::{Line, Span};
8use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Wrap};
9use ratatui::Frame;
10
11use crate::grex_integration::GrexOptions;
12use crate::input::editor::Editor;
13use crate::ui::{centered_overlay, theme};
14
15#[derive(Default)]
16pub struct GrexOverlayState {
17    pub editor: Editor,
18    pub options: GrexOptions,
19    pub generated_pattern: Option<String>,
20    pub generation_counter: u64,
21    pub debounce_deadline: Option<Instant>,
22}
23
24pub fn render(frame: &mut Frame, area: Rect, state: &GrexOverlayState) {
25    render_with_border(frame, area, state, BorderType::Plain);
26}
27
28pub fn render_with_border(frame: &mut Frame, area: Rect, state: &GrexOverlayState, bt: BorderType) {
29    let width = area.width.clamp(60, 80);
30    let height = area.height.saturating_sub(4).clamp(12, 18);
31    let overlay_area = centered_overlay(frame, area, width, height);
32
33    let dim = Style::default().fg(theme::SUBTEXT);
34    let title_style = Style::default()
35        .fg(theme::BLUE)
36        .add_modifier(Modifier::BOLD);
37    let label_style = Style::default().fg(theme::MAUVE);
38
39    let mut lines: Vec<Line<'static>> = Vec::with_capacity(16);
40    lines.push(Line::from(Span::styled(
41        "Generate Regex from Examples",
42        title_style,
43    )));
44    lines.push(Line::from(""));
45    lines.push(build_flag_row(&state.options));
46    lines.push(Line::from(""));
47    lines.push(Line::from(Span::styled(
48        "Examples (one per line):",
49        label_style,
50    )));
51
52    let content = state.editor.content();
53    if content.is_empty() {
54        lines.push(Line::from(Span::styled(
55            "  Enter one example per line. Tab to accept.",
56            dim,
57        )));
58    } else {
59        for line in content.lines() {
60            lines.push(Line::from(format!("  {line}")));
61        }
62    }
63
64    lines.push(Line::from(""));
65    lines.push(Line::from(Span::styled("Generated pattern:", label_style)));
66    match state.generated_pattern.as_deref() {
67        Some(p) if !p.is_empty() => {
68            lines.push(Line::from(Span::styled(
69                format!("  {p}"),
70                Style::default().fg(theme::GREEN),
71            )));
72        }
73        _ => {
74            lines.push(Line::from(Span::styled("  (none yet)", dim)));
75        }
76    }
77
78    lines.push(Line::from(""));
79    lines.push(Line::from(Span::styled(
80        "Alt+d/a/c: toggle flags    Tab: accept    Esc: cancel",
81        dim,
82    )));
83
84    let block = Block::default()
85        .borders(Borders::ALL)
86        .border_type(bt)
87        .border_style(Style::default().fg(theme::BLUE))
88        .title(Span::styled(
89            " Generate Regex from Examples (Ctrl+X) ",
90            Style::default().fg(theme::TEXT),
91        ))
92        .style(Style::default().bg(theme::BASE));
93
94    let paragraph = Paragraph::new(lines)
95        .block(block)
96        .wrap(Wrap { trim: false });
97
98    frame.render_widget(paragraph, overlay_area);
99}
100
101fn build_flag_row(options: &GrexOptions) -> Line<'static> {
102    fn flag_span(label: &'static str, on: bool) -> Vec<Span<'static>> {
103        let marker = if on { "●" } else { "○" };
104        let style = if on {
105            Style::default().fg(theme::GREEN)
106        } else {
107            Style::default().fg(theme::SUBTEXT)
108        };
109        vec![
110            Span::raw(label),
111            Span::raw(" "),
112            Span::styled(marker, style),
113        ]
114    }
115    let mut spans: Vec<Span<'static>> =
116        vec![Span::styled("Flags:  ", Style::default().fg(theme::MAUVE))];
117    spans.extend(flag_span("[D]igit", options.digit));
118    spans.push(Span::raw("   "));
119    spans.extend(flag_span("[A]nchors", options.anchors));
120    spans.push(Span::raw("   "));
121    spans.extend(flag_span("[C]ase-insensitive", options.case_insensitive));
122    Line::from(spans)
123}