Skip to content

Stage 8: UI Automation integration (fixes #1)#2

Merged
zergius-eggstream merged 5 commits intomainfrom
feat/stage-8-uia
Apr 18, 2026
Merged

Stage 8: UI Automation integration (fixes #1)#2
zergius-eggstream merged 5 commits intomainfrom
feat/stage-8-uia

Conversation

@zergius-eggstream
Copy link
Copy Markdown
Owner

Draft PR for Stage 8 work tracked in #1.

What's in this PR so far

Commit 1 — infra only:

  • Cargo.toml: enabled Win32_UI_Accessibility and Win32_System_Com
    features in the windows crate.
  • src/config.rs: new [general] use_uia = true toggle (default on).
  • src/ui.rs: new checkbox in the settings window — "Use UI Automation
    for reading selections", with caption "(uncheck if switching behaves
    oddly in some program)".

No behavior change yet: the toggle is saved to config but nothing reads
it. UIA wiring lands in the next commits.

What still needs to land before merge

  • src/uia.rs — safe Rust wrappers over IUIAutomation,
    IUIAutomationElement, IUIAutomationTextPattern,
    IUIAutomationTextRange. COM init/shutdown lifecycle.
  • Wire UIA read path into perform_conversion and
    perform_word_conversion. Fall back to clipboard+SendInput when
    UIA isn't available or the user disabled it.
  • Manual test matrix: Notepad (old + Win11), WordPad, VS Code,
    Chrome address bar and web textarea, Word, Telegram, Outlook.

Out of scope (tracked separately)

  • "Convert all" via UIA DocumentRange — separate future feature.
  • ValuePattern.SetValue for single-line fields — optimization, later.

Acceptance criteria — see #1 for the full list.

web-flow and others added 5 commits April 18, 2026 10:31
…tings UI

Prep work for #1. No UIA behavior yet; just the scaffolding:

- Cargo.toml: enable `Win32_UI_Accessibility` and `Win32_System_Com`
  features in the `windows` crate.
- config: new `[general] use_uia = true` toggle (default on). Old
  configs without the field deserialize fine via the serde default.
- settings window: new checkbox under "Start with Windows" titled
  "Use UI Automation for reading selections", with a sub-caption
  "(uncheck if switching behaves oddly in some program)".
  Window height bumped by 46 px to fit the checkbox and caption.
- save_settings: reads the new checkbox and persists to config.

Refs #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Primary user-visible win: the smart-copy bug in Notepad Win11, VS Code,
and Electron editors is gone when UIA is available. `perform_conversion`
now asks UIA for the actual selection before touching the clipboard, so
an unselected current line can never be confused for an explicit
selection.

- New `src/uia.rs`: thread-local COM init, safe wrappers for
  `IUIAutomation`, `IUIAutomationElement`, `IUIAutomationTextPattern`,
  `IUIAutomationTextRange`. COM objects are apartment-threaded, so the
  cache lives in a `thread_local!` `RefCell` on the main message-loop
  thread where init and all read calls happen.
- `main.rs`: call `uia::set_enabled(config.general.use_uia)` then
  `uia::init()` at startup. If the checkbox is off or init fails,
  the helpers short-circuit to `None` and callers fall back.
- `conversion.rs`:
  * `perform_conversion` tries `uia::get_selected_text()` first.
    Falls back to clipboard + `Ctrl+C` only when UIA didn't return
    text (no focused TextPattern element, empty selection, or
    toggle off).
  * `perform_word_conversion` tries `uia::select_word_at_caret()`,
    which expands the caret's zero-length range to a word unit
    and calls `Select()` — the subsequent paste replaces just that
    word. Falls back to `Ctrl+Shift+Left` + `Ctrl+C` when UIA
    isn't available.
- Paste still goes through clipboard + `Ctrl+V`. This is deliberate:
  `ValuePattern.SetValue` support varies wildly across apps, while
  clipboard paste is universally reliable, and by this point we
  already know the real selection so there's no smart-copy risk.

Refs #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-reported bugs with the UIA word path:

1. In Notepad Win11 and Word, UIA's TextUnit_Word includes the
   trailing space, so after paste the caret lands past the space,
   inside the next word. Cycling then re-converts the wrong word.

2. UIA's TextUnit_Word splits on punctuation. A converted "ghb,jh"
   (ua "прибор" → en) is two UIA-words. Pressing the hotkey again
   cycles only one half: user sees "ghb,ор" instead of round-tripping
   back to "прибор".

Switch to whitespace-delimited definition: expand the caret range
to TextUnit_Line, read line text, locate the caret column by reading
the prefix range, then walk outward in Rust until whitespace is hit
on each side. Then shrink line_range's endpoints by the measured
prefix/tail char counts to end up with exactly [start_col, end_col).

Result:
- `ghb,jh` is one unit → converts to `прибор` round-trip.
- Trailing/leading whitespace never included, caret lands inside
  the pasted text, cycling works in Notepad/Word/everywhere.
- Caret on whitespace → returns None, falls back to clipboard flow
  or does nothing (matches earlier no-op semantics).

Refs #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notepad++ and a few other editors don't expose a UIA TextPattern on
the focused edit control, so the word-conversion fallback path kicked
in — which used OS Ctrl+Shift+Left. That stops at punctuation, so
"ghb,jh" (round-trip of "прибор") ended up as two separate words and
cycling only converted one half.

The UIA path already uses a whitespace-delimited definition of "word"
(commit 436b980). This commit teaches the clipboard fallback the same
trick, without needing UIA:

- `input::send_select_to_line_start()` — Shift+Home, one keypress
  that selects from the caret all the way to the line start.
- Read the clipboard, walk the selected chars from the right until
  the first whitespace → that's where our word begins.
- `input::send_select_n_right(n)` — Shift+Right × N to shrink the
  selection from the left, so only the word remains highlighted.
- Then hand off to `convert_and_paste` as usual; Ctrl+V replaces
  the shrunk selection with the converted text.

Result: in Notepad++ (and any editor without UIA), "прибор" now
round-trips correctly — `[fallback] word:` line in the log replaces
the old `[uia] selected word:`.

Dead code: the original `send_select_word_left` (Ctrl+Shift+Left) is
no longer used anywhere and has been removed.

Refs #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the crate version and snapshots the CHANGELOG. The 0.3.0
section covers everything on the feat/stage-8-uia branch —
infrastructure, UIA read path, whitespace-bounded word selection
(both UIA and clipboard-fallback flavours), the About dialog, the
MIT license, and the GitHub Actions workflows.

Build metadata (Cargo.toml version + build.rs timestamp) now lines
up with the tag the next commit will create: when the user clicks
"About..." in the tray they'll see "LightSwitch v0.3.0".

Refs #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zergius-eggstream zergius-eggstream marked this pull request as ready for review April 18, 2026 08:38
@zergius-eggstream zergius-eggstream merged commit bf0fc5c into main Apr 18, 2026
1 check passed
@zergius-eggstream zergius-eggstream deleted the feat/stage-8-uia branch April 18, 2026 08:39
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.

2 participants