diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
index e63762653..42aa368e7 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
@@ -10,6 +10,7 @@ public class AnsiKeyboardParser
{
new Ss3Pattern (),
new CsiKeyPattern (),
+ new CsiCursorPattern(),
new EscAsAltPattern { IsLastMinute = true }
};
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiCursorPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiCursorPattern.cs
new file mode 100644
index 000000000..9d0fac963
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiCursorPattern.cs
@@ -0,0 +1,73 @@
+#nullable enable
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+///
+/// Detects ansi escape sequences in strings that have been read from
+/// the terminal (see ).
+/// Handles navigation CSI key parsing such as \x1b[A (Cursor up)
+/// and \x1b[1;5A (Cursor up with Ctrl)
+///
+public class CsiCursorPattern : AnsiKeyboardParserPattern
+{
+ private readonly Regex _pattern = new (@"^\u001b\[(?:1;(\d+))?([A-DHF])$");
+
+ private readonly Dictionary _cursorMap = new ()
+ {
+ { 'A', Key.CursorUp },
+ { 'B', Key.CursorDown },
+ { 'C', Key.CursorRight },
+ { 'D', Key.CursorLeft },
+ { 'H', Key.Home },
+ { 'F', Key.End }
+ };
+
+ ///
+ public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
+
+ ///
+ /// Called by the base class to determine the key that matches the input.
+ ///
+ ///
+ ///
+ 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;
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
index dce820c8d..d70d42da1 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
@@ -5,58 +5,39 @@ namespace Terminal.Gui;
///
/// Detects ansi escape sequences in strings that have been read from
-/// the terminal (see ). This pattern
-/// handles keys that begin Esc[ e.g. Esc[A - cursor up
+/// the terminal (see ).
+/// Handles CSI key parsing such as \x1b[3;5~ (Delete with Ctrl)
///
public class CsiKeyPattern : AnsiKeyboardParserPattern
{
- private readonly Dictionary _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 _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 }
+ };
///
public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
- ///
- /// Creates a new instance of the class.
- ///
- 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+~)$");
- }
-
- ///
- /// Called by the base class to determine the key that matches the input.
- ///
- ///
- ///
+ ///
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;
}
}
diff --git a/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
index 0dcbda50b..d7bcedd18 100644
--- a/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
+++ b/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
@@ -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)