AnsiResponseParser.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Text;
  4. namespace Terminal.Gui;
  5. class AnsiResponseParser
  6. {
  7. private bool inResponse = false;
  8. private StringBuilder held = new StringBuilder ();
  9. private string? currentTerminator = null;
  10. private Action<string>? currentResponse = null;
  11. private List<Func<string, bool>> _ignorers = new ();
  12. // Enum to manage the parser's state
  13. private enum ParserState
  14. {
  15. Normal,
  16. ExpectingBracket,
  17. InResponse
  18. }
  19. // Current state of the parser
  20. private ParserState currentState = ParserState.Normal;
  21. /*
  22. * ANSI Input Sequences
  23. *
  24. * \x1B[A // Up Arrow key pressed
  25. * \x1B[B // Down Arrow key pressed
  26. * \x1B[C // Right Arrow key pressed
  27. * \x1B[D // Left Arrow key pressed
  28. * \x1B[3~ // Delete key pressed
  29. * \x1B[2~ // Insert key pressed
  30. * \x1B[5~ // Page Up key pressed
  31. * \x1B[6~ // Page Down key pressed
  32. * \x1B[1;5D // Ctrl + Left Arrow
  33. * \x1B[1;5C // Ctrl + Right Arrow
  34. * \x1B[0;10;20M // Mouse button pressed at position (10, 20)
  35. * \x1B[0c // Device Attributes Response (e.g., terminal identification)
  36. */
  37. public AnsiResponseParser ()
  38. {
  39. // Add more common ANSI sequences to be ignored
  40. _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event
  41. _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("A")); // Up arrow
  42. _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("B")); // Down arrow
  43. _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("C")); // Right arrow
  44. _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("D")); // Left arrow
  45. _ignorers.Add (s => s.StartsWith ("\x1B[3~")); // Delete
  46. _ignorers.Add (s => s.StartsWith ("\x1B[5~")); // Page Up
  47. _ignorers.Add (s => s.StartsWith ("\x1B[6~")); // Page Down
  48. _ignorers.Add (s => s.StartsWith ("\x1B[2~")); // Insert
  49. // Add more if necessary
  50. }
  51. /// <summary>
  52. /// Processes input which may be a single character or multiple.
  53. /// Returns what should be passed on to any downstream input processing
  54. /// (i.e., removes expected ANSI responses from the input stream).
  55. /// </summary>
  56. public string ProcessInput (string input)
  57. {
  58. StringBuilder output = new StringBuilder (); // Holds characters that should pass through
  59. int index = 0; // Tracks position in the input string
  60. while (index < input.Length)
  61. {
  62. char currentChar = input [index];
  63. switch (currentState)
  64. {
  65. case ParserState.Normal:
  66. if (currentChar == '\x1B')
  67. {
  68. // Escape character detected, move to ExpectingBracket state
  69. currentState = ParserState.ExpectingBracket;
  70. held.Append (currentChar); // Hold the escape character
  71. index++;
  72. }
  73. else
  74. {
  75. // Normal character, append to output
  76. output.Append (currentChar);
  77. index++;
  78. }
  79. break;
  80. case ParserState.ExpectingBracket:
  81. if (currentChar == '[' || currentChar == ']')
  82. {
  83. // Detected '[' or ']', transition to InResponse state
  84. currentState = ParserState.InResponse;
  85. held.Append (currentChar); // Hold the '[' or ']'
  86. index++;
  87. }
  88. else
  89. {
  90. // Invalid sequence, release held characters and reset to Normal
  91. output.Append (held.ToString ());
  92. output.Append (currentChar); // Add current character
  93. ResetState ();
  94. index++;
  95. }
  96. break;
  97. case ParserState.InResponse:
  98. held.Append (currentChar);
  99. // Check if the held content should be released
  100. var handled = HandleHeldContent ();
  101. if (!string.IsNullOrEmpty (handled))
  102. {
  103. output.Append (handled);
  104. ResetState (); // Exit response mode and reset
  105. }
  106. index++;
  107. break;
  108. }
  109. }
  110. return output.ToString (); // Return all characters that passed through
  111. }
  112. /// <summary>
  113. /// Resets the parser's state when a response is handled or finished.
  114. /// </summary>
  115. private void ResetState ()
  116. {
  117. currentState = ParserState.Normal;
  118. held.Clear ();
  119. currentTerminator = null;
  120. currentResponse = null;
  121. }
  122. /// <summary>
  123. /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected response.
  124. /// </summary>
  125. private string HandleHeldContent ()
  126. {
  127. // If we're expecting a specific terminator, check if the content matches
  128. if (currentTerminator != null && held.ToString ().EndsWith (currentTerminator))
  129. {
  130. // If it matches the expected response, invoke the callback and return nothing for output
  131. currentResponse?.Invoke (held.ToString ());
  132. return string.Empty;
  133. }
  134. // Handle common ANSI sequences (such as mouse input or arrow keys)
  135. if (_ignorers.Any(m=>m.Invoke (held.ToString())))
  136. {
  137. // Detected mouse input, release it without triggering the delegate
  138. return held.ToString ();
  139. }
  140. // Add more cases here for other standard sequences (like arrow keys, function keys, etc.)
  141. // If no match, continue accumulating characters
  142. return string.Empty;
  143. }
  144. /// <summary>
  145. /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed.
  146. /// </summary>
  147. public void ExpectResponse (string terminator, Action<string> response)
  148. {
  149. currentTerminator = terminator;
  150. currentResponse = response;
  151. }
  152. }