Fixes #4096 Fix ctrl+Del ansi escape sequence not parsing (#4097)

* Fix ctrl+Del ansi escape sequence not parsing

* Add more tests

* cleanup

* xml doc

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Thomas Nind
2025-05-29 23:00:32 +01:00
committed by GitHub
parent 2b100c3c18
commit 128591281d
4 changed files with 179 additions and 60 deletions

View File

@@ -10,6 +10,7 @@ public class AnsiKeyboardParser
{
new Ss3Pattern (),
new CsiKeyPattern (),
new CsiCursorPattern(),
new EscAsAltPattern { IsLastMinute = true }
};

View File

@@ -0,0 +1,73 @@
#nullable enable
using System.Text.RegularExpressions;
namespace Terminal.Gui;
/// <summary>
/// Detects ansi escape sequences in strings that have been read from
/// the terminal (see <see cref="IAnsiResponseParser"/>).
/// Handles navigation CSI key parsing such as <c>\x1b[A</c> (Cursor up)
/// and <c>\x1b[1;5A</c> (Cursor up with Ctrl)
/// </summary>
public class CsiCursorPattern : AnsiKeyboardParserPattern
{
private readonly Regex _pattern = new (@"^\u001b\[(?:1;(\d+))?([A-DHF])$");
private readonly Dictionary<char, Key> _cursorMap = new ()
{
{ 'A', Key.CursorUp },
{ 'B', Key.CursorDown },
{ 'C', Key.CursorRight },
{ 'D', Key.CursorLeft },
{ 'H', Key.Home },
{ 'F', Key.End }
};
/// <inheritdoc/>
public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
/// <summary>
/// Called by the base class to determine the key that matches the input.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
protected override Key? GetKeyImpl (string? input)
{
Match match = _pattern.Match (input!);
if (!match.Success)
{
return null;
}
string modifierGroup = match.Groups [1].Value;
char terminator = match.Groups [2].Value [0];
if (!_cursorMap.TryGetValue (terminator, out Key? key))
{
return null;
}
if (string.IsNullOrEmpty (modifierGroup))
{
return key;
}
if (int.TryParse (modifierGroup, out int modifier))
{
key = modifier switch
{
2 => key.WithShift,
3 => key.WithAlt,
4 => key.WithAlt.WithShift,
5 => key.WithCtrl,
6 => key.WithCtrl.WithShift,
7 => key.WithCtrl.WithAlt,
8 => key.WithCtrl.WithAlt.WithShift,
_ => key
};
}
return key;
}
}

View File

@@ -5,58 +5,39 @@ namespace Terminal.Gui;
/// <summary>
/// Detects ansi escape sequences in strings that have been read from
/// the terminal (see <see cref="IAnsiResponseParser"/>). This pattern
/// handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up
/// the terminal (see <see cref="IAnsiResponseParser"/>).
/// Handles CSI key parsing such as <c>\x1b[3;5~</c> (Delete with Ctrl)
/// </summary>
public class CsiKeyPattern : AnsiKeyboardParserPattern
{
private readonly Dictionary<string, Key> _terminators = new()
{
{ "A", Key.CursorUp },
{ "B", Key.CursorDown },
{ "C", Key.CursorRight },
{ "D", Key.CursorLeft },
{ "H", Key.Home }, // Home (older variant)
{ "F", Key.End }, // End (older variant)
{ "1~", Key.Home }, // Home (modern variant)
{ "4~", Key.End }, // End (modern variant)
{ "5~", Key.PageUp },
{ "6~", Key.PageDown },
{ "2~", Key.InsertChar },
{ "3~", Key.Delete },
{ "11~", Key.F1 },
{ "12~", Key.F2 },
{ "13~", Key.F3 },
{ "14~", Key.F4 },
{ "15~", Key.F5 },
{ "17~", Key.F6 },
{ "18~", Key.F7 },
{ "19~", Key.F8 },
{ "20~", Key.F9 },
{ "21~", Key.F10 },
{ "23~", Key.F11 },
{ "24~", Key.F12 }
};
private readonly Regex _pattern = new (@"^\u001b\[(\d+)(?:;(\d+))?~$");
private readonly Regex _pattern;
private readonly Dictionary<int, Key> _keyCodeMap = new ()
{
{ 1, Key.Home }, // Home (modern variant)
{ 4, Key.End }, // End (modern variant)
{ 5, Key.PageUp },
{ 6, Key.PageDown },
{ 2, Key.InsertChar },
{ 3, Key.Delete },
{ 11, Key.F1 },
{ 12, Key.F2 },
{ 13, Key.F3 },
{ 14, Key.F4 },
{ 15, Key.F5 },
{ 17, Key.F6 },
{ 18, Key.F7 },
{ 19, Key.F8 },
{ 20, Key.F9 },
{ 21, Key.F10 },
{ 23, Key.F11 },
{ 24, Key.F12 }
};
/// <inheritdoc/>
public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
/// <summary>
/// Creates a new instance of the <see cref="CsiKeyPattern"/> class.
/// </summary>
public CsiKeyPattern ()
{
var terms = new string (_terminators.Select (k => k.Key [0]).Where (k => !char.IsDigit (k)).ToArray ());
_pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$");
}
/// <summary>
/// Called by the base class to determine the key that matches the input.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
/// <inheritdoc/>
protected override Key? GetKeyImpl (string? input)
{
Match match = _pattern.Match (input!);
@@ -66,26 +47,37 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern
return null;
}
string terminator = match.Groups [3].Value;
string modifierGroup = match.Groups [2].Value;
// Group 1: Key code (e.g. 3, 17, etc.)
// Group 2: Optional modifier code (e.g. 2 = Shift, 5 = Ctrl)
Key? key = _terminators.GetValueOrDefault (terminator);
if (key is {} && int.TryParse (modifierGroup, out int modifier))
if (!int.TryParse (match.Groups [1].Value, out int keyCode))
{
key = modifier switch
{
2 => key.WithShift,
3 => key.WithAlt,
4 => key.WithAlt.WithShift,
5 => key.WithCtrl,
6 => key.WithCtrl.WithShift,
7 => key.WithCtrl.WithAlt,
8 => key.WithCtrl.WithAlt.WithShift,
_ => key
};
return null;
}
if (!_keyCodeMap.TryGetValue (keyCode, out Key? key))
{
return null;
}
// If there's no modifier, just return the key.
if (!int.TryParse (match.Groups [2].Value, out int modifier))
{
return key;
}
key = modifier switch
{
2 => key.WithShift,
3 => key.WithAlt,
4 => key.WithAlt.WithShift,
5 => key.WithCtrl,
6 => key.WithCtrl.WithShift,
7 => key.WithCtrl.WithAlt,
8 => key.WithCtrl.WithAlt.WithShift,
_ => key
};
return key;
}
}

