Skip to content

Commit 1285912

Browse files
tznindtig
andauthored
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>
1 parent 2b100c3 commit 1285912

File tree

4 files changed

+177
-58
lines changed

4 files changed

+177
-58
lines changed

Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class AnsiKeyboardParser
1010
{
1111
new Ss3Pattern (),
1212
new CsiKeyPattern (),
13+
new CsiCursorPattern(),
1314
new EscAsAltPattern { IsLastMinute = true }
1415
};
1516

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#nullable enable
2+
using System.Text.RegularExpressions;
3+
4+
namespace Terminal.Gui;
5+
6+
/// <summary>
7+
/// Detects ansi escape sequences in strings that have been read from
8+
/// the terminal (see <see cref="IAnsiResponseParser"/>).
9+
/// Handles navigation CSI key parsing such as <c>\x1b[A</c> (Cursor up)
10+
/// and <c>\x1b[1;5A</c> (Cursor up with Ctrl)
11+
/// </summary>
12+
public class CsiCursorPattern : AnsiKeyboardParserPattern
13+
{
14+
private readonly Regex _pattern = new (@"^\u001b\[(?:1;(\d+))?([A-DHF])$");
15+
16+
private readonly Dictionary<char, Key> _cursorMap = new ()
17+
{
18+
{ 'A', Key.CursorUp },
19+
{ 'B', Key.CursorDown },
20+
{ 'C', Key.CursorRight },
21+
{ 'D', Key.CursorLeft },
22+
{ 'H', Key.Home },
23+
{ 'F', Key.End }
24+
};
25+
26+
/// <inheritdoc/>
27+
public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
28+
29+
/// <summary>
30+
/// Called by the base class to determine the key that matches the input.
31+
/// </summary>
32+
/// <param name="input"></param>
33+
/// <returns></returns>
34+
protected override Key? GetKeyImpl (string? input)
35+
{
36+
Match match = _pattern.Match (input!);
37+
38+
if (!match.Success)
39+
{
40+
return null;
41+
}
42+
43+
string modifierGroup = match.Groups [1].Value;
44+
char terminator = match.Groups [2].Value [0];
45+
46+
if (!_cursorMap.TryGetValue (terminator, out Key? key))
47+
{
48+
return null;
49+
}
50+
51+
if (string.IsNullOrEmpty (modifierGroup))
52+
{
53+
return key;
54+
}
55+
56+
if (int.TryParse (modifierGroup, out int modifier))
57+
{
58+
key = modifier switch
59+
{
60+
2 => key.WithShift,
61+
3 => key.WithAlt,
62+
4 => key.WithAlt.WithShift,
63+
5 => key.WithCtrl,
64+
6 => key.WithCtrl.WithShift,
65+
7 => key.WithCtrl.WithAlt,
66+
8 => key.WithCtrl.WithAlt.WithShift,
67+
_ => key
68+
};
69+
}
70+
71+
return key;
72+
}
73+
}

Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,39 @@ namespace Terminal.Gui;
55

