Text expansion tools (Raycast, Espanso) fail due to synthetic backspace events being dropped
Text expansion tools (Raycast, Espanso) fail due to synthetic backspace events being dropped
Summary
Text expansion tools like Raycast Snippets and Espanso fail to expand correctly in Rio on macOS. When a snippet trigger keyword is typed, the expansion tool fires multiple synthetic CGEvent backspace key events in rapid succession (sub-millisecond intervals) to delete the keyword, then injects the expanded text. Rio's macOS key event pipeline processes these rapid backspaces as a bundled chord rather than individual sequential keystrokes, causing most of them to be dropped — the keyword is not fully deleted before the expansion text arrives.
Environment
- Rio version: 0.5.25
- OS: macOS (Apple Silicon)
- Affected tools: Raycast Snippets (auto-expansion), Espanso (Inject backend)
- Comparison: Ghostty and iTerm2 handle this correctly
What happens under the hood
- User types a snippet keyword (e.g.
;ssh1) in Rio - The expansion tool (Raycast/Espanso) detects the keyword
- The tool fires N synthetic backspace
CGEvents to erase the keyword (where N = keyword length) - These backspaces arrive at near-zero interval (same millisecond)
- Rio reads them as a single bundled key group (chord) and drops most of them
- Rio only deletes 0-1 characters, then the expanded snippet text is injected immediately after the leftover keyword characters
Expected behavior
Each synthetic backspace should be processed as an individual, sequential key event — deleting one character per event — the same way Ghostty and iTerm2 handle them. The keyword should be fully erased before the expansion text arrives.
Actual behavior
Most synthetic backspaces are dropped. The keyword characters remain, and the expanded text is inserted right next to them, producing garbled output like ;ssh1ssh user@host1 instead of ssh user@host1.
Why Ghostty handles this correctly
Ghostty uses native macOS event queues that process each CGEvent individually through the standard NSTextInputClient pipeline. Rio's GPU-driven event loop in rio-window (src/platform_impl/macos/view.rs) samples inputs at a rate that causes rapid sequential events to coalesce.
Relevance to PR #1847
PR #1847 ("macos: decide IME key handling once per event, ghostty style") restructures the NSTextInputClient flow to accumulate insertText: calls and decide once in keyDown after interpretKeyEvents completes. While that PR targets IME handling, the architectural change — processing per-event instead of across scattered callbacks — may also affect how rapid synthetic key events are handled. Worth verifying whether the accumulator pattern helps or whether a separate timing/queueing fix is needed.
Raycast settings that do NOT fix this
The following Raycast snippet settings were tested and do not resolve the issue:
- Injection Delay — set to Long and Extra Long (gives time before expansion text, but backspaces still arrive as a burst)
- Response Time — set to Delayed and Extended (controls clipboard processing speed, not key event spacing)
- Expansion Mode — tried Immediately and After Delimiter (does not change how backspaces are delivered)
Reproduction
- Install Raycast on macOS
- Create a snippet with keyword
;testthat expands tohello world - Open Rio terminal
- Type
;testin the shell - Observe that the keyword is not fully deleted — output is garbled (e.g.
;testhello worldor;teshello world) - Repeat in Ghostty or iTerm2 — works correctly
Source: raphamorim/rio