diff --git a/docs/adr/0015-android-terminal-input.md b/docs/adr/0015-android-terminal-input.md
index 5fdc1eb..c26c765 100644
--- a/docs/adr/0015-android-terminal-input.md
+++ b/docs/adr/0015-android-terminal-input.md
@@ -84,6 +84,7 @@ and keeps it for as long as that surface is up.
- `TextInputOptions.ShowSuggestions = false`, with `ContentType`, `Multiline` and `AutoCapitalization`
left at the defaults the guard above requires. This is the entire fix for the suggestion strip.
+- `TextInputOptions.ReturnKeyType = Return`, which is the fix for Enter — see the correction below.
- It implements `TextInputMethodClient` with an empty surrounding text and a zero-length selection.
A terminal has no document for an IME to edit; committed text is forwarded and nothing is retained.
- Committed text and key events are encoded to bytes and written to the session through the same path
@@ -144,9 +145,35 @@ software keyboard. It never did before — everything typed went straight into t
`SendAsync`, which is where the latch lives — so `ctrl` then `c` was inert unless the second key also came
from the accessory row. Check 11.10d covers it.
-**Verify before building anything on this:** checks 11.10a–11.10d in `docs/manual-checks.md`, and 11.10b
+**Verify before building anything on this:** checks 11.10a–11.10e in `docs/manual-checks.md`, and 11.10b
first of all, because it is the falsification test at the end of this document.
+> **⚠️ Corrected on the first device run: every Enter closed the keyboard.** The premise held — the
+> suggestion strip, autocorrect and predictive text were all gone — but the field had been left with
+> Avalonia's default `ReturnKeyType`, and the same `OnCreateInputConnection` maps that, on a field that is
+> not `Multiline`, to `IME_ACTION_DONE`:
+>
+> ```csharp
+> _ => options.Multiline ? 1073741824 : 6, // 6 is IME_ACTION_DONE
+> ```
+>
+> Done means done: the keyboard performs the action and dismisses itself. On a text box that is right, and
+> on a terminal it fires on every command. `TextInputReturnKeyType.Return` — value 1, so
+> `returnKeyType - 1 => 0 => 1073741824` — is `IME_ACTION_NONE`, which leaves Enter a plain newline with no
+> action for the IME to perform and no reason for it to hide.
+>
+> **The trap worth recording, because it is the same trap 11.10b already warned about from the other side:**
+> `Multiline = true` also reaches `IME_ACTION_NONE`, through the default arm, and is the wrong way there. It
+> ORs `TYPE_TEXT_FLAG_MULTI_LINE` into `InputType`, which moves it off `1` and silently fails the
+> `ShowSuggestions` guard. Taking the obvious route to a working Enter would have handed the suggestion
+> strip back and looked like the whole ADR had failed. `ReturnKeyType` touches `imeOptions` only.
+>
+> Enter's *byte* was never in doubt and is not what changed — `IME_ACTION_DONE` was already synthesising
+> `KEYCODE_ENTER` through `AvaloniaInputConnection.PerformEditorAction`, so commands ran; the keyboard just
+> left afterwards. `OnTextInput` now also normalises line endings to CR, for the case where a keyboard with
+> no editor action commits `"\n"` as ordinary text instead of sending a key event, and so that a pasted
+> `"\r\n"` does not run its line twice.
+
### Stage 2 — the keys around it
Only once Stage 1 is proven, because each of these is cheap *given* Avalonia owning input and awkward
diff --git a/docs/manual-checks.md b/docs/manual-checks.md
index d65e4f1..ca62099 100644
--- a/docs/manual-checks.md
+++ b/docs/manual-checks.md
@@ -1919,6 +1919,24 @@ could not pass *before* ADR 0015 and is not a regression test but a new capabili
only to the accessory row's own keys, because everything typed went straight into the page and never passed
the code that holds the latch.
+### 11.10e Enter runs the command and the keyboard stays where it is
+
+With a shell open and the software keyboard up, type `pwd` and press Enter. Then type another command.
+
+**Pass:** the command runs and **the keyboard is still up**, unmoved, ready for the next one. The Enter key
+reads as a return arrow rather than as `DONE` or a tick. Ten commands in a row cost ten Enters and no taps
+to bring the keyboard back.
+
+**Failure means:** `TextInputOptions.ReturnKeyType` is not `Return` on `TerminalInputSurface`, so Avalonia
+has mapped the field's default to `IME_ACTION_DONE` and the keyboard is doing exactly what Done means. This
+was the first defect the first device run found. **Do not fix it with `Multiline = true`** — that reaches
+the same `IME_ACTION_NONE` by a route that ORs `TYPE_TEXT_FLAG_MULTI_LINE` into `InputType`, which fails the
+`ShowSuggestions` guard and hands the suggestion strip back; check 11.10b would then start failing and the
+cause would look unrelated. See the correction in [ADR 0015](adr/0015-android-terminal-input.md).
+
+Worth pressing Enter on an empty prompt a few times too: each one should give a fresh prompt and nothing
+else. A doubled prompt per press means a line ending is arriving as both CR and LF.
+
### 11.11 Closing a connection and opening a new one both take you somewhere real
Open a shell, close its tab, then open a different one from HOSTS.
diff --git a/src/DodoSSH.Client.Android/Views/TerminalInputSurface.cs b/src/DodoSSH.Client.Android/Views/TerminalInputSurface.cs
index bdb37f1..6428d7b 100644
--- a/src/DodoSSH.Client.Android/Views/TerminalInputSurface.cs
+++ b/src/DodoSSH.Client.Android/Views/TerminalInputSurface.cs
@@ -31,10 +31,17 @@ namespace DodoSSH.Client.Android.Views;
///
/// 0x80080 is TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | TYPE_TEXT_FLAG_NO_SUGGESTIONS — the exact
/// pair Termux's own TerminalView sets, and the reason typing in Termux feels like a terminal. The
-/// guard is why the constructor sets only
-/// and leaves ContentType, Multiline and AutoCapitalization alone: any of those would
-/// move InputType off 1 and the branch would never be taken. ShowSuggestions is a
-/// bool? defaulting to null, so it must be set to false rather than merely left.
+/// guard is why the constructor leaves ContentType, Multiline and AutoCapitalization
+/// alone: any of those would move InputType off 1 and the branch would never be taken.
+/// ShowSuggestions is a bool? defaulting to null, so it must be set to false
+/// rather than merely left.
+///
+///
+/// Two options are set, and the second one is not optional either. The default
+/// ReturnKeyType maps to IME_ACTION_DONE on a non-multiline field, which closed the keyboard
+/// on every Enter — that is, on every command. ReturnKeyType.Return is IME_ACTION_NONE and
+/// touches nothing else. The constructor carries the full argument, including why the obvious alternative
+/// of Multiline cannot be used without giving the suggestion strip back.
///
///
/// One pixel, and never drawn. Focus in Avalonia has nothing to do with hit-testing, so the input
@@ -105,9 +112,26 @@ internal sealed class TerminalInputSurface : Control
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top;
IsHitTestVisible = false;
- // The one line that fixes the keyboard. See the remarks for why it is the only option set.
+ // The one line that fixes the keyboard. See the remarks for why almost nothing else is set.
TextInputOptions.SetShowSuggestions(this, false);
+ // ◆ AND THE ONE THAT KEEPS THE KEYBOARD UP WHEN A COMMAND IS RUN.
+ //
+ // Avalonia maps ReturnKeyType.Default, on a field that is not Multiline, to IME_ACTION_DONE — and
+ // Done means done: the keyboard performs the action and dismisses itself. Every Enter therefore
+ // closed the keyboard, which on a terminal is every command.
+ //
+ // Return is the member that maps to IME_ACTION_NONE, leaving the key as a plain newline with no
+ // action attached and no reason for the IME to hide. Its whole switch arm is
+ // `returnKeyType - 1 => 0 => 1073741824`, which is 0x40000000, IME_ACTION_NONE.
+ //
+ // Multiline=true reaches the same IME_ACTION_NONE through the default arm and is the wrong way to
+ // get there: it also ORs TYPE_TEXT_FLAG_MULTI_LINE into InputType, which moves InputType off 1 and
+ // silently fails the ShowSuggestions guard above. The two options cannot both be had that way — one
+ // trades the suggestion strip back for a working Enter. ReturnKeyType costs nothing and is the
+ // reason this file sets it rather than Multiline.
+ TextInputOptions.SetReturnKeyType(this, TextInputReturnKeyType.Return);
+
AddHandler(TextInputMethodClientRequestedEvent, OnTextInputMethodClientRequested);
}
@@ -156,7 +180,14 @@ internal sealed class TerminalInputSurface : Control
return;
}
- Input?.Invoke(this, new TerminalInputEventArgs(Encoding.UTF8.GetBytes(e.Text)));
+ // Newlines arrive as carriage returns, whichever way they came. A keyboard with no editor action
+ // attached may commit "\n" as ordinary text rather than sending KEYCODE_ENTER, and a paste carries
+ // whatever the clipboard held — but a terminal's line terminator from a keyboard is CR, and a shell
+ // handed LF for Enter is a shell that looks like it ignored the key. Normalised rather than special
+ // -cased so that a pasted "\r\n" does not run the line twice.
+ var text = e.Text.ReplaceLineEndings("\r");
+
+ Input?.Invoke(this, new TerminalInputEventArgs(Encoding.UTF8.GetBytes(text)));
e.Handled = true;
base.OnTextInput(e);