AnsiResponseParser.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. internal class AnsiResponseParser
  4. {
  5. private readonly StringBuilder held = new ();
  6. private string? currentTerminator;
  7. private Action<string>? currentResponse;
  8. private readonly List<Func<string, bool>> _ignorers = new ();
  9. // Enum to manage the parser's state
  10. private enum ParserState
  11. {
  12. Normal,
  13. ExpectingBracket,
  14. InResponse
  15. }
  16. // Current state of the parser
  17. private ParserState currentState = ParserState.Normal;
  18. private HashSet<string> _knownTerminators = new HashSet<string> ();
  19. /*
  20. * ANSI Input Sequences
  21. *
  22. * \x1B[A // Up Arrow key pressed
  23. * \x1B[B // Down Arrow key pressed
  24. * \x1B[C // Right Arrow key pressed
  25. * \x1B[D // Left Arrow key pressed
  26. * \x1B[3~ // Delete key pressed
  27. * \x1B[2~ // Insert key pressed
  28. * \x1B[5~ // Page Up key pressed
  29. * \x1B[6~ // Page Down key pressed
  30. * \x1B[1;5D // Ctrl + Left Arrow
  31. * \x1B[1;5C // Ctrl + Right Arrow
  32. * \x1B[0;10;20M // Mouse button pressed at position (10, 20)
  33. * \x1B[0c // Device Attributes Response (e.g., terminal identification)
  34. */
  35. public AnsiResponseParser ()
  36. {
  37. // These all are valid terminators on ansi responses,
  38. // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
  39. _knownTerminators.Add ("@");
  40. _knownTerminators.Add ("A");
  41. _knownTerminators.Add ("B");
  42. _knownTerminators.Add ("C");
  43. _knownTerminators.Add ("D");
  44. _knownTerminators.Add ("E");
  45. _knownTerminators.Add ("F");
  46. _knownTerminators.Add ("G");
  47. _knownTerminators.Add ("G");
  48. _knownTerminators.Add ("H");
  49. _knownTerminators.Add ("I");
  50. _knownTerminators.Add ("J");
  51. _knownTerminators.Add ("K");
  52. _knownTerminators.Add ("L");
  53. _knownTerminators.Add ("M");
  54. // No - N or O
  55. _knownTerminators.Add ("P");
  56. _knownTerminators.Add ("Q");
  57. _knownTerminators.Add ("R");
  58. _knownTerminators.Add ("S");
  59. _knownTerminators.Add ("T");
  60. _knownTerminators.Add ("W");
  61. _knownTerminators.Add ("X");
  62. _knownTerminators.Add ("Z");
  63. _knownTerminators.Add ("^");
  64. _knownTerminators.Add ("`");
  65. _knownTerminators.Add ("~");
  66. _knownTerminators.Add ("a");
  67. _knownTerminators.Add ("b");
  68. _knownTerminators.Add ("c");
  69. _knownTerminators.Add ("d");
  70. _knownTerminators.Add ("e");
  71. _knownTerminators.Add ("f");
  72. _knownTerminators.Add ("g");
  73. _knownTerminators.Add ("h");
  74. _knownTerminators.Add ("i");
  75. _knownTerminators.Add ("l");
  76. _knownTerminators.Add ("m");
  77. _knownTerminators.Add ("n");
  78. _knownTerminators.Add ("p");
  79. _knownTerminators.Add ("q");
  80. _knownTerminators.Add ("r");
  81. _knownTerminators.Add ("s");
  82. _knownTerminators.Add ("t");
  83. _knownTerminators.Add ("u");
  84. _knownTerminators.Add ("v");
  85. _knownTerminators.Add ("w");
  86. _knownTerminators.Add ("x");
  87. _knownTerminators.Add ("y");
  88. _knownTerminators.Add ("z");
  89. // Add more common ANSI sequences to be ignored
  90. _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event
  91. // Add more if necessary
  92. }
  93. /// <summary>
  94. /// Processes input which may be a single character or multiple.
  95. /// Returns what should be passed on to any downstream input processing
  96. /// (i.e., removes expected ANSI responses from the input stream).
  97. /// </summary>
  98. public string ProcessInput (string input)
  99. {
  100. var output = new StringBuilder (); // Holds characters that should pass through
  101. var index = 0; // Tracks position in the input string
  102. while (index < input.Length)
  103. {
  104. char currentChar = input [index];
  105. switch (currentState)
  106. {
  107. case ParserState.Normal:
  108. if (currentChar == '\x1B')
  109. {
  110. // Escape character detected, move to ExpectingBracket state
  111. currentState = ParserState.ExpectingBracket;
  112. held.Append (currentChar); // Hold the escape character
  113. index++;
  114. }
  115. else
  116. {
  117. // Normal character, append to output
  118. output.Append (currentChar);
  119. index++;
  120. }
  121. break;
  122. case ParserState.ExpectingBracket:
  123. if (currentChar == '[' )
  124. {
  125. // Detected '[' , transition to InResponse state
  126. currentState = ParserState.InResponse;
  127. held.Append (currentChar); // Hold the '['
  128. index++;
  129. }
  130. else
  131. {
  132. // Invalid sequence, release held characters and reset to Normal
  133. output.Append (held.ToString ());
  134. output.Append (currentChar); // Add current character
  135. ResetState ();
  136. index++;
  137. }
  138. break;
  139. case ParserState.InResponse:
  140. held.Append (currentChar);
  141. // Check if the held content should be released
  142. string handled = HandleHeldContent ();
  143. if (!string.IsNullOrEmpty (handled))
  144. {
  145. output.Append (handled);
  146. ResetState (); // Exit response mode and reset
  147. }
  148. index++;
  149. break;
  150. }
  151. }
  152. return output.ToString (); // Return all characters that passed through
  153. }
  154. /// <summary>
  155. /// Resets the parser's state when a response is handled or finished.
  156. /// </summary>
  157. private void ResetState ()
  158. {
  159. currentState = ParserState.Normal;
  160. held.Clear ();
  161. }
  162. /// <summary>
  163. /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected
  164. /// response.
  165. /// </summary>
  166. private string HandleHeldContent ()
  167. {
  168. var cur = held.ToString ();
  169. // If we're expecting a specific terminator, check if the content matches
  170. if (currentTerminator != null && cur.EndsWith (currentTerminator))
  171. {
  172. DispatchResponse ();
  173. return string.Empty;
  174. }
  175. if (_knownTerminators.Any (cur.EndsWith) && cur.StartsWith (EscSeqUtils.CSI))
  176. {
  177. // Detected a response that we were not expecting
  178. return held.ToString ();
  179. }
  180. // Handle common ANSI sequences (such as mouse input or arrow keys)
  181. if (_ignorers.Any (m => m.Invoke (held.ToString ())))
  182. {
  183. // Detected mouse input, release it without triggering the delegate
  184. return held.ToString ();
  185. }
  186. // Add more cases here for other standard sequences (like arrow keys, function keys, etc.)
  187. // If no match, continue accumulating characters
  188. return string.Empty;
  189. }
  190. private void DispatchResponse ()
  191. {
  192. // If it matches the expected response, invoke the callback and return nothing for output
  193. currentResponse?.Invoke (held.ToString ());
  194. currentResponse = null;
  195. currentTerminator = null;
  196. ResetState ();
  197. }
  198. /// <summary>
  199. /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is
  200. /// completed.
  201. /// </summary>
  202. public void ExpectResponse (string terminator, Action<string> response)
  203. {
  204. currentTerminator = terminator;
  205. currentResponse = response;
  206. }
  207. }