diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 700c5e50980..16dd27094de 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -424,7 +424,7 @@ public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSessio // Other special keys. case 'e': - resultingCodePoint = /*Escape*/ 27; + resultingKeyCode = KeyEvent.KEYCODE_ESCAPE; break; case '.': resultingCodePoint = /*^.*/ 28; @@ -454,9 +454,16 @@ public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSessio if (resultingKeyCode != -1) { TerminalEmulator term = session.getEmulator(); - session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode())); + session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), + term.isKeypadApplicationMode(), term.getKittyKeyboardFlags(), KeyHandler.KEY_EVENT_PRESS)); } else if (resultingCodePoint != -1) { - session.writeCodePoint(altDown, resultingCodePoint); + String code = null; + if (session.getEmulator().isKittyKeyboardEnabled()) { + code = KeyHandler.getKittyCodePoint(resultingCodePoint, altDown ? KeyHandler.KEYMOD_ALT : 0, + session.getEmulator().getKittyKeyboardFlags(), KeyHandler.KEY_EVENT_PRESS); + } + if (code != null) session.write(code); + else session.writeCodePoint(altDown, resultingCodePoint); } return true; } else if (ctrlDown) { diff --git a/app/src/test/java/com/termux/app/terminal/KittyKeyboardInputTest.java b/app/src/test/java/com/termux/app/terminal/KittyKeyboardInputTest.java new file mode 100644 index 00000000000..e8662fa73e0 --- /dev/null +++ b/app/src/test/java/com/termux/app/terminal/KittyKeyboardInputTest.java @@ -0,0 +1,647 @@ +package com.termux.app.terminal; + +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; +import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase; +import com.termux.terminal.TerminalEmulator; +import com.termux.terminal.TerminalSession; +import com.termux.view.TerminalView; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowKeyCharacterMap; +import org.robolectric.util.ReflectionHelpers; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** Exercise Android key events through TerminalView into the real session input queue, without a PTY. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28) +public class KittyKeyboardInputTest { + private TerminalView mView; + private TerminalSession mSession; + + @Before + public void setUp() { + mSession = new TerminalSession("/bin/sh", "/", new String[0], new String[0], 100, + new TermuxTerminalSessionClientBase()); + TerminalEmulator emulator = new TerminalEmulator(mSession, 20, 4, 8, 16, 100, null); + ReflectionHelpers.setField(mSession, "mEmulator", emulator); + ReflectionHelpers.setField(mSession, "mShellPid", 1); + mView = new TerminalView(RuntimeEnvironment.getApplication(), null); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase()); + ReflectionHelpers.setField(mView, "mTermSession", mSession); + mView.mEmulator = emulator; + } + + private void enter(String sequence) { + byte[] bytes = sequence.getBytes(StandardCharsets.UTF_8); + mView.mEmulator.append(bytes, bytes.length); + } + + private String drainInput() throws Exception { + Object queue = ReflectionHelpers.getField(mSession, "mTerminalToProcessIOQueue"); + Method read = queue.getClass().getDeclaredMethod("read", byte[].class, boolean.class); + read.setAccessible(true); + byte[] bytes = new byte[4096]; + int count = (Integer) read.invoke(queue, bytes, false); + return new String(bytes, 0, count, StandardCharsets.UTF_8); + } + + private void press(int keyCode, int metaState, String expected) throws Exception { + KeyEvent event = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, metaState, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0); + assertTrue(mView.onKeyDown(keyCode, event)); + assertEquals(expected, drainInput()); + } + + @Test + public void hardwareShortcutsRetainKeyIdentity() throws Exception { + enter("\033[>1u"); + press(KeyEvent.KEYCODE_0, KeyEvent.META_CTRL_ON, "\033[48;5u"); + press(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5u"); + press(KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON, "\033[97;6u"); + press(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON, "\033[61;6u"); + } + + @Test + public void modifiedEnterAndTabStayDistinct() throws Exception { + enter("\033[>1u"); + press(KeyEvent.KEYCODE_ENTER, KeyEvent.META_SHIFT_ON, "\033[13;2u"); + press(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, "\033[13;5u"); + press(KeyEvent.KEYCODE_TAB, KeyEvent.META_CTRL_ON, "\033[9;5u"); + press(KeyEvent.KEYCODE_TAB, 0, "\t"); + press(KeyEvent.KEYCODE_ENTER, 0, "\r"); + press(KeyEvent.KEYCODE_ESCAPE, 0, "\033[27u"); + } + + @Test + public void plainTextAndRestoredModeKeepLegacyBehavior() throws Exception { + enter("\033[>1u"); + press(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, "A"); + enter("\033[1u"); + mView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, '0', true, false); + assertEquals("\033[48;5u", drainInput()); + mView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, 0x4E2D, false, false); + assertEquals("中", drainInput()); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase() { + @Override + public boolean onKeyDown(int keyCode, KeyEvent event, TerminalSession session) { + return true; + } + }); + press(KeyEvent.KEYCODE_0, KeyEvent.META_CTRL_ON, ""); + } + + /** A layout fixture where right Alt composes NBSP on Space and Euro on E. */ + @Implements(KeyCharacterMap.class) + public static class AltGrCharacterMap extends ShadowKeyCharacterMap { + @Override + @Implementation + protected int get(int keyCode, int metaState) { + if ((metaState & KeyEvent.META_ALT_RIGHT_ON) != 0) { + if (keyCode == KeyEvent.KEYCODE_SPACE) return 0x00A0; + if (keyCode == KeyEvent.KEYCODE_E) return 0x20AC; + } + return super.get(keyCode, metaState); + } + } + + @Test + @Config(shadows = AltGrCharacterMap.class) + public void altGrTextUsesTheActiveLayout() throws Exception { + int altGr = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + press(KeyEvent.KEYCODE_SPACE, altGr, "\u00a0"); + enter("\033[>1u"); + press(KeyEvent.KEYCODE_SPACE, altGr, "\u00a0"); + press(KeyEvent.KEYCODE_E, altGr, "\u20ac"); + press(KeyEvent.KEYCODE_SPACE, altGr | KeyEvent.META_CTRL_ON, "\033[160;5u"); + enter("\033[1u"); + press(KeyEvent.KEYCODE_SPACE, 0, " "); + press(KeyEvent.KEYCODE_SPACE, KeyEvent.META_CTRL_ON, "\033[32;5u"); + press(KeyEvent.KEYCODE_SPACE, KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON, "\033[32;3u"); + enter("\033[1u"); + press(KeyEvent.KEYCODE_PAGE_UP, KeyEvent.META_SHIFT_ON, ""); + assertEquals(-mView.mEmulator.mRows, (int) ReflectionHelpers.getField(mView, "mTopRow")); + press(KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.META_SHIFT_ON, ""); + assertEquals(0, (int) ReflectionHelpers.getField(mView, "mTopRow")); + if (kitty) enter("\033[1u"); + press(KeyEvent.KEYCODE_PAGE_UP, KeyEvent.META_CTRL_ON, "\033[5;5~"); + press(KeyEvent.KEYCODE_PAGE_DOWN, 0, "\033[6~"); + } + private void keyEvent(int action, int keyCode, int repeat, int metaState, int deviceId, String expected) throws Exception { + KeyEvent event = new KeyEvent(0, 0, action, keyCode, repeat, metaState, deviceId, 0); + assertTrue(action == KeyEvent.ACTION_UP ? mView.onKeyUp(keyCode, event) : mView.onKeyDown(keyCode, event)); + assertEquals(expected, drainInput()); + } + + private void repeat(int keyCode, int metaState, String expected) throws Exception { + keyEvent(KeyEvent.ACTION_DOWN, keyCode, 1, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, expected); + } + + private void release(int keyCode, int metaState, String expected) throws Exception { + keyEvent(KeyEvent.ACTION_UP, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, expected); + } + + @Test + public void negotiatedRepeatsAndReleasesReachSession() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + repeat(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[1;1:2A"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[1;1:3A"); + press(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5u"); + repeat(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5:2u"); + release(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5:3u"); + press(KeyEvent.KEYCODE_F3, KeyEvent.META_SHIFT_ON, "\033[13;2~"); + release(KeyEvent.KEYCODE_F3, KeyEvent.META_SHIFT_ON, "\033[13;2:3~"); + press(KeyEvent.KEYCODE_ESCAPE, 0, "\033[27u"); + release(KeyEvent.KEYCODE_ESCAPE, 0, "\033[27;1:3u"); + } + + @Test + public void eventReportingCanBeEnabledWithoutDisambiguation() throws Exception { + enter("\033[>2u"); + press(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\t"); + repeat(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5:2u"); + release(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\033[105;5:3u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[1;1:3A"); + press(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, "\r"); + release(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, ""); + } + + @Test + public void textAndRecoveryKeysKeepPressAndRepeatPaths() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_A, 0, "a"); + repeat(KeyEvent.KEYCODE_A, 0, "a"); + release(KeyEvent.KEYCODE_A, 0, ""); + press(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, "A"); + release(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, ""); + int[] keys = {KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_DEL}; + String[] bytes = {"\r", "\t", "\u007f"}; + for (int i = 0; i < keys.length; i++) { + press(keys[i], 0, bytes[i]); + repeat(keys[i], 0, bytes[i]); + release(keys[i], 0, ""); + } + press(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, "\033[13;5u"); + repeat(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, "\033[13;5:2u"); + release(KeyEvent.KEYCODE_ENTER, KeyEvent.META_CTRL_ON, ""); + } + + @Test + public void releasesUseCurrentModifiersAndRememberLayoutIdentity() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON, "\033[61;6u"); + release(KeyEvent.KEYCODE_EQUALS, 0, "\033[61;1:3u"); + press(KeyEvent.KEYCODE_DPAD_UP, KeyEvent.META_CTRL_ON, "\033[1;5A"); + release(KeyEvent.KEYCODE_DPAD_UP, KeyEvent.META_SHIFT_ON, "\033[1;2:3A"); + } + + @Test + public void orphanedCanceledAndModeChangedReleasesAreSuppressed() throws Exception { + enter("\033[>3u"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + enter("\033[>0u\033[3u"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + KeyEvent canceled = new KeyEvent(0, 0, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_UP, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_CANCELED); + assertTrue(mView.onKeyUp(KeyEvent.KEYCODE_DPAD_UP, canceled)); + assertEquals("", drainInput()); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + } + + @Test + public void releasesDoNotCrossScreensOrDevices() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + enter("\033[?1049h\033[>3u"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + enter("\033[?1049l"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + keyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_UP, 0, 0, 42, ""); + release(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[1;1:3A"); + } + + @Test + public void shortcutsScrollAndSoftInputDoNotSynthesizeReleases() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_PAGE_UP, KeyEvent.META_SHIFT_ON, ""); + release(KeyEvent.KEYCODE_PAGE_UP, 0, ""); + mView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, 'i', true, false); + assertEquals("\033[105;5u", drainInput()); + release(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, ""); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase() { + @Override + public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) { + return true; + } + }); + press(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, ""); + release(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, ""); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase() { + @Override + public boolean onKeyDown(int keyCode, KeyEvent event, TerminalSession session) { + return true; + } + }); + press(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + } + + @Test + public void disabledEventReportingKeepsRepeatAsPress() throws Exception { + for (int flags : new int[]{0, 1}) { + enter("\033[=" + flags + "u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + repeat(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + } + } + + @Test + public void keypadReleaseKeepsIdentityWhenNumLockChanges() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[57399;129u"); + release(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[57399;1:3u"); + press(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[57425u"); + release(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[57425;129:3u"); + enter("\033[=2u"); + press(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[2~"); + release(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[2;129:3~"); + } + + @Test + public void escapeEventsDoNotRequireDisambiguation() throws Exception { + enter("\033[>2u"); + press(KeyEvent.KEYCODE_ESCAPE, 0, "\033"); + repeat(KeyEvent.KEYCODE_ESCAPE, 0, "\033[27;1:2u"); + release(KeyEvent.KEYCODE_ESCAPE, 0, "\033[27;1:3u"); + } + + @Test + public void focusLossAndScreenRoundTripDiscardPendingReleases() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + enter("\033[?1049h\033[?1049l"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + ReflectionHelpers.callInstanceMethod(mView, "onFocusChanged", + ReflectionHelpers.ClassParameter.from(boolean.class, false), + ReflectionHelpers.ClassParameter.from(int.class, 0), + ReflectionHelpers.ClassParameter.from(android.graphics.Rect.class, null)); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + } + + @Test + @Config(shadows = AltGrCharacterMap.class) + public void altGrTextDoesNotProduceReleaseEvents() throws Exception { + enter("\033[>3u"); + int altGr = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + press(KeyEvent.KEYCODE_SPACE, altGr, "\u00a0"); + repeat(KeyEvent.KEYCODE_SPACE, altGr, "\u00a0"); + release(KeyEvent.KEYCODE_SPACE, altGr, ""); + press(KeyEvent.KEYCODE_E, altGr | KeyEvent.META_CTRL_ON, "\033[8364;5u"); + release(KeyEvent.KEYCODE_E, altGr, "\033[8364;1:3u"); + } + + @Test + public void applicationKeyUpCanConsumeRelease() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase() { + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return true; + } + }); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + } + + @Test + public void extraKeyBridgeSendsOnePressWithoutPendingRelease() throws Exception { + enter("\033[>3u"); + new com.termux.shared.termux.terminal.io.TerminalExtraKeys(mView) { + void tap() { + onTerminalExtraKeyButtonClick(null, "UP", true, false, false, false); + } + }.tap(); + assertEquals("\033[1;5A", drainInput()); + keyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_UP, 0, KeyEvent.META_CTRL_ON, 0, ""); + } + + @Test + public void virtualFnPressEncodingUsesActualNegotiatedFlags() throws Exception { + TermuxTerminalViewClient client = new TermuxTerminalViewClient(null, null); + client.mVirtualFnKeyDown = true; + for (int flags : new int[]{0, 1, 2, 3}) { + enter("\033[=" + flags + "u"); + assertTrue(client.onCodePoint('b', false, mSession)); + assertEquals((flags & 1) != 0 ? "\033[98;3u" : "\033b", drainInput()); + assertTrue(client.onCodePoint('e', false, mSession)); + assertEquals((flags & 1) != 0 ? "\033[27u" : "\033", drainInput()); + // The separately deferred virtual Fn Ctrl-loss behavior remains unchanged. + assertTrue(client.onCodePoint('w', true, mSession)); + assertEquals("\033[A", drainInput()); + } + } + + @Test + public void keypadRepeatsPreserveInitialReleaseIdentity() throws Exception { + enter("\033[>3u"); + press(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[57399;129u"); + repeat(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[57425;1:2u"); + release(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[57399;1:3u"); + press(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[57425u"); + repeat(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[57399;129:2u"); + release(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[57425;129:3u"); + enter("\033[=2u"); + press(KeyEvent.KEYCODE_NUMPAD_0, 0, "\033[2~"); + repeat(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "0"); + release(KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.META_NUM_LOCK_ON, "\033[2;129:3~"); + } + + @Test + public void repeatsDoNotCreateOrRevivePendingReleases() throws Exception { + enter("\033[>3u"); + repeat(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[1;1:2A"); + release(KeyEvent.KEYCODE_DPAD_UP, 0, ""); + press(KeyEvent.KEYCODE_DPAD_UP, 0, "\033[A"); + enter("\033[>0u\033[2u"); + int[] keys = {KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_DEL}; + int[] modifiers = {KeyEvent.META_CTRL_ON, KeyEvent.META_SHIFT_ON, KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON}; + String[] pressBytes = {"\r", "\033[Z", "\033\u007f"}; + String[] repeatBytes = {"\033[13;5:2u", "\033[9;2:2u", "\033[127;3:2u"}; + for (int i = 0; i < keys.length; i++) { + press(keys[i], modifiers[i], pressBytes[i]); + repeat(keys[i], modifiers[i], repeatBytes[i]); + release(keys[i], modifiers[i], ""); + } + String[] plainBytes = {"\r", "\t", "\u007f"}; + for (int i = 0; i < keys.length; i++) { + press(keys[i], KeyEvent.META_NUM_LOCK_ON | KeyEvent.META_CAPS_LOCK_ON, plainBytes[i]); + repeat(keys[i], KeyEvent.META_NUM_LOCK_ON | KeyEvent.META_CAPS_LOCK_ON, plainBytes[i]); + release(keys[i], 0, ""); + } + } + + /** Robolectric 4.10 omits Shift+=; provide the US layout entry explicitly. */ + @Implements(KeyCharacterMap.class) + public static class PunctuationCharacterMap extends ShadowKeyCharacterMap { + @Override + @Implementation + protected int get(int keyCode, int metaState) { + if (keyCode == KeyEvent.KEYCODE_EQUALS) + return (metaState & KeyEvent.META_SHIFT_ON) != 0 ? '+' : '='; + return super.get(keyCode, metaState); + } + } + + @Test + @Config(shadows = PunctuationCharacterMap.class) + public void alternateShiftedShortcutsUseTheAndroidLayout() throws Exception { + enter("\033[>7u"); + int modifiers = KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON; + press(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[61:43;6u"); + repeat(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[61:43;6:2u"); + release(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[61:43;6:3u"); + press(KeyEvent.KEYCODE_A, modifiers, "\033[97:65;6u"); + release(KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON, "\033[97;5:3u"); + press(KeyEvent.KEYCODE_A, 0, "a"); + release(KeyEvent.KEYCODE_A, 0, ""); + press(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, "A"); + release(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, ""); + } + + /** Synthetic non-Latin layout, including a Meta mapping to check alternate-only legacy isolation. */ + @Implements(KeyCharacterMap.class) + public static class AlternateCharacterMap extends ShadowKeyCharacterMap { + static boolean greek; + @Override + @Implementation + protected int get(int keyCode, int metaState) { + if (keyCode == KeyEvent.KEYCODE_D) + return (metaState & KeyEvent.META_SHIFT_ON) != 0 ? '!' : 'a'; + if (keyCode == KeyEvent.KEYCODE_C) { + if ((metaState & KeyEvent.META_META_ON) != 0) return 0x03BB; + boolean shift = (metaState & KeyEvent.META_SHIFT_ON) != 0; + return greek ? (shift ? 0x03A0 : 0x03C0) : (shift ? 0x0421 : 0x0441); + } + return super.get(keyCode, metaState); + } + } + + @Test + @Config(shadows = AlternateCharacterMap.class) + public void alternateBaseLayoutFieldsPreserveTheInitialReleaseIdentity() throws Exception { + AlternateCharacterMap.greek = false; + enter("\033[>7u"); + int modifiers = KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON; + try { + press(KeyEvent.KEYCODE_C, modifiers, "\033[1089:1057:99;6u"); + AlternateCharacterMap.greek = true; + repeat(KeyEvent.KEYCODE_C, modifiers, "\033[960:928:99;6:2u"); + release(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON, "\033[1089::99;5:3u"); + press(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON, "\033[960::99;5u"); + release(KeyEvent.KEYCODE_C, 0, "\033[960::99;1:3u"); + } finally { + AlternateCharacterMap.greek = false; + } + } + + @Test + @Config(shadows = AlternateCharacterMap.class) + public void alternateOnlyFlagDoesNotChangeLegacyTextOrShortcuts() throws Exception { + AlternateCharacterMap.greek = false; + for (int flags : new int[]{0, 4}) { + enter("\033[=" + flags + "u"); + press(KeyEvent.KEYCODE_C, KeyEvent.META_META_ON, "\u03bb"); + press(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\t"); + repeat(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, "\t"); + release(KeyEvent.KEYCODE_I, KeyEvent.META_CTRL_ON, ""); + press(KeyEvent.KEYCODE_ESCAPE, 0, "\033"); + } + } + + @Test + @Config(shadows = AltGrCharacterMap.class) + public void alternateReportingRetainsAltGrTextAndBaseKey() throws Exception { + enter("\033[>7u"); + int altGr = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + press(KeyEvent.KEYCODE_SPACE, altGr, "\u00a0"); + release(KeyEvent.KEYCODE_SPACE, altGr, ""); + press(KeyEvent.KEYCODE_E, altGr | KeyEvent.META_CTRL_ON, "\033[8364::101;5u"); + release(KeyEvent.KEYCODE_E, altGr, "\033[8364::101;1:3u"); + } + + @Test + public void alternateFlagCombinationsKeepTextAndModeRestoration() throws Exception { + int modifiers = KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON; + for (int flags = 0; flags < 8; flags++) { + enter("\033[=" + flags + "u"); + press(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON, "A"); + boolean disambiguate = (flags & 1) != 0, events = (flags & 2) != 0, alternate = (flags & 4) != 0; + String key = alternate ? "97:65" : "97"; + press(KeyEvent.KEYCODE_A, modifiers, disambiguate ? "\033[" + key + ";6u" : "\u0001"); + repeat(KeyEvent.KEYCODE_A, modifiers, events ? "\033[" + key + ";6:2u" : + disambiguate ? "\033[" + key + ";6u" : "\u0001"); + release(KeyEvent.KEYCODE_A, modifiers, events ? "\033[" + key + ";6:3u" : ""); + } + enter("\033[>0u"); + press(KeyEvent.KEYCODE_A, modifiers, "\u0001"); + enter("\033[7u"); + mView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, 'a', true, false); + assertEquals("\033[97;5u", drainInput()); + mView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, 0x4E2D, false, false); + assertEquals("中", drainInput()); + KeyEvent soft = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_C, 0, + KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD); + assertTrue(mView.onKeyDown(KeyEvent.KEYCODE_C, soft)); + assertEquals("\033[1089:1057;6u", drainInput()); + } + + @Test + @Config(shadows = AlternateCharacterMap.class) + public void shiftedAlternateIsNotAssumedToBeUppercase() throws Exception { + enter("\033[>5u"); + press(KeyEvent.KEYCODE_D, KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON, "\033[97:33:100;6u"); + } + + /** Distinct generic, left, right, both and right-Shift+AltGr mappings. */ + @Implements(KeyCharacterMap.class) + public static class SideShiftCharacterMap extends ShadowKeyCharacterMap { + @Override + @Implementation + protected int get(int keyCode, int metaState) { + if (keyCode == KeyEvent.KEYCODE_EQUALS) { + boolean left = (metaState & KeyEvent.META_SHIFT_LEFT_ON) != 0; + boolean right = (metaState & KeyEvent.META_SHIFT_RIGHT_ON) != 0; + if ((metaState & KeyEvent.META_ALT_RIGHT_ON) != 0) return right ? 0x00D7 : 0x20AC; + if (left && right) return '^'; + if (right) return '*'; + if (left) return '#'; + return (metaState & KeyEvent.META_SHIFT_ON) != 0 ? '+' : '='; + } + return super.get(keyCode, metaState); + } + } + + @Test + @Config(shadows = SideShiftCharacterMap.class) + public void alternateShiftedCharactersRetainPhysicalShiftSides() throws Exception { + int[] sides = {KeyEvent.META_SHIFT_LEFT_ON, KeyEvent.META_SHIFT_RIGHT_ON, + KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON, 0}; + int[] codePoints = {'#', '*', '^', '+'}; + for (int flags : new int[]{5, 7}) { + enter("\033[=" + flags + "u"); + for (int i = 0; i < sides.length; i++) { + int modifiers = KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON | sides[i]; + String key = "\033[61:" + codePoints[i] + ";6"; + press(KeyEvent.KEYCODE_EQUALS, modifiers, key + "u"); + repeat(KeyEvent.KEYCODE_EQUALS, modifiers, key + (flags == 7 ? ":2u" : "u")); + release(KeyEvent.KEYCODE_EQUALS, modifiers, flags == 7 ? key + ":3u" : ""); + } + } + } + + @Test + @Config(shadows = SideShiftCharacterMap.class) + public void rightShiftAndAltGrResolveTheActualLayoutCombination() throws Exception { + enter("\033[>7u"); + int modifiers = KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON | + KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON; + press(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[8364:215:61;6u"); + repeat(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[8364:215:61;6:2u"); + release(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[8364:215:61;6:3u"); + press(KeyEvent.KEYCODE_EQUALS, modifiers, "\033[8364:215:61;6u"); + release(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON, + "\033[8364::61;1:3u"); + } + + @Test + @Config(shadows = SideShiftCharacterMap.class) + public void physicalRightShiftDoesNotGainAnArtificialLeftShift() throws Exception { + for (int flags : new int[]{0, 4, 7}) { + enter("\033[=" + flags + "u"); + press(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON, "*"); + release(KeyEvent.KEYCODE_EQUALS, 0, ""); + } + } + + @Test + @Config(shadows = SideShiftCharacterMap.class) + public void virtualShiftKeepsItsExistingLeftShiftLayoutBehavior() throws Exception { + enter("\033[>7u"); + mView.setTerminalViewClient(new TermuxTerminalViewClientBase() { + @Override + public boolean readShiftKey() { + return true; + } + }); + press(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_CTRL_ON, "\033[61:35;6u"); + release(KeyEvent.KEYCODE_EQUALS, KeyEvent.META_CTRL_ON, "\033[61:35;6:3u"); + } + +} diff --git a/docs/kitty-keyboard-alternate-device-validation.json b/docs/kitty-keyboard-alternate-device-validation.json new file mode 100644 index 00000000000..cc67ca50af2 --- /dev/null +++ b/docs/kitty-keyboard-alternate-device-validation.json @@ -0,0 +1,3827 @@ +{ + "method": "Android framework KeyEvent delivered directly to production TerminalView; real TerminalSession/native PTY and bash raw byte capture; not physical HID or OS input dispatch", + "model": "PTP-AN10", + "android": "16", + "checks": 125, + "failures": 0, + "results": [ + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 0, + "expected_hex": "1b1b", + "actual_hex": "1b1b", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "1b5b3130353b35751b5b3130353b3575", + "actual_hex": "1b5b3130353b35751b5b3130353b3575", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 1, + "expected_hex": "1b5b3237751b5b323775", + "actual_hex": "1b5b3237751b5b323775", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 4, + "expected_hex": "1b1b", + "actual_hex": "1b1b", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "1b5b3130353b35751b5b3130353b3575", + "actual_hex": "1b5b3130353b35751b5b3130353b3575", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "1b5b3237751b5b323775", + "actual_hex": "1b5b3237751b5b323775", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "0d1b5b31333b353a3275", + "actual_hex": "0d1b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b5b5a1b5b393b323a3275", + "actual_hex": "1b5b5a1b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b7f1b5b3132373b333a3275", + "actual_hex": "1b7f1b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "091b5b3130353b313a3375", + "actual_hex": "091b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b31333b35751b5b31333b353a3275", + "actual_hex": "1b5b31333b35751b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b393b32751b5b393b323a3275", + "actual_hex": "1b5b393b32751b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b3132373b33751b5b3132373b333a3275", + "actual_hex": "1b5b3132373b33751b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b3130353b35751b5b3130353b313a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "0d1b5b31333b353a3275", + "actual_hex": "0d1b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b5b5a1b5b393b323a3275", + "actual_hex": "1b5b5a1b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b7f1b5b3132373b333a3275", + "actual_hex": "1b7f1b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "091b5b3130353b313a3375", + "actual_hex": "091b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b31333b35751b5b31333b353a3275", + "actual_hex": "1b5b31333b35751b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b393b32751b5b393b323a3275", + "actual_hex": "1b5b393b32751b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b3132373b33751b5b3132373b333a3275", + "actual_hex": "1b5b3132373b33751b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b3130353b35751b5b3130353b313a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P1 NumLock on to off during repeats", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b35373339393b313239751b5b35373432353b313a32751b5b35373339393b313a3375", + "actual_hex": "1b5b35373339393b313239751b5b35373432353b313a32751b5b35373339393b313a3375", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 2097152, + 0 + ], + [ + 0, + 144, + 1, + 0, + 0 + ], + [ + 1, + 144, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P1 NumLock off to on during repeats", + "synthetic_layout": false, + "flags": 3, + "expected_hex": "1b5b3537343235751b5b35373339393b3132393a32751b5b35373432353b3132393a3375", + "actual_hex": "1b5b3537343235751b5b35373339393b3132393a32751b5b35373432353b3132393a3375", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 0, + 0 + ], + [ + 0, + 144, + 1, + 2097152, + 0 + ], + [ + 1, + 144, + 0, + 2097152, + 0 + ] + ] + }, + { + "name": "P1 legacy keypad navigation to text repeat", + "synthetic_layout": false, + "flags": 2, + "expected_hex": "1b5b327e301b5b323b3132393a337e", + "actual_hex": "1b5b327e301b5b323b3132393a337e", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 0, + 0 + ], + [ + 0, + 144, + 1, + 2097152, + 0 + ], + [ + 1, + 144, + 0, + 2097152, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+= press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "1b5b36313a34333b36751b5b36313a34333b3675", + "actual_hex": "1b5b36313a34333b36751b5b36313a34333b3675", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4097, + 0 + ], + [ + 0, + 70, + 1, + 4097, + 0 + ], + [ + 1, + 70, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+A press repeat release", + "synthetic_layout": false, + "flags": 5, + "expected_hex": "1b5b39373a36353b36751b5b39373a36353b3675", + "actual_hex": "1b5b39373a36353b36751b5b39373a36353b3675", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 4097, + 0 + ], + [ + 0, + 29, + 1, + 4097, + 0 + ], + [ + 1, + 29, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+= press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "2b1b5b36313a34333b363a32751b5b36313a34333b363a3375", + "actual_hex": "2b1b5b36313a34333b363a32751b5b36313a34333b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4097, + 0 + ], + [ + 0, + 70, + 1, + 4097, + 0 + ], + [ + 1, + 70, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+A press repeat release", + "synthetic_layout": false, + "flags": 6, + "expected_hex": "011b5b39373a36353b363a32751b5b39373a36353b363a3375", + "actual_hex": "011b5b39373a36353b363a32751b5b39373a36353b363a3375", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 4097, + 0 + ], + [ + 0, + 29, + 1, + 4097, + 0 + ], + [ + 1, + 29, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+= press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b36313a34333b36751b5b36313a34333b363a32751b5b36313a34333b363a3375", + "actual_hex": "1b5b36313a34333b36751b5b36313a34333b363a32751b5b36313a34333b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4097, + 0 + ], + [ + 0, + 70, + 1, + 4097, + 0 + ], + [ + 1, + 70, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Alternate Ctrl+Shift+A press repeat release", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b39373a36353b36751b5b39373a36353b363a32751b5b39373a36353b363a3375", + "actual_hex": "1b5b39373a36353b36751b5b39373a36353b363a32751b5b39373a36353b363a3375", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 4097, + 0 + ], + [ + 0, + 29, + 1, + 4097, + 0 + ], + [ + 1, + 29, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Shift released before shortcut key", + "synthetic_layout": false, + "flags": 7, + "expected_hex": "1b5b39373a36353b36751b5b39373b353a3375", + "actual_hex": "1b5b39373a36353b36751b5b39373b353a3375", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 4097, + 0 + ], + [ + 1, + 29, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "Fixture Cyrillic primary shifted and base", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b313038393a313035373a39393b36751b5b313038393a313035373a39393b363a32751b5b313038393a313035373a39393b363a3375", + "actual_hex": "1b5b313038393a313035373a39393b36751b5b313038393a313035373a39393b363a32751b5b313038393a313035373a39393b363a3375", + "passed": true, + "events": [ + [ + 0, + 31, + 0, + 4097, + 0 + ], + [ + 0, + 31, + 1, + 4097, + 0 + ], + [ + 1, + 31, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Fixture Cyrillic base-only and modifier-first release", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b313038393a3a39393b35751b5b313038393a3a39393b313a3375", + "actual_hex": "1b5b313038393a3a39393b35751b5b313038393a3a39393b313a3375", + "passed": true, + "events": [ + [ + 0, + 31, + 0, + 4096, + 0 + ], + [ + 1, + 31, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Fixture soft keyboard omits physical base", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b313038393a313035373b36751b5b313038393a313035373b363a3375", + "actual_hex": "1b5b313038393a313035373b36751b5b313038393a313035373b363a3375", + "passed": true, + "events": [ + [ + 0, + 31, + 0, + 4097, + 2 + ], + [ + 1, + 31, + 0, + 4097, + 2 + ] + ] + } + ], + "date": "2026-10-09", + "apk_version": "0.118.0+kitty.59c2f1ea.10429391625c", + "apk_arm64_sha256": "3a228d5a3bf52604670879663ad6017dcf0218acabd942e71a1e826182cd56d1", + "source_snapshot_sha256": "10429391625cb048ef9ea822c421b00758a0ccdf4671d78cd51a0d93ccb6ce2e", + "base_commit": "59c2f1ea591fed2c282380b0542597d4b5c29ba1", + "all_recorded_checks_passed": true, + "counts": { + "real_android_virtual_layout": 122, + "synthetic_layout_values": 3 + }, + "notes": [ + "Framework events are directly delivered to production TerminalView, not injected through OS input dispatch.", + "Three cases override Unicode resolution in a synthetic KeyEvent fixture to exercise Cyrillic/base-only/soft keyboard boundaries.", + "Flags 0 through 7 use raw negotiation emitted by a real bash process.", + "Capture uses foreground dd, a start handshake, and an actual foreground target activity to avoid background freezing." + ], + "uncovered": [ + "No new flag 4 physical Bluetooth keyboard, real IME touches, hardware non-Latin layout, AltGr hardware, SSH or multiplexer verification. Historical phase 2 evidence does not validate flag 4." + ], + "cleanup": "Fixture processes exited; temporary capture directory removed; temporary instrumentation package uninstalled; Termux reopened. Application data and host toolchain/APKs retained." +} diff --git a/docs/kitty-keyboard-alternate-review-validation.json b/docs/kitty-keyboard-alternate-review-validation.json new file mode 100644 index 00000000000..7becb96c6ab --- /dev/null +++ b/docs/kitty-keyboard-alternate-review-validation.json @@ -0,0 +1,407 @@ +{ + "method": "Android framework KeyEvent delivered directly to production TerminalView; real TerminalSession/native PTY and bash raw byte capture; not physical HID or OS input dispatch", + "model": "PTP-AN10", + "android": "16", + "checks": 138, + "failures": 0, + "date": "2026-10-09", + "apk_version": "0.118.0+kitty.817dfb43.edcdab58162c", + "apk_arm64_sha256": "37b2b7587bd31c69f9c33067f22a21276014f5c79ab9e6ccc7a29f625d7e7afc", + "source_snapshot_sha256": "edcdab58162c2689b21bdabcc30201f3c9abe74c5f70b76a3b11673157e38dd4", + "base_commit": "817dfb4322a6e072bc006638da20e6e24532fc2b", + "all_recorded_checks_passed": true, + "counts": { + "android_virtual_layout": 122, + "synthetic_layout_fixture": 16 + }, + "notes": [ + "Framework KeyEvents are delivered directly to production TerminalView and the native PTY; not physical HID or OS injection.", + "Original 125 regression cases plus 13 new synthetic Shift-side cases are verified on the new APK.", + "New fixtures cover generic/left/right/both Shift, right Shift+AltGr, ordinary right-Shift text, and virtual Shift default." + ], + "uncovered": [ + "No new physical side-dependent keyboard layout or real IME verification is claimed for these fixes." + ], + "regression_checks": 125, + "regression_reference": "kitty-keyboard-alternate-device-validation.json (same expected cases, rerun on this APK)", + "new_checks": [ + { + "name": "Fixture Shift side 64 shortcut hold", + "synthetic_layout": true, + "flags": 5, + "expected_hex": "1b5b36313a33353b36751b5b36313a33353b3675", + "actual_hex": "1b5b36313a33353b36751b5b36313a33353b3675", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4161, + 0 + ], + [ + 0, + 70, + 1, + 4161, + 0 + ], + [ + 1, + 70, + 0, + 4161, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 128 shortcut hold", + "synthetic_layout": true, + "flags": 5, + "expected_hex": "1b5b36313a34323b36751b5b36313a34323b3675", + "actual_hex": "1b5b36313a34323b36751b5b36313a34323b3675", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4225, + 0 + ], + [ + 0, + 70, + 1, + 4225, + 0 + ], + [ + 1, + 70, + 0, + 4225, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 192 shortcut hold", + "synthetic_layout": true, + "flags": 5, + "expected_hex": "1b5b36313a39343b36751b5b36313a39343b3675", + "actual_hex": "1b5b36313a39343b36751b5b36313a39343b3675", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4289, + 0 + ], + [ + 0, + 70, + 1, + 4289, + 0 + ], + [ + 1, + 70, + 0, + 4289, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 0 shortcut hold", + "synthetic_layout": true, + "flags": 5, + "expected_hex": "1b5b36313a34333b36751b5b36313a34333b3675", + "actual_hex": "1b5b36313a34333b36751b5b36313a34333b3675", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4097, + 0 + ], + [ + 0, + 70, + 1, + 4097, + 0 + ], + [ + 1, + 70, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 64 shortcut hold", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b36313a33353b36751b5b36313a33353b363a32751b5b36313a33353b363a3375", + "actual_hex": "1b5b36313a33353b36751b5b36313a33353b363a32751b5b36313a33353b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4161, + 0 + ], + [ + 0, + 70, + 1, + 4161, + 0 + ], + [ + 1, + 70, + 0, + 4161, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 128 shortcut hold", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b36313a34323b36751b5b36313a34323b363a32751b5b36313a34323b363a3375", + "actual_hex": "1b5b36313a34323b36751b5b36313a34323b363a32751b5b36313a34323b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4225, + 0 + ], + [ + 0, + 70, + 1, + 4225, + 0 + ], + [ + 1, + 70, + 0, + 4225, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 192 shortcut hold", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b36313a39343b36751b5b36313a39343b363a32751b5b36313a39343b363a3375", + "actual_hex": "1b5b36313a39343b36751b5b36313a39343b363a32751b5b36313a39343b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4289, + 0 + ], + [ + 0, + 70, + 1, + 4289, + 0 + ], + [ + 1, + 70, + 0, + 4289, + 0 + ] + ] + }, + { + "name": "Fixture Shift side 0 shortcut hold", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b36313a34333b36751b5b36313a34333b363a32751b5b36313a34333b363a3375", + "actual_hex": "1b5b36313a34333b36751b5b36313a34333b363a32751b5b36313a34333b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4097, + 0 + ], + [ + 0, + 70, + 1, + 4097, + 0 + ], + [ + 1, + 70, + 0, + 4097, + 0 + ] + ] + }, + { + "name": "Fixture right Shift+AltGr retains side", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b383336343a3231353a36313b36751b5b383336343a3231353a36313b363a32751b5b383336343a3a36313b313a3375", + "actual_hex": "1b5b383336343a3231353a36313b36751b5b383336343a3231353a36313b363a32751b5b383336343a3a36313b313a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4259, + 0 + ], + [ + 0, + 70, + 1, + 4259, + 0 + ], + [ + 1, + 70, + 0, + 34, + 0 + ] + ] + }, + { + "name": "Fixture plain right Shift stays text", + "synthetic_layout": true, + "flags": 0, + "expected_hex": "2a", + "actual_hex": "2a", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 129, + 0 + ], + [ + 1, + 70, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Fixture plain right Shift stays text", + "synthetic_layout": true, + "flags": 4, + "expected_hex": "2a", + "actual_hex": "2a", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 129, + 0 + ], + [ + 1, + 70, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Fixture plain right Shift stays text", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "2a", + "actual_hex": "2a", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 129, + 0 + ], + [ + 1, + 70, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Fixture virtual Shift keeps left-side default", + "synthetic_layout": true, + "flags": 7, + "expected_hex": "1b5b36313a33353b36751b5b36313a33353b363a3375", + "actual_hex": "1b5b36313a33353b36751b5b36313a33353b363a3375", + "passed": true, + "events": [ + [ + 0, + 70, + 0, + 4096, + 0 + ], + [ + 1, + 70, + 0, + 4096, + 0 + ] + ] + } + ], + "full_report_artifact": "kitty-alternate-review-fixes/device-validation.json", + "full_report_sha256": "ff0d8f483ef14350b860c5c160a455e083e1ff88bad7c2b5c469a77717a90da1", + "cleanup": "Fixture processes exited; temporary capture directory and helper removed; Termux reopened; application data, host APKs/reports and toolchain retained." +} diff --git a/docs/kitty-keyboard-device-validation.json b/docs/kitty-keyboard-device-validation.json new file mode 100644 index 00000000000..efdaf0c7658 --- /dev/null +++ b/docs/kitty-keyboard-device-validation.json @@ -0,0 +1,333 @@ +{ + "device": "PTP-AN10", + "android": "16", + "abi": "arm64-v8a", + "source_commit": "91ae4885", + "method": "Android system-injected key events through installed Termux TerminalView, native PTY and raw byte capture", + "tests": [ + { + "mode": "kitty", + "protocol_replies_hex": "1b5b3f30751b5b3f31751b5b3f30751b5b3f31751b5b3f30751b5b3f31751b5b3f30751b5b3f3175", + "results": [ + { + "key": "Ctrl+0", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "key": "Ctrl+1", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + }, + { + "key": "Ctrl+2", + "expected_hex": "1b5b35303b3575", + "actual_hex": "1b5b35303b3575", + "passed": true + }, + { + "key": "Ctrl+3", + "expected_hex": "1b5b35313b3575", + "actual_hex": "1b5b35313b3575", + "passed": true + }, + { + "key": "Ctrl+4", + "expected_hex": "1b5b35323b3575", + "actual_hex": "1b5b35323b3575", + "passed": true + }, + { + "key": "Ctrl+5", + "expected_hex": "1b5b35333b3575", + "actual_hex": "1b5b35333b3575", + "passed": true + }, + { + "key": "Ctrl+6", + "expected_hex": "1b5b35343b3575", + "actual_hex": "1b5b35343b3575", + "passed": true + }, + { + "key": "Ctrl+7", + "expected_hex": "1b5b35353b3575", + "actual_hex": "1b5b35353b3575", + "passed": true + }, + { + "key": "Ctrl+8", + "expected_hex": "1b5b35363b3575", + "actual_hex": "1b5b35363b3575", + "passed": true + }, + { + "key": "Ctrl+9", + "expected_hex": "1b5b35373b3575", + "actual_hex": "1b5b35373b3575", + "passed": true + }, + { + "key": "Ctrl+I", + "expected_hex": "1b5b3130353b3575", + "actual_hex": "1b5b3130353b3575", + "passed": true + }, + { + "key": "Ctrl+Shift+A", + "expected_hex": "1b5b39373b3675", + "actual_hex": "1b5b39373b3675", + "passed": true + }, + { + "key": "Ctrl+Shift+=", + "expected_hex": "1b5b36313b3675", + "actual_hex": "1b5b36313b3675", + "passed": true + }, + { + "key": "Shift+Enter", + "expected_hex": "1b5b31333b3275", + "actual_hex": "1b5b31333b3275", + "passed": true + }, + { + "key": "Ctrl+Enter", + "expected_hex": "1b5b31333b3575", + "actual_hex": "1b5b31333b3575", + "passed": true + }, + { + "key": "Alt+Enter", + "expected_hex": "1b5b31333b3375", + "actual_hex": "1b5b31333b3375", + "passed": true + }, + { + "key": "Ctrl+Tab", + "expected_hex": "1b5b393b3575", + "actual_hex": "1b5b393b3575", + "passed": true + }, + { + "key": "Shift+Tab", + "expected_hex": "1b5b393b3275", + "actual_hex": "1b5b393b3275", + "passed": true + }, + { + "key": "Ctrl+Shift+Tab", + "expected_hex": "1b5b393b3675", + "actual_hex": "1b5b393b3675", + "passed": true + }, + { + "key": "Plain Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "key": "Plain Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "key": "Backspace", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "key": "Escape", + "expected_hex": "1b5b323775", + "actual_hex": "1b5b323775", + "passed": true + }, + { + "key": "F3", + "expected_hex": "1b5b31337e", + "actual_hex": "1b5b31337e", + "passed": true + }, + { + "key": "Plain A", + "expected_hex": "61", + "actual_hex": "61", + "passed": true + }, + { + "key": "Shift+A", + "expected_hex": "41", + "actual_hex": "41", + "passed": true + } + ] + }, + { + "mode": "legacy", + "protocol_replies_hex": "1b5b3f3075", + "results": [ + { + "key": "Ctrl+0", + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "key": "Ctrl+1", + "expected_hex": "31", + "actual_hex": "31", + "passed": true + }, + { + "key": "Ctrl+2", + "expected_hex": "00", + "actual_hex": "00", + "passed": true + }, + { + "key": "Ctrl+3", + "expected_hex": "1b", + "actual_hex": "1b", + "passed": true + }, + { + "key": "Ctrl+4", + "expected_hex": "1c", + "actual_hex": "1c", + "passed": true + }, + { + "key": "Ctrl+5", + "expected_hex": "1d", + "actual_hex": "1d", + "passed": true + }, + { + "key": "Ctrl+6", + "expected_hex": "1e", + "actual_hex": "1e", + "passed": true + }, + { + "key": "Ctrl+7", + "expected_hex": "1f", + "actual_hex": "1f", + "passed": true + }, + { + "key": "Ctrl+8", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "key": "Ctrl+9", + "expected_hex": "39", + "actual_hex": "39", + "passed": true + }, + { + "key": "Ctrl+I", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "key": "Ctrl+Shift+A", + "expected_hex": "01", + "actual_hex": "01", + "passed": true + }, + { + "key": "Ctrl+Shift+=", + "expected_hex": "2b", + "actual_hex": "2b", + "passed": true + }, + { + "key": "Shift+Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "key": "Ctrl+Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "key": "Alt+Enter", + "expected_hex": "1b0d", + "actual_hex": "1b0d", + "passed": true + }, + { + "key": "Ctrl+Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "key": "Shift+Tab", + "expected_hex": "1b5b5a", + "actual_hex": "1b5b5a", + "passed": true + }, + { + "key": "Ctrl+Shift+Tab", + "expected_hex": "1b5b5a", + "actual_hex": "1b5b5a", + "passed": true + }, + { + "key": "Plain Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "key": "Plain Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "key": "Backspace", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "key": "Escape", + "expected_hex": "1b", + "actual_hex": "1b", + "passed": true + }, + { + "key": "F3", + "expected_hex": "1b4f52", + "actual_hex": "1b4f52", + "passed": true + }, + { + "key": "Plain A", + "expected_hex": "61", + "actual_hex": "61", + "passed": true + }, + { + "key": "Shift+A", + "expected_hex": "41", + "actual_hex": "41", + "passed": true + } + ] + } + ] +} diff --git a/docs/kitty-keyboard-events-device-validation.json b/docs/kitty-keyboard-events-device-validation.json new file mode 100644 index 00000000000..d0691af0dd8 --- /dev/null +++ b/docs/kitty-keyboard-events-device-validation.json @@ -0,0 +1,1769 @@ +{ + "method": "Android framework KeyEvent delivered directly to production TerminalView; real TerminalSession/native PTY and bash raw byte capture; not physical HID or OS input dispatch", + "model": "PTP-AN10", + "android": "16", + "checks": 59, + "failures": 0, + "results": [ + { + "name": "Up press repeat release", + "flags": 0, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "flags": 0, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "flags": 0, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "flags": 0, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "flags": 0, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "flags": 0, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "flags": 0, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "flags": 0, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "flags": 0, + "expected_hex": "1b1b", + "actual_hex": "1b1b", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "flags": 1, + "expected_hex": "1b5b411b5b41", + "actual_hex": "1b5b411b5b41", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "flags": 1, + "expected_hex": "1b5b3130353b35751b5b3130353b3575", + "actual_hex": "1b5b3130353b35751b5b3130353b3575", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "flags": 1, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "flags": 1, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "flags": 1, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "flags": 1, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "flags": 1, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "flags": 1, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "flags": 1, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "flags": 1, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "flags": 1, + "expected_hex": "1b5b3237751b5b323775", + "actual_hex": "1b5b3237751b5b323775", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "flags": 2, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "flags": 2, + "expected_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "091b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "flags": 2, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "flags": 2, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "flags": 2, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "flags": 2, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "flags": 2, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "flags": 2, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "flags": 2, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "flags": 2, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "flags": 2, + "expected_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b1b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Up press repeat release", + "flags": 3, + "expected_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "actual_hex": "1b5b411b5b313b313a32411b5b313b313a3341", + "passed": true, + "events": [ + [ + 0, + 19, + 0, + 0, + 0 + ], + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Ctrl+I press repeat release", + "flags": 3, + "expected_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b353a32751b5b3130353b353a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 0, + 37, + 1, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "plain text A press repeat release", + "flags": 3, + "expected_hex": "6161", + "actual_hex": "6161", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 0, + 0 + ], + [ + 0, + 29, + 1, + 0, + 0 + ], + [ + 1, + 29, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Shift text A press repeat release", + "flags": 3, + "expected_hex": "4141", + "actual_hex": "4141", + "passed": true, + "events": [ + [ + 0, + 29, + 0, + 1, + 0 + ], + [ + 0, + 29, + 1, + 1, + 0 + ], + [ + 1, + 29, + 0, + 1, + 0 + ] + ] + }, + { + "name": "plain recovery 66", + "flags": 3, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 0, + 0 + ], + [ + 0, + 66, + 1, + 0, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 66", + "flags": 3, + "expected_hex": "0d0d", + "actual_hex": "0d0d", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 3145728, + 0 + ], + [ + 0, + 66, + 1, + 3145728, + 0 + ], + [ + 1, + 66, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 61", + "flags": 3, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 0, + 0 + ], + [ + 0, + 61, + 1, + 0, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 61", + "flags": 3, + "expected_hex": "0909", + "actual_hex": "0909", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 3145728, + 0 + ], + [ + 0, + 61, + 1, + 3145728, + 0 + ], + [ + 1, + 61, + 0, + 0, + 0 + ] + ] + }, + { + "name": "plain recovery 67", + "flags": 3, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 0, + 0 + ], + [ + 0, + 67, + 1, + 0, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "locked recovery 67", + "flags": 3, + "expected_hex": "7f7f", + "actual_hex": "7f7f", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 3145728, + 0 + ], + [ + 0, + 67, + 1, + 3145728, + 0 + ], + [ + 1, + 67, + 0, + 0, + 0 + ] + ] + }, + { + "name": "Escape press repeat release", + "flags": 3, + "expected_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "actual_hex": "1b5b3237751b5b32373b313a32751b5b32373b313a3375", + "passed": true, + "events": [ + [ + 0, + 111, + 0, + 0, + 0 + ], + [ + 0, + 111, + 1, + 0, + 0 + ], + [ + 1, + 111, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "flags": 2, + "expected_hex": "0d1b5b31333b353a3275", + "actual_hex": "0d1b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "flags": 2, + "expected_hex": "1b5b5a1b5b393b323a3275", + "actual_hex": "1b5b5a1b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "flags": 2, + "expected_hex": "1b7f1b5b3132373b333a3275", + "actual_hex": "1b7f1b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "flags": 2, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "flags": 2, + "expected_hex": "091b5b3130353b313a3375", + "actual_hex": "091b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "flags": 2, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P2 Ctrl+Enter repeated", + "flags": 3, + "expected_hex": "1b5b31333b35751b5b31333b353a3275", + "actual_hex": "1b5b31333b35751b5b31333b353a3275", + "passed": true, + "events": [ + [ + 0, + 66, + 0, + 4096, + 0 + ], + [ + 0, + 66, + 1, + 4096, + 0 + ], + [ + 1, + 66, + 0, + 4096, + 0 + ] + ] + }, + { + "name": "P2 Shift+Tab repeated", + "flags": 3, + "expected_hex": "1b5b393b32751b5b393b323a3275", + "actual_hex": "1b5b393b32751b5b393b323a3275", + "passed": true, + "events": [ + [ + 0, + 61, + 0, + 1, + 0 + ], + [ + 0, + 61, + 1, + 1, + 0 + ], + [ + 1, + 61, + 0, + 1, + 0 + ] + ] + }, + { + "name": "P2 Alt+Backspace repeated", + "flags": 3, + "expected_hex": "1b5b3132373b33751b5b3132373b333a3275", + "actual_hex": "1b5b3132373b33751b5b3132373b333a3275", + "passed": true, + "events": [ + [ + 0, + 67, + 0, + 18, + 0 + ], + [ + 0, + 67, + 1, + 18, + 0 + ], + [ + 1, + 67, + 0, + 18, + 0 + ] + ] + }, + { + "name": "F3 Shift press repeat release", + "flags": 3, + "expected_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "actual_hex": "1b5b31333b327e1b5b31333b323a327e1b5b31333b323a337e", + "passed": true, + "events": [ + [ + 0, + 133, + 0, + 1, + 0 + ], + [ + 0, + 133, + 1, + 1, + 0 + ], + [ + 1, + 133, + 0, + 1, + 0 + ] + ] + }, + { + "name": "modifier released before letter", + "flags": 3, + "expected_hex": "1b5b3130353b35751b5b3130353b313a3375", + "actual_hex": "1b5b3130353b35751b5b3130353b313a3375", + "passed": true, + "events": [ + [ + 0, + 37, + 0, + 4096, + 0 + ], + [ + 1, + 37, + 0, + 0, + 0 + ] + ] + }, + { + "name": "orphaned repeat has no release", + "flags": 3, + "expected_hex": "1b5b313b313a3241", + "actual_hex": "1b5b313b313a3241", + "passed": true, + "events": [ + [ + 0, + 19, + 1, + 0, + 0 + ], + [ + 1, + 19, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P1 NumLock on to off during repeats", + "flags": 3, + "expected_hex": "1b5b35373339393b313239751b5b35373432353b313a32751b5b35373339393b313a3375", + "actual_hex": "1b5b35373339393b313239751b5b35373432353b313a32751b5b35373339393b313a3375", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 2097152, + 0 + ], + [ + 0, + 144, + 1, + 0, + 0 + ], + [ + 1, + 144, + 0, + 0, + 0 + ] + ] + }, + { + "name": "P1 NumLock off to on during repeats", + "flags": 3, + "expected_hex": "1b5b3537343235751b5b35373339393b3132393a32751b5b35373432353b3132393a3375", + "actual_hex": "1b5b3537343235751b5b35373339393b3132393a32751b5b35373432353b3132393a3375", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 0, + 0 + ], + [ + 0, + 144, + 1, + 2097152, + 0 + ], + [ + 1, + 144, + 0, + 2097152, + 0 + ] + ] + }, + { + "name": "P1 legacy keypad navigation to text repeat", + "flags": 2, + "expected_hex": "1b5b327e301b5b323b3132393a337e", + "actual_hex": "1b5b327e301b5b323b3132393a337e", + "passed": true, + "events": [ + [ + 0, + 144, + 0, + 0, + 0 + ], + [ + 0, + 144, + 1, + 2097152, + 0 + ], + [ + 1, + 144, + 0, + 2097152, + 0 + ] + ] + } + ], + "date": "2026-10-08", + "apk_version": "0.118.0+kitty.events.p2.60a581a1.2439221fb851", + "apk_arm64_sha256": "e792ba8bea09cccbafaa262154fc6f6ecda32f9ab5180f70020716960f22c392", + "source_patch_sha256": "2439221fb851e0e8ddef6501402d9aca9ea2149ad17c0a39e4a0aa43bc98f02c", + "base_commit": "60a581a175acbf632960af0a5226f90e94ca7818", + "all_recorded_checks_passed": true, + "uncovered": [ + "Physical HID keyboards and OS input-dispatch injection", + "Real IME/extra-key touches and device lifecycle/session-switch behavior", + "SSH and multiplexer behavior", + "AltGr+Space with a character-producing hardware layout" + ], + "harness_notes": [ + "Foreground target activity avoids OEM background process freezing.", + "Capture uses timeout --foreground and an explicit start-file handshake.", + "A previous attempt recorded 58 passes and one infrastructure timeout; report retained separately.", + "Initial background harness stalled; user authorized terminating its Termux process. No application data cleared." + ] +} diff --git a/docs/kitty-keyboard-ime-validation.json b/docs/kitty-keyboard-ime-validation.json new file mode 100644 index 00000000000..76b8dee2384 --- /dev/null +++ b/docs/kitty-keyboard-ime-validation.json @@ -0,0 +1,50 @@ +{ + "device": "PTP-AN10", + "android": "16", + "source_commit": "91ae4885", + "method": "Touching real Termux extra keys and Rime soft keyboard; app lifecycle and keyboard resize; shell capture setup through temporary instrumentation", + "results": [ + { + "name": "Termux CTRL extra key + real Rime IME 0", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "name": "IME plain 0 after one-shot CTRL clears", + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "name": "Termux CTRL extra key + real Rime IME 2", + "expected_hex": "1b5b35303b3575", + "actual_hex": "1b5b35303b3575", + "passed": true + }, + { + "name": "Termux CTRL extra key + real Rime IME 1", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + }, + { + "name": "Real Rime IME Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "name": "CTRL extra key + IME 0 after app background and foreground", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "name": "CTRL extra key + IME 1 after keyboard close/open and view resize", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + } + ] +} diff --git a/docs/kitty-keyboard-real-input-validation.json b/docs/kitty-keyboard-real-input-validation.json new file mode 100644 index 00000000000..964fecdc5f2 --- /dev/null +++ b/docs/kitty-keyboard-real-input-validation.json @@ -0,0 +1,365 @@ +{ + "started_date": "2026-10-08", + "completed_date": "2026-10-09", + "device": { + "model": "PTP-AN10", + "android": "16", + "abi": "arm64-v8a" + }, + "apk_version": "0.118.0+kitty.events.p2.60a581a1.2439221fb851", + "apk_sha256": "e792ba8bea09cccbafaa262154fc6f6ecda32f9ab5180f70020716960f22c392", + "source_patch_sha256": "2439221fb851e0e8ddef6501402d9aca9ea2149ad17c0a39e4a0aa43bc98f02c", + "bluetooth": { + "keyboard": "RAPOO Keyboard-1 Keyboard", + "android_device_id": 9, + "sources": "0x01000311", + "keyboard_type": 2, + "actual_hardware": true, + "input_injection": false, + "flag3_group": { + "stage": "bt_flag3_retry_1791470731", + "flags": 3, + "method": "Actual Bluetooth keyboard hardware input through Android input dispatch, production Termux activity/client/view and native PTY. No injected key events.", + "keyboard": "RAPOO Keyboard-1 Keyboard", + "raw_hex": "1b5b411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a32411b5b313b313a33411b5b3130353b35751b5b3130353b353a33751b5b3130353b35751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a33751b5b39373b36751b5b39373b363a33751b5b31333b35751b5b393b32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b33323b33751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a32751b5b33323b333a33751b5b3237751b5b32373b313a33751b5b3130353b35751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a32751b5b3130353b353a337561410d097f", + "sequence_counts": { + "ESC[A": 1, + "ESC[1;1:2A": 13, + "ESC[1;1:3A": 1, + "ESC[105;5u": 3, + "ESC[105;5:3u": 3, + "ESC[105;5:2u": 27, + "ESC[97;6u": 1, + "ESC[97;6:3u": 1, + "ESC[13;5u": 1, + "ESC[9;2u": 1, + "ESC[9;2:2u": 28, + "ESC[32;3u": 1, + "ESC[32;3:2u": 25, + "ESC[32;3:3u": 1, + "ESC[27u": 1, + "ESC[27;1:3u": 1 + }, + "passed_observations": [ + "Up press/repeat/release", + "Ctrl+I press/repeat/release", + "Ctrl+Shift+A press/release", + "Esc press/release", + "plain a and Shift+A text", + "plain Enter/Tab/Backspace control bytes without releases" + ], + "pending": [ + "Ctrl+Enter repeat absent", + "requested Alt+Backspace yielded Alt+Space", + "modifier-first I release still carried Ctrl" + ], + "text_tail_hex": "61410d097f", + "all_requested_checks_passed": false + }, + "flag2_recovery_supplement": { + "flags": 2, + "keyboard": "RAPOO Keyboard-1 Keyboard", + "method": "Actual Bluetooth keyboard hardware input through Android input dispatch, production Termux activity/client/view and native PTY. No injected key events.", + "hardware_event_log": "bt_flag2_recovery_1791470895-hardware-events.log", + "raw_capture": "bt_flag2_recovery_1791470895.bin", + "raw_hex": "0d1b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b31333b353a32751b5b5a1b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b5b393b323a32751b7f1b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a32751b5b3132373b333a3275", + "checks": [ + { + "input": "Ctrl+Enter hold", + "repeat_count": 34, + "press_hex": "0d", + "repeat_hex": "1b5b31333b353a3275", + "release": "none", + "passed": true + }, + { + "input": "Shift+Tab hold", + "repeat_count": 37, + "press_hex": "1b5b5a", + "repeat_hex": "1b5b393b323a3275", + "release": "none", + "passed": true + }, + { + "input": "left Alt+Backspace hold", + "repeat_count": 39, + "press_hex": "1b7f", + "repeat_hex": "1b5b3132373b333a3275", + "release": "none", + "passed": true + } + ], + "all_checks_passed": true + }, + "limitations": [ + "No numeric keypad: P1 Num Lock/keypad hardware scenarios skipped.", + "Original flag3 group had no Ctrl+Enter repeat, Alt+Space instead of requested Alt+Backspace, and I released while Ctrl still active; these original cases were not passed.", + "The simpler flag2 supplement confirms real Ctrl+Enter, Shift+Tab, Alt+Backspace holds and correct repeated bytes; it does not retroactively pass the missing flag3 cases.", + "Modifier-first I release remains unverified on hardware.", + "AltGr character-producing layout not tested." + ] + }, + "ime": { + "id": "org.fcitx.fcitx5.android/.input.FcitxInputMethodService", + "engines": [ + "English", + "Rime 中州韵(薄)" + ], + "method": "ADB touchscreen taps and gestures on actual IME and Termux extra-key UI, through actual Termux activity/client/view/session and native PTY; no direct text commit or injected key events", + "checks": [ + { + "name": "ime_ctrl0", + "flags": 3, + "stage": "ime_ctrl0_1791471312968", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 280, + 1750 + ], + [ + 640, + 2570 + ] + ], + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "name": "ime_plain0_after_ctrl", + "flags": 3, + "stage": "ime_plain0_after_ctrl_1791471314212", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 640, + 2570 + ] + ], + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "name": "ime_alt1", + "flags": 3, + "stage": "ime_alt1_1791471315228", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 460, + 1750 + ], + [ + 340, + 2050 + ] + ], + "expected_hex": "1b5b34393b3375", + "actual_hex": "1b5b34393b3375", + "passed": true + }, + { + "name": "ime_plain1_after_alt", + "flags": 3, + "stage": "ime_plain1_after_alt_1791471316621", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 340, + 2050 + ] + ], + "expected_hex": "31", + "actual_hex": "31", + "passed": true + }, + { + "name": "ime_enter", + "flags": 3, + "stage": "ime_enter_1791471317758", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 1180, + 2570 + ] + ], + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "name": "ime_ctrl_enter", + "flags": 3, + "stage": "ime_ctrl_enter_1791471318893", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 280, + 1750 + ], + [ + 1180, + 2570 + ] + ], + "expected_hex": "1b5b31333b3575", + "actual_hex": "1b5b31333b3575", + "passed": true + }, + { + "name": "ime_chinese_candidate_commit", + "flags": 3, + "stage": "ime_chinese_composition_1791471362808", + "method": "ADB touchscreen taps on actual Fcitx Rime engine; zhongwen composition and visible 中文 candidate tap; native PTY", + "composition_before_commit_hex": "", + "expected_hex": "e4b8ade69687", + "actual_hex": "e4b8ade69687", + "passed": true, + "taps": [ + [ + 256, + 2400 + ], + [ + 768, + 2200 + ], + [ + 1088, + 2025 + ], + [ + 896, + 2400 + ], + [ + 640, + 2200 + ], + [ + 192, + 2025 + ], + [ + 320, + 2025 + ], + [ + 896, + 2400 + ], + [ + 110, + 1890 + ] + ] + }, + { + "name": "ime_flag2_ctrl0_legacy_press", + "flags": 2, + "stage": "ime_flag2_ctrl0_legacy_press_1791471462027", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 280, + 1750 + ], + [ + 640, + 2570 + ] + ], + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "name": "ime_flag2_plain0_after_ctrl", + "flags": 2, + "stage": "ime_flag2_plain0_after_ctrl_1791471544202", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 640, + 2570 + ] + ], + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "name": "ime_flag2_alt1_legacy_press", + "flags": 2, + "stage": "ime_flag2_alt1_legacy_press_1791471545353", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 460, + 1750 + ], + [ + 340, + 2050 + ] + ], + "expected_hex": "1b31", + "actual_hex": "1b31", + "passed": true + }, + { + "name": "ime_flag2_ctrl_enter_legacy_press", + "flags": 2, + "stage": "ime_flag2_ctrl_enter_legacy_press_1791471546825", + "method": "ADB touchscreen taps on real Fcitx IME/Termux UI; no injected text or key events; real native PTY capture", + "taps": [ + [ + 280, + 1750 + ], + [ + 1180, + 2570 + ] + ], + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "name": "ime_background_foreground", + "flags": 3, + "stage": "ime_background_foreground_1791471548286", + "method": "Actual IME/extra-key taps, Home touchscreen gesture and Termux resume; real native PTY", + "expected_hex": "1b5b34383b3575301b5b34383b3575", + "actual_hex": "1b5b34383b3575301b5b34383b3575", + "passed": true, + "operations": [ + "tap CTRL then IME 0", + "touchscreen Home gesture", + "am start TermuxActivity", + "tap IME 0 without modifier", + "tap CTRL then IME 0" + ] + } + ], + "total_checks": 12, + "failures": 0, + "all_checks_passed": true + }, + "test_harness": { + "setup": "Temporary instrumentation created a named dedicated Termux session and wrote only the capture script startup command. Test input came from user Bluetooth hardware or touchscreen taps.", + "control": "Foreground bash with raw /dev/tty dd capture; each test negotiates and pops its own keyboard mode.", + "infrastructure_note": "A temporary request-file race during extra flag2 IME checks was corrected with atomic rename; no invalid stage was counted.", + "cleanup": "Capture shell exited and restored modes; temporary helper uninstalled; fixture directory and raw event/UI dump files removed; original IME display setting restored; Termux reopened. Host artifacts and toolchain retained." + }, + "remaining": [ + "Physical Num Lock transitions and modifier-first key release", + "Untested hardware combinations/layouts", + "Other IMEs", + "SSH and multiplexers" + ] +} diff --git a/docs/kitty-keyboard-review-device-validation.json b/docs/kitty-keyboard-review-device-validation.json new file mode 100644 index 00000000000..2ec83e732d2 --- /dev/null +++ b/docs/kitty-keyboard-review-device-validation.json @@ -0,0 +1,732 @@ +{ + "source_commit": "65c625b2bc7d4b1552084f449400bda18cb00b84", + "apk_version": "0.118.0+kitty.65c625b2", + "date": "2026-10-07", + "device": { + "model": "PTP-AN10", + "android": "16", + "abi": "arm64-v8a" + }, + "counts": { + "framework_key_pty_and_scroll": 83, + "current_virtual_layout": 2, + "rime_touch_and_lifecycle": 7, + "rime_space_and_modifiers": 4 + }, + "total_checks": 96, + "all_recorded_checks_passed": true, + "uncovered": [ + "AltGr+Space producing NBSP or another layout character remains unverified. Automated device checks used a virtual mapping that returns no character; the user-reported combination retest does not specifically confirm this layout case." + ], + "reports": { + "framework_key_pty_and_scroll": { + "source_commit": "65c625b2", + "method": "Android framework KeyEvent delivered directly to installed production TerminalView; real session queue, native PTY and foreground shell capture", + "results": [ + { + "mode": "kitty", + "key": "Ctrl+0", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+1", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+2", + "expected_hex": "1b5b35303b3575", + "actual_hex": "1b5b35303b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+3", + "expected_hex": "1b5b35313b3575", + "actual_hex": "1b5b35313b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+4", + "expected_hex": "1b5b35323b3575", + "actual_hex": "1b5b35323b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+5", + "expected_hex": "1b5b35333b3575", + "actual_hex": "1b5b35333b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+6", + "expected_hex": "1b5b35343b3575", + "actual_hex": "1b5b35343b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+7", + "expected_hex": "1b5b35353b3575", + "actual_hex": "1b5b35353b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+8", + "expected_hex": "1b5b35363b3575", + "actual_hex": "1b5b35363b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+9", + "expected_hex": "1b5b35373b3575", + "actual_hex": "1b5b35373b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+I", + "expected_hex": "1b5b3130353b3575", + "actual_hex": "1b5b3130353b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+Shift+A", + "expected_hex": "1b5b39373b3675", + "actual_hex": "1b5b39373b3675", + "passed": true + }, + { + "mode": "kitty", + "key": "Shift+Enter", + "expected_hex": "1b5b31333b3275", + "actual_hex": "1b5b31333b3275", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+Enter", + "expected_hex": "1b5b31333b3575", + "actual_hex": "1b5b31333b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+Tab", + "expected_hex": "1b5b393b3575", + "actual_hex": "1b5b393b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Shift+Tab", + "expected_hex": "1b5b393b3275", + "actual_hex": "1b5b393b3275", + "passed": true + }, + { + "mode": "kitty", + "key": "Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "mode": "kitty", + "key": "Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "mode": "kitty", + "key": "Backspace", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "mode": "kitty", + "key": "Escape", + "expected_hex": "1b5b323775", + "actual_hex": "1b5b323775", + "passed": true + }, + { + "mode": "kitty", + "key": "F3", + "expected_hex": "1b5b31337e", + "actual_hex": "1b5b31337e", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+Space", + "expected_hex": "1b5b33323b3575", + "actual_hex": "1b5b33323b3575", + "passed": true + }, + { + "mode": "kitty", + "key": "Left Alt+Space", + "expected_hex": "1b5b33323b3375", + "actual_hex": "1b5b33323b3375", + "passed": true + }, + { + "mode": "kitty", + "key": "Ctrl+PageUp", + "expected_hex": "1b5b353b357e", + "actual_hex": "1b5b353b357e", + "passed": true + }, + { + "mode": "kitty", + "key": "PageDown", + "expected_hex": "1b5b367e", + "actual_hex": "1b5b367e", + "passed": true + }, + { + "mode": "kitty", + "key": "Shift+PageUp local scroll", + "expected_hex": "", + "actual_hex": "", + "passed": true + }, + { + "mode": "kitty", + "key": "Scrollback offset after Shift+PageUp", + "actual": -27, + "expected": -27, + "passed": true + }, + { + "mode": "kitty", + "key": "Shift+PageDown local scroll", + "expected_hex": "", + "actual_hex": "", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+0", + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+1", + "expected_hex": "31", + "actual_hex": "31", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+2", + "expected_hex": "00", + "actual_hex": "00", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+3", + "expected_hex": "1b", + "actual_hex": "1b", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+4", + "expected_hex": "1c", + "actual_hex": "1c", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+5", + "expected_hex": "1d", + "actual_hex": "1d", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+6", + "expected_hex": "1e", + "actual_hex": "1e", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+7", + "expected_hex": "1f", + "actual_hex": "1f", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+8", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+9", + "expected_hex": "39", + "actual_hex": "39", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+I", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+Shift+A", + "expected_hex": "01", + "actual_hex": "01", + "passed": true + }, + { + "mode": "legacy", + "key": "Shift+Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "mode": "legacy", + "key": "Shift+Tab", + "expected_hex": "1b5b5a", + "actual_hex": "1b5b5a", + "passed": true + }, + { + "mode": "legacy", + "key": "Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "mode": "legacy", + "key": "Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "mode": "legacy", + "key": "Backspace", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "mode": "legacy", + "key": "Escape", + "expected_hex": "1b", + "actual_hex": "1b", + "passed": true + }, + { + "mode": "legacy", + "key": "F3", + "expected_hex": "1b4f52", + "actual_hex": "1b4f52", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+Space", + "expected_hex": "00", + "actual_hex": "00", + "passed": true + }, + { + "mode": "legacy", + "key": "Left Alt+Space", + "expected_hex": "1b20", + "actual_hex": "1b20", + "passed": true + }, + { + "mode": "legacy", + "key": "Ctrl+PageUp", + "expected_hex": "1b5b353b357e", + "actual_hex": "1b5b353b357e", + "passed": true + }, + { + "mode": "legacy", + "key": "PageDown", + "expected_hex": "1b5b367e", + "actual_hex": "1b5b367e", + "passed": true + }, + { + "mode": "legacy", + "key": "Shift+PageUp local scroll", + "expected_hex": "", + "actual_hex": "", + "passed": true + }, + { + "mode": "legacy", + "key": "Scrollback offset after Shift+PageUp", + "actual": -27, + "expected": -27, + "passed": true + }, + { + "mode": "legacy", + "key": "Shift+PageDown local scroll", + "expected_hex": "", + "actual_hex": "", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+0", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+1", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+2", + "expected_hex": "1b5b35303b3575", + "actual_hex": "1b5b35303b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+3", + "expected_hex": "1b5b35313b3575", + "actual_hex": "1b5b35313b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+4", + "expected_hex": "1b5b35323b3575", + "actual_hex": "1b5b35323b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+5", + "expected_hex": "1b5b35333b3575", + "actual_hex": "1b5b35333b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+6", + "expected_hex": "1b5b35343b3575", + "actual_hex": "1b5b35343b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+7", + "expected_hex": "1b5b35353b3575", + "actual_hex": "1b5b35353b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+8", + "expected_hex": "1b5b35363b3575", + "actual_hex": "1b5b35363b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+9", + "expected_hex": "1b5b35373b3575", + "actual_hex": "1b5b35373b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+I", + "expected_hex": "1b5b3130353b3575", + "actual_hex": "1b5b3130353b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+Shift+A", + "expected_hex": "1b5b39373b3675", + "actual_hex": "1b5b39373b3675", + "passed": true + }, + { + "mode": "alt", + "key": "Shift+Enter", + "expected_hex": "1b5b31333b3275", + "actual_hex": "1b5b31333b3275", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+Enter", + "expected_hex": "1b5b31333b3575", + "actual_hex": "1b5b31333b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+Tab", + "expected_hex": "1b5b393b3575", + "actual_hex": "1b5b393b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Shift+Tab", + "expected_hex": "1b5b393b3275", + "actual_hex": "1b5b393b3275", + "passed": true + }, + { + "mode": "alt", + "key": "Tab", + "expected_hex": "09", + "actual_hex": "09", + "passed": true + }, + { + "mode": "alt", + "key": "Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "mode": "alt", + "key": "Backspace", + "expected_hex": "7f", + "actual_hex": "7f", + "passed": true + }, + { + "mode": "alt", + "key": "Escape", + "expected_hex": "1b5b323775", + "actual_hex": "1b5b323775", + "passed": true + }, + { + "mode": "alt", + "key": "F3", + "expected_hex": "1b5b31337e", + "actual_hex": "1b5b31337e", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+Space", + "expected_hex": "1b5b33323b3575", + "actual_hex": "1b5b33323b3575", + "passed": true + }, + { + "mode": "alt", + "key": "Left Alt+Space", + "expected_hex": "1b5b33323b3375", + "actual_hex": "1b5b33323b3375", + "passed": true + }, + { + "mode": "alt", + "key": "Ctrl+PageUp", + "expected_hex": "1b5b353b357e", + "actual_hex": "1b5b353b357e", + "passed": true + }, + { + "mode": "alt", + "key": "PageDown", + "expected_hex": "1b5b367e", + "actual_hex": "1b5b367e", + "passed": true + }, + { + "mode": "alt", + "key": "Shift+PageUp alternate scroll", + "expected_hex": "1b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b41", + "actual_hex": "1b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b411b5b41", + "passed": true + }, + { + "mode": "alt", + "key": "Shift+PageDown alternate scroll", + "expected_hex": "1b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b42", + "actual_hex": "1b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b421b5b42", + "passed": true + } + ] + }, + "current_virtual_layout": { + "source_commit": "65c625b2", + "method": "Android framework KeyEvent delivered directly to installed production TerminalView; real session queue, native PTY and foreground shell capture", + "layouts": [ + { + "device": "virtual keyboard", + "altgr_space_codepoint": 0 + }, + { + "device": "Virtual", + "virtual": true, + "altgr_space_codepoint": 0 + } + ], + "results": [ + { + "mode": "kitty", + "key": "Right Alt+Space actual virtual layout", + "expected_hex": "", + "actual_hex": "", + "passed": true + }, + { + "mode": "legacy", + "key": "Right Alt+Space actual virtual layout", + "expected_hex": "", + "actual_hex": "", + "passed": true + } + ] + }, + "rime_touch_and_lifecycle": { + "source_commit": "65c625b2", + "method": "Real Rime touch input and Termux CTRL extra key across native PTY, with application lifecycle and keyboard resize checks; capture setup through temporary instrumentation", + "results": [ + { + "name": "CTRL extra key + real Rime 0", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "name": "Plain Rime 0 after one-shot CTRL clears", + "expected_hex": "30", + "actual_hex": "30", + "passed": true + }, + { + "name": "CTRL extra key + real Rime 2", + "expected_hex": "1b5b35303b3575", + "actual_hex": "1b5b35303b3575", + "passed": true + }, + { + "name": "CTRL extra key + real Rime 1", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + }, + { + "name": "Real Rime Enter", + "expected_hex": "0d", + "actual_hex": "0d", + "passed": true + }, + { + "name": "CTRL + Rime 0 after background and foreground", + "expected_hex": "1b5b34383b3575", + "actual_hex": "1b5b34383b3575", + "passed": true + }, + { + "name": "CTRL + Rime 1 after keyboard close/open and resize", + "expected_hex": "1b5b34393b3575", + "actual_hex": "1b5b34393b3575", + "passed": true + } + ] + }, + "rime_space_and_modifiers": { + "source_commit": "65c625b2", + "method": "Real Rime Space touches plus Termux CTRL/ALT extra keys, captured after native PTY", + "results": [ + { + "name": "Plain Rime Space", + "expected_hex": "20", + "actual_hex": "20", + "passed": true + }, + { + "name": "CTRL extra key + real Rime Space", + "expected_hex": "1b5b33323b3575", + "actual_hex": "1b5b33323b3575", + "passed": true + }, + { + "name": "ALT extra key + real Rime Space", + "expected_hex": "1b5b33323b3375", + "actual_hex": "1b5b33323b3375", + "passed": true + }, + { + "name": "Plain Space after one-shot ALT clears", + "expected_hex": "20", + "actual_hex": "20", + "passed": true + } + ] + } + }, + "user_reported_manual_validation": { + "source_commit": "65c625b2bc7d4b1552084f449400bda18cb00b84", + "date": "2026-10-07", + "source": "user report", + "status": "completed", + "coverage": [ + "on-device and external physical keyboards", + "local Termux and SSH", + "Zellij and tmux" + ], + "versions_and_raw_captures": "not supplied", + "altgr_character_producing_layout": "not specifically confirmed" + } +} diff --git a/docs/kitty-keyboard.md b/docs/kitty-keyboard.md new file mode 100644 index 00000000000..afa0cb59e8f --- /dev/null +++ b/docs/kitty-keyboard.md @@ -0,0 +1,370 @@ +# Kitty keyboard protocol support + +The terminal supports disambiguate escape codes (flag `1`), report event +types (flag `2`) and report alternate keys (flag `4`). It does not implement +reporting all keys or associated text. Unsupported flags are ignored and are never reported +as enabled by a query. + +Applications negotiate support using `CSI ? u`, enable it using `CSI > 1 u`, and +restore the previous mode using `CSI < u`. Direct flag updates (`CSI = flags ; +mode u`) and popping multiple entries are supported. The main and alternate +screens have independent stacks, limited to 32 entries including the current mode. Terminal reset +clears both stacks. Without negotiation the existing keyboard encoding is used. + +Examples after enabling flag `1`: + +| Input | Bytes sent | +| --- | --- | +| Ctrl+0 | `ESC [ 48 ; 5 u` | +| Ctrl+I | `ESC [ 105 ; 5 u` | +| Ctrl+Shift+A | `ESC [ 97 ; 6 u` | +| Shift+Enter | `ESC [ 13 ; 2 u` | +| Ctrl+Enter | `ESC [ 13 ; 5 u` | +| Escape | `ESC [ 27 u` | +| Plain Tab / Enter / Backspace | Existing recovery control bytes | + +Hardware shortcuts use the active Android layout's unshifted Unicode character. +Shift, Alt, Ctrl and Android Meta (mapped to Super) are encoded. Num Lock and +Caps Lock are included for functional keys; keypad keys use dedicated codes. +Plain text, IME commits and paste retain their existing paths. Soft keyboard or +extra-key shortcuts encode the available character and modifiers; an IME text +commit cannot recover a physical key identity or its original modifier state. +Termux application shortcuts continue to take precedence over terminal input. +This includes the existing Shift+PageUp/PageDown scroll bindings. Main-screen +scrollback, alternate-screen arrow scrolling and mouse reporting retain their +existing scroll behavior. Space is resolved through the Android layout before +shortcut encoding, so right Alt/AltGr text is preserved; left Alt+Space and +Ctrl+Space still use negotiated keyboard encoding. + +The program receiving the input must support the protocol. Intermediaries such +as terminal multiplexers also need to pass or translate the sequences correctly. + +Specification: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + +## Repeat and release events + +Enable both enhancements with `CSI > 3 u`, or add event reporting to an existing +mode with `CSI = 2 ; 2 u`. A query returns the enabled supported flags, including +`2` or `3`. Flag `2` can also be enabled independently with `CSI > 2 u`. + +Android `ACTION_DOWN` with a nonzero repeat count is encoded as event type `2`; +`ACTION_UP` is encoded as type `3`. Press events omit the optional event type. +Examples with flags `3`: + +| Input | Press | Repeat | Release | +| --- | --- | --- | --- | +| Up | `ESC [ A` | `ESC [ 1 ; 1 : 2 A` | `ESC [ 1 ; 1 : 3 A` | +| Ctrl+I | `ESC [ 105 ; 5 u` | `ESC [ 105 ; 5 : 2 u` | `ESC [ 105 ; 5 : 3 u` | +| Escape | `ESC [ 27 u` | `ESC [ 27 ; 1 : 2 u` | `ESC [ 27 ; 1 : 3 u` | +| Plain Enter | `CR` | `CR` | None | +| Plain A | `a` | `a` | None | + +With flag `2` alone, shortcut press bytes retain legacy encoding, while their +repeat and release events use CSI event encoding. Escape likewise retains its +legacy press byte. Modified Enter, Tab and Backspace repeats also use CSI +with event type `2`; their presses keep legacy bytes and releases stay suppressed. +Plain recovery-key repeats remain control bytes even with lock modifiers enabled. +Functional keys use CSI event encoding. Legacy keypad text +and application-keypad encodings retain their existing input paths; keypad +navigation events use the equivalent normal navigation key identity. Flag `1` +is required for dedicated keypad identities. + +Enter, Tab and Backspace do not emit release events, including modified variants. +Plain and Shift-only text, AltGr text, IME commits and paste retain their text +paths. Standalone modifier key events require the unsupported report-all-keys +flag `8`. Soft input and virtual Fn mappings do not manufacture repeat or release +events from text commits or virtual shortcuts. + +Only keys delivered to the terminal have a matching release. Application +shortcuts and Shift+PageUp/PageDown remain prior to terminal encoding. Pending +releases are discarded on canceled input, focus loss, view detachment, session +changes, screen switches, terminal reset, or keyboard mode updates. Device IDs +and press timestamps prevent an unrelated key-up from matching a held key. +Release events preserve the layout/keypad identity resolved on key-down and use +the current physical modifiers, including when Ctrl, Shift or Num Lock changes +before the key is released. Virtual modifiers used for the original shortcut +are retained for its release. + +## Alternate key codes + +Flag `4` enhances an existing encoded key event with optional shifted and +standard PC-101 layout code points. Enable it with disambiguation using +`CSI > 5 u`, or with disambiguation and events using `CSI > 7 u`. +It can be added using `CSI = 4 ; 2 u` and removed using `CSI = 4 ; 3 u`. +Queries report supported combinations `0` through `7`; flags `8` and `16` +remain unsupported. Flag `4` alone is queryable but keeps legacy input. + +The key field is `primary:shifted:base`. The primary stays the active layout's +unshifted character. The shifted value is resolved through Android's current +layout, not by assuming uppercase, and is included only when Shift is active. +Physical left/right Shift states are retained for side-specific layout mappings, +including right Shift with AltGr. Virtual Shift retains its left-Shift default. +The base value uses known Android letter, digit, Space and punctuation key codes +to identify their standard PC-101 equivalents. Unknown and extended positions +are omitted. Values identical to the primary are redundant and omitted. +A base-only alternate keeps an empty shifted subfield: `primary::base`. + +Examples on layouts with the indicated characters: + +| Input and flags | Bytes | +| --- | --- | +| Ctrl+Shift+A, flags `5` | `ESC [ 97 : 65 ; 6 u` | +| Ctrl+Shift+= producing `+`, flags `5` | `ESC [ 61 : 43 ; 6 u` | +| Same key repeated, flags `7` | `ESC [ 61 : 43 ; 6 : 2 u` | +| Cyrillic `с` on the C position, Ctrl, flags `5` | `ESC [ 1089 : : 99 ; 5 u` | +| Cyrillic `с` on C, Ctrl+Shift, flags `5` | `ESC [ 1089 : 1057 : 99 ; 6 u` | +| Plain Shift+A, flags `7` | UTF-8 `A` | + +Presses retain an immutable layout identity for matching releases. A release +uses the initial primary/base identity and the current modifier state; if Shift +has been released, its shifted subfield is omitted. Functional-key sequences +retain their dedicated identities and never acquire printable alternates. + +IME commits and virtual shortcuts supply no physical identity and do not invent +base or shifted fields. Soft-keyboard flagged KeyEvents can report a known +layout-shifted character, but omit the physical base. Dead-key composition +fallbacks also omit unavailable alternates. Right Alt/AltGr remains part of +layout text resolution, and plain text keeps its UTF-8 path without flag `8`. +The base mapping assumes standard Android key-code assignments; custom firmware +or key-layout remapping needs separate validation. + +## Validation status + +### Alternate-key review fixes (2026-10-09) + +- Preserve actual left/right Shift states when resolving the shifted alternate; + a physical right Shift no longer gains an artificial left-Shift bit. Four + new Android regressions cover generic/left/right/both Shift, repeats and + releases, right Shift with AltGr, ordinary text and virtual Shift's existing + default. These side-specific layouts are test fixtures, not hardware layouts. +- Archive raw Gradle test directories before parsing their XML. Damaged reports + are retained and recorded in `test_result_errors`, other valid suites are + still summarized, and the runner reports failure. Three isolated tool checks + include an end-to-end simulated Gradle failure with truncated XML. +- All 388 Debug/Release Gradle test executions passed (155 emulator and 39 app + tests per variant), along with the three validation-tool checks; five Debug + APKs were rebuilt and retained. Version: `0.118.0+kitty.817dfb43.edcdab58162c`. +- All 138 native PTY checks passed on Android 16 PTP-AN10: the previous 125 + cases plus 13 new Shift-side fixtures. The 13 new cases are synthetic layout + fixtures delivered as framework events, not physical keyboard layouts. See + [the review-fix report](kitty-keyboard-alternate-review-validation.json). + +### Alternate keys (2026-10-09) + +- The complete upstream `./gradlew test` task passed with 155 terminal-emulator + and 35 app tests per Debug/Release variant: 380 executions, zero failures, + errors or skips. Three terminal and seven Android input tests were added. +- Negotiation covers every supported flag combination, unsupported masks, + set/add/remove, stack restoration, separate screens and reset. Encoding + covers shifted punctuation, base-only/non-Latin fields, supplementary Unicode, + omitted invalid/duplicate alternatives, functional and recovery boundaries. +- Robolectric fixtures exercise Cyrillic and changed Greek layouts, a shifted + character that is not uppercase, release identity after layout/modifier + changes, AltGr, soft input, and alternate-only legacy isolation. These are + synthetic layout fixtures, not physical non-Latin keyboard coverage. The + Robolectric 4.10 map lacks Shift+=; that US entry is supplied explicitly. +- Debug APK assembly succeeded for the four ABIs and universal package. + Toolchain retained: Temurin 21.0.12.1, Gradle 9.2.1, Android API 36, + Build Tools 36.0.0, NDK 29.0.14206865. The upstream CI currently uses Temurin + 17; this local run used the retained compatible JDK 21. +- Reproduce and retain the source snapshot, all unit-test XML, log, versioned + APKs and checksums with the [validation runner](../tools/kitty-keyboard/README.md). + This entry point was run successfully, retaining five uniquely versioned APKs + and all 380 Debug/Release test results. +- On the Android 16 arm64 PTP-AN10, all 125 new native PTY checks passed against + installed version `0.118.0+kitty.59c2f1ea.10429391625c`. See + [the alternate-key device report](kitty-keyboard-alternate-device-validation.json). + It includes all flags `0` through `7`, shifted A and punctuation, dropping the + shifted subfield after Shift release, and earlier event/text/recovery cases. +- Of these, 122 checks use Android's real virtual keyboard mapping and three + override Unicode values in a synthetic KeyEvent layout fixture. All input was + delivered directly to production `TerminalView`, not through OS dispatch or + physical HID. The three fixture checks cover Cyrillic primary/shifted/base, + base-only fields and soft-keyboard base suppression across a real native PTY. + No new flag `4` physical layout, real IME, SSH or multiplexer verification is + claimed; the historical reports below still validate their original flags. + + +### Event reporting (2026-10-08) + +- All 152 terminal-emulator and 28 app tests passed with zero failures, errors + or skips. This adds two terminal tests and eighteen Android input tests. +- Negotiation covers both flag `2` alone and flags `3`, set/add/remove, + push/pop, independent screens, and reset. Byte comparisons cover repeated + and released navigation, shortcuts, Escape, F3 and keypad keys. +- Android events pass through the production `TerminalView` into the session + input queue under Robolectric API 28. Coverage includes current release + modifiers, Num Lock identity changes, AltGr layout text, recovery keys, + consumed shortcuts, canceled/orphaned releases, separate device IDs, + screen round trips, mode changes, reset and focus loss. The real extra-key + bridge and virtual Fn press paths are covered; the deferred virtual Fn Ctrl + limitation remains unchanged. Two P1 regression tests additionally cover + Num Lock changes during repeats and prevent orphaned repeats or mode changes + from creating or reviving a pending release. Only the initial press records + the release identity. A P2 regression additionally checks flag `2` modified + Enter/Tab/Backspace repeats and ordinary recovery repeats with lock modifiers. + These tests do not + start a native PTY or exercise physical keyboard hardware. +- Debug APK assembly passed for arm64-v8a, armeabi-v7a, x86, x86_64 and universal + using the default apt-android-7 package variant. Toolchain: Temurin + 21.0.12.1, Gradle 9.2.1, API 36, Build Tools 36.0.0, NDK 29.0.14206865. +- `git diff --check` passed. The initial local build had no attached device. + Subsequent P1/P2 device verification on the Android 16 arm64 PTP-AN10 passed + all 59 raw byte checks against the installed production `TerminalView`, real + session input queue and native PTY. See + [the event-reporting device report](kitty-keyboard-events-device-validation.json). +- Device coverage includes flags `0`/`1`/`2`/`3`, ordinary and modified repeats, + matching releases, recovery/text boundaries, modifier-first release, F3, + orphaned repeats, the three P1 Num Lock transitions, and P2 modified recovery + repeats under both flags `2` and `3`. Android framework `KeyEvent` objects + were delivered directly to the production view through temporary + instrumentation. This does not exercise physical HID or OS input dispatch. +- APK version: `0.118.0+kitty.events.p2.60a581a1.2439221fb851`. The source patch + and APK checksums are recorded with the report. A first attempt had one + infrastructure timeout due to background freezing; the final foreground + rerun passed all cases. Capture used foreground `dd` with a start handshake. +- New physical-keyboard, real IME UI, SSH, multiplexer, lifecycle and session + switching checks remain outside this recorded device coverage. + +For device acceptance, negotiate `3` in a raw PTY capture, hold and release Up, +Ctrl+I, Escape, F3 and a Num Lock keypad key, and compare press/repeat/release +bytes against this table. Repeat with flag `2` alone, restore with `CSI < u`, +and confirm that plain text and recovery keys emit no releases. Check consumed +application/scroll shortcuts, modifier-first release, screen/session changes, +background/foreground, IME commits and extra keys. Record injection separately +from physical HID events and record SSH/multiplexer versions with raw captures. +The historical device reports below validate flag `1`, not this new flag `2`. + +### Bluetooth keyboard and real IME validation (2026-10-08/09) + +- The user connected a RAPOO Keyboard-1 Keyboard over Bluetooth. Android + recognized an external alphabetic keyboard (device ID 9, keyboard type 2). + Test keys passed through actual Android input dispatch and the installed + Termux activity/client/view into the native PTY; no synthetic key events + were used. See [the real-input report](kitty-keyboard-real-input-validation.json). +- With flags `3`, recorded physical input confirms Up and Ctrl+I + press/repeat/release, Ctrl+Shift+A and Escape press/release, modified + Shift+Tab repeats, ordinary text, and ordinary recovery bytes without + releases. The first group also contained incomplete or different actions: + Ctrl+Enter had no repeat, the requested Alt+Backspace was Alt+Space, and + the modifier-first I release still carried Ctrl. These were not passed. +- A simpler physical flags `2` supplement passed all three recovery-key + cases. Raw Linux events confirm actual Ctrl+Enter, Shift+Tab and left + Alt+Backspace held for about two seconds. PTY captures contain their legacy + press bytes followed by 34, 37 and 39 correct repeat sequences, respectively, + with no recovery-key releases. This validates the P2 fix on real hardware. +- All 12 real IME checks passed. ADB touchscreen taps operated Fcitx's real + English keyboard, Rime engine and Termux CTRL/ALT extra keys. They cover + Ctrl+0, Alt+1, clearing one-shot modifiers before plain digits, Enter and + Ctrl+Enter, flags `2` legacy soft-input presses, and Home/foreground return. + During `zhongwen` composition the PTY received no bytes; tapping the visible + `中文` candidate committed exactly its UTF-8 bytes. No direct text or key + injection was used for these IME checks. +- This keyboard has no numeric keypad, so the P1 Num Lock transitions still + rely on the earlier 59 framework/native-PTY checks and local regressions. + Modifier-first release, additional physical layouts/keys, character-producing + AltGr, other IMEs, SSH and multiplexers remain outside the new coverage. + +### Review fixes (2026-10-07) + +- Resolve printable Space through the keyboard layout before encoding; a + synthetic AltGr layout now checks NBSP and Euro text, including Ctrl+AltGr + modifier handling. Ctrl+Space and left Alt+Space keep their negotiated codes. +- Run the existing scroll actions before key encoding, so Shift+PageUp/PageDown + retain precedence in both legacy and kitty mode. Other Page keys still reach + the terminal application. +- Select functional-key encoding through one `KeyHandler.getCode` entry point + used by the view and virtual Fn mappings. Original commits were reworded to + follow the repository's commit-message requirements without changing their + source trees; the original history is retained in the local + `backup/kitty-pre-review-20261007` branch. +- All 150 terminal-emulator and 10 app tests passed locally (zero failures, + errors or skips). The app suite includes four new regression tests for the + above layout and shortcut cases. The AltGr mapping is a test fixture, not a + claim of coverage for every Android keyboard layout. +- The revised APK (`0.118.0+kitty.65c625b2`, source `65c625b2`) was installed as + an update on the Android 16 arm64 PTP-AN10 device, preserving application data. + All 96 recorded checks passed; see + [the review-fix device report](kitty-keyboard-review-device-validation.json). +- Of these, 83 checks delivered Android framework `KeyEvent` objects directly + to the installed production `TerminalView` through a temporary instrumentation + component, then checked the real session queue, native PTY and shell capture. + They include Ctrl/left-Alt+Space, page-key delivery, primary-screen scrollback + offsets, and alternate-screen arrow scrolling. These are not physical HID or + OS input-dispatch tests. +- Two more checks verified right Alt+Space against the current virtual keyboard + mapping in kitty and legacy modes. That mapping returns no character; both + modes correctly sent no bytes. This does not exercise an NBSP-producing layout. +- Eleven checks used real touches on the Rime keyboard and Termux CTRL/ALT extra + keys, including Space combinations, one-shot modifier clearing, Enter, + background/foreground and keyboard close/open. Capture setup used temporary + instrumentation to avoid Chinese composition of the startup command. +- The user reports completing manual combination retests on this revision across + on-device/external physical keyboards, local Termux/SSH, and Zellij/tmux. + This is user-reported verification; tool versions and raw captures were not + supplied. The earlier manual report below remains historical evidence. +- The external-keyboard AltGr/NBSP case remains uncovered; the combination + retest report does not specifically confirm a character-producing AltGr layout. + +Remaining manual verification is an AltGr+Space layout that produces NBSP or +another layout character. + +### Historical validation before review fixes + +Validated on 2026-10-06 with an isolated temporary toolchain: + +- Eclipse Temurin JDK 21.0.12.1, Gradle 9.2.1, Android API 36, Build Tools 36.0.0, + and NDK 29.0.14206865. +- All 150 terminal-emulator unit tests passed, including the five negotiation, + state restoration, screen isolation, parsing and encoding tests in + `KittyKeyboardTest`. +- All six app unit tests passed, including four Robolectric API 28 tests in + `KittyKeyboardInputTest`. Those tests exercise Android keyboard events through + `TerminalView` into the session input queue for Ctrl+0, Ctrl+I, Ctrl+Shift+A, + shifted punctuation, modified Enter/Tab, mode restoration, soft input, and + application shortcut handling. They do not start a native PTY or run a shell. +- Debug APK assembly succeeded for arm64-v8a, armeabi-v7a, x86, x86_64 and the + universal APK using the default apt-android-7 package variant. +- The initial test run caught F3 being treated as the Enter recovery key because + both use parameter 13. Recovery handling now also checks the sequence suffix. +- On a physical PTP-AN10 device running Android 16 (arm64-v8a), the installed + Termux APK passed 52 raw input byte comparisons: 26 combinations in kitty + mode and the same 26 in legacy mode. Android's `input` command injected the + key events; they passed through the production `TerminalView`, native PTY and + a foreground shell capture. Mode queries also verified push/pop restoration + and isolation between the main and alternate screens. The source code tested + was commit `91ae4885`, with APK version `0.118.0+kitty.91ae4885`. +- See [the real-device report](kitty-keyboard-device-validation.json) for each + expected and actual byte sequence. This is device execution evidence, not + Bluetooth/USB keyboard hardware coverage. +- Seven additional checks passed using real touches on the device's Rime numeric + keyboard and Termux CTRL extra key: Ctrl+0/1/2, plain 0 after one-shot CTRL + clears, IME Enter, and modifier encoding after background/foreground and soft + keyboard close/open. See [the IME report](kitty-keyboard-ime-validation.json). + A temporary instrumentation component started the ASCII capture command to + avoid Rime composing the setup command as Chinese; the checked input itself + came from the real keyboard UI and crossed the native PTY. The component is + removed after validation. +- The user additionally reports successful manual verification combining + on-device and external physical keyboards, local Termux and SSH, and + Zellij/tmux. Kitty-encoded input reached the receiver in the tested + combinations. This is user-reported verification; tool versions and raw + captures were not supplied. +- Broader keyboard layouts, IME composition and additional application or + multiplexer versions are outside the recorded coverage. Text-only IME commits cannot expose + physical Shift/Alt key identity that Android does not deliver to the terminal. +- `git diff --check` passed. + +With the toolchain paths exported as `JAVA_HOME` and `ANDROID_HOME`, reproduce +the checks from the repository root: + +```sh +./gradlew --no-daemon --max-workers=2 :terminal-emulator:testDebugUnitTest +./gradlew --no-daemon --max-workers=2 :app:testDebugUnitTest :app:assembleDebug +``` + +For temporary setups, keep `GRADLE_USER_HOME` and `ANDROID_USER_HOME` inside the +same temporary directory as the JDK and SDK. Retain the toolchain, APKs, +checksums and validation reports while device verification is pending. The +current event-reporting validation keeps its temporary toolchain for reuse, +following the user's explicit retention requirement. diff --git a/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java b/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java index 8ecfb630e42..9850e101b2a 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/KeyHandler.java @@ -1,5 +1,6 @@ package com.termux.terminal; +import android.view.KeyEvent; import java.util.HashMap; import java.util.Map; @@ -56,10 +57,246 @@ public final class KeyHandler { + public static final int KITTY_DISAMBIGUATE = 1; + public static final int KITTY_REPORT_EVENTS = 2; + public static final int KITTY_REPORT_ALTERNATE_KEYS = 4; + public static final int KEY_EVENT_PRESS = 1; + public static final int KEY_EVENT_REPEAT = 2; + public static final int KEY_EVENT_RELEASE = 3; + public static final int KEYMOD_ALT = 0x80000000; public static final int KEYMOD_CTRL = 0x40000000; public static final int KEYMOD_SHIFT = 0x20000000; public static final int KEYMOD_NUM_LOCK = 0x10000000; + public static final int KEYMOD_SUPER = 0x08000000; + public static final int KEYMOD_CAPS_LOCK = 0x04000000; + private static final int[] KEYPAD_NAVIGATION_KEYS = { + KEYCODE_INSERT, KEYCODE_MOVE_END, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN, KEYCODE_DPAD_LEFT, + KEYCODE_NUMPAD_5, KEYCODE_DPAD_RIGHT, KEYCODE_MOVE_HOME, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP + }; + private static final int[] KITTY_KEYPAD_NAVIGATION = { + 57425, 57424, 57420, 57422, 57417, 57427, 57418, 57423, 57419, 57421 + }; + + /** Encode a non-text key in negotiated kitty disambiguation mode, or return null for text. */ + public static String getKittyCode(int keyCode, int keyMode) { + return getKittyCode(keyCode, keyMode, KEY_EVENT_PRESS); + } + + private static String getKittyCode(int keyCode, int keyMode, int eventType) { + if (eventType == KEY_EVENT_RELEASE && isKittyRecoveryKey(keyCode)) return null; + int codePoint; + char suffix = 'u'; + boolean numLock = (keyMode & KEYMOD_NUM_LOCK) != 0; + if (keyCode >= KEYCODE_NUMPAD_0 && keyCode <= KEYCODE_NUMPAD_9) { + if (numLock) { + codePoint = 57399 + keyCode - KEYCODE_NUMPAD_0; + } else { + // Insert, End, Down, Page Down, Left, Begin, Right, Home, Up, Page Up. + codePoint = KITTY_KEYPAD_NAVIGATION[keyCode - KEYCODE_NUMPAD_0]; + } + return codePoint == 57427 ? kittySequence(1, keyMode, 'E', eventType) : kittySequence(codePoint, keyMode, 'u', eventType); + } + switch (keyCode) { + case KEYCODE_ESCAPE: + case KEYCODE_BACK: codePoint = 27; break; + case KEYCODE_ENTER: + case KEYCODE_DPAD_CENTER: codePoint = 13; break; + case KEYCODE_TAB: codePoint = 9; break; + case KEYCODE_DEL: codePoint = 127; break; + // Printable keys, including Space, must first be resolved through the Android layout. + // AltGr+Space may produce a different character instead of an Alt shortcut. + case KEYCODE_SPACE: return null; + case KEYCODE_DPAD_UP: suffix = 'A'; codePoint = 1; break; + case KEYCODE_DPAD_DOWN: suffix = 'B'; codePoint = 1; break; + case KEYCODE_DPAD_RIGHT: suffix = 'C'; codePoint = 1; break; + case KEYCODE_DPAD_LEFT: suffix = 'D'; codePoint = 1; break; + case KEYCODE_MOVE_HOME: suffix = 'H'; codePoint = 1; break; + case KEYCODE_MOVE_END: suffix = 'F'; codePoint = 1; break; + case KEYCODE_INSERT: suffix = '~'; codePoint = 2; break; + case KEYCODE_FORWARD_DEL: suffix = '~'; codePoint = 3; break; + case KEYCODE_PAGE_UP: suffix = '~'; codePoint = 5; break; + case KEYCODE_PAGE_DOWN: suffix = '~'; codePoint = 6; break; + case KEYCODE_F1: suffix = 'P'; codePoint = 1; break; + case KEYCODE_F2: suffix = 'Q'; codePoint = 1; break; + // CSI R overlaps with cursor position reports; kitty uses CSI 13 ~ for F3. + case KEYCODE_F3: suffix = '~'; codePoint = 13; break; + case KEYCODE_F4: suffix = 'S'; codePoint = 1; break; + case KEYCODE_F5: suffix = '~'; codePoint = 15; break; + case KEYCODE_F6: suffix = '~'; codePoint = 17; break; + case KEYCODE_F7: suffix = '~'; codePoint = 18; break; + case KEYCODE_F8: suffix = '~'; codePoint = 19; break; + case KEYCODE_F9: suffix = '~'; codePoint = 20; break; + case KEYCODE_F10: suffix = '~'; codePoint = 21; break; + case KEYCODE_F11: suffix = '~'; codePoint = 23; break; + case KEYCODE_F12: suffix = '~'; codePoint = 24; break; + case KEYCODE_NUMPAD_DOT: codePoint = numLock ? 57409 : 57426; break; + case KEYCODE_NUMPAD_DIVIDE: codePoint = 57410; break; + case KEYCODE_NUMPAD_MULTIPLY: codePoint = 57411; break; + case KEYCODE_NUMPAD_SUBTRACT: codePoint = 57412; break; + case KEYCODE_NUMPAD_ADD: codePoint = 57413; break; + case KEYCODE_NUMPAD_ENTER: codePoint = 57414; break; + case KEYCODE_NUMPAD_EQUALS: codePoint = 57415; break; + case KEYCODE_NUMPAD_COMMA: codePoint = 57416; break; + case KeyEvent.KEYCODE_CAPS_LOCK: codePoint = 57358; break; + case KeyEvent.KEYCODE_SCROLL_LOCK: codePoint = 57359; break; + case KEYCODE_NUM_LOCK: codePoint = 57360; break; + case KEYCODE_SYSRQ: codePoint = 57361; break; + case KEYCODE_BREAK: codePoint = 57362; break; + default: return null; + } + int ordinaryModifiers = keyMode & (KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL | KEYMOD_SUPER); + if (ordinaryModifiers == 0 && suffix == 'u') { + // Preserve the recovery keys even when Caps Lock or Num Lock is on. + if (codePoint == 13) return "\r"; + if (codePoint == 9) return "\t"; + if (codePoint == 127) return "\u007f"; + } + return kittySequence(codePoint, keyMode, suffix, eventType); + } + + /** Immutable layout identity, also retained across repeats for a matching release. */ + public static final class KittyKey { + public final int codePoint; + public final int shiftedCodePoint; + public final int baseLayoutCodePoint; + + public KittyKey(int codePoint, int shiftedCodePoint, int baseLayoutCodePoint) { + this.codePoint = codePoint; + this.shiftedCodePoint = validAlternate(shiftedCodePoint, codePoint); + this.baseLayoutCodePoint = validAlternate(baseLayoutCodePoint, codePoint); + } + + private static int validAlternate(int alternate, int primary) { + return alternate >= 32 && Character.isValidCodePoint(alternate) && + !(alternate >= 0xD800 && alternate <= 0xDFFF) && alternate != primary ? alternate : 0; + } + } + + /** Known standard PC-101 positions from Android key codes; unknown or extended keys are omitted. */ + public static int getKittyBaseLayoutCodePoint(int keyCode) { + if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) + return 'a' + keyCode - KeyEvent.KEYCODE_A; + if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) + return '0' + keyCode - KeyEvent.KEYCODE_0; + switch (keyCode) { + case KEYCODE_SPACE: return ' '; + case KeyEvent.KEYCODE_GRAVE: return '`'; + case KeyEvent.KEYCODE_MINUS: return '-'; + case KeyEvent.KEYCODE_EQUALS: return '='; + case KeyEvent.KEYCODE_LEFT_BRACKET: return '['; + case KeyEvent.KEYCODE_RIGHT_BRACKET: return ']'; + case KeyEvent.KEYCODE_BACKSLASH: return '\\'; + case KeyEvent.KEYCODE_SEMICOLON: return ';'; + case KeyEvent.KEYCODE_APOSTROPHE: return '\''; + case KeyEvent.KEYCODE_COMMA: return ','; + case KeyEvent.KEYCODE_PERIOD: return '.'; + case KeyEvent.KEYCODE_SLASH: return '/'; + default: return 0; + } + } + + /** Plain and Shift-only text stays UTF-8; shortcut keys use the unshifted code point. */ + public static String getKittyCodePoint(int unshiftedCodePoint, int keyMode) { + if (unshiftedCodePoint < 32 || !Character.isValidCodePoint(unshiftedCodePoint)) return null; + if ((keyMode & (KEYMOD_ALT | KEYMOD_CTRL | KEYMOD_SUPER)) == 0) return null; + return kittySequence(unshiftedCodePoint, keyMode & ~(KEYMOD_NUM_LOCK | KEYMOD_CAPS_LOCK), 'u'); + } + + /** Recovery control keys deliberately have no release events without report-all-keys support. */ + public static boolean isKittyRecoveryKey(int keyCode) { + return keyCode == KEYCODE_ENTER || keyCode == KEYCODE_DPAD_CENTER || + keyCode == KEYCODE_TAB || keyCode == KEYCODE_DEL; + } + + public static String getKittyCodePoint(int unshiftedCodePoint, int keyMode, int flags, int eventType) { + return getKittyCodePoint(new KittyKey(unshiftedCodePoint, 0, 0), keyMode, flags, eventType); + } + + /** Alternate reporting enhances an encoded event without turning layout text into an escape code. */ + public static String getKittyCodePoint(KittyKey key, int keyMode, int flags, int eventType) { + if ((flags & KITTY_REPORT_EVENTS) == 0) { + if (eventType == KEY_EVENT_RELEASE) return null; + eventType = KEY_EVENT_PRESS; + } + if ((flags & KITTY_DISAMBIGUATE) == 0 && eventType == KEY_EVENT_PRESS) return null; + if (key.codePoint < 32 || !Character.isValidCodePoint(key.codePoint)) return null; + if (eventType != KEY_EVENT_RELEASE && getKittyCodePoint(key.codePoint, keyMode) == null) return null; + String number = Integer.toString(key.codePoint); + if ((flags & KITTY_REPORT_ALTERNATE_KEYS) != 0) { + int shifted = (keyMode & KEYMOD_SHIFT) != 0 ? key.shiftedCodePoint : 0; + if (shifted != 0 || key.baseLayoutCodePoint != 0) + number += ":" + (shifted == 0 ? "" : shifted) + + (key.baseLayoutCodePoint == 0 ? "" : ":" + key.baseLayoutCodePoint); + } + return kittySequence(number, keyMode & ~(KEYMOD_NUM_LOCK | KEYMOD_CAPS_LOCK), 'u', eventType); + } + + /** Reuse a previously encoded functional identity with the release event's current modifiers. */ + public static String getKittyReleaseCode(String releaseCode, int keyMode) { + int separator = releaseCode.indexOf(';'); + int number = Integer.parseInt(releaseCode.substring(2, separator)); + return kittySequence(number, keyMode, releaseCode.charAt(releaseCode.length() - 1), KEY_EVENT_RELEASE); + } + + private static String kittySequence(int number, int keyMode, char suffix) { + return kittySequence(number, keyMode, suffix, KEY_EVENT_PRESS); + } + + private static String kittySequence(int number, int keyMode, char suffix, int eventType) { + return kittySequence(Integer.toString(number), keyMode, suffix, eventType); + } + + private static String kittySequence(String number, int keyMode, char suffix, int eventType) { + int modifiers = 1; + if ((keyMode & KEYMOD_SHIFT) != 0) modifiers += 1; + if ((keyMode & KEYMOD_ALT) != 0) modifiers += 2; + if ((keyMode & KEYMOD_CTRL) != 0) modifiers += 4; + if ((keyMode & KEYMOD_SUPER) != 0) modifiers += 8; + if ((keyMode & KEYMOD_CAPS_LOCK) != 0) modifiers += 64; + if ((keyMode & KEYMOD_NUM_LOCK) != 0) modifiers += 128; + String prefix = number.equals("1") && suffix != 'u' && suffix != '~' && + modifiers == 1 && eventType == KEY_EVENT_PRESS ? "" : number; + return "\033[" + prefix + (modifiers == 1 && eventType == KEY_EVENT_PRESS ? "" : ";" + modifiers) + + (eventType == KEY_EVENT_PRESS ? "" : ":" + eventType) + suffix; + } + + /** Select negotiated or legacy functional-key encoding. Null leaves layout text to the caller. */ + public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication, + boolean kittyKeyboard) { + return kittyKeyboard ? getKittyCode(keyCode, keyMode) : + getCode(keyCode, keyMode, cursorApp, keypadApplication); + } + + /** Event reporting is independent of disambiguation; text-producing legacy keypad keys stay text. */ + public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication, + int flags, int eventType) { + if ((flags & KITTY_REPORT_EVENTS) == 0) { + if (eventType == KEY_EVENT_RELEASE) return null; + return getCode(keyCode, keyMode, cursorApp, keypadApplication, + (flags & KITTY_DISAMBIGUATE) != 0); + } + if ((flags & KITTY_DISAMBIGUATE) == 0) { + String legacy = getCode(keyCode, keyMode, cursorApp, keypadApplication); + if (isKittyRecoveryKey(keyCode)) { + if (eventType == KEY_EVENT_RELEASE) return null; + // Keep legacy presses and plain recovery repeats, but report modified repeats. + return eventType == KEY_EVENT_REPEAT ? getKittyCode(keyCode, keyMode, eventType) : legacy; + } + if ((keyCode == KEYCODE_ESCAPE || keyCode == KEYCODE_BACK) && eventType == KEY_EVENT_PRESS) + return legacy; + if (keyCode >= KEYCODE_NUMPAD_0 && keyCode <= KEYCODE_NUMPAD_9) { + if ((keyMode & KEYMOD_NUM_LOCK) != 0 || keyCode == KEYCODE_NUMPAD_5) + return eventType == KEY_EVENT_RELEASE ? null : legacy; + keyCode = KEYPAD_NAVIGATION_KEYS[keyCode - KEYCODE_NUMPAD_0]; + } else if (keyCode == KEYCODE_NUMPAD_DOT && (keyMode & KEYMOD_NUM_LOCK) == 0) { + keyCode = KEYCODE_FORWARD_DEL; + } else if (keyCode >= KEYCODE_NUMPAD_DIVIDE && keyCode <= KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN) { + return eventType == KEY_EVENT_RELEASE ? null : legacy; + } + } + return getKittyCode(keyCode, keyMode, eventType); + } private static final Map TERMCAP_TO_KEYCODE = new HashMap<>(); diff --git a/terminal-emulator/src/main/java/com/termux/terminal/KittyKeyboardState.java b/terminal-emulator/src/main/java/com/termux/terminal/KittyKeyboardState.java new file mode 100644 index 00000000000..2f6450071a3 --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/KittyKeyboardState.java @@ -0,0 +1,60 @@ +package com.termux.terminal; + +/** Per-screen progressive keyboard flags. Supports disambiguation, event and alternate-key reporting. */ +final class KittyKeyboardState { + // The current flags occupy one entry; reserve at most 31 saved entries. + private static final int MAX_DEPTH = 31; + private final int[] mSavedFlags = new int[MAX_DEPTH]; + private int mDepth; + private int mFlags; + private long mGeneration; + + int getFlags() { + return mFlags; + } + + long getGeneration() { + return mGeneration; + } + + void invalidateEvents() { + mGeneration++; + } + + void set(int flags, int mode) { + flags &= KeyHandler.KITTY_DISAMBIGUATE | KeyHandler.KITTY_REPORT_EVENTS | + KeyHandler.KITTY_REPORT_ALTERNATE_KEYS; + if (mode >= 1 && mode <= 3) mGeneration++; + switch (mode) { + case 1: mFlags = flags; break; + case 2: mFlags |= flags; break; + case 3: mFlags &= ~flags; break; + default: break; + } + } + + void push(int flags) { + if (mDepth == MAX_DEPTH) { + System.arraycopy(mSavedFlags, 1, mSavedFlags, 0, MAX_DEPTH - 1); + mDepth--; + } + mSavedFlags[mDepth++] = mFlags; + set(flags, 1); + } + + void pop(int count) { + if (count <= 0) return; + mGeneration++; + if (count > mDepth) { + reset(); + } else { + mDepth -= count; + mFlags = mSavedFlags[mDepth]; + } + } + + void reset() { + mDepth = mFlags = 0; + mGeneration++; + } +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java index 5e943ab5fc1..87ba5bd3ee0 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -33,6 +33,29 @@ public final class TerminalEmulator { /** Log unknown or unimplemented escape sequences received from the shell process. */ private static final boolean LOG_ESCAPE_SEQUENCES = false; + private static final int ESC_CSI_KITTY_KEYBOARD = 24; + private int mKittyKeyboardCommand; + private final KittyKeyboardState mKittyKeyboardMain = new KittyKeyboardState(); + private final KittyKeyboardState mKittyKeyboardAlt = new KittyKeyboardState(); + + private KittyKeyboardState kittyKeyboardState() { + return mScreen == mAltBuffer ? mKittyKeyboardAlt : mKittyKeyboardMain; + } + + /** Whether an enhancement changes key encoding; alternate reporting alone keeps legacy input. */ + public boolean isKittyKeyboardEnabled() { + return (getKittyKeyboardFlags() & (KeyHandler.KITTY_DISAMBIGUATE | KeyHandler.KITTY_REPORT_EVENTS)) != 0; + } + + public int getKittyKeyboardFlags() { + return kittyKeyboardState().getFlags(); + } + + /** Changes whenever the active keyboard mode is updated, including reset and stack operations. */ + public long getKittyKeyboardGeneration() { + return kittyKeyboardState().getGeneration(); + } + public static final int MOUSE_LEFT_BUTTON = 0; /** Mouse moving while having left mouse button pressed. */ @@ -901,6 +924,23 @@ public void processCodePoint(int b) { case ESC_CSI_BIGGERTHAN: doCsiBiggerThan(b); break; + case ESC_CSI_KITTY_KEYBOARD: + if (b == 'u') { + // Reject subparameters and excess parameters rather than changing modes. + if (mArgsSubParamsBitSet == 0 && mArgIndex <= (mKittyKeyboardCommand == '=' ? 1 : 0)) { + if (mKittyKeyboardCommand == '<') + kittyKeyboardState().pop(getArg0(1)); + else + kittyKeyboardState().set(getArg0(0), getArg1(1)); + } + } else if ((b >= '0' && b <= '9') || b == ';' || b == ':') { + parseArg(b); + } else if (b >= 0x20 && b <= 0x3F) { + continueSequence(b >= 0x30 ? ESC_CSI_UNSUPPORTED_PARAMETER_BYTE : ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE); + } else { + unknownSequence(b); + } + break; case ESC_CSI_DOLLAR: boolean originMode = isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE); int effectiveTopMargin = originMode ? mTopMargin : 0; @@ -1703,6 +1743,10 @@ private void doCsiUnsupportedParameterOrIntermediateByte(int b) { /** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */ private void doCsiQuestionMark(int b) { switch (b) { + case 'u': + if (mArgIndex == 0 && mArgs[0] == -1) + mSession.write("\033[?" + kittyKeyboardState().getFlags() + "u"); + break; case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED. case 'K': // Selective erase in line (DECSEL) - http://vt100.net/docs/vt510-rm/DECSEL. mAboutToAutoWrap = false; @@ -1857,7 +1901,9 @@ public void doDecSetOrReset(boolean setting, int externalBit) { if (newScreen != mScreen) { boolean resized = !(newScreen.mColumns == mColumns && newScreen.mScreenRows == mRows); if (setting) saveCursor(); + kittyKeyboardState().invalidateEvents(); mScreen = newScreen; + kittyKeyboardState().invalidateEvents(); if (!setting) { int col = mSavedStateMain.mSavedCursorCol; int row = mSavedStateMain.mSavedCursorRow; @@ -1887,6 +1933,10 @@ public void doDecSetOrReset(boolean setting, int externalBit) { private void doCsiBiggerThan(int b) { switch (b) { + case 'u': + if (mArgIndex == 0 && mArgsSubParamsBitSet == 0) + kittyKeyboardState().push(getArg0(0)); + break; case 'c': // "${CSI}>c" or "${CSI}>c". Secondary Device Attributes (DA2). // Originally this was used for the terminal to respond with "identification code, firmware version level, // and hardware options" (http://vt100.net/docs/vt510-rm/DA2), with the first "41" meaning the VT420 @@ -2308,7 +2358,8 @@ private void doCsi(int b) { break; case '<': // "Esc [ <" -- start of a private parameter byte case '=': // "Esc [ =" -- start of a private parameter byte - continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE); + mKittyKeyboardCommand = b; + continueSequence(ESC_CSI_KITTY_KEYBOARD); break; case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA). setCursorColRespectingOriginMode(getArg0(1) - 1); @@ -3484,6 +3535,8 @@ public void setTopRow(int topRow) { /** Reset terminal state so user can interact with it regardless of present state. */ public void reset() { + mKittyKeyboardMain.reset(); + mKittyKeyboardAlt.reset(); setCursorStyle(); mArgIndex = 0; mContinueSequence = false; diff --git a/terminal-emulator/src/test/java/com/termux/terminal/KittyKeyboardTest.java b/terminal-emulator/src/test/java/com/termux/terminal/KittyKeyboardTest.java new file mode 100644 index 00000000000..ee0b027d211 --- /dev/null +++ b/terminal-emulator/src/test/java/com/termux/terminal/KittyKeyboardTest.java @@ -0,0 +1,181 @@ +package com.termux.terminal; + +import android.view.KeyEvent; + +/** Negotiation goes through the real terminal parser; key output is checked against protocol bytes. */ +public class KittyKeyboardTest extends TerminalTestCase { + public void testNegotiationAndRestore() { + withTerminalSized(20, 4); + assertEnteringStringGivesResponse("\033[?u", "\033[?0u"); + enterString("\033[>1u"); + assertEnteringStringGivesResponse("\033[?u", "\033[?1u"); + enterString("\033[>0u"); + assertEnteringStringGivesResponse("\033[?u", "\033[?0u"); + enterString("\033[1;2u"); + assertFalse(mTerminal.isKittyKeyboardEnabled()); + } + + public void testScreenIsolationAndReset() { + withTerminalSized(20, 4); + enterString("\033[>1u\033[?1049h"); + assertFalse(mTerminal.isKittyKeyboardEnabled()); + enterString("\033[>1u\033[>0u\033[?1049l"); + assertTrue(mTerminal.isKittyKeyboardEnabled()); + enterString("\033[1u\033[?u".toCharArray()) enterString(String.valueOf(c)); + assertEquals("\033[?1u", mOutput.getOutputAndClear()); + for (int i = 0; i < 100; i++) enterString("\033[>1u"); + enterString("\033[<9999u"); + assertFalse(mTerminal.isKittyKeyboardEnabled()); + enterString("\033[=1 $uOK"); + assertLineIs(0, "OK "); + } + + public void testShortcutBytesAndTextFallback() { + assertEquals("\033[48;5u", KeyHandler.getKittyCodePoint('0', KeyHandler.KEYMOD_CTRL)); + assertEquals("\033[105;5u", KeyHandler.getKittyCodePoint('i', KeyHandler.KEYMOD_CTRL)); + assertEquals("\033[97;6u", KeyHandler.getKittyCodePoint('a', KeyHandler.KEYMOD_CTRL | KeyHandler.KEYMOD_SHIFT)); + assertEquals("\033[61;6u", KeyHandler.getKittyCodePoint('=', KeyHandler.KEYMOD_CTRL | KeyHandler.KEYMOD_SHIFT)); + assertEquals("\033[91;3u", KeyHandler.getKittyCodePoint('[', KeyHandler.KEYMOD_ALT)); + assertNull(KeyHandler.getKittyCodePoint('a', 0)); + assertNull(KeyHandler.getKittyCodePoint('a', KeyHandler.KEYMOD_SHIFT)); + assertNull(KeyHandler.getKittyCodePoint(0x4E2D, 0)); + assertEquals("\033[13;2u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_ENTER, KeyHandler.KEYMOD_SHIFT)); + assertEquals("\033[13;5u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_ENTER, KeyHandler.KEYMOD_CTRL)); + assertEquals("\033[9;6u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT | KeyHandler.KEYMOD_CTRL)); + assertEquals("\r", KeyHandler.getKittyCode(KeyEvent.KEYCODE_ENTER, KeyHandler.KEYMOD_NUM_LOCK)); + assertEquals("\t", KeyHandler.getKittyCode(KeyEvent.KEYCODE_TAB, 0)); + assertEquals("\u007f", KeyHandler.getKittyCode(KeyEvent.KEYCODE_DEL, 0)); + assertEquals("\033[27u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_ESCAPE, 0)); + assertEquals("\033[13~", KeyHandler.getKittyCode(KeyEvent.KEYCODE_F3, 0)); + assertEquals("\033[1;2A", KeyHandler.getKittyCode(KeyEvent.KEYCODE_DPAD_UP, KeyHandler.KEYMOD_SHIFT)); + assertEquals("\033[57399;129u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_NUMPAD_0, KeyHandler.KEYMOD_NUM_LOCK)); + assertEquals("\033[57425u", KeyHandler.getKittyCode(KeyEvent.KEYCODE_NUMPAD_0, 0)); + assertEquals("\033[E", KeyHandler.getKittyCode(KeyEvent.KEYCODE_NUMPAD_5, 0)); + assertEquals("\033[97;9u", KeyHandler.getKittyCodePoint('a', KeyHandler.KEYMOD_SUPER)); + assertEquals("\r", KeyHandler.getCode(KeyEvent.KEYCODE_ENTER, KeyHandler.KEYMOD_SHIFT, false, false)); + assertEquals("\033", KeyHandler.getCode(KeyEvent.KEYCODE_ESCAPE, 0, false, false)); + } + public void testEventFlagsRestoreAndScreenIsolation() { + withTerminalSized(20, 4); + enterString("\033[>3u\033[>2u"); + assertEnteringStringGivesResponse("\033[?u", "\033[?2u"); + enterString("\033[2u"); + assertEnteringStringGivesResponse("\033[?u", "\033[?2u"); + enterString("\033[?1049l"); + assertEnteringStringGivesResponse("\033[?u", "\033[?3u"); + enterString("\033c\033[?1049h"); + assertEnteringStringGivesResponse("\033[?u", "\033[?0u"); + } + + public void testEventEncoding() { + for (int flags : new int[]{2, 3}) { + assertEquals("\033[1;1:2A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, 0, true, false, flags, 2)); + assertEquals("\033[1;1:3A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, 0, true, false, flags, 3)); + assertEquals("\033[13;5:2~", KeyHandler.getCode(KeyEvent.KEYCODE_F3, KeyHandler.KEYMOD_CTRL, false, false, flags, 2)); + for (int key : new int[]{KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_DEL}) { + assertNull(KeyHandler.getCode(key, 0, false, false, flags, 3)); + assertNull(KeyHandler.getCode(key, KeyHandler.KEYMOD_CTRL, false, false, flags, 3)); + } + } + assertEquals("\033[105;5:2u", KeyHandler.getKittyCodePoint('i', KeyHandler.KEYMOD_CTRL, 3, 2)); + assertEquals("\033[105;1:3u", KeyHandler.getKittyCodePoint('i', 0, 3, 3)); + assertEquals("\033[105;5u", KeyHandler.getKittyCodePoint('i', KeyHandler.KEYMOD_CTRL, 1, 2)); + assertNull(KeyHandler.getKittyCodePoint('i', KeyHandler.KEYMOD_CTRL, 2, 1)); + assertNull(KeyHandler.getKittyCodePoint('a', 0, 3, 2)); + assertEquals("\033[57399;129:3u", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_0, + KeyHandler.KEYMOD_NUM_LOCK, false, false, 3, 3)); + assertEquals("0", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_0, + KeyHandler.KEYMOD_NUM_LOCK, false, false, 2, 2)); + assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_0, + KeyHandler.KEYMOD_NUM_LOCK, false, false, 2, 3)); + assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, 0, false, false, 1, 3)); + } + + public void testAlternateFlagCombinationsAndRestoration() { + withTerminalSized(20, 4); + for (int flags = 0; flags <= 31; flags++) { + enterString("\033[=" + flags + "u"); + assertEnteringStringGivesResponse("\033[?u", "\033[?" + (flags & 7) + "u"); + assertEquals((flags & 3) != 0, mTerminal.isKittyKeyboardEnabled()); + } + enterString("\033[=4u\033[>7u\033[5u\033[?1049h"); + assertEnteringStringGivesResponse("\033[?u", "\033[?0u"); + enterString("\033[>6u\033[?1049l"); + assertEnteringStringGivesResponse("\033[?u", "\033[?5u"); + enterString("\033c\033[?1049h"); + assertEnteringStringGivesResponse("\033[?u", "\033[?0u"); + } + + public void testAlternateCodePointFields() { + KeyHandler.KittyKey equals = new KeyHandler.KittyKey('=', '+', '='); + int modifiers = KeyHandler.KEYMOD_CTRL | KeyHandler.KEYMOD_SHIFT; + assertEquals("\033[61:43;6u", KeyHandler.getKittyCodePoint(equals, modifiers, 5, 1)); + assertEquals("\033[61:43;6:2u", KeyHandler.getKittyCodePoint(equals, modifiers, 7, 2)); + assertEquals("\033[61;5:3u", KeyHandler.getKittyCodePoint(equals, KeyHandler.KEYMOD_CTRL, 7, 3)); + assertEquals("\033[61;6u", KeyHandler.getKittyCodePoint(equals, modifiers, 3, 1)); + assertNull(KeyHandler.getKittyCodePoint(equals, modifiers, 4, 1)); + assertNull(KeyHandler.getKittyCodePoint(equals, modifiers, 6, 1)); + assertEquals("\033[61:43;6:2u", KeyHandler.getKittyCodePoint(equals, modifiers, 6, 2)); + assertNull(KeyHandler.getKittyCodePoint(equals, modifiers, 5, 3)); + assertNull(KeyHandler.getKittyCodePoint(equals, KeyHandler.KEYMOD_SHIFT, 7, 1)); + KeyHandler.KittyKey cyrillic = new KeyHandler.KittyKey(1089, 1057, 'c'); + assertEquals("\033[1089:1057:99;6u", KeyHandler.getKittyCodePoint(cyrillic, modifiers, 5, 1)); + assertEquals("\033[1089::99;5u", KeyHandler.getKittyCodePoint(cyrillic, KeyHandler.KEYMOD_CTRL, 5, 1)); + assertEquals("\033[1089::99;1:3u", KeyHandler.getKittyCodePoint(cyrillic, 0, 7, 3)); + KeyHandler.KittyKey unknown = new KeyHandler.KittyKey('a', 0xD800, 0x80000000); + assertEquals("\033[97;6u", KeyHandler.getKittyCodePoint(unknown, modifiers, 5, 1)); + KeyHandler.KittyKey supplementary = new KeyHandler.KittyKey(0x1F600, 0x1F601, 'a'); + assertEquals("\033[128512:128513:97;6u", KeyHandler.getKittyCodePoint(supplementary, modifiers, 5, 1)); + } + + public void testBaseLayoutMappingAndFunctionalKeyBoundaries() { + for (int key = KeyEvent.KEYCODE_A; key <= KeyEvent.KEYCODE_Z; key++) + assertEquals('a' + key - KeyEvent.KEYCODE_A, KeyHandler.getKittyBaseLayoutCodePoint(key)); + for (int key = KeyEvent.KEYCODE_0; key <= KeyEvent.KEYCODE_9; key++) + assertEquals('0' + key - KeyEvent.KEYCODE_0, KeyHandler.getKittyBaseLayoutCodePoint(key)); + assertEquals('=', KeyHandler.getKittyBaseLayoutCodePoint(KeyEvent.KEYCODE_EQUALS)); + assertEquals('\\', KeyHandler.getKittyBaseLayoutCodePoint(KeyEvent.KEYCODE_BACKSLASH)); + assertEquals(0, KeyHandler.getKittyBaseLayoutCodePoint(KeyEvent.KEYCODE_UNKNOWN)); + assertEquals(0, KeyHandler.getKittyBaseLayoutCodePoint(KeyEvent.KEYCODE_PLUS)); + assertEquals(0, KeyHandler.getKittyBaseLayoutCodePoint(KeyEvent.KEYCODE_NUMPAD_0)); + assertEquals("\033[13;2:2~", KeyHandler.getCode(KeyEvent.KEYCODE_F3, KeyHandler.KEYMOD_SHIFT, false, false, 7, 2)); + assertEquals("\r", KeyHandler.getCode(KeyEvent.KEYCODE_ENTER, 0, false, false, 7, 2)); + } + +} diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index f4f82ad7c51..0d1cebb8913 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -34,6 +34,9 @@ import android.view.inputmethod.InputConnection; import android.widget.Scroller; +import java.util.HashMap; +import java.util.Map; + import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -86,6 +89,53 @@ public final class TerminalView extends View { /** If non-zero, this is the last unicode code point received if that was a combining character. */ int mCombiningAccent; + /** Only terminal-delivered physical keys may produce a matching release. */ + private final Map mKittyKeyPresses = new HashMap<>(); + + private static final class KittyKeyPress { + final KeyHandler.KittyKey textKey; + final int virtualModifiers; + final String releaseCode; + final TerminalEmulator emulator; + final Object screen; + final long generation; + final long downTime; + + KittyKeyPress(KeyHandler.KittyKey textKey, int virtualModifiers, String releaseCode, TerminalEmulator emulator, KeyEvent event) { + this.textKey = textKey; + this.virtualModifiers = virtualModifiers; + this.releaseCode = releaseCode; + this.emulator = emulator; + screen = emulator.getScreen(); + generation = emulator.getKittyKeyboardGeneration(); + downTime = event.getDownTime(); + } + } + + private static long kittyKeyId(KeyEvent event) { + return ((long) event.getDeviceId() << 32) | (event.getKeyCode() & 0xffffffffL); + } + + private static int kittyEventModifiers(KeyEvent event) { + int modifiers = 0; + if (event.isCtrlPressed()) modifiers |= KeyHandler.KEYMOD_CTRL; + if (event.isAltPressed()) modifiers |= KeyHandler.KEYMOD_ALT; + if (event.isShiftPressed()) modifiers |= KeyHandler.KEYMOD_SHIFT; + if (event.isMetaPressed()) modifiers |= KeyHandler.KEYMOD_SUPER; + if (event.isCapsLockOn()) modifiers |= KeyHandler.KEYMOD_CAPS_LOCK; + if (event.isNumLockOn()) modifiers |= KeyHandler.KEYMOD_NUM_LOCK; + return modifiers; + } + + private void rememberKittyKey(KeyEvent event, KeyHandler.KittyKey textKey, int modifiers, String releaseCode) { + // Repeats must not replace the identity or mode captured by the initial press. + if (event == null || event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() != 0 || + (mEmulator.getKittyKeyboardFlags() & KeyHandler.KITTY_REPORT_EVENTS) == 0 || + KeyHandler.isKittyRecoveryKey(event.getKeyCode())) return; + mKittyKeyPresses.put(kittyKeyId(event), new KittyKeyPress(textKey, + modifiers & ~kittyEventModifiers(event), releaseCode, mEmulator, event)); + } + /** * The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}. * @@ -290,6 +340,7 @@ public void setIsTerminalViewKeyLoggingEnabled(boolean value) { public boolean attachSession(TerminalSession session) { if (session == mTermSession) return false; + mKittyKeyPresses.clear(); mTermSession = session; mEmulator = null; mCombiningAccent = 0; @@ -775,7 +826,10 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { stopTextSelectionMode(); } + // Forget stale or intercepted presses before dispatching this event to application shortcuts. + if (event.getRepeatCount() == 0) mKittyKeyPresses.remove(kittyKeyId(event)); if (mClient.onKeyDown(keyCode, event, mTermSession)) { + mKittyKeyPresses.remove(kittyKeyId(event)); invalidate(); return true; } else if (event.isSystem() && (!mClient.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) { @@ -798,14 +852,19 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT; if (shiftDown) keyMod |= KeyHandler.KEYMOD_SHIFT; if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK; + if (mEmulator.isKittyKeyboardEnabled()) { + if (event.isMetaPressed()) keyMod |= KeyHandler.KEYMOD_SUPER; + if (event.isCapsLockOn()) keyMod |= KeyHandler.KEYMOD_CAPS_LOCK; + } // https://github.com/termux/termux-app/issues/731 - if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) { + if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod, event)) { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event"); return true; } // Clear Ctrl since we handle that ourselves: int bitsToClear = KeyEvent.META_CTRL_MASK; + if (mEmulator.isKittyKeyboardEnabled()) bitsToClear |= KeyEvent.META_META_MASK; if (rightAltDownFromEvent) { // Let right Alt/Alt Gr be used to compose characters. } else { @@ -814,7 +873,11 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { } int effectiveMetaState = event.getMetaState() & ~bitsToClear; - if (shiftDown) effectiveMetaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON; + if (shiftDown) { + effectiveMetaState |= KeyEvent.META_SHIFT_ON; + // Virtual Shift keeps the existing left-Shift default; physical sides stay intact. + if (!event.isShiftPressed()) effectiveMetaState |= KeyEvent.META_SHIFT_LEFT_ON; + } if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON; int result = event.getUnicodeChar(effectiveMetaState); @@ -836,7 +899,26 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { if (combinedChar > 0) result = combinedChar; mCombiningAccent = 0; } - inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown); + // Keep the layout's unshifted key before legacy Ctrl mapping destroys its identity. + int unshiftedMetaState = effectiveMetaState & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_CAPS_LOCK_ON); + int unshiftedCodePoint = event.getUnicodeChar(unshiftedMetaState); + if (oldCombiningAccent != 0 || unshiftedCodePoint <= 0 || (unshiftedCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0) + unshiftedCodePoint = Character.toLowerCase(result); + int textKeyMod = keyMod; + // Right Alt is used by the keyboard layout (AltGr), rather than a terminal Alt shortcut. + if (rightAltDownFromEvent && !leftAltDown) textKeyMod &= ~KeyHandler.KEYMOD_ALT; + int shiftedCodePoint = 0, baseLayoutCodePoint = 0; + if ((mEmulator.getKittyKeyboardFlags() & KeyHandler.KITTY_REPORT_ALTERNATE_KEYS) != 0 && + oldCombiningAccent == 0 && unshiftedCodePoint > 0 && + (event.getUnicodeChar(unshiftedMetaState) & KeyCharacterMap.COMBINING_ACCENT) == 0) { + if (shiftDown) shiftedCodePoint = event.getUnicodeChar(effectiveMetaState & ~KeyEvent.META_CAPS_LOCK_ON); + if (event.getAction() == KeyEvent.ACTION_DOWN && + (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) == 0) + baseLayoutCodePoint = KeyHandler.getKittyBaseLayoutCodePoint(keyCode); + } + KeyHandler.KittyKey textKey = new KeyHandler.KittyKey(unshiftedCodePoint, + shiftedCodePoint, baseLayoutCodePoint); + inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown, textKey, textKeyMod, event); } if (mCombiningAccent != oldCombiningAccent) invalidate(); @@ -845,6 +927,12 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { } public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) { + inputCodePoint(eventSource, codePoint, controlDownFromEvent, leftAltDownFromEvent, + new KeyHandler.KittyKey(Character.toLowerCase(codePoint), 0, 0), 0, null); + } + + private void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, + boolean leftAltDownFromEvent, KeyHandler.KittyKey textKey, int keyMod, KeyEvent event) { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) { mClient.logInfo(LOG_TAG, "inputCodePoint(eventSource=" + eventSource + ", codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent=" + leftAltDownFromEvent + ")"); @@ -859,7 +947,26 @@ public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFr final boolean controlDown = controlDownFromEvent || mClient.readControlKey(); final boolean altDown = leftAltDownFromEvent || mClient.readAltKey(); - if (mClient.onCodePoint(codePoint, controlDown, mTermSession)) return; + if (mClient.onCodePoint(codePoint, controlDown, mTermSession)) { + if (event != null) mKittyKeyPresses.remove(kittyKeyId(event)); + return; + } + + if (mEmulator != null && mEmulator.isKittyKeyboardEnabled()) { + if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL; + if (altDown) keyMod |= KeyHandler.KEYMOD_ALT; + if (mClient.readShiftKey()) keyMod |= KeyHandler.KEYMOD_SHIFT; + int eventType = event != null && event.getRepeatCount() > 0 ? + KeyHandler.KEY_EVENT_REPEAT : KeyHandler.KEY_EVENT_PRESS; + String kittyCode = KeyHandler.getKittyCodePoint(textKey, keyMod, + mEmulator.getKittyKeyboardFlags(), eventType); + if (KeyHandler.getKittyCodePoint(textKey.codePoint, keyMod) != null) + rememberKittyKey(event, textKey, keyMod, null); + if (kittyCode != null) { + mTermSession.write(kittyCode); + return; + } + } if (controlDown) { if (codePoint >= 'a' && codePoint <= 'z') { @@ -911,17 +1018,28 @@ public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFr /** Input the specified keyCode if applicable and return if the input was consumed. */ public boolean handleKeyCode(int keyCode, int keyMod) { + return handleKeyCode(keyCode, keyMod, null); + } + + private boolean handleKeyCode(int keyCode, int keyMod, KeyEvent event) { // Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys if (mEmulator != null) mEmulator.setCursorBlinkState(true); - if (handleKeyCodeAction(keyCode, keyMod)) + if (handleKeyCodeAction(keyCode, keyMod)) { + if (event != null) mKittyKeyPresses.remove(kittyKeyId(event)); return true; + } TerminalEmulator term = mTermSession.getEmulator(); - String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()); + String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), + term.isKeypadApplicationMode(), term.getKittyKeyboardFlags(), + event != null && event.getRepeatCount() > 0 ? KeyHandler.KEY_EVENT_REPEAT : KeyHandler.KEY_EVENT_PRESS); if (code == null) return false; mTermSession.write(code); + String releaseCode = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), + term.isKeypadApplicationMode(), term.getKittyKeyboardFlags(), KeyHandler.KEY_EVENT_RELEASE); + if (releaseCode != null) rememberKittyKey(event, null, keyMod, releaseCode); return true; } @@ -961,17 +1079,36 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { // to exit the activity. if (mEmulator == null && keyCode != KeyEvent.KEYCODE_BACK) return true; + KittyKeyPress press = mKittyKeyPresses.remove(kittyKeyId(event)); if (mClient.onKeyUp(keyCode, event)) { invalidate(); return true; - } else if (event.isSystem()) { + } else if (event.isSystem() && (!mClient.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) { // Let system key events through. return super.onKeyUp(keyCode, event); } + if (press != null && mEmulator == press.emulator && mEmulator.getScreen() == press.screen && + mEmulator.getKittyKeyboardGeneration() == press.generation && event.getDownTime() == press.downTime && + (mEmulator.getKittyKeyboardFlags() & KeyHandler.KITTY_REPORT_EVENTS) != 0 && !event.isCanceled()) { + int modifiers = kittyEventModifiers(event) | press.virtualModifiers; + if (press.textKey != null && (event.getMetaState() & KeyEvent.META_ALT_RIGHT_ON) != 0 && + (event.getMetaState() & KeyEvent.META_ALT_LEFT_ON) == 0) + modifiers &= ~KeyHandler.KEYMOD_ALT; + String code = press.textKey == null ? KeyHandler.getKittyReleaseCode(press.releaseCode, modifiers) : + KeyHandler.getKittyCodePoint(press.textKey, modifiers, + mEmulator.getKittyKeyboardFlags(), KeyHandler.KEY_EVENT_RELEASE); + if (code != null) mTermSession.write(code); + } return true; } + @Override + protected void onFocusChanged(boolean gainFocus, int direction, android.graphics.Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (!gainFocus) mKittyKeyPresses.clear(); + } + /** * This is called during layout when the size of this view has changed. If you were just added to the view * hierarchy, you're called with the old values of 0. @@ -1462,6 +1599,7 @@ protected void onAttachedToWindow() { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + mKittyKeyPresses.clear(); if (mTextSelectionCursorController != null) { // Might solve the following exception diff --git a/tools/kitty-keyboard/README.md b/tools/kitty-keyboard/README.md new file mode 100644 index 00000000000..8e990841b21 --- /dev/null +++ b/tools/kitty-keyboard/README.md @@ -0,0 +1,56 @@ +# Keyboard validation + +This runner uses the same `./gradlew test` task as the official +[unit-test workflow](https://github.com/termux/termux-app/blob/master/.github/workflows/run_tests.yml), +then builds the default `apt-android-7` Debug variant and checks the five APKs +and SHA-256 hashes, following the +[build workflow](https://github.com/termux/termux-app/blob/master/.github/workflows/debug_build.yml). +The official CI uses Temurin 17; a compatible local JDK and the SDK/NDK versions +specified in `gradle.properties` must already be installed. This runner does +not download or delete toolchains, install APKs, or publish changes. + +Export `JAVA_HOME` and `ANDROID_HOME` (or `ANDROID_SDK_ROOT`). For an isolated +setup, also export `GRADLE_USER_HOME` and `ANDROID_USER_HOME` to the retained +toolchain directory. Run from the repository root: + +```sh +python3 tools/kitty-keyboard/validate.py --output-dir ../termux-app-artifacts/kitty-alternates +``` + +Use `--offline` only with a provisioned dependency cache. Choose a new, empty +output directory for each run. It retains the source diff and newly added files, +version, Gradle log, Debug/Release test XML, five versioned APKs, checksums and +`validation.json`. This uses the default package variant; the official build CI +also tests `apt-android-5` independently. A runner failure preserves its results; +inspect `gradle.log` and the XML rather than treating old artifacts as a new pass. +Raw test directories are archived before their XML is parsed. Damaged or invalid +reports are listed in `validation.json` under `test_result_errors`; the remaining +valid reports are summarized and the runner returns failure. A nonzero Gradle +exit code is preserved even if some XML reports are unreadable. + +Exercise evidence-retention regressions without a Java toolchain or device: + +```sh +python3 -B tools/kitty-keyboard/test_validate.py +``` + +These checks use temporary fixture repositories and a simulated failing Gradle +command; they do not replace the actual app tests. + +Device acceptance remains a separate step. Record the installed APK version and +hash, negotiation flags, expected and actual PTY bytes, event source and cleanup. +Direct framework delivery, OS injection, touchscreen operations and physical HID +must be labelled separately. A previous feature's device results do not validate +a newer enhancement. Physical actions remain manual; test helpers must never +claim hardware verification for synthetic events. Retain tooling and APKs for +retesting; uninstall temporary helpers and restore terminal modes after capture. + +Contribution guidance comes from the app +[README](https://github.com/termux/termux-app#for-maintainers-and-contributors), +`.editorconfig`, and the +[Termux Libraries development guide](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development). +Shared app/plugin utilities belong in the appropriate library; keyboard protocol +encoding remains in `terminal-emulator` and Android input handling in +`terminal-view`. Follow the repository's capitalized commit types and semantic +version requirements. Local Maven publishing is needed when testing dependent +plugins against modified libraries, not for building this app's own modules. diff --git a/tools/kitty-keyboard/test_validate.py b/tools/kitty-keyboard/test_validate.py new file mode 100644 index 00000000000..3a98d2d09c0 --- /dev/null +++ b/tools/kitty-keyboard/test_validate.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Exercise failure-evidence retention without a Java toolchain or Android device.""" + +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +SOURCE = Path(__file__).with_name("validate.py") +spec = importlib.util.spec_from_file_location("kitty_validation", SOURCE) +validation = importlib.util.module_from_spec(spec) +spec.loader.exec_module(validation) + + +def write(path, text): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + + +class ValidationEvidenceTest(unittest.TestCase): + def test_damaged_xml_is_archived_and_other_modules_are_summarized(self): + with tempfile.TemporaryDirectory(prefix="kitty-validation-evidence-") as temporary: + root = Path(temporary) + repo, output = root / "repo", root / "output" + damaged = repo / "terminal-emulator/build/test-results/testDebugUnitTest/TEST-damaged.xml" + valid = repo / "app/build/test-results/testDebugUnitTest/TEST-valid.xml" + write(damaged, '') + summaries, errors = validation.archive_test_results(repo, output) + self.assertEqual((output / "test-results/terminal-emulator/testDebugUnitTest/TEST-damaged.xml").read_bytes(), + damaged.read_bytes()) + self.assertEqual((output / "test-results/app/testDebugUnitTest/TEST-valid.xml").read_bytes(), + valid.read_bytes()) + self.assertEqual(summaries["app:testDebugUnitTest"]["tests"], 3) + self.assertEqual(len(errors), 1) + self.assertIn("TEST-damaged.xml", errors[0]["path"]) + + def test_invalid_counts_preserve_raw_evidence_and_valid_suites(self): + with tempfile.TemporaryDirectory(prefix="kitty-validation-counts-") as temporary: + root = Path(temporary) + directory = root / "repo/app/build/test-results/testReleaseUnitTest" + write(directory / "TEST-invalid.xml", '') + write(directory / "TEST-negative.xml", '') + write(directory / "TEST-valid.xml", '') + summaries, errors = validation.archive_test_results(root / "repo", root / "output") + self.assertEqual(summaries["app:testReleaseUnitTest"]["tests"], 2) + self.assertEqual(summaries["app:testReleaseUnitTest"]["failures"], 1) + self.assertEqual(len(errors), 2) + self.assertEqual(len(list((root / "output/test-results/app/testReleaseUnitTest").glob("*.xml"))), 3) + + def test_cli_failure_still_writes_summary_and_archives_damaged_xml(self): + with tempfile.TemporaryDirectory(prefix="kitty-validation-cli-") as temporary: + root = Path(temporary) + repo, output = root / "repo", root / "output" + script = repo / "tools/kitty-keyboard/validate.py" + script.parent.mkdir(parents=True) + shutil.copy2(SOURCE, script) + write(repo / "app/build.gradle", ' versionName "0.118.0"\n') + gradle = repo / "gradlew" + write(gradle, """#!/bin/sh +mkdir -p terminal-emulator/build/test-results/testDebugUnitTest app/build/test-results/testDebugUnitTest +printf ' terminal-emulator/build/test-results/testDebugUnitTest/TEST-damaged.xml +printf '' > app/build/test-results/testDebugUnitTest/TEST-valid.xml +printf 'Simulated Gradle failure for evidence-retention regression\\n' +exit 19 +""") + gradle.chmod(0o755) + subprocess.run(["git", "init", "--quiet", str(repo)], check=True) + subprocess.run(["git", "add", "."], cwd=repo, check=True) + subprocess.run(["git", "-c", "user.name=Kitty Validation Tests", "-c", "user.email=kitty-tests@example.invalid", + "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", + "commit", "--quiet", "-m", "Fixture"], cwd=repo, check=True) + result = subprocess.run([sys.executable, "-B", str(script), "--offline", "--output-dir", str(output)], + cwd=repo, capture_output=True, text=True) + self.assertEqual(result.returncode, 19, result.stderr) + report = json.loads((output / "validation.json").read_text()) + self.assertEqual(report["gradle_exit_code"], 19) + self.assertEqual(len(report["test_result_errors"]), 1) + self.assertEqual(report["unit_tests"]["app:testDebugUnitTest"]["tests"], 3) + self.assertEqual(report["apks"], {}) + self.assertEqual((output / "test-results/terminal-emulator/testDebugUnitTest/TEST-damaged.xml").read_text(), + '