66
/// <summary>
77
/// Detects ansi escape sequences in strings that have been read from
8-
/// the terminal (see <see cref="IAnsiResponseParser"/>). This pattern
9-
/// handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up
8+
/// the terminal (see <see cref="IAnsiResponseParser"/>).
9+
/// Handles CSI key parsing such as <c>\x1b[3;5~</c> (Delete with Ctrl)
1010
/// </summary>
1111
public class CsiKeyPattern : AnsiKeyboardParserPattern
1212
{
13-
private readonly Dictionary<string, Key> _terminators = new()
13+
private readonly Regex _pattern = new (@"^\u001b\[(\d+)(?:;(\d+))?~$");
14+
15+
private readonly Dictionary<int, Key> _keyCodeMap = new ()
1416
{
15-
{ "A", Key.CursorUp },
16-
{ "B", Key.CursorDown },
17-
{ "C", Key.CursorRight },
18-
{ "D", Key.CursorLeft },
19-
{ "H", Key.Home }, // Home (older variant)
20-
{ "F", Key.End }, // End (older variant)
21-
{ "1~", Key.Home }, // Home (modern variant)
22-
{ "4~", Key.End }, // End (modern variant)
23-
{ "5~", Key.PageUp },
24-
{ "6~", Key.PageDown },
25-
{ "2~", Key.InsertChar },
26-
{ "3~", Key.Delete },
27-
{ "11~", Key.F1 },
28-
{ "12~", Key.F2 },
29-
{ "13~", Key.F3 },
30-
{ "14~", Key.F4 },
31-
{ "15~", Key.F5 },
32-
{ "17~", Key.F6 },
33-
{ "18~", Key.F7 },
34-
{ "19~", Key.F8 },
35-
{ "20~", Key.F9 },
36-
{ "21~", Key.F10 },
37-
{ "23~", Key.F11 },
38-
{ "24~", Key.F12 }
17+
{ 1, Key.Home }, // Home (modern variant)
18+
{ 4, Key.End }, // End (modern variant)
19+
{ 5, Key.PageUp },
20+
{ 6, Key.PageDown },
21+
{ 2, Key.InsertChar },
22+
{ 3, Key.Delete },
23+
{ 11, Key.F1 },
24+
{ 12, Key.F2 },
25+
{ 13, Key.F3 },
26+
{ 14, Key.F4 },
27+
{ 15, Key.F5 },
28+
{ 17, Key.F6 },
29+
{ 18, Key.F7 },
30+
{ 19, Key.F8 },
31+
{ 20, Key.F9 },
32+
{ 21, Key.F10 },
33+
{ 23, Key.F11 },
34+
{ 24, Key.F12 }
3935
};
4036

41-
private readonly Regex _pattern;
42-
4337
/// <inheritdoc/>
4438
public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
4539

46-
/// <summary>
47-
/// Creates a new instance of the <see cref="CsiKeyPattern"/> class.
48-
/// </summary>
49-
public CsiKeyPattern ()
50-
{
51-
var terms = new string (_terminators.Select (k => k.Key [0]).Where (k => !char.IsDigit (k)).ToArray ());
52-
_pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$");
53-
}
54-
55-
/// <summary>
56-
/// Called by the base class to determine the key that matches the input.
57-
/// </summary>
58-
/// <param name="input"></param>
59-
/// <returns></returns>
40+
/// <inheritdoc/>
6041
protected override Key? GetKeyImpl (string? input)
6142
{
6243
Match match = _pattern.Match (input!);
@@ -66,26 +47,37 @@ public CsiKeyPattern ()
6647
return null;
6748
}
6849

69-
string terminator = match.Groups [3].Value;
70-
string modifierGroup = match.Groups [2].Value;
50+
// Group 1: Key code (e.g. 3, 17, etc.)
51+
// Group 2: Optional modifier code (e.g. 2 = Shift, 5 = Ctrl)
52+
53+
if (!int.TryParse (match.Groups [1].Value, out int keyCode))
54+
{
55+
return null;
56+
}
7157

72-
Key? key = _terminators.GetValueOrDefault (terminator);
58+
if (!_keyCodeMap.TryGetValue (keyCode, out Key? key))
59+
{
60+
return null;
61+
}
7362

74-
if (key is {} && int.TryParse (modifierGroup, out int modifier))
63+
// If there's no modifier, just return the key.
64+
if (!int.TryParse (match.Groups [2].Value, out int modifier))
7565
{
76-
key = modifier switch
77-
{
78-
2 => key.WithShift,
79-
3 => key.WithAlt,
80-
4 => key.WithAlt.WithShift,
81-
5 => key.WithCtrl,
82-
6 => key.WithCtrl.WithShift,
83-
7 => key.WithCtrl.WithAlt,
84-
8 => key.WithCtrl.WithAlt.WithShift,
85-
_ => key
86-
};
66+
return key;
8767
}
8868

69+
key = modifier switch
70+
{
71+
2 => key.WithShift,
72+
3 => key.WithAlt,
73+
4 => key.WithAlt.WithShift,
74+
5 => key.WithCtrl,
75+
6 => key.WithCtrl.WithShift,
76+
7 => key.WithCtrl.WithAlt,
77+
8 => key.WithCtrl.WithAlt.WithShift,
78+
_ => key
79+
};
80+
8981
return key;
9082
}
9183
}

Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,59 @@ public class AnsiKeyboardParserTests
5252
yield return new object [] { "\u001b[1", null! };
5353
yield return new object [] { "\u001b[AB", null! };
5454
yield return new object [] { "\u001b[;A", null! };
55+
56+
57+
// Test data for various ANSI escape sequences and their expected Key values
58+
yield return new object [] { "\u001b[3;5~", Key.Delete.WithCtrl };
59+
60+
// Additional special keys
61+
yield return new object [] { "\u001b[H", Key.Home };
62+
yield return new object [] { "\u001b[F", Key.End };
63+
yield return new object [] { "\u001b[2~", Key.InsertChar };
64+
yield return new object [] { "\u001b[5~", Key.PageUp };
65+
yield return new object [] { "\u001b[6~", Key.PageDown };
66+
67+
// Home, End with modifiers
68+
yield return new object [] { "\u001b[1;2H", Key.Home.WithShift };
69+
yield return new object [] { "\u001b[1;3H", Key.Home.WithAlt };
70+
yield return new object [] { "\u001b[1;5F", Key.End.WithCtrl };
71+
72+
// Insert with modifiers
73+
yield return new object [] { "\u001b[2;2~", Key.InsertChar.WithShift };
74+
yield return new object [] { "\u001b[2;3~", Key.InsertChar.WithAlt };
75+
yield return new object [] { "\u001b[2;5~", Key.InsertChar.WithCtrl };
76+
77+
// PageUp/PageDown with modifiers
78+
yield return new object [] { "\u001b[5;2~", Key.PageUp.WithShift };
79+
yield return new object [] { "\u001b[6;3~", Key.PageDown.WithAlt };
80+
yield return new object [] { "\u001b[6;5~", Key.PageDown.WithCtrl };
81+
82+
// Function keys F1-F4 (common ESC O sequences)
83+
yield return new object [] { "\u001bOP", Key.F1 };
84+
yield return new object [] { "\u001bOQ", Key.F2 };
85+
yield return new object [] { "\u001bOR", Key.F3 };
86+
yield return new object [] { "\u001bOS", Key.F4 };
87+
88+
// Extended function keys F1-F12 with CSI sequences
89+
yield return new object [] { "\u001b[11~", Key.F1 };
90+
yield return new object [] { "\u001b[12~", Key.F2 };
91+
yield return new object [] { "\u001b[13~", Key.F3 };
92+
yield return new object [] { "\u001b[14~", Key.F4 };
93+
yield return new object [] { "\u001b[15~", Key.F5 };
94+
yield return new object [] { "\u001b[17~", Key.F6 };
95+
yield return new object [] { "\u001b[18~", Key.F7 };
96+
yield return new object [] { "\u001b[19~", Key.F8 };
97+
yield return new object [] { "\u001b[20~", Key.F9 };
98+
yield return new object [] { "\u001b[21~", Key.F10 };
99+
yield return new object [] { "\u001b[23~", Key.F11 };
100+
yield return new object [] { "\u001b[24~", Key.F12 };
101+
102+
// Function keys with modifiers
103+
/* Not currently supported
104+
yield return new object [] { "\u001b[1;2P", Key.F1.WithShift };
105+
yield return new object [] { "\u001b[1;3Q", Key.F2.WithAlt };
106+
yield return new object [] { "\u001b[1;5R", Key.F3.WithCtrl };
107+
*/
55108
}
56109

57110
// Consolidated test for all keyboard events (e.g., arrow keys)

0 commit comments

Comments
 (0)