Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern
}

string modifierField = match.Groups [4].Value;
modifierField = ApplyImplicitModifierState (key, modifierField);

if (!string.IsNullOrEmpty (modifierField))
{
Expand Down Expand Up @@ -170,6 +171,61 @@ private static string ParseAssociatedText (string textField)
return builder.ToString ();
}

private static string ApplyImplicitModifierState (Key key, string modifierField)
{
if (!key.IsModifierOnly)
{
return modifierField;
}

int implicitEncodedModifiers = key.ModifierKey switch
{
ModifierKey.Shift or ModifierKey.LeftShift or ModifierKey.RightShift => 2,
ModifierKey.Ctrl or ModifierKey.LeftCtrl or ModifierKey.RightCtrl => 5,
ModifierKey.Alt or ModifierKey.LeftAlt or ModifierKey.RightAlt or ModifierKey.AltGr => 3,
_ => 1
};

if (string.IsNullOrEmpty (modifierField))
{
return implicitEncodedModifiers.ToString (CultureInfo.InvariantCulture);
}

string [] parts = modifierField.Split (':');

// Check for release event BEFORE parsing modifiers, to handle case where modifierField is just the event type
bool isRelease = parts.Length > 1 && parts [1] == "3";

if (!int.TryParse (parts [0], CultureInfo.InvariantCulture, out int encodedModifiers) || encodedModifiers < 1)
{
parts [0] = implicitEncodedModifiers.ToString (CultureInfo.InvariantCulture);

return string.Join (':', parts);
}

// If it's a release event, preserve the event type and don't try to merge implicit modifiers
if (isRelease)
{
// For release events of modifier-only keys, ensure explicit modifiers are correct
int explicitModifiers = encodedModifiers - 1;
int implicitModifiers = implicitEncodedModifiers - 1;

// Only merge modifiers if the explicit modifiers don't already match the implicit ones
if (explicitModifiers != implicitModifiers)
{
parts [0] = ((explicitModifiers | implicitModifiers) + 1).ToString (CultureInfo.InvariantCulture);
}

return string.Join (':', parts);
}

int explicitModifiersPress = encodedModifiers - 1;
int implicitModifiersPress = implicitEncodedModifiers - 1;
parts [0] = ((explicitModifiersPress | implicitModifiersPress) + 1).ToString (CultureInfo.InvariantCulture);

return string.Join (':', parts);
}

private static (Key Key, string ModifierField) NormalizeShiftedPrintableKey (Key key, string modifierField)
{
string [] parts = modifierField.Split (':');
Expand Down Expand Up @@ -222,6 +278,7 @@ private static (Key Key, string ModifierField) NormalizeShiftedPrintableKey (Key

Key printableKey = new (printableRune.Value)
{
ModifierKey = key.ModifierKey,
ShiftedKeyCode = key.ShiftedKeyCode,
BaseLayoutKeyCode = key.BaseLayoutKeyCode,
AssociatedText = key.AssociatedText
Expand Down
10 changes: 10 additions & 0 deletions Terminal.Gui/Input/Keyboard/Key.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ public string AsGrapheme
return GetSingleGraphemeOrEmpty (AssociatedText);
}

if (IsAlt || IsCtrl)
{
return string.Empty;
}

if (IsShift && ShiftedKeyCode != KeyCode.Null)
{
Rune shiftedRune = ToRune (ShiftedKeyCode);
Expand Down Expand Up @@ -266,6 +271,11 @@ public Rune AsRune
return enumerator.MoveNext () ? default (Rune) : associatedRune;
}

if (IsAlt || IsCtrl)
{
return default (Rune);
}

if (IsShift && ShiftedKeyCode != KeyCode.Null)
{
Rune shiftedRune = ToRune (ShiftedKeyCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public void KittyPattern_LeftShift_Standalone ()
Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftShift, key.ModifierKey);
Assert.True (key.IsShift);
}

[Fact]
Expand All @@ -144,6 +145,7 @@ public void KittyPattern_LeftCtrl_Standalone ()
Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftCtrl, key.ModifierKey);
Assert.True (key.IsCtrl);
}

[Fact]
Expand All @@ -155,6 +157,7 @@ public void KittyPattern_LeftAlt_Standalone ()
Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftAlt, key.ModifierKey);
Assert.True (key.IsAlt);
}

[Fact]
Expand Down Expand Up @@ -250,13 +253,15 @@ public void KittyPattern_AltGr_WithEventType_Release ()
[Fact]
public void KittyPattern_LeftAlt_WithCtrlModifier_PreservesBothStates ()
{
// ESC[57443;5u = LeftAlt (implicit Alt) with Ctrl held (explicit Ctrl=5)
// After the fix, implicit Alt is combined with explicit Ctrl
Key? key = _pattern.GetKey ("\u001b[57443;5u");

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftAlt, key.ModifierKey);
Assert.True (key.IsCtrl);
Assert.False (key.IsAlt);
Assert.True (key.IsAlt);
Assert.Equal (KeyEventType.Press, key.EventType);
}

Expand Down Expand Up @@ -286,6 +291,68 @@ public void KittyPattern_LeftCtrl_Release_WithCtrlModifier_PreservesState ()
Assert.Equal (KeyEventType.Release, key.EventType);
}

[Fact]
public void KittyPattern_LeftCtrl_WithCapsLockModifier_PreservesCtrlState ()
{
Key? key = _pattern.GetKey ("\u001b[57442;65u");

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftCtrl, key.ModifierKey);
Assert.True (key.IsCtrl);
Assert.False (key.IsAlt);
Assert.False (key.IsShift);
Assert.Equal (KeyEventType.Press, key.EventType);
}

