The decided injection behaviour becomes pinned, observable fact. Behaviour tests lock the fallback order per desktop class — portal first only where a backend implements it, then direct typing, then the virtual-input tool, then XWayland-only injection, then manual paste — including IBus exclusion on niri, terminal paste-shortcut selection, and waiting for held modifiers to release. User documentation states the niri first-run experience plainly (no permission prompt exists there; typing goes direct), never recommends pinning the portal on niri, and describes the portal grant, persistence, and deny behaviour only for desktops that have one. Where current behaviour already matches the decision, this ticket records that evidence and adds only the missing tests or words.
Acceptance criteria
Tests demonstrate the full fallback order and each demotion trigger without real compositors, including the niri path never selecting IBus or the portal.
Tests demonstrate terminal detection choosing the terminal paste shortcut and a held modifier delaying delivery until release.
User documentation describes first dictation on niri (no prompt, direct typing), on GNOME/KDE (one-time grant, remembered where supported, deny falls back without losing the dictation), using the renamed setting and override names.
Any place current behaviour already matched the decision is recorded on this ticket with the test or code evidence, and changed code is limited to real gaps found.
## Parent
[Spec: Voculinux — Void Linux (glibc) Wayland/niri port of Vocalinux](https://git.bongbetic.com/xavierk/voculinux/issues/16)
## What to build
The decided injection behaviour becomes pinned, observable fact. Behaviour tests lock the fallback order per desktop class — portal first only where a backend implements it, then direct typing, then the virtual-input tool, then XWayland-only injection, then manual paste — including IBus exclusion on niri, terminal paste-shortcut selection, and waiting for held modifiers to release. User documentation states the niri first-run experience plainly (no permission prompt exists there; typing goes direct), never recommends pinning the portal on niri, and describes the portal grant, persistence, and deny behaviour only for desktops that have one. Where current behaviour already matches the decision, this ticket records that evidence and adds only the missing tests or words.
## Acceptance criteria
- [ ] Tests demonstrate the full fallback order and each demotion trigger without real compositors, including the niri path never selecting IBus or the portal.
- [ ] Tests demonstrate terminal detection choosing the terminal paste shortcut and a held modifier delaying delivery until release.
- [ ] User documentation describes first dictation on niri (no prompt, direct typing), on GNOME/KDE (one-time grant, remembered where supported, deny falls back without losing the dictation), using the renamed setting and override names.
- [ ] Any place current behaviour already matched the decision is recorded on this ticket with the test or code evidence, and changed code is limited to real gaps found.
## Blocked by
- [Rename user-visible identity to voculinux with one-time legacy import](https://git.bongbetic.com/xavierk/voculinux/issues/18)
Current behaviour already matches the decision in every path I exercised, so src/ is unchanged — the change is a new pinning suite plus user words:
tests/test_injection_ladder.py (8 tests, all passing with the repo suite): niri skips IBus even with the daemon running, niri never selects the portal where no backend implements it, the niri first rung is direct wtype typing with the clipboard untouched, GNOME selects the portal where a backend exists, a portal deny demotes to direct typing with the full dictation kept (session not retried), terminal auto-detect chooses Ctrl+Shift+V end to end, a held modifier delays the XWayland paste until release, and total failure still leaves the dictation on the clipboard with the manual-paste notice.
docs/USER_GUIDE.md gained 'First dictation on niri, GNOME, and KDE': niri has no prompt and typing goes direct (never pin portal there; IBus stays excluded), GNOME/KDE grant is lazy keyboard-only at first dictation with an owner-only restore token where supported, and deny demotes for the run without losing the dictation. Uses text_injection.backend and VOCULINUX_FORCE_BACKEND.
One judgement call recorded: where both wtype and ydotool are installed, the existing startup preference (portal, ydotool, wtype, xdotool) is kept as-is — the ladder contract pinned here is the rung order and its demotion triggers, and the niri effective-first-rung holds through direct typing with the clipboard preserved. Reordering the two typing tools would touch Flatpak/KDE paths and is left for a follow-up if hardware acceptance asks for it.
Ticket #31 evidence (local branch feature/31, commit e6f7938; no push yet).
Current behaviour already matches the decision in every path I exercised, so src/ is unchanged — the change is a new pinning suite plus user words:
- tests/test_injection_ladder.py (8 tests, all passing with the repo suite): niri skips IBus even with the daemon running, niri never selects the portal where no backend implements it, the niri first rung is direct wtype typing with the clipboard untouched, GNOME selects the portal where a backend exists, a portal deny demotes to direct typing with the full dictation kept (session not retried), terminal auto-detect chooses Ctrl+Shift+V end to end, a held modifier delays the XWayland paste until release, and total failure still leaves the dictation on the clipboard with the manual-paste notice.
- docs/USER_GUIDE.md gained 'First dictation on niri, GNOME, and KDE': niri has no prompt and typing goes direct (never pin portal there; IBus stays excluded), GNOME/KDE grant is lazy keyboard-only at first dictation with an owner-only restore token where supported, and deny demotes for the run without losing the dictation. Uses text_injection.backend and VOCULINUX_FORCE_BACKEND.
One judgement call recorded: where both wtype and ydotool are installed, the existing startup preference (portal, ydotool, wtype, xdotool) is kept as-is — the ladder contract pinned here is the rung order and its demotion triggers, and the niri effective-first-rung holds through direct typing with the clipboard preserved. Reordering the two typing tools would touch Flatpak/KDE paths and is left for a follow-up if hardware acceptance asks for it.
What changed and why: the prior evidence kept startup order portal>ydotool>wtype, but spec #16 and this ticket decide portal > direct keysym typing (wtype) > virtual-input tool (ydotool) > XWayland (xdotool) > manual paste. Aligned code to the decided order:
_check_dependencies auto-selection is now portal > wtype > ydotool (daemon ready, then daemonless with warning) > xdotool. The KDE ydotool-first exception (#911) is removed, so a working portal leads on KDE too; the init wtype probe (previously dead whenever the daemon was ready) now actually validates wtype and demotes to ydotool on compositor rejection.
_demote_portal_backend mirrors the new order (portal deny demotes to wtype when installed).
inject_text: a wtype runtime failure now retries the remainder through ydotool before the XWayland-only path (partial-type accounting preserved).
_switch_to_non_ibus_backend and _try_recover_from_fallback reordered to wtype > ydotool for the same ladder.
tests/test_injection_ladder.py: 14 tests (was 8). New pins: KDE selects portal with ydotool ready; no-portal with both installed selects wtype; portal deny with both demotes to wtype; wtype runtime failure demotes to ydotool via inject_text; daemonless-ydotool branch preserved. Superseded the file's old wtype-vs-ydotool carve-out note.
Two existing tests updated to the decided order (they pinned the old one): test_kde_auto_prefers_ydotool_over_portal renamed to test_kde_auto_prefers_portal_over_ydotool, and the explicit-auto test now expects wtype (saved pin switched to ydotool so the test still discriminates).
docs/USER_GUIDE.md states the full rung order and that the portal leads on GNOME and KDE, with deny demoting to direct typing.
Verification: ladder + all injection suites pass (test_text_injector, ext, portal, paste-shortcut, modifier-wait, ibus, clipboard-restore); flake8 E9,F63,F7,F82 clean; black/isort clean; mypy clean. Full-suite failures (2 installer TTY/upgrade, 8 perf-baseline) reproduce identically on the unmodified integration tip, so pre-existing/environmental.
Residual risk to watch on hardware acceptance (#32): #911 (KWin portal scrambles letter case) motivated the removed KDE exception. Portal failure/deny still demotes safely, but silent case-garbling would not trigger demotion. If KDE hardware runs show garbled case through the portal, say so and the KDE exception can return as an explicit spec amendment.
Ticket #31 fix (branch feature/31, on integration/spec16 tip): review gaps closed, code changed.
What changed and why: the prior evidence kept startup order portal>ydotool>wtype, but spec #16 and this ticket decide portal > direct keysym typing (wtype) > virtual-input tool (ydotool) > XWayland (xdotool) > manual paste. Aligned code to the decided order:
- _check_dependencies auto-selection is now portal > wtype > ydotool (daemon ready, then daemonless with warning) > xdotool. The KDE ydotool-first exception (#911) is removed, so a working portal leads on KDE too; the __init__ wtype probe (previously dead whenever the daemon was ready) now actually validates wtype and demotes to ydotool on compositor rejection.
- _demote_portal_backend mirrors the new order (portal deny demotes to wtype when installed).
- inject_text: a wtype runtime failure now retries the remainder through ydotool before the XWayland-only path (partial-type accounting preserved).
- _switch_to_non_ibus_backend and _try_recover_from_fallback reordered to wtype > ydotool for the same ladder.
- tests/test_injection_ladder.py: 14 tests (was 8). New pins: KDE selects portal with ydotool ready; no-portal with both installed selects wtype; portal deny with both demotes to wtype; wtype runtime failure demotes to ydotool via inject_text; daemonless-ydotool branch preserved. Superseded the file's old wtype-vs-ydotool carve-out note.
- Two existing tests updated to the decided order (they pinned the old one): test_kde_auto_prefers_ydotool_over_portal renamed to test_kde_auto_prefers_portal_over_ydotool, and the explicit-auto test now expects wtype (saved pin switched to ydotool so the test still discriminates).
- docs/USER_GUIDE.md states the full rung order and that the portal leads on GNOME and KDE, with deny demoting to direct typing.
Verification: ladder + all injection suites pass (test_text_injector, ext, portal, paste-shortcut, modifier-wait, ibus, clipboard-restore); flake8 E9,F63,F7,F82 clean; black/isort clean; mypy clean. Full-suite failures (2 installer TTY/upgrade, 8 perf-baseline) reproduce identically on the unmodified integration tip, so pre-existing/environmental.
Residual risk to watch on hardware acceptance (#32): #911 (KWin portal scrambles letter case) motivated the removed KDE exception. Portal failure/deny still demotes safely, but silent case-garbling would not trigger demotion. If KDE hardware runs show garbled case through the portal, say so and the KDE exception can return as an explicit spec amendment.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Parent
Spec: Voculinux — Void Linux (glibc) Wayland/niri port of Vocalinux
What to build
The decided injection behaviour becomes pinned, observable fact. Behaviour tests lock the fallback order per desktop class — portal first only where a backend implements it, then direct typing, then the virtual-input tool, then XWayland-only injection, then manual paste — including IBus exclusion on niri, terminal paste-shortcut selection, and waiting for held modifiers to release. User documentation states the niri first-run experience plainly (no permission prompt exists there; typing goes direct), never recommends pinning the portal on niri, and describes the portal grant, persistence, and deny behaviour only for desktops that have one. Where current behaviour already matches the decision, this ticket records that evidence and adds only the missing tests or words.
Acceptance criteria
Blocked by
Ticket #31 evidence (local branch feature/31, commit e6f7938; no push yet).
Current behaviour already matches the decision in every path I exercised, so src/ is unchanged — the change is a new pinning suite plus user words:
One judgement call recorded: where both wtype and ydotool are installed, the existing startup preference (portal, ydotool, wtype, xdotool) is kept as-is — the ladder contract pinned here is the rung order and its demotion triggers, and the niri effective-first-rung holds through direct typing with the clipboard preserved. Reordering the two typing tools would touch Flatpak/KDE paths and is left for a follow-up if hardware acceptance asks for it.
Ticket #31 fix (branch feature/31, on integration/spec16 tip): review gaps closed, code changed.
What changed and why: the prior evidence kept startup order portal>ydotool>wtype, but spec #16 and this ticket decide portal > direct keysym typing (wtype) > virtual-input tool (ydotool) > XWayland (xdotool) > manual paste. Aligned code to the decided order:
Verification: ladder + all injection suites pass (test_text_injector, ext, portal, paste-shortcut, modifier-wait, ibus, clipboard-restore); flake8 E9,F63,F7,F82 clean; black/isort clean; mypy clean. Full-suite failures (2 installer TTY/upgrade, 8 perf-baseline) reproduce identically on the unmodified integration tip, so pre-existing/environmental.
Residual risk to watch on hardware acceptance (#32): #911 (KWin portal scrambles letter case) motivated the removed KDE exception. Portal failure/deny still demotes safely, but silent case-garbling would not trigger demotion. If KDE hardware runs show garbled case through the portal, say so and the KDE exception can return as an explicit spec amendment.
Merged in integration/spec16
1dd3986383pushed to fork/integration/spec16 (https://github.com/soubarnak/vocalinux/tree/integration/spec16), review PASS, gates flake8 E9,F63,F7,F82 + black --check + isort --check + mypy src/ clean, pytest 37 passed, npm jest 11 suites/92 tests + lint 0 errors + tsc clean + build OK