diff --git a/docs/adr/0015-android-terminal-input.md b/docs/adr/0015-android-terminal-input.md new file mode 100644 index 0000000..5fdc1eb --- /dev/null +++ b/docs/adr/0015-android-terminal-input.md @@ -0,0 +1,219 @@ +# ADR 0015 — The Android terminal's input path leaves the WebView + +- Status: proposed — **Stage 1 built, unverified on a device** +- Date: 2026-08-25 +- Builds on: [`docs/android-port.md`](../android-port.md) §9, [ADR 0011](0011-android-distribution.md) +- Does not settle: whether the renderer stays xterm.js. That is [Stage 3](#stage-3--the-renderer-deliberately-unsettled), and it is deliberately left open. + +## Context + +The phone got the desktop's terminal, and the reasons were good ones. `TerminalDataPlane` already +served the page and a binary protocol over a loopback WebSocket, the xterm.js bundles were embedded +resources and platform-neutral, and `Avalonia.Controls.WebView` shipped a `net10.0-android36.0` +target. `docs/android-port.md` called it *"better news than expected"*, and for rendering it was. + +**For typing it was not, and typing is the complaint.** Not scrollback, not colour, not throughput — +the act of putting characters into a shell. That narrows the problem usefully, because the input path +and the render path are separable, and only one of them is wrong. + +### Three things sit between a finger and a byte, and the middle one is the fault + +**1. Avalonia and the WebView fight over native focus.** `AvaloniaView.DispatchTouchEvent` ends every +handled touch — DOWN and UP alike — with a `RequestFocus()` for Avalonia's own view. The WebView's +input connection dies with its focus, so the keyboard swaps layouts and the inset churn can park it +over the accessory row that was tapped. `Platform/TerminalFocus.cs` exists to hand focus back, posted +to the next main-looper message so the platform has taken its turn first. It works. It is also a +counter-move in a fight that only exists because the thing holding the input connection is not the +thing the framework thinks is focused. + +**2. The keyboard is a prose keyboard, and no DOM attribute makes it stop.** This is the one that was +worth checking rather than assuming, and the check inverted the obvious conclusion. + +The vendored xterm.js *already* sets every hint the web platform has on its helper textarea — +`autocapitalize="off"`, `autocorrect="off"`, `spellcheck="false"`. They are set, and Gboard shows the +suggestion strip and predictive text anyway. So this is not a missing attribute; it is a ceiling. + +Termux's `TerminalView.onCreateInputConnection` says what the ceiling is. It sets: + +``` +InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS +``` + +**`VISIBLE_PASSWORD` is the load-bearing half.** `NO_SUGGESTIONS` alone is advisory and most keyboards +ignore it — a point made in Avalonia's own Android keyboard PR discussion, not only in Termux's. What +actually turns off autocorrect, the suggestion strip and the predictive composing layer is the +*variation*: a field the keyboard believes is a visible password gets no dictionary. That is an +`EditorInfo.inputType` on a native `InputConnection`, and a page inside a WebView has no way to reach +it — Chromium computes the `EditorInfo` from the focused DOM node, and no combination of attributes on +a `