Skip to content

feat(ocr): show scan progress and support whole-image recognition - #350

Merged
devmobasa merged 4 commits into
mainfrom
feat/ocr-scan-overlay
Aug 23, 2026
Merged

feat(ocr): show scan progress and support whole-image recognition#350
devmobasa merged 4 commits into
mainfrom
feat/ocr-scan-overlay

Conversation

@devmobasa

Copy link
Copy Markdown
Owner

Summary

  • Show a sweeping highlight over the selected region while OCR is running.
  • Show a short result card with the outcome and character count.
  • Let Ctrl+A recognize the whole displayed image without requiring a drag.
  • Allow finished result cards to be dismissed by keyboard, pointer, touch, or stylus.
  • Respect reduced-motion settings with a static indicator and no continuous repainting.

Privacy and reliability

  • The overlay never displays recognized text; text goes directly from the OCR worker to the clipboard.
  • Progress starts only after the OCR controller accepts the request.
  • Results are correlated by request ID, so stale completions cannot update a newer scan.
  • Input cannot accidentally dismiss recognition while it is still running.
  • Dragged-region and whole-image OCR share the same submission path.

Documentation

  • Document the Ctrl+A behavior, progress indicator, result card, dismissal rules, and reduced-motion behavior.

Recognition ran on a worker with nothing on screen to say the region had even
been read, and a toast afterwards that gave no sense of where it came from. A
band now sweeps the rectangle while the worker runs, and a short card beside it
reports the outcome.

The card carries an outcome and a character count, never the recognized text.
`src/ocr` keeps screen contents out of application state entirely — the worker
publishes to the clipboard inside one stack frame and reports back only a count
— and painting a transcript here would put exactly what that invariant exists
to exclude into the overlay's own UI.

`Ctrl+A` now takes the whole displayed image for recognition as well as for
capture. Reading a full screen of text should not need a drag across the whole
output. Measure is the only purpose left out, having nothing to submit.

Details worth stating:

- The hand-over deadline is fixed when recognition settles, not recomputed per
  tick. A deadline derived from the current elapsed time moves along with it and
  is only ever met on an exact sweep boundary, which a frame clock does not hit:
  the band would sweep forever and the card would never appear.
- The overlay starts only for a request the controller accepted, and carries its
  id. A refused submission produces no completion, and an overlay waiting on one
  would sweep until something dismissed it.
- Under `[ui] reduced_motion` the region takes a static tint instead of a moving
  band, the result shows as soon as it arrives rather than waiting out an
  animation that never ran, and the overlay asks for no frames — one deadline
  expires the card instead of pinning a repaint for its whole life.
- Dismissal takes a finished card, not a running sweep: the sweep is progress
  feedback for work still in flight, and dropping it would discard the result
  about to arrive. Pointer, touch, stylus and keyboard all dismiss, the stylus
  before its surface-specific routing so a press on the Review bar or a toolbar
  counts too.

Both entry points share one submit transaction, so a fix to the dragged-region
path cannot leave whole-image recognition behind.
Whole-image recognition is a keystroke with nothing on screen to suggest it,
so `Ctrl+A` was findable only by reading the documentation.

The gap was wider than one key. Recognition never went through the capture
picker at all — `render_capture_picker` gates on capture and measure purposes,
so OCR falls to its own overlay of a scrim and scan brackets — and every
affordance the picker gained, the hint strip included, simply never reached it.

The strip is now shared rather than copied, so the two selectors cannot drift
apart in wording or position, and it honours the same `show_legend` setting.
Its text is its own: recognition's selection policy sets `allow_square: false`,
so advertising `Shift: square` would be a lie, and `Ctrl+A` reads everything
rather than selecting everything.

Dismissal and damage both came for free, which is the sign the seam was in the
right place: `begin_selection` already clears the legend for any purpose, and
`mark_region_dirty` already marks full damage for every purpose but measure, so
the frame that dismisses the strip is the frame that clears it.
Recognition shipped without a chord, and the stated reason — `O` is the orange
quick colour — read as "we could not find a key" rather than "it should not
have one". Now that it has a scan overlay, a hint strip, whole-image `Ctrl+A`,
a palette entry and a toolbar button, being keyless was the last thing making
it feel optional.

`Ctrl+Shift+X` for e**X**tract text. Every mnemonic was taken: `Ctrl+Shift+O`
captures the active window, `Ctrl+Alt+O` opens the capture folder, `R` is spoken
for in both modifier sets, and `Ctrl+Shift+T` returns to transparent. This keeps
recognition in the `Ctrl+Shift+` block the rest of the capture family lives in,
on a letter nothing else wants.

Two candidates were rejected for reasons a free-letter search does not show:
`Ctrl+Shift+U` is the IBus Unicode entry sequence, which collides with text
input, and `Ctrl+Alt+T` opens a terminal nearly everywhere — the overlay holds
keyboard focus and would win, quietly doing something else with a chord that
deep in muscle memory.

Additive, so nothing existing changes: anyone who already bound the action keeps
their chord, and the new default stands down through the skipped-default path.

The configurator's conflict fixture used `Ctrl+Shift+X` precisely because it was
free, and its claimant count is exact. It moves to a chord that is still
unclaimed and now asserts that up front, so the next default to take it fails
there saying why.
Three leftovers from binding recognition to Ctrl+Shift+X.

Two comments still said the action was unbound, and one of them argued against
ever giving it a default. Both now describe the binding that exists.

The configurator's conflict fixtures used Ctrl+Shift+X because nothing claimed
it. One of them was more than untidy: `replace_removes_only_the_contested_binding`
runs a replace that strips the contested chord from every claimant, so with a
default on that chord it was silently rewriting `copy_text_from_screen` — a
field the test never mentions and never asserts. It passed while exercising a
polluted scenario. Every draft-based fixture moves to Ctrl+Shift+Q, which the
conflict test now asserts is unclaimed so the next default to take it fails
there with the reason.

Chord parsing and formatting tests keep Ctrl+Shift+X: they take no draft and
one of them is about that chord's canonical spelling.

`render_region_legend` only forwarded to `draw_legend`, so the rename replaces
the pair.
@devmobasa
devmobasa merged commit 3a08ca2 into main Aug 23, 2026
2 checks passed
@devmobasa
devmobasa deleted the feat/ocr-scan-overlay branch August 24, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant