Wayland terminal injection choice #6

Closed
opened 2026-11-05 15:23:07 +00:00 by xavierk · 1 comment
Owner

Part of #1
Blocked by: #5

Question

Which Wayland text-injection mechanism puts dictation at the cursor in Ghostty under niri without touching the clipboard, and what is the fallback order when it fails?

Part of #1 Blocked by: #5 ## Question Which Wayland text-injection mechanism puts dictation at the cursor in Ghostty under niri without touching the clipboard, and what is the fallback order when it fails?
xavierk added the wayfinder:grilling label 2026-11-05 15:23:07 +00:00
xavierk self-assigned this 2026-11-05 16:04:05 +00:00
Author
Owner

Wayland terminal injection choice — decision

Decision: Dictation into Ghostty under niri goes through the RemoteDesktop portal (NotifyKeyboardKeysym) as the primary path, with this fallback order when a level fails: wtype raw type, then ydotool, then xdotool XWayland (XWayland windows only), then clipboard-copy with manual paste as the last resort. IBus is explicitly excluded on niri. Clipboard paste uses Ctrl+Shift+V in Ghostty via the existing terminal auto-detect, and the text_injection.backend setting plus VOCALINUX_FORCE_BACKEND stay as the per-machine override.

Provenance: Decided by agent under the user's explicit delegation for this run (user acts as orchestrator and decision maker; no human was interviewed). Grounded in the map Destination and Decisions-so-far, the niri/Dank decision, and the injection code read this session.

Rationale and evidence:

  • Ghostty runs as a native Wayland client under niri, so anything X11-only cannot reach it. The only Wayland-sanctioned key path that needs no /dev/uinput and no helper daemon is the RemoteDesktop portal, whose inject_text maps every character (including non-Latin-1 via Unicode keysyms) through char_to_keysym (src/vocalinux/text_injection/remote_desktop_portal.py). It never touches the clipboard, which is exactly what the ticket asks for. Autodetection already probes the portal first on non-KDE compositors (src/vocalinux/text_injection/text_injector.py, _check_dependencies).
  • IBus is out on niri: niri is on the _IBUS_UNBRIDGED_COMPOSITORS denylist, where commit_text() reports success while the text is silently dropped; the code falls back to virtual-keyboard injection there instead (text_injector.py, _wayland_compositor_bridges_ibus). A pinned VOCALINUX_FORCE_BACKEND=ibus even logs an explicit silent-failure warning on such compositors.
  • First fallback is wtype raw type (wtype -- <chunk>), not ydotool: wtype sends layout-independent keysyms with no clipboard contact, keeping the first two levels clipboard-free. ydotool's preferred mode is a clipboard paste (one chord instead of per-character evdev codes), which only touches the clipboard transiently and restores the previous content after a short delay — acceptable as a fallback, not as the primary. The existing startup probe already demotes wtype automatically when the compositor rejects the virtual-keyboard protocol, so this order degrades on its own.
  • Ghostty terminal handling is already in place: ghostty is a recognised terminal token and hint (src/vocalinux/text_injection/focused_window.py, TERMINAL_APP_TOKENS, covered by tests/test_focused_window.py::test_ghostty_class with com.mitchellh.ghostty), niri exposes the focused window via niri msg -j focused-window (_focused_window_wayland), and _should_use_terminal_paste / _clipboard_paste_command send Ctrl+Shift+V whenever a clipboard-paste path fires in a terminal, with a Settings paste_shortcut override.
  • Trigger side is settled by the niri/Dank decision: niri keybinds spawn the existing D-Bus toggle. _wait_for_modifiers_released() runs ahead of every backend (_inject_with_wayland_tool), so a still-held push-to-talk modifier cannot corrupt the paste chord — that invariant stays.
  • xdotool XWayland is a documented dead end for native Ghostty (it reaches XWayland windows only) but stays in the order for XWayland apps; when nothing delivers, the existing clipboard-copy plus notify-send manual-paste path is the last resort.

Consequences for later tickets:

  • Rename mechanics: the rename must cover the user-visible setting names (text_injection.backend, paste_shortcut) and the VOCALINUX_FORCE_BACKEND escape hatch consistently with whatever binary/config prefix is chosen.
  • Python performance baseline: measure portal vs wtype injection latency as part of the baseline, since the order above assumes the portal's session handshake cost is paid once (restore token persisted on interface v2, re-prompted per start on v1).
  • New fog for the map: first-run UX of the portal's one-time permission prompt under niri (when it appears, what the user must click) is not yet specified.
