From 07f59c25579125a6ff6031130670c82647612a0f Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 01:47:21 +0100 Subject: [PATCH 1/6] Fix Ctrl+Shift+Alt+A/Z combinations Co-authored-by: Copilot --- .../AnsiHandling/KittyKeyboardPattern.cs | 47 +++- Terminal.Gui/Input/Keyboard/Key.cs | 4 +- .../AnsiHandling/KittyAlternateKeyTests.cs | 8 +- .../AnsiHandling/KittyKeyboardParsingTests.cs | 218 +++++++++++++++++- 4 files changed, 264 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs index b598cd8c74..93fc9a3fc0 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs @@ -91,6 +91,13 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern return null; } + var baseKeyCode = ""; + + if (key.KeyCode < KeyCode.CharMask && (key.KeyCode & (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask)) == 0) + { + baseKeyCode = new Rune ((uint)key.KeyCode).ToString (); + } + // Extract alternate key codes (kitty flag 4: report alternate keys) var shiftedKeyCode = KeyCode.Null; var baseLayoutKeyCode = KeyCode.Null; @@ -117,8 +124,8 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern key = new Key (key) { ShiftedKeyCode = shiftedKeyCode, BaseLayoutKeyCode = baseLayoutKeyCode, AssociatedText = associatedText }; } - string modifierField = match.Groups [4].Value; - modifierField = ApplyImplicitModifierState (key, modifierField); + string originalModifierField = match.Groups [4].Value; + string modifierField = ApplyImplicitModifierState (key, originalModifierField); if (!string.IsNullOrEmpty (modifierField)) { @@ -127,10 +134,10 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern if (!string.IsNullOrEmpty (modifierField)) { - key = ApplyModifiersAndEventType (modifierField, key); + key = ApplyModifiersAndEventType (MaxModifierFieldValue (originalModifierField, modifierField), key); } - if ((key.IsAlt || key.IsCtrl) && !string.IsNullOrEmpty (key.AssociatedText)) + if ((key.IsAlt || key.IsCtrl) && (key.ShiftedKeyCode != KeyCode.Null || baseKeyCode.Equals (key.AssociatedText, StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrEmpty (key.AssociatedText)) { key = new Key (key) { AssociatedText = string.Empty }; } @@ -222,6 +229,38 @@ private static string ApplyImplicitModifierState (Key key, string modifierField) return string.Join (':', parts); } + private static string MaxModifierFieldValue (string modifierField1, string modifierField2) + { + if (string.IsNullOrEmpty (modifierField1)) + { + return modifierField2; + } + + if (string.IsNullOrEmpty (modifierField2)) + { + return modifierField1; + } + + string [] parts1 = modifierField1.Split (':'); + string [] parts2 = modifierField2.Split (':'); + + if (parts1.Length == 0 || parts2.Length == 0) + { + return modifierField1; + } + + if (!int.TryParse (parts1 [0], CultureInfo.InvariantCulture, out int encodedModifiers1) + || !int.TryParse (parts2 [0], CultureInfo.InvariantCulture, out int encodedModifiers2)) + { + return modifierField1; + } + + int maxEncodedModifiers = Math.Max (encodedModifiers1, encodedModifiers2); + parts1 [0] = maxEncodedModifiers.ToString (CultureInfo.InvariantCulture); + + return string.Join (':', parts1); + } + private static (Key Key, string ModifierField) NormalizeShiftedPrintableKey (Key key, string modifierField) { string [] parts = modifierField.Split (':'); diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index b0b64287f9..1a5dee5d27 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -217,7 +217,7 @@ public string AsGrapheme if (IsShift && ShiftedKeyCode != KeyCode.Null) { - Rune shiftedRune = ToRune (ShiftedKeyCode); + Rune shiftedRune = ToRune (ShiftedKeyCode | KeyCode.ShiftMask); if (shiftedRune != default (Rune)) { @@ -278,7 +278,7 @@ public Rune AsRune if (IsShift && ShiftedKeyCode != KeyCode.Null) { - Rune shiftedRune = ToRune (ShiftedKeyCode); + Rune shiftedRune = ToRune (ShiftedKeyCode | KeyCode.ShiftMask); if (shiftedRune != default (Rune)) { diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs index 7fa60246fb..56faa131aa 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs @@ -108,7 +108,7 @@ public void ViewKeyDown_ShiftedAndBase_Preserved () // View level — same alternate key fields must arrive Assert.Single (viewDown); - Assert.Equal (new Key ('@'), viewDown [0]); + Assert.Equal (new Key ('@').WithShift, viewDown [0]); Assert.Equal ((KeyCode)64, viewDown [0].ShiftedKeyCode); Assert.Equal (KeyCode.Null, viewDown [0].BaseLayoutKeyCode); Assert.Equal ("@", viewDown [0].AssociatedText); @@ -125,7 +125,7 @@ public void ViewKeyDown_AssociatedText_Preserved () Assert.Equal ("!", appDown [0].GetPrintableText ()); Assert.Single (viewDown); - Assert.Equal (new Key ('!'), viewDown [0]); + Assert.Equal (new Key ('!').WithShift, viewDown [0]); Assert.Equal ("!", viewDown [0].AssociatedText); Assert.Equal ("!", viewDown [0].GetPrintableText ()); } @@ -178,7 +178,7 @@ public void ViewKeyDown_WithModifiersAndEventType_Preserved () (_, List viewDown, _) = InjectRawSequenceToView ("\x1b[50:64;6:1u"); Assert.Single (viewDown); - Assert.Equal (new Key ('@').WithCtrl, viewDown [0]); + Assert.Equal (new Key ('@').WithCtrl.WithShift, viewDown [0]); Assert.Equal ((KeyCode)64, viewDown [0].ShiftedKeyCode); Assert.Equal (KeyCode.Null, viewDown [0].BaseLayoutKeyCode); Assert.Equal (KeyEventType.Press, viewDown [0].EventType); @@ -194,7 +194,7 @@ public void ViewKeyUp_AlternateKeys_Preserved () // Release events go to KeyUp, not KeyDown Assert.Empty (viewDown); Assert.Single (viewUp); - Assert.Equal (new Key ('@'), viewUp [0]); + Assert.Equal (new Key ('@').WithShift, viewUp [0]); Assert.Equal ((KeyCode)64, viewUp [0].ShiftedKeyCode); Assert.Equal (KeyCode.Null, viewUp [0].BaseLayoutKeyCode); Assert.Equal (KeyEventType.Release, viewUp [0].EventType); diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs index 7b08fa79db..8ba6c3a2c3 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs @@ -567,7 +567,8 @@ public void KittyPattern_AlternateKeys_WithModifiers () Key? key = _pattern.GetKey ("\u001b[50:64;2;64u"); Assert.NotNull (key); - Assert.Equal (new Key ('@'), key); + Assert.Equal ("@", key.AsGrapheme); + Assert.Equal (new Key ('@').WithShift, key); Assert.Equal ((KeyCode)64, key.ShiftedKeyCode); Assert.Equal (KeyCode.Null, key.BaseLayoutKeyCode); Assert.Equal ("@", key.AssociatedText); @@ -581,7 +582,8 @@ public void KittyPattern_AlternateKeys_WithModifiersAndEventType () Key? key = _pattern.GetKey ("\u001b[50:64;2:3;64u"); Assert.NotNull (key); - Assert.Equal (new Key ('@'), key); + Assert.Equal ("@", key.AsGrapheme); + Assert.Equal (new Key ('@').WithShift, key); Assert.Equal ((KeyCode)64, key.ShiftedKeyCode); Assert.Equal (KeyCode.Null, key.BaseLayoutKeyCode); Assert.Equal (KeyEventType.Release, key.EventType); @@ -595,7 +597,7 @@ public void KittyPattern_AssociatedText_ShiftedPrintableKey () Key? key = _pattern.GetKey ("\u001b[49;2;33u"); Assert.NotNull (key); - Assert.Equal (new Key ('!'), key); + Assert.Equal (new Key ('!').WithShift, key); Assert.Equal ("!", key.AssociatedText); Assert.Equal ("!", key.GetPrintableText ()); } @@ -738,4 +740,214 @@ public void KittyRequestedFlags_IncludesReportAssociatedText () => + "for printable text fidelity."); #endregion + + #region Regression Tests - Modifiers Preservation + + // Copilot - ChatGPT v4 + /// + /// Regression test for issue where Ctrl+Shift+Alt+A was being parsed as Ctrl+Alt+A. + /// The bug was in NormalizeShiftedPrintableKey modifying the modifierField, + /// causing double-decoding in ApplyModifiersAndEventType. + /// Input: ESC[97:65;8u = 'a' with shifted key 'A' (65), modifiers 8 (Shift+Ctrl+Alt) + /// Expected: Ctrl+Shift+Alt+A + /// + [Fact] + public void KittyPattern_ShiftCtrlAlt_A_PreservesAllModifiers () + { + // ESC[97:65;8u = 'a' with shifted key 'A' (65), modifiers 8 (0b111 = Shift+Ctrl+Alt) + // mask = 8 - 1 = 7 which means 7 = 1 (Shift) + 2 (Alt) + 4 (Ctrl) + Key? key = _pattern.GetKey ("\u001b[97:65;8u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithShift.WithCtrl.WithAlt, key); + Assert.True (key.IsShift, "Shift should be preserved"); + Assert.True (key.IsCtrl, "Ctrl should be preserved"); + Assert.True (key.IsAlt, "Alt should be preserved"); + Assert.Equal (new Key (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask), key.WithShift.WithCtrl.WithAlt); + Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Ctrl+A (without Shift). + /// This should remain as Ctrl+A, not regress. + /// Input: ESC[97;5u = 'a', modifiers 5 (0b100 = Ctrl, no Shift) + /// + [Fact] + public void KittyPattern_Ctrl_A_WithoutShift () + { + // ESC[97;5u = 'a', modifiers 5 (0b100 = Ctrl, no Shift) + // mask = 5 - 1 = 4 which means 4 = 4 (Ctrl) + Key? key = _pattern.GetKey ("\u001b[97;5u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithCtrl, key); + Assert.False (key.IsShift, "Shift should not be present"); + Assert.True (key.IsCtrl, "Ctrl should be present"); + Assert.False (key.IsAlt, "Alt should not be present"); + Assert.Equal (new Key ('a'), key.NoCtrl); + Assert.Equal (KeyCode.A | KeyCode.CtrlMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Ctrl+Alt+A (without Shift). + /// This should remain as Ctrl+Alt+A, not regress. + /// Input: ESC[97;7u = 'a', modifiers 7 (0b110 = Ctrl+Alt, no Shift) + /// + [Fact] + public void KittyPattern_CtrlAlt_A_WithoutShift () + { + // ESC[97;7u = 'a', modifiers 7 (0b110 = Ctrl+Alt, no Shift) + // mask = 7 - 1 = 6 which means 6 = 2 (Alt) + 4 (Ctrl) + Key? key = _pattern.GetKey ("\u001b[97;7u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithCtrl.WithAlt, key); + Assert.False (key.IsShift, "Shift should not be present"); + Assert.True (key.IsCtrl, "Ctrl should be present"); + Assert.True (key.IsAlt, "Alt should be present"); + Assert.Equal (new Key ('a'), key.NoCtrl.NoAlt); + Assert.Equal (KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Shift+A (with shifted character 65 and only shift modifier). + /// Input: ESC[97:65;2u = 'a' with shifted key 'A' (65), modifiers 2 (0b001 = Shift only) + /// Expected: Shift+A + /// + [Fact] + public void KittyPattern_Shift_A_WithShiftedCharacter () + { + // ESC[97:65;2u = 'a' with shifted key 'A' (65), modifiers 2 (0b001 = Shift) + // mask = 2 - 1 = 1 which means 1 = Shift only + Key? key = _pattern.GetKey ("\u001b[97:65;2u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithShift, key); + Assert.True (key.IsShift, "Shift should be preserved"); + Assert.False (key.IsCtrl, "Ctrl should not be present"); + Assert.False (key.IsAlt, "Alt should not be present"); + Assert.Equal ("A", key.AsGrapheme); + Assert.Equal (new Key ('A'), key); + Assert.Equal (new Key ('a'), key.NoShift); + Assert.Equal (KeyCode.A | KeyCode.ShiftMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Shift+Ctrl+A (without Alt). + /// Input: ESC[97:65;6u = 'a' with shifted key 'A' (65), modifiers 6 (0b0101 = Shift+Ctrl) + /// Expected: Shift+Ctrl+A + /// + [Fact] + public void KittyPattern_ShiftCtrl_A_PreservesAllModifiers () + { + // ESC[97:65;6u = 'a' with shifted key 'A' (65), modifiers 6 (0b0101 = Shift+Ctrl) + // mask = 6 - 1 = 5 which means 5 = 1 (Shift) + 4 (Ctrl) + Key? key = _pattern.GetKey ("\u001b[97:65;6u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithShift.WithCtrl, key); + Assert.True (key.IsShift, "Shift should be preserved"); + Assert.True (key.IsCtrl, "Ctrl should be preserved"); + Assert.False (key.IsAlt, "Alt should not be present"); + Assert.Equal (new Key ('a'), key.NoShift.NoCtrl); + Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Shift+Alt+A (without Ctrl). + /// Input: ESC[97:65;4u = 'a' with shifted key 'A' (65), modifiers 4 (0b011 = Shift+Alt) + /// Expected: Shift+Alt+A + /// + [Fact] + public void KittyPattern_ShiftAlt_A_PreservesAllModifiers () + { + // ESC[97:65;4u = 'a' with shifted key 'A' (65), modifiers 4 (0b011 = Shift+Alt) + // mask = 4 - 1 = 3 which means 3 = 1 (Shift) + 2 (Alt) + Key? key = _pattern.GetKey ("\u001b[97:65;4u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithShift.WithAlt, key); + Assert.True (key.IsShift, "Shift should be preserved"); + Assert.False (key.IsCtrl, "Ctrl should not be present"); + Assert.True (key.IsAlt, "Alt should be preserved"); + Assert.Equal (new Key ('a'), key.NoShift.NoAlt); + Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask, key.KeyCode); + } + + // Copilot - ChatGPT v4 + /// + /// Regression test for Ctrl+Alt+A (without Shift). + /// This should remain as Ctrl+Alt+A, not regress. + /// Input: ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift) + /// + [Fact] + public void KittyPattern_Alt_A_WithoutShift () + { + // ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift) + // mask = 3 - 1 = 2 which means 2 = 2 (Alt) + Key? key = _pattern.GetKey ("\u001b[97;3u"); + + Assert.NotNull (key); + Assert.Equal (Key.A.WithAlt, key); + Assert.False (key.IsShift, "Shift should not be present"); + Assert.False (key.IsCtrl, "Ctrl should not be present"); + Assert.True (key.IsAlt, "Alt should be present"); + Assert.Equal (new Key ('a'), key.NoAlt); + Assert.Equal (KeyCode.A | KeyCode.AltMask, key.KeyCode); + } + + #endregion + + #region Regression Tests - Printable AltGr keys + + /// + /// Regression test for AltGr+@. + /// This should remain as @, not regress. + /// Input: ESC[50;3;64u = '@', modifiers 3 (0b010 = Alt) + /// + [Fact] + public void KittyPattern_Alt_Arroba_WithAltGr () + { + // ESC[50;3;64u = '@', modifiers 3 (0b010 = Alt) + // mask = 3 - 1 = 2 which means 2 = 2 (Alt) + Key? key = _pattern.GetKey ("\u001b[50;3;64u"); + + Assert.NotNull (key); + Assert.Equal (Key.D2.WithAlt, key); + Assert.Equal ("@", key.AsGrapheme); + Assert.False (key.IsShift, "Shift should not be present"); + Assert.False (key.IsCtrl, "Ctrl should not be present"); + Assert.True (key.IsAlt, "Alt should be present"); + Assert.Equal (Key.D2, key.NoAlt); + Assert.Equal (KeyCode.D2 | KeyCode.AltMask, key.KeyCode); + } + + /// + /// Regression test for AltGr+€. + /// This should remain as €, not regress. + /// Input: ESC[101;3;8364u = '€', modifiers 3 (0b010 = Alt) + /// + [Fact] + public void KittyPattern_Alt_Euro_WithAltGr () + { + // ESC[101;3;8364u = '€', modifiers 3 (0b010 = Alt) + // mask = 3 - 1 = 2 which means 2 = 2 (Alt) + Key? key = _pattern.GetKey ("\u001b[101;3;8364u"); + + Assert.NotNull (key); + Assert.Equal (Key.E.WithAlt, key); + Assert.Equal ("€", key.AsGrapheme); + Assert.False (key.IsShift, "Shift should not be present"); + Assert.False (key.IsCtrl, "Ctrl should not be present"); + Assert.True (key.IsAlt, "Alt should be present"); + Assert.Equal (Key.E, key.NoAlt); + Assert.Equal (KeyCode.E | KeyCode.AltMask, key.KeyCode); + } + + #endregion } From 5185d6bcddccf7f5cb96e0aaf1670e30ccadecf0 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 01:48:19 +0100 Subject: [PATCH 2/6] Fix valid printable AltGr+key combinations Co-authored-by: Copilot --- Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs | 5 ++++- Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs | 5 ++++- Tests/UnitTestsParallelizable/Views/TextFieldTests.cs | 4 +++- Tests/UnitTestsParallelizable/Views/TextViewTests.cs | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs index 93dbbc019e..69db6a40c8 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs @@ -73,7 +73,10 @@ protected override bool OnKeyDownNotHandled (Key a) return false; } - if (a.IsAlt || a.IsCtrl) + // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key. + // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"), + // while still preventing most modified keys from being inserted into the TextField. + if ((a.IsAlt && string.IsNullOrEmpty(a.AsGrapheme)) || a.IsCtrl) { // Never insert modified keys return false; diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs index 961df362e6..34cbf2194c 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs @@ -142,7 +142,10 @@ protected override bool OnKeyDownNotHandled (Key a) return Autocomplete.ProcessKey (a); } - if (a.IsAlt || a.IsCtrl) + // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key. + // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"), + // while still preventing most modified keys from being inserted into the TextField. + if ((a.IsAlt && string.IsNullOrEmpty (a.AsGrapheme)) || a.IsCtrl) { // Never insert modified keys return false; diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs index ded262f9bf..f77b1147e8 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -1517,7 +1517,9 @@ public void AltKey_With_AssociatedText_Does_Not_Insert_Into_TextField () TextField tf = new () { Width = 20 }; tf.SetFocus (); - Key altT = new (Key.T.WithAlt) { AssociatedText = "t" }; + // Simulate Alt+T with AssociatedText set by Kitty keyboard protocol which in a real scenario would be set AssociatedText as empty string + // for Alt+letter keys, with the exception of AltGr+key which would set AssociatedText to "key" (e.g. "€" for AltGr+E) + Key altT = new (Key.T.WithAlt) { AssociatedText = "" }; tf.NewKeyDownEvent (altT); Assert.Equal ("", tf.Text); diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs index 1f216d645e..4e6ebfb67e 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs @@ -3960,7 +3960,9 @@ public void AltKey_With_AssociatedText_Does_Not_Insert_Into_TextView () TextView tv = new () { Width = 20, Height = 5 }; tv.SetFocus (); - Key altT = new (Key.T.WithAlt) { AssociatedText = "t" }; + // Simulate Alt+T with AssociatedText set by Kitty keyboard protocol which in a real scenario would be set AssociatedText as empty string + // for Alt+letter keys, with the exception of AltGr+key which would set AssociatedText to "key" (e.g. "€" for AltGr+E) + Key altT = new (Key.T.WithAlt) { AssociatedText = "" }; tv.NewKeyDownEvent (altT); Assert.Equal ("", tv.Text); From 5c2c68aee37ff91e641aec37afd3bbcca3cb5901 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 02:02:26 +0100 Subject: [PATCH 3/6] Fix view name --- Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs index 34cbf2194c..52746b2f4c 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs @@ -144,7 +144,7 @@ protected override bool OnKeyDownNotHandled (Key a) // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key. // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"), - // while still preventing most modified keys from being inserted into the TextField. + // while still preventing most modified keys from being inserted into the TextView. if ((a.IsAlt && string.IsNullOrEmpty (a.AsGrapheme)) || a.IsCtrl) { // Never insert modified keys From e004724929f00954419e8a9afd489d22e6a3ee20 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 17:38:14 +0100 Subject: [PATCH 4/6] Using explicit base type --- Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs index 93fc9a3fc0..393df328b0 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs @@ -91,7 +91,7 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern return null; } - var baseKeyCode = ""; + string baseKeyCode = ""; if (key.KeyCode < KeyCode.CharMask && (key.KeyCode & (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask)) == 0) { @@ -99,8 +99,8 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern } // Extract alternate key codes (kitty flag 4: report alternate keys) - var shiftedKeyCode = KeyCode.Null; - var baseLayoutKeyCode = KeyCode.Null; + KeyCode shiftedKeyCode = KeyCode.Null; + KeyCode baseLayoutKeyCode = KeyCode.Null; if (match.Groups [2].Success && int.TryParse (match.Groups [2].Value, CultureInfo.InvariantCulture, out int shiftedCode) && shiftedCode > 0) { @@ -112,7 +112,7 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern baseLayoutKeyCode = (KeyCode)baseCode; } - var associatedText = string.Empty; + string associatedText = string.Empty; if (match.Groups [5].Success) { From 14e159b0fe9e972528f6632ff1d1fb0578111e56 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 17:47:25 +0100 Subject: [PATCH 5/6] Fix formatting --- Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs index 69db6a40c8..5b95dc29d1 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs @@ -76,7 +76,7 @@ protected override bool OnKeyDownNotHandled (Key a) // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key. // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"), // while still preventing most modified keys from being inserted into the TextField. - if ((a.IsAlt && string.IsNullOrEmpty(a.AsGrapheme)) || a.IsCtrl) + if ((a.IsAlt && string.IsNullOrEmpty (a.AsGrapheme)) || a.IsCtrl) { // Never insert modified keys return false; From 2cfcdc66ca9fca8ed8c3a02f83700c24db70dd78 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 28 Apr 2026 18:20:31 +0100 Subject: [PATCH 6/6] Fix sequence in unit test --- .../Drivers/AnsiHandling/KittyKeyboardParsingTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs index 8ba6c3a2c3..4de75d0b79 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs @@ -881,8 +881,8 @@ public void KittyPattern_ShiftAlt_A_PreservesAllModifiers () // Copilot - ChatGPT v4 /// - /// Regression test for Ctrl+Alt+A (without Shift). - /// This should remain as Ctrl+Alt+A, not regress. + /// Regression test for Alt+A (without Shift and Ctrl). + /// This should remain as Alt+A, not regress. /// Input: ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift) /// [Fact] @@ -890,7 +890,7 @@ public void KittyPattern_Alt_A_WithoutShift () { // ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift) // mask = 3 - 1 = 2 which means 2 = 2 (Alt) - Key? key = _pattern.GetKey ("\u001b[97;3u"); + Key? key = _pattern.GetKey ("\u001b[97;3;97u"); Assert.NotNull (key); Assert.Equal (Key.A.WithAlt, key);