AnsiResponseParser.cs 8.0 KB

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