AnsiResponseParser.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. #nullable enable
  2. using Microsoft.Extensions.Logging;
  3. namespace Terminal.Gui;
  4. internal abstract class AnsiResponseParserBase : IAnsiResponseParser
  5. {
  6. private const char ESCAPE = '\x1B';
  7. private readonly AnsiMouseParser _mouseParser = new ();
  8. #pragma warning disable IDE1006 // Naming Styles
  9. protected readonly AnsiKeyboardParser _keyboardParser = new ();
  10. protected object _lockExpectedResponses = new ();
  11. protected object _lockState = new ();
  12. protected readonly IHeld _heldContent;
  13. /// <summary>
  14. /// Responses we are expecting to come in.
  15. /// </summary>
  16. protected readonly List<AnsiResponseExpectation> _expectedResponses = [];
  17. /// <summary>
  18. /// Collection of responses that we <see cref="StopExpecting"/>.
  19. /// </summary>
  20. protected readonly List<AnsiResponseExpectation> _lateResponses = [];
  21. /// <summary>
  22. /// Responses that you want to look out for that will come in continuously e.g. mouse events.
  23. /// Key is the terminator.
  24. /// </summary>
  25. protected readonly List<AnsiResponseExpectation> _persistentExpectations = [];
  26. #pragma warning restore IDE1006 // Naming Styles
  27. /// <summary>
  28. /// Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true
  29. /// </summary>
  30. public event EventHandler<MouseEventArgs>? Mouse;
  31. /// <summary>
  32. /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/>
  33. /// </summary>
  34. public event EventHandler<Key>? Keyboard;
  35. /// <summary>
  36. /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event.
  37. /// Defaults to <see langword="false"/>
  38. /// </summary>
  39. public bool HandleMouse { get; set; } = false;
  40. /// <summary>
  41. /// True to explicitly handle keyboard escape sequences (such as cursor keys) by passing them to <see cref="Keyboard"/>
  42. /// event
  43. /// </summary>
  44. public bool HandleKeyboard { get; set; } = false;
  45. private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
  46. /// <inheritdoc/>
  47. public AnsiResponseParserState State
  48. {
  49. get => _state;
  50. protected set
  51. {
  52. StateChangedAt = DateTime.Now;
  53. _state = value;
  54. }
  55. }
  56. /// <summary>
  57. /// When <see cref="State"/> was last changed.
  58. /// </summary>
  59. public DateTime StateChangedAt { get; private set; } = DateTime.Now;
  60. // These all are valid terminators on ansi responses,
  61. // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
  62. // No - N or O
  63. protected readonly HashSet<char> _knownTerminators =
  64. [
  65. '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
  66. // No - N or O
  67. 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z',
  68. '^', '`', '~',
  69. 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
  70. 'l', 'm', 'n',
  71. 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
  72. ];
  73. protected AnsiResponseParserBase (IHeld heldContent) { _heldContent = heldContent; }
  74. protected void ResetState ()
  75. {
  76. State = AnsiResponseParserState.Normal;
  77. lock (_lockState)
  78. {
  79. _heldContent.ClearHeld ();
  80. }
  81. }
  82. /// <summary>
  83. /// Processes an input collection of objects <paramref name="inputLength"/> long.
  84. /// You must provide the indexers to return the objects and the action to append
  85. /// to output stream.
  86. /// </summary>
  87. /// <param name="getCharAtIndex">The character representation of element i of your input collection</param>
  88. /// <param name="getObjectAtIndex">The actual element in the collection (e.g. char or Tuple&lt;char,T&gt;)</param>
  89. /// <param name="appendOutput">
  90. /// Action to invoke when parser confirms an element of the current collection or a previous
  91. /// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder).
  92. /// </param>
  93. /// <param name="inputLength">The total number of elements in your collection</param>
  94. protected void ProcessInputBase (
  95. Func<int, char> getCharAtIndex,
  96. Func<int, object> getObjectAtIndex,
  97. Action<object> appendOutput,
  98. int inputLength
  99. )
  100. {
  101. lock (_lockState)
  102. {
  103. ProcessInputBaseImpl (getCharAtIndex, getObjectAtIndex, appendOutput, inputLength);
  104. }
  105. }
  106. private void ProcessInputBaseImpl (
  107. Func<int, char> getCharAtIndex,
  108. Func<int, object> getObjectAtIndex,
  109. Action<object> appendOutput,
  110. int inputLength
  111. )
  112. {
  113. var index = 0; // Tracks position in the input string
  114. while (index < inputLength)
  115. {
  116. char currentChar = getCharAtIndex (index);
  117. object currentObj = getObjectAtIndex (index);
  118. bool isEscape = currentChar == ESCAPE;
  119. switch (State)
  120. {
  121. case AnsiResponseParserState.Normal:
  122. if (isEscape)
  123. {
  124. // Escape character detected, move to ExpectingBracket state
  125. State = AnsiResponseParserState.ExpectingEscapeSequence;
  126. _heldContent.AddToHeld (currentObj); // Hold the escape character
  127. }
  128. else
  129. {
  130. // Normal character, append to output
  131. appendOutput (currentObj);
  132. }
  133. break;
  134. case AnsiResponseParserState.ExpectingEscapeSequence:
  135. if (isEscape)
  136. {
  137. // Second escape so we must release first
  138. ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence);
  139. _heldContent.AddToHeld (currentObj); // Hold the new escape
  140. }
  141. else if (_heldContent.Length == 1)
  142. {
  143. //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1
  144. //We need any letter or digit for Alt+Letter (see EscAsAltPattern)
  145. //In fact lets just always see what comes after esc
  146. // Detected '[' or 'O', transition to InResponse state
  147. State = AnsiResponseParserState.InResponse;
  148. _heldContent.AddToHeld (currentObj); // Hold the letter
  149. }
  150. else
  151. {
  152. // Invalid sequence, release held characters and reset to Normal
  153. ReleaseHeld (appendOutput);
  154. appendOutput (currentObj); // Add current character
  155. }
  156. break;
  157. case AnsiResponseParserState.InResponse:
  158. // if seeing another esc, we must resolve the current one first
  159. if (isEscape)
  160. {
  161. ReleaseHeld (appendOutput);
  162. State = AnsiResponseParserState.ExpectingEscapeSequence;
  163. _heldContent.AddToHeld (currentObj);
  164. }
  165. else
  166. {
  167. // Non esc, so continue to build sequence
  168. _heldContent.AddToHeld (currentObj);
  169. // Check if the held content should be released
  170. if (ShouldReleaseHeldContent ())
  171. {
  172. ReleaseHeld (appendOutput);
  173. }
  174. }
  175. break;
  176. }
  177. index++;
  178. }
  179. }
  180. private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
  181. {
  182. TryLastMinuteSequences ();
  183. foreach (object o in _heldContent.HeldToObjects ())
  184. {
  185. appendOutput (o);
  186. }
  187. State = newState;
  188. _heldContent.ClearHeld ();
  189. }
  190. /// <summary>
  191. /// Checks current held chars against any sequences that have
  192. /// conflicts with longer sequences e.g. Esc as Alt sequences
  193. /// which can conflict if resolved earlier e.g. with EscOP ss3
  194. /// sequences.
  195. /// </summary>
  196. protected void TryLastMinuteSequences ()
  197. {
  198. lock (_lockState)
  199. {
  200. string? cur = _heldContent.HeldToString ();
  201. if (HandleKeyboard)
  202. {
  203. AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur, true);
  204. if (pattern != null)
  205. {
  206. RaiseKeyboardEvent (pattern, cur);
  207. _heldContent.ClearHeld ();
  208. return;
  209. }
  210. }
  211. // We have something totally unexpected, not a CSI and
  212. // still Esc+<something>. So give last minute swallow chance
  213. if (cur!.Length >= 2 && cur [0] == ESCAPE)
  214. {
  215. // Maybe swallow anyway if user has custom delegate
  216. bool swallow = ShouldSwallowUnexpectedResponse ();
  217. if (swallow)
  218. {
  219. _heldContent.ClearHeld ();
  220. Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
  221. }
  222. }
  223. }
  224. }
  225. // Common response handler logic
  226. protected bool ShouldReleaseHeldContent ()
  227. {
  228. lock (_lockState)
  229. {
  230. string? cur = _heldContent.HeldToString ();
  231. if (HandleMouse && IsMouse (cur))
  232. {
  233. RaiseMouseEvent (cur);
  234. ResetState ();
  235. return false;
  236. }
  237. if (HandleKeyboard)
  238. {
  239. AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur);
  240. if (pattern != null)
  241. {
  242. RaiseKeyboardEvent (pattern, cur);
  243. ResetState ();
  244. return false;
  245. }
  246. }
  247. lock (_lockExpectedResponses)
  248. {
  249. // Look for an expected response for what is accumulated so far (since Esc)
  250. if (MatchResponse (
  251. cur,
  252. _expectedResponses,
  253. true,
  254. true))
  255. {
  256. return false;
  257. }
  258. // Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
  259. if (MatchResponse (
  260. cur,
  261. _lateResponses,
  262. false,
  263. true))
  264. {
  265. return false;
  266. }
  267. // Look for persistent requests
  268. if (MatchResponse (
  269. cur,
  270. _persistentExpectations,
  271. true,
  272. false))
  273. {
  274. return false;
  275. }
  276. }
  277. // Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity)
  278. // then we can release it back to input processing stream
  279. if (_knownTerminators.Contains (cur!.Last ()) && cur!.StartsWith (EscSeqUtils.CSI))
  280. {
  281. // We have found a terminator so bail
  282. State = AnsiResponseParserState.Normal;
  283. // Maybe swallow anyway if user has custom delegate
  284. bool swallow = ShouldSwallowUnexpectedResponse ();
  285. if (swallow)
  286. {
  287. _heldContent.ClearHeld ();
  288. Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
  289. // Do not send back to input stream
  290. return false;
  291. }
  292. // Do release back to input stream
  293. return true;
  294. }
  295. }
  296. return false; // Continue accumulating
  297. }
  298. private void RaiseMouseEvent (string? cur)
  299. {
  300. MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur);
  301. if (ev != null)
  302. {
  303. Mouse?.Invoke (this, ev);
  304. }
  305. }
  306. private bool IsMouse (string? cur) { return _mouseParser.IsMouse (cur); }
  307. protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string? cur)
  308. {
  309. Key? k = pattern.GetKey (cur);
  310. if (k is null)
  311. {
  312. Logging.Logger.LogError ($"Failed to determine a Key for given Keyboard escape sequence '{cur}'");
  313. }
  314. else
  315. {
  316. Keyboard?.Invoke (this, k);
  317. }
  318. }
  319. /// <summary>
  320. /// <para>
  321. /// When overriden in a derived class, indicates whether the unexpected response
  322. /// currently in <see cref="_heldContent"/> should be released or swallowed.
  323. /// Use this to enable default event for escape codes.
  324. /// </para>
  325. /// <remarks>
  326. /// Note this is only called for complete responses.
  327. /// Based on <see cref="_knownTerminators"/>
  328. /// </remarks>
  329. /// </summary>
  330. /// <returns></returns>
  331. protected abstract bool ShouldSwallowUnexpectedResponse ();
  332. private bool MatchResponse (string? cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
  333. {
  334. // Check for expected responses
  335. AnsiResponseExpectation? matchingResponse = collection.FirstOrDefault (r => r.Matches (cur));
  336. if (matchingResponse?.Response != null)
  337. {
  338. Logging.Trace ($"AnsiResponseParser processed '{cur}'");
  339. if (invokeCallback)
  340. {
  341. matchingResponse.Response.Invoke (_heldContent);
  342. }
  343. ResetState ();
  344. if (removeExpectation)
  345. {
  346. collection.Remove (matchingResponse);
  347. }
  348. return true;
  349. }
  350. return false;
  351. }
  352. /// <inheritdoc/>
  353. public void ExpectResponse (string? terminator, Action<string?> response, Action? abandoned, bool persistent)
  354. {
  355. lock (_lockExpectedResponses)
  356. {
  357. if (persistent)
  358. {
  359. _persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
  360. }
  361. else
  362. {
  363. _expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
  364. }
  365. }
  366. }
  367. /// <inheritdoc/>
  368. public bool IsExpecting (string? terminator)
  369. {
  370. lock (_lockExpectedResponses)
  371. {
  372. // If any of the new terminator matches any existing terminators characters it's a collision so true.
  373. return _expectedResponses.Any (r => r.Terminator!.Intersect (terminator!).Any ());
  374. }
  375. }
  376. /// <inheritdoc/>
  377. public void StopExpecting (string? terminator, bool persistent)
  378. {
  379. lock (_lockExpectedResponses)
  380. {
  381. if (persistent)
  382. {
  383. AnsiResponseExpectation [] removed = _persistentExpectations.Where (r => r.Matches (terminator)).ToArray ();
  384. foreach (AnsiResponseExpectation toRemove in removed)
  385. {
  386. _persistentExpectations.Remove (toRemove);
  387. toRemove.Abandoned?.Invoke ();
  388. }
  389. }
  390. else
  391. {
  392. AnsiResponseExpectation [] removed = _expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
  393. foreach (AnsiResponseExpectation r in removed)
  394. {
  395. _expectedResponses.Remove (r);
  396. _lateResponses.Add (r);
  397. r.Abandoned?.Invoke ();
  398. }
  399. }
  400. }
  401. }
  402. }
  403. internal class AnsiResponseParser<T> : AnsiResponseParserBase
  404. {
  405. public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
  406. /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
  407. public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
  408. public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
  409. {
  410. List<Tuple<char, T>> output = new ();
  411. ProcessInputBase (
  412. i => input [i].Item1,
  413. i => input [i],
  414. c => AppendOutput (output, c),
  415. input.Length);
  416. return output;
  417. }
  418. private void AppendOutput (List<Tuple<char, T>> output, object c)
  419. {
  420. Tuple<char, T> tuple = (Tuple<char, T>)c;
  421. Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
  422. output.Add (tuple);
  423. }
  424. public Tuple<char, T> [] Release ()
  425. {
  426. // Lock in case Release is called from different Thread from parse
  427. lock (_lockState)
  428. {
  429. TryLastMinuteSequences ();
  430. Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
  431. ResetState ();
  432. return result;
  433. }
  434. }
  435. private IEnumerable<Tuple<char, T>> HeldToEnumerable () { return (IEnumerable<Tuple<char, T>>)_heldContent.HeldToObjects (); }
  436. /// <summary>
  437. /// 'Overload' for specifying an expectation that requires the metadata as well as characters. Has
  438. /// a unique name because otherwise most lamdas will give ambiguous overload errors.
  439. /// </summary>
  440. /// <param name="terminator"></param>
  441. /// <param name="response"></param>
  442. /// <param name="abandoned"></param>
  443. /// <param name="persistent"></param>
  444. public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
  445. {
  446. lock (_lockExpectedResponses)
  447. {
  448. if (persistent)
  449. {
  450. _persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
  451. }
  452. else
  453. {
  454. _expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
  455. }
  456. }
  457. }
  458. /// <inheritdoc/>
  459. protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); }
  460. }
  461. internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ())
  462. {
  463. /// <summary>
  464. /// <para>
  465. /// Delegate for handling unrecognized escape codes. Default behaviour
  466. /// is to return <see langword="false"/> which simply releases the
  467. /// characters back to input stream for downstream processing.
  468. /// </para>
  469. /// <para>
  470. /// Implement a method to handle if you want and return <see langword="true"/> if you want the
  471. /// keystrokes 'swallowed' (i.e. not returned to input stream).
  472. /// </para>
  473. /// </summary>
  474. public Func<string?, bool> UnknownResponseHandler { get; set; } = _ => false;
  475. public string ProcessInput (string input)
  476. {
  477. var output = new StringBuilder ();
  478. ProcessInputBase (
  479. i => input [i],
  480. i => input [i], // For string there is no T so object is same as char
  481. c => AppendOutput (output, (char)c),
  482. input.Length);
  483. return output.ToString ();
  484. }
  485. private void AppendOutput (StringBuilder output, char c)
  486. {
  487. Logging.Trace ($"AnsiResponseParser releasing '{c}'");
  488. output.Append (c);
  489. }
  490. public string? Release ()
  491. {
  492. lock (_lockState)
  493. {
  494. TryLastMinuteSequences ();
  495. string? output = _heldContent.HeldToString ();
  496. ResetState ();
  497. return output;
  498. }
  499. }
  500. /// <inheritdoc/>
  501. protected override bool ShouldSwallowUnexpectedResponse ()
  502. {
  503. lock (_lockState)
  504. {
  505. return UnknownResponseHandler.Invoke (_heldContent.HeldToString ());
  506. }
  507. }
  508. }