2
0

AnsiResponseParser.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. #nullable enable
  2. using Microsoft.Extensions.Logging;
  3. namespace Terminal.Gui.Drivers;
  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. // Logging.Trace($"Processing character '{currentChar}' (isEscape: {isEscape})");
  120. switch (State)
  121. {
  122. case AnsiResponseParserState.Normal:
  123. if (isEscape)
  124. {
  125. // Escape character detected, move to ExpectingBracket state
  126. State = AnsiResponseParserState.ExpectingEscapeSequence;
  127. _heldContent.AddToHeld (currentObj); // Hold the escape character
  128. }
  129. else
  130. {
  131. // Normal character, append to output
  132. appendOutput (currentObj);
  133. }
  134. break;
  135. case AnsiResponseParserState.ExpectingEscapeSequence:
  136. if (isEscape)
  137. {
  138. // Second escape so we must release first
  139. ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence);
  140. _heldContent.AddToHeld (currentObj); // Hold the new escape
  141. }
  142. else if (_heldContent.Length == 1)
  143. {
  144. //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1
  145. //We need any letter or digit for Alt+Letter (see EscAsAltPattern)
  146. //In fact lets just always see what comes after esc
  147. // Detected '[' or 'O', transition to InResponse state
  148. State = AnsiResponseParserState.InResponse;
  149. _heldContent.AddToHeld (currentObj); // Hold the letter
  150. }
  151. else
  152. {
  153. // Invalid sequence, release held characters and reset to Normal
  154. ReleaseHeld (appendOutput);
  155. appendOutput (currentObj); // Add current character
  156. }
  157. break;
  158. case AnsiResponseParserState.InResponse:
  159. // if seeing another esc, we must resolve the current one first
  160. if (isEscape)
  161. {
  162. ReleaseHeld (appendOutput);
  163. State = AnsiResponseParserState.ExpectingEscapeSequence;
  164. _heldContent.AddToHeld (currentObj);
  165. }
  166. else
  167. {
  168. // Non esc, so continue to build sequence
  169. _heldContent.AddToHeld (currentObj);
  170. // Check if the held content should be released
  171. if (ShouldReleaseHeldContent ())
  172. {
  173. ReleaseHeld (appendOutput);
  174. }
  175. }
  176. break;
  177. }
  178. index++;
  179. }
  180. }
  181. private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
  182. {
  183. TryLastMinuteSequences ();
  184. foreach (object o in _heldContent.HeldToObjects ())
  185. {
  186. appendOutput (o);
  187. }
  188. State = newState;
  189. _heldContent.ClearHeld ();
  190. }
  191. /// <summary>
  192. /// Checks current held chars against any sequences that have
  193. /// conflicts with longer sequences e.g. Esc as Alt sequences
  194. /// which can conflict if resolved earlier e.g. with EscOP ss3
  195. /// sequences.
  196. /// </summary>
  197. protected void TryLastMinuteSequences ()
  198. {
  199. lock (_lockState)
  200. {
  201. string? cur = _heldContent.HeldToString ();
  202. if (HandleKeyboard)
  203. {
  204. AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur, true);
  205. if (pattern != null)
  206. {
  207. RaiseKeyboardEvent (pattern, cur);
  208. _heldContent.ClearHeld ();
  209. return;
  210. }
  211. }
  212. // We have something totally unexpected, not a CSI and
  213. // still Esc+<something>. So give last minute swallow chance
  214. if (cur!.Length >= 2 && cur [0] == ESCAPE)
  215. {
  216. // Maybe swallow anyway if user has custom delegate
  217. bool swallow = ShouldSwallowUnexpectedResponse ();
  218. if (swallow)
  219. {
  220. _heldContent.ClearHeld ();
  221. //Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
  222. }
  223. }
  224. }
  225. }
  226. // Common response handler logic
  227. protected bool ShouldReleaseHeldContent ()
  228. {
  229. lock (_lockState)
  230. {
  231. string? cur = _heldContent.HeldToString ();
  232. if (HandleMouse && IsMouse (cur))
  233. {
  234. RaiseMouseEvent (cur);
  235. ResetState ();
  236. return false;
  237. }
  238. if (HandleKeyboard)
  239. {
  240. AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur);
  241. if (pattern != null)
  242. {
  243. RaiseKeyboardEvent (pattern, cur);
  244. ResetState ();
  245. return false;
  246. }
  247. }
  248. lock (_lockExpectedResponses)
  249. {
  250. // Look for an expected response for what is accumulated so far (since Esc)
  251. if (MatchResponse (
  252. cur,
  253. _expectedResponses,
  254. true,
  255. true))
  256. {
  257. return false;
  258. }
  259. // Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
  260. if (MatchResponse (
  261. cur,
  262. _lateResponses,
  263. false,
  264. true))
  265. {
  266. return false;
  267. }
  268. // Look for persistent requests
  269. if (MatchResponse (
  270. cur,
  271. _persistentExpectations,
  272. true,
  273. false))
  274. {
  275. return false;
  276. }
  277. }
  278. // Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity)
  279. // then we can release it back to input processing stream
  280. if (_knownTerminators.Contains (cur!.Last ()) && cur!.StartsWith (EscSeqUtils.CSI))
  281. {
  282. // We have found a terminator so bail
  283. State = AnsiResponseParserState.Normal;
  284. // Maybe swallow anyway if user has custom delegate
  285. bool swallow = ShouldSwallowUnexpectedResponse ();
  286. if (swallow)
  287. {
  288. _heldContent.ClearHeld ();
  289. //Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
  290. // Do not send back to input stream
  291. return false;
  292. }
  293. // Do release back to input stream
  294. return true;
  295. }
  296. }
  297. return false; // Continue accumulating
  298. }
  299. private void RaiseMouseEvent (string? cur)
  300. {
  301. MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur);
  302. if (ev != null)
  303. {
  304. Mouse?.Invoke (this, ev);
  305. }
  306. }
  307. private bool IsMouse (string? cur) { return _mouseParser.IsMouse (cur); }
  308. protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string? cur)
  309. {
  310. Key? k = pattern.GetKey (cur);
  311. if (k is null)
  312. {
  313. Logging.Logger.LogError ($"Failed to determine a Key for given Keyboard escape sequence '{cur}'");
  314. }
  315. else
  316. {
  317. Keyboard?.Invoke (this, k);
  318. }
  319. }
  320. /// <summary>
  321. /// <para>
  322. /// When overriden in a derived class, indicates whether the unexpected response
  323. /// currently in <see cref="_heldContent"/> should be released or swallowed.
  324. /// Use this to enable default event for escape codes.
  325. /// </para>
  326. /// <remarks>
  327. /// Note this is only called for complete responses.
  328. /// Based on <see cref="_knownTerminators"/>
  329. /// </remarks>
  330. /// </summary>
  331. /// <returns></returns>
  332. protected abstract bool ShouldSwallowUnexpectedResponse ();
  333. private bool MatchResponse (string? cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
  334. {
  335. // Check for expected responses
  336. AnsiResponseExpectation? matchingResponse = collection.FirstOrDefault (r => r.Matches (cur));
  337. if (matchingResponse?.Response != null)
  338. {
  339. //Logging.Trace ($"AnsiResponseParser processed '{cur}'");
  340. if (invokeCallback)
  341. {
  342. matchingResponse.Response.Invoke (_heldContent);
  343. }
  344. ResetState ();
  345. if (removeExpectation)
  346. {
  347. collection.Remove (matchingResponse);
  348. }
  349. return true;
  350. }
  351. return false;
  352. }
  353. /// <inheritdoc/>
  354. public void ExpectResponse (string? terminator, Action<string?> response, Action? abandoned, bool persistent)
  355. {
  356. lock (_lockExpectedResponses)
  357. {
  358. if (persistent)
  359. {
  360. _persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
  361. }
  362. else
  363. {
  364. _expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
  365. }
  366. }
  367. }
  368. /// <inheritdoc/>
  369. public bool IsExpecting (string? terminator)
  370. {
  371. lock (_lockExpectedResponses)
  372. {
  373. // If any of the new terminator matches any existing terminators characters it's a collision so true.
  374. return _expectedResponses.Any (r => r.Terminator!.Intersect (terminator!).Any ());
  375. }
  376. }
  377. /// <inheritdoc/>
  378. public void StopExpecting (string? terminator, bool persistent)
  379. {
  380. lock (_lockExpectedResponses)
  381. {
  382. if (persistent)
  383. {
  384. AnsiResponseExpectation [] removed = _persistentExpectations.Where (r => r.Matches (terminator)).ToArray ();
  385. foreach (AnsiResponseExpectation toRemove in removed)
  386. {
  387. _persistentExpectations.Remove (toRemove);
  388. toRemove.Abandoned?.Invoke ();
  389. }
  390. }
  391. else
  392. {
  393. AnsiResponseExpectation [] removed = _expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
  394. foreach (AnsiResponseExpectation r in removed)
  395. {
  396. _expectedResponses.Remove (r);
  397. _lateResponses.Add (r);
  398. r.Abandoned?.Invoke ();
  399. }
  400. }
  401. }
  402. }
  403. }
  404. internal class AnsiResponseParser<TInputRecord> () : AnsiResponseParserBase (new GenericHeld<TInputRecord> ())
  405. {
  406. /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
  407. public Func<IEnumerable<Tuple<char, TInputRecord>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
  408. public IEnumerable<Tuple<char, TInputRecord>> ProcessInput (params Tuple<char, TInputRecord> [] input)
  409. {
  410. List<Tuple<char, TInputRecord>> output = [];
  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, TInputRecord>> output, object c)
  419. {
  420. Tuple<char, TInputRecord> tuple = (Tuple<char, TInputRecord>)c;
  421. //Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
  422. output.Add (tuple);
  423. }
  424. public Tuple<char, TInputRecord> [] Release ()
  425. {
  426. // Lock in case Release is called from different Thread from parse
  427. lock (_lockState)
  428. {
  429. TryLastMinuteSequences ();
  430. Tuple<char, TInputRecord> [] result = HeldToEnumerable ().ToArray ();
  431. ResetState ();
  432. return result;
  433. }
  434. }
  435. private IEnumerable<Tuple<char, TInputRecord>> HeldToEnumerable () { return (IEnumerable<Tuple<char, TInputRecord>>)_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, TInputRecord>>> 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. }