[Fact]
public void KittyPattern_LeftShift_WithCapsLockModifier_PreservesShiftState ()
{
Key? key = _pattern.GetKey ("\u001b[57441;65u");

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftShift, key.ModifierKey);
Assert.True (key.IsShift);
Assert.False (key.IsAlt);
Assert.False (key.IsCtrl);
Assert.Equal (KeyEventType.Press, key.EventType);
}

// Regression test for issue where modifier combinations weren't being combined correctly
[Fact]
public void KittyPattern_LeftCtrl_WithShiftModifier_CombinesImplicitAndExplicit ()
{
// ESC[57442;2u = LeftCtrl (implicit Ctrl) with Shift held (explicit Shift=2)
// Should combine to Ctrl+Shift, not just Shift
Key? key = _pattern.GetKey ("\u001b[57442;2u");

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftCtrl, key.ModifierKey);
Assert.True (key.IsCtrl);
Assert.True (key.IsShift);
Assert.False (key.IsAlt);
Assert.Equal (KeyEventType.Press, key.EventType);
}

// Regression test for issue where modifier combinations weren't being combined correctly
[Fact]
public void KittyPattern_LeftAlt_WithShiftAndCtrlModifiers_CombinesAllModifiers ()
{
// ESC[57443;6u = LeftAlt (implicit Alt) with Shift+Ctrl held (explicit Shift+Ctrl=6)
// Should combine to Alt+Shift+Ctrl, not just Shift+Ctrl
Key? key = _pattern.GetKey ("\u001b[57443;6u");

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (ModifierKey.LeftAlt, key.ModifierKey);
Assert.True (key.IsAlt);
Assert.True (key.IsShift);
Assert.True (key.IsCtrl);
Assert.Equal (KeyEventType.Press, key.EventType);
}

[Fact]
public void KittyPattern_NonModifierKey_IsNotModifierOnly ()
{
Expand All @@ -308,6 +375,40 @@ public void KittyPattern_LeftSuper_Standalone ()
Assert.Equal (ModifierKey.LeftSuper, key.ModifierKey);
}

[Theory]
[InlineData ("\u001b[57358u", ModifierKey.CapsLock, false, false, false)]
[InlineData ("\u001b[57359u", ModifierKey.ScrollLock, false, false, false)]
[InlineData ("\u001b[57360u", ModifierKey.NumLock, false, false, false)]
[InlineData ("\u001b[57441u", ModifierKey.LeftShift, true, false, false)]
[InlineData ("\u001b[57442u", ModifierKey.LeftCtrl, false, false, true)]
[InlineData ("\u001b[57443u", ModifierKey.LeftAlt, false, true, false)]
[InlineData ("\u001b[57444u", ModifierKey.LeftSuper, false, false, false)]
[InlineData ("\u001b[57445u", ModifierKey.LeftHyper, false, false, false)]
[InlineData ("\u001b[57447u", ModifierKey.RightShift, true, false, false)]
[InlineData ("\u001b[57448u", ModifierKey.RightCtrl, false, false, true)]
[InlineData ("\u001b[57449u", ModifierKey.RightAlt, false, true, false)]
[InlineData ("\u001b[57450u", ModifierKey.RightSuper, false, false, false)]
[InlineData ("\u001b[57451u", ModifierKey.RightHyper, false, false, false)]
[InlineData ("\u001b[57453u", ModifierKey.AltGr, false, true, false)]
public void KittyPattern_AllMappedModifierPresses_ParseWithExpectedImplicitState (
string sequence,
ModifierKey expectedModifier,
bool expectedShift,
bool expectedAlt,
bool expectedCtrl
)
{
Key? key = _pattern.GetKey (sequence);

Assert.NotNull (key);
Assert.True (key.IsModifierOnly);
Assert.Equal (expectedModifier, key.ModifierKey);
Assert.Equal (expectedShift, key.IsShift);
Assert.Equal (expectedAlt, key.IsAlt);
Assert.Equal (expectedCtrl, key.IsCtrl);
Assert.Equal (KeyEventType.Press, key.EventType);
}

#endregion

#region CSI ~ and Cursor Key Event Types
Expand Down Expand Up @@ -507,9 +608,30 @@ public void KittyPattern_AssociatedText_AltModifiedPrintableKey_IsSuppressed ()

Assert.NotNull (key);
Assert.True (key.IsAlt);
Assert.Equal (Key.T.WithAlt, key);
Assert.Equal (Key.T.WithAlt.KeyCode, key.KeyCode);
Assert.Equal (string.Empty, key.AssociatedText);
Assert.Equal (string.Empty, key.GetPrintableText ());
Assert.Equal (string.Empty, key.AsGrapheme);
Assert.Equal (0, key.AsRune.Value);
}

[Fact]
public void KittyPattern_AssociatedText_ShiftAltModifiedPrintableKey_IsSuppressed ()
{
// ESC[116:84;4;84u = Shift+Alt+T with shifted key 'T' and associated text 'T'
Key? key = _pattern.GetKey ("\u001b[116:84;4;84u");

Assert.NotNull (key);
Assert.True (key.IsShift);
Assert.True (key.IsAlt);
Assert.Equal (Key.T.WithShift.WithAlt, key);
Assert.Equal (Key.T.WithShift.WithAlt.KeyCode, key.KeyCode);
Assert.Equal ((KeyCode)'T', key.ShiftedKeyCode);
Assert.Equal (string.Empty, key.AssociatedText);
Assert.Equal (string.Empty, key.GetPrintableText ());
Assert.Equal (string.Empty, key.AsGrapheme);
Assert.Equal (0, key.AsRune.Value);
}

[Fact]
Expand Down
Loading
Loading