AnsiResponseParser.cs 20 KB

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