# Wayland terminal injection choice — decision **Decision:** Dictation into Ghostty under niri goes through the RemoteDesktop portal (`NotifyKeyboardKeysym`) as the primary path, with this fallback order when a level fails: wtype raw type, then ydotool, then xdotool XWayland (XWayland windows only), then clipboard-copy with manual paste as the last resort. IBus is explicitly excluded on niri. Clipboard paste uses Ctrl+Shift+V in Ghostty via the existing terminal auto-detect, and the `text_injection.backend` setting plus `VOCALINUX_FORCE_BACKEND` stay as the per-machine override. **Provenance:** Decided by agent under the user's explicit delegation for this run (user acts as orchestrator and decision maker; no human was interviewed). Grounded in the map Destination and Decisions-so-far, the niri/Dank decision, and the injection code read this session. **Rationale and evidence:** - Ghostty runs as a native Wayland client under niri, so anything X11-only cannot reach it. The only Wayland-sanctioned key path that needs no `/dev/uinput` and no helper daemon is the RemoteDesktop portal, whose `inject_text` maps every character (including non-Latin-1 via Unicode keysyms) through `char_to_keysym` (`src/vocalinux/text_injection/remote_desktop_portal.py`). It never touches the clipboard, which is exactly what the ticket asks for. Autodetection already probes the portal first on non-KDE compositors (`src/vocalinux/text_injection/text_injector.py`, `_check_dependencies`). - IBus is out on niri: niri is on the `_IBUS_UNBRIDGED_COMPOSITORS` denylist, where `commit_text()` reports success while the text is silently dropped; the code falls back to virtual-keyboard injection there instead (`text_injector.py`, `_wayland_compositor_bridges_ibus`). A pinned `VOCALINUX_FORCE_BACKEND=ibus` even logs an explicit silent-failure warning on such compositors. - First fallback is wtype raw type (`wtype -- <chunk>`), not ydotool: wtype sends layout-independent keysyms with no clipboard contact, keeping the first two levels clipboard-free. ydotool's preferred mode is a clipboard paste (one chord instead of per-character evdev codes), which only touches the clipboard transiently and restores the previous content after a short delay — acceptable as a fallback, not as the primary. The existing startup probe already demotes wtype automatically when the compositor rejects the virtual-keyboard protocol, so this order degrades on its own. - Ghostty terminal handling is already in place: `ghostty` is a recognised terminal token and hint (`src/vocalinux/text_injection/focused_window.py`, `TERMINAL_APP_TOKENS`, covered by `tests/test_focused_window.py::test_ghostty_class` with `com.mitchellh.ghostty`), niri exposes the focused window via `niri msg -j focused-window` (`_focused_window_wayland`), and `_should_use_terminal_paste` / `_clipboard_paste_command` send Ctrl+Shift+V whenever a clipboard-paste path fires in a terminal, with a Settings `paste_shortcut` override. - Trigger side is settled by the niri/Dank decision: niri keybinds spawn the existing D-Bus toggle. `_wait_for_modifiers_released()` runs ahead of every backend (`_inject_with_wayland_tool`), so a still-held push-to-talk modifier cannot corrupt the paste chord — that invariant stays. - xdotool XWayland is a documented dead end for native Ghostty (it reaches XWayland windows only) but stays in the order for XWayland apps; when nothing delivers, the existing clipboard-copy plus `notify-send` manual-paste path is the last resort. **Consequences for later tickets:** - [Rename mechanics](https://git.bongbetic.com/xavierk/voculinux/issues/8): the rename must cover the user-visible setting names (`text_injection.backend`, `paste_shortcut`) and the `VOCALINUX_FORCE_BACKEND` escape hatch consistently with whatever binary/config prefix is chosen. - [Python performance baseline](https://git.bongbetic.com/xavierk/voculinux/issues/9): measure portal vs wtype injection latency as part of the baseline, since the order above assumes the portal's session handshake cost is paid once (restore token persisted on interface v2, re-prompted per start on v1). - New fog for the map: first-run UX of the portal's one-time permission prompt under niri (when it appears, what the user must click) is not yet specified.
Sign in to join this conversation.