View File

@@ -52,6 +52,59 @@ public class AnsiKeyboardParserTests
yield return new object [] { "\u001b[1", null! };
yield return new object [] { "\u001b[AB", null! };
yield return new object [] { "\u001b[;A", null! };
// Test data for various ANSI escape sequences and their expected Key values
yield return new object [] { "\u001b[3;5~", Key.Delete.WithCtrl };
// Additional special keys
yield return new object [] { "\u001b[H", Key.Home };
yield return new object [] { "\u001b[F", Key.End };
yield return new object [] { "\u001b[2~", Key.InsertChar };
yield return new object [] { "\u001b[5~", Key.PageUp };
yield return new object [] { "\u001b[6~", Key.PageDown };
// Home, End with modifiers
yield return new object [] { "\u001b[1;2H", Key.Home.WithShift };
yield return new object [] { "\u001b[1;3H", Key.Home.WithAlt };
yield return new object [] { "\u001b[1;5F", Key.End.WithCtrl };
// Insert with modifiers
yield return new object [] { "\u001b[2;2~", Key.InsertChar.WithShift };
yield return new object [] { "\u001b[2;3~", Key.InsertChar.WithAlt };
yield return new object [] { "\u001b[2;5~", Key.InsertChar.WithCtrl };
// PageUp/PageDown with modifiers
yield return new object [] { "\u001b[5;2~", Key.PageUp.WithShift };
yield return new object [] { "\u001b[6;3~", Key.PageDown.WithAlt };
yield return new object [] { "\u001b[6;5~", Key.PageDown.WithCtrl };
// Function keys F1-F4 (common ESC O sequences)
yield return new object [] { "\u001bOP", Key.F1 };
yield return new object [] { "\u001bOQ", Key.F2 };
yield return new object [] { "\u001bOR", Key.F3 };
yield return new object [] { "\u001bOS", Key.F4 };
// Extended function keys F1-F12 with CSI sequences
yield return new object [] { "\u001b[11~", Key.F1 };
yield return new object [] { "\u001b[12~", Key.F2 };
yield return new object [] { "\u001b[13~", Key.F3 };
yield return new object [] { "\u001b[14~", Key.F4 };
yield return new object [] { "\u001b[15~", Key.F5 };
yield return new object [] { "\u001b[17~", Key.F6 };
yield return new object [] { "\u001b[18~", Key.F7 };
yield return new object [] { "\u001b[19~", Key.F8 };
yield return new object [] { "\u001b[20~", Key.F9 };
yield return new object [] { "\u001b[21~", Key.F10 };
yield return new object [] { "\u001b[23~", Key.F11 };
yield return new object [] { "\u001b[24~", Key.F12 };
// Function keys with modifiers
/* Not currently supported
yield return new object [] { "\u001b[1;2P", Key.F1.WithShift };
yield return new object [] { "\u001b[1;3Q", Key.F2.WithAlt };
yield return new object [] { "\u001b[1;5R", Key.F3.WithCtrl };
*/
}
// Consolidated test for all keyboard events (e.g., arrow keys)