#1882·rio

Text expansion tools (Raycast, Espanso) fail due to synthetic backspace events being dropped

Author: carpagCreated Aug 17, 2026Updated Sep 19, 2026

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

  1. User types a snippet keyword (e.g. ;ssh1) in Rio
  2. The expansion tool (Raycast/Espanso) detects the keyword
  3. The tool fires N synthetic backspace CGEvents to erase the keyword (where N = keyword length)
  4. These backspaces arrive at near-zero interval (same millisecond)
  5. Rio reads them as a single bundled key group (chord) and drops most of them
  6. 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

  1. Install Raycast on macOS
  2. Create a snippet with keyword ;test that expands to hello world
  3. Open Rio terminal
  4. Type ;test in the shell
  5. Observe that the keyword is not fully deleted — output is garbled (e.g. ;testhello world or ;teshello world)
  6. Repeat in Ghostty or iTerm2 — works correctly