# grex Overlay Design Spec

**Status:** Approved 2026-04-11
**Target release:** v0.11.0 — the final polish release before rgx transitions to feature-complete + maintenance mode (Road A per [docs/ROADMAP.md](../../ROADMAP.md))

## Summary

Add a new overlay (`Ctrl+X`) that lets users enter example strings and get back a regex pattern generated by the [grex](https://crates.io/crates/grex) crate. On accept, the generated pattern loads into the main regex editor. This is rgx's "inverse workflow" feature — for users who have strings and need to derive a regex, rather than having a regex and wanting to test it. Compounds with the existing codegen (Ctrl+G) and recipes (Ctrl+R) overlays as a third entry point to "pattern production."

## Design decisions

All six locked in during brainstorming on 2026-04-11:

1. **Input model:** single multi-line text area, one example per line. Newline = new example. Matches grex CLI input semantics; pairs naturally with clipboard paste.
2. **Regeneration trigger:** debounced ~150ms after the last keystroke. Feels live without thrashing grex on every character.
3. **Flag exposure:** three toggles — `[D]igit`, `[A]nchors`, `[C]ase-insensitive` — as a single-letter indicator row at the top of the overlay, mirroring the existing main-UI flag row (`i m s u x`). `Alt+d`, `Alt+a`, `Alt+c` toggle. Aggressive `\w+`/`\s+` generalization is deferred; if users ask for it, it is a v0.11.x addition.
4. **Accept flow:** `Tab` accepts the generated pattern (loads it into the main regex editor and closes the overlay). `Esc` cancels. `Enter` is reserved for newline-in-example-list and never accepts.
5. **Empty/error states:** dimmed placeholder text when the editor is empty (`Enter one example per line. Tab to accept.`), and dimmed `(none yet)` for the pattern preview before the first generation completes. No computing indicator. No input size cap. Rely on `tokio::task::spawn_blocking` + the 150ms debounce to keep the UI responsive.
6. **Shortcut:** `Ctrl+X` (mnemonic: e**X**amples). Consistent with the existing Ctrl-letter overlay family — Ctrl+R recipes, Ctrl+G codegen, Ctrl+D debugger. Ctrl-modified keys auto-bypass vim mode.

### Default flag values

- `digit = true` — most useful generalization for real-world inputs (emails, IPs, versions, etc.)
- `anchors = true` — most users want the generated pattern to match whole strings
- `case_insensitive = false` — most patterns are case-sensitive

## Architecture

### New files

**`src/grex_integration.rs`** — pure wrapper around the `grex` crate. Exposes a `GrexOptions` struct and a `generate()` function. No async, no I/O, no dependency on `App`. Fully unit-testable.

```rust
pub struct GrexOptions {
    pub digit: bool,
    pub anchors: bool,
    pub case_insensitive: bool,
}

impl Default for GrexOptions {
    fn default() -> Self {
        Self { digit: true, anchors: true, case_insensitive: false }
    }
}

pub fn generate(examples: &[String], options: GrexOptions) -> String {
    if examples.is_empty() {
        return String::new();
    }
    // Construct a grex::RegExpBuilder from examples, apply options, call build().
    // Exact API verified against the crate docs during implementation.
}
```

The empty-input short-circuit exists because `grex::RegExpBuilder::from()` panics on an empty slice per the crate's source. The wrapper shields callers from that edge case.

**`src/ui/grex_overlay.rs`** — the overlay widget and its state.

```rust
pub struct GrexOverlayState {
    pub editor: crate::input::editor::Editor,
    pub options: crate::grex_integration::GrexOptions,
    pub generated_pattern: Option<String>,
    pub generation_counter: u64,
    pub debounce_deadline: Option<std::time::Instant>,
}

pub fn render(f: &mut ratatui::Frame, area: ratatui::layout::Rect, state: &GrexOverlayState);
```

The overlay owns its own `Editor` rather than reusing the main `regex_input` or `test_input`, because its lifecycle is tied to the overlay being open — closing the overlay must discard its contents without touching the main editors.

**`tests/grex_tests.rs`** — unit tests for the wrapper, snapshot tests for overlay rendering, and one end-to-end test that exercises the full Ctrl+X → type → debounce → Tab flow.

### Modified files

**`Cargo.toml`** — add `grex = "1"`. Exact version will be pinned to the latest stable at implementation time.

**`src/input/mod.rs`** — add `Action::OpenGrex` variant. Bind `Ctrl+X` to it in `key_to_action`. No new Action variants for the Alt+d/a/c flag toggles — those are handled directly inside the overlay's own key handler since they are only meaningful when the overlay is open.

**`src/app.rs`** — add `grex: Option<GrexOverlayState>` field to `OverlayState`. Add a `tokio::sync::mpsc::UnboundedSender<(u64, String)>` / `UnboundedReceiver<(u64, String)>` pair owned by `App` for delivering grex results from `spawn_blocking` tasks back to the main event loop. Implement:

- `App::handle_action(Action::OpenGrex)` — set `overlay_state.grex = Some(GrexOverlayState::default())`.
- `App::tick()` — check the overlay's `debounce_deadline`; if set and passed, spawn the grex task. Drain the result channel and apply matching results to the current generation counter.

**`src/main.rs`** — when `app.overlay_state.grex.is_some()`, route keystrokes into a new `handle_grex_overlay_key(app, key)` function that:

- `Tab` → if `generated_pattern` is `Some` and non-empty, call `app.regex_input.set_content(pattern)`, then `app.recompute()`, then `app.overlay_state.grex = None`. If no pattern is available yet, Tab is a silent no-op.
- `Esc` → `app.overlay_state.grex = None`. Discard.
- `Alt+d` / `Alt+a` / `Alt+c` → flip the corresponding flag on `overlay.options`; reset `debounce_deadline` so the next tick regenerates with the new options.
- Anything else → dispatch to `overlay.editor` via the same `Editor` methods used by the main pattern editor.

**`src/ui/mod.rs`** — after rendering the main UI, if `overlay_state.grex.is_some()`, call `grex_overlay::render(...)` with a centered popup rect. Update `build_help_pages()` page 0 to list `Ctrl+X — Generate regex from examples`.

## Data flow — one generation cycle

1. User presses `Ctrl+X` → `App::handle_action(OpenGrex)` → `overlay_state.grex = Some(GrexOverlayState::default())` with the default flags. No pattern is generated yet.
2. User types in the overlay editor. Each keystroke is routed to `overlay.editor`. After every edit, `App` sets `overlay.debounce_deadline = Some(Instant::now() + 150ms)`.
3. On each tokio tick, `App::tick()` checks: is `overlay.debounce_deadline` set and passed? If yes:
   - `overlay.generation_counter += 1`
   - Clone examples: `let examples = overlay.editor.content().lines().map(String::from).collect::<Vec<_>>();`
   - Clone options: `let options = overlay.options;`
   - Capture `let counter = overlay.generation_counter;` and `let tx = self.grex_result_tx.clone();`
   - `tokio::task::spawn_blocking(move || { let pattern = grex_integration::generate(&examples, options); let _ = tx.send((counter, pattern)); });`
   - `overlay.debounce_deadline = None;`
4. The same tick then drains `self.grex_result_rx`. For each received `(counter, pattern)`:
   - If `counter == overlay.generation_counter`, set `overlay.generated_pattern = Some(pattern)`.
   - Otherwise drop silently (stale result from a since-superseded generation).
5. User presses `Tab`:
   - If `overlay.generated_pattern.as_deref().is_some_and(|p| !p.is_empty())` → load into `regex_input`, recompute, close overlay.
   - Otherwise → silent no-op. User must wait for the first generation to complete.
6. User presses `Esc` → `overlay_state.grex = None`. All state discarded, no save, no history entry.

### Concurrency rationale

`spawn_blocking` is the right tool because grex is CPU-bound synchronous code — wrapping it in a future buys nothing. The 60fps render loop must stay responsive regardless.

The generation counter pattern sidesteps cancellation (which `spawn_blocking` does not support cleanly): stale tasks run to completion, but their results are dropped on arrival. The maximum wasted work is one in-flight grex call per rapid-edit burst, which is negligible for typical input sizes (well under 100 examples).

## Layout

### Populated state

```
┌─ Generate Regex from Examples ─────────────────────────────────┐
│                                                                │
│ Flags:  [D]igit ●   [A]nchors ●   [C]ase-insensitive ○         │
│                                                                │
│ Examples (one per line):                                       │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ hello@example.com                                          │ │
│ │ foo.bar@test.org                                           │ │
│ │ admin+tag@domain.co.uk                                     │ │
│ │                                                            │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ Generated pattern:                                             │
│ ^[a-z.+@]+\.[a-z.]+$                                           │
│                                                                │
│ Alt+d/a/c: toggle flags    Tab: accept    Esc: cancel          │
└────────────────────────────────────────────────────────────────┘
```

- Flag indicators: `●` = on (cyan), `○` = off (dim gray). Mirrors the main-UI flag row.
- Pattern preview is rendered via the existing `syntax_highlight::highlight()` — free reuse, no new code, and visually consistent with the main regex editor.
- Editor area scrolls vertically if the examples exceed its visible height.

### Empty state

- The examples editor shows dimmed placeholder text: `Enter one example per line. Tab to accept.` — replaced by real content on first keystroke.
- The pattern preview line shows dimmed `(none yet)` until the first debounced generation completes.

### Sizing

- Width: `min(terminal_width * 0.8, 80)`
- Height: `min(terminal_height - 6, 18)`
- Centered on both axes.
- Fixed chrome: ~8 lines (top border, title, blank, flag row, blank, "Examples:" label, editor-frame top border, editor-frame bottom border, blank, "Generated pattern:" label, pattern line, blank, footer, bottom border). Remaining vertical space goes to the editor body.

## Testing strategy

### Unit tests — `tests/grex_tests.rs`

- `empty_input_returns_empty_pattern` — `generate(&[], GrexOptions::default())` returns `""`. No panic.
- `single_example_returns_escaped_literal` — `generate(&["hello".into()], options)` returns grex's known stable output for that single input.
- `digit_flag_generates_digit_class` — inputs like `["a1", "b2", "c3"]` with `digit=true` — assert the result contains `\d`.
- `anchors_flag_adds_caret_dollar` — assert the result starts with `^` and ends with `$`.
- `case_insensitive_flag_includes_case_modifier` — assert the result contains `(?i)` or an equivalent case class.
- `flags_off_returns_literal_alternation` — all flags off on `["a", "b", "c"]` → expect a plain alternation form.

Pick inputs where grex's output is documented as stable across versions. Avoid asserting on patterns that rely on substring detection or repetition detection internals, since those can drift between grex releases.

### Snapshot tests — `tests/grex_tests.rs`

Use `insta`, mirroring the pattern from `tests/ui_tests.rs`:

- `grex_overlay_empty` — fresh overlay, placeholder visible, dimmed `(none yet)`.
- `grex_overlay_populated` — 3 examples, default flags, non-empty pattern preview.
- `grex_overlay_all_flags_off` — 3 examples, all toggles off.
- `grex_overlay_case_insensitive_on` — mixed-case examples, case flag on.

### End-to-end test — `tests/grex_tests.rs`

```rust
#[tokio::test(start_paused = true)]
async fn grex_roundtrip_loads_pattern_into_regex_editor() {
    let mut app = App::new_for_tests();
    app.handle_action(Action::OpenGrex);
    for ch in "foo\nbar\nbaz".chars() {
        app.dispatch_overlay_char(ch);
    }
    tokio::time::advance(Duration::from_millis(200)).await;
    app.tick();
    // Drain grex result channel deterministically under paused time.
    app.drain_grex_results_blocking();
    app.dispatch_overlay_key(KeyCode::Tab);
    assert!(app.overlay_state.grex.is_none());
    assert_eq!(app.regex_input.content(), EXPECTED_PATTERN);
}
```

The `EXPECTED_PATTERN` constant is determined during implementation by running grex once on the exact input set and pinning the output. If grex output drifts in a later crate version, this test will catch it and the constant gets updated as part of the dep bump.

### Vim mode tests — `tests/vim_tests.rs`

- `ctrl_x_opens_grex_in_vim_normal_mode` — launch with vim mode enabled, send `Ctrl+X`, assert `overlay_state.grex.is_some()`.
- `plain_x_still_deletes_char_in_vim_normal_mode` — regression guard; ensures adding the Ctrl+X binding did not steal the plain `x` key path.

## Out of scope for v0.11.0

The following are explicitly deferred and documented here so they are not accidentally pulled into the MVP:

- Aggressive generalization toggles (`\w+`, `\s+`)
- `--min-repetitions` / `--min-substring-length` threshold controls
- Importing examples from a file path or from stdin
- Saving an example set to a workspace file or recipe
- Clipboard export of the generated pattern directly from the overlay (the existing `Ctrl+Y` on the main regex editor after Tab already covers this)
- Shareable permalinks for overlay state (tracked separately in ROADMAP as an optional v0.11.x round-out)
- A user-saved pattern library (tracked separately in ROADMAP as an optional v0.11.x round-out)

## Follow-ups after ship

- Update `README.md` feature list to mention `Ctrl+X` grex generation
- Update help overlay page 0 already covered in the implementation task
- Regenerate the demo GIF to include the grex overlay step (`assets/demo.tape`)
- Update `CLAUDE.md` architecture section to list `src/grex_integration.rs` and `src/ui/grex_overlay.rs`
