mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* 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:
@@ -10,6 +10,7 @@ public class AnsiKeyboardParser
|
||||
{
|
||||
new Ss3Pattern (),
|
||||
new CsiKeyPattern (),
|
||||
new CsiCursorPattern(),
|
||||
new EscAsAltPattern { IsLastMinute = true }
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user