AnsiResponseParser.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Runtime.ConstrainedExecution;
  4. namespace Terminal.Gui;
  5. public class AnsiRequestScheduler(IAnsiResponseParser parser)
  6. {
  7. public static int sent = 0;
  8. public List<AnsiEscapeSequenceRequest> Requsts = new ();
  9. private ConcurrentDictionary<string, DateTime> _lastSend = new ();
  10. private TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
  11. /// <summary>
  12. /// Sends the <paramref name="request"/> immediately or queues it if there is already
  13. /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
  14. /// </summary>
  15. /// <param name="request"></param>
  16. /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
  17. public bool SendOrSchedule (AnsiEscapeSequenceRequest request )
  18. {
  19. if (CanSend(request))
  20. {
  21. Send (request);
  22. return true;
  23. }
  24. else
  25. {
  26. Requsts.Add (request);
  27. return false;
  28. }
  29. }
  30. /// <summary>
  31. /// Identifies and runs any <see cref="Requsts"/> that can be sent based on the
  32. /// current outstanding requests of the parser.
  33. /// </summary>
  34. /// <returns><see langword="true"/> if a request was found and run. <see langword="false"/>
  35. /// if no outstanding requests or all have existing outstanding requests underway in parser.</returns>
  36. public bool RunSchedule ()
  37. {
  38. var opportunity = Requsts.FirstOrDefault (CanSend);
  39. if (opportunity != null)
  40. {
  41. Requsts.Remove (opportunity);
  42. Send (opportunity);
  43. return true;
  44. }
  45. return false;
  46. }
  47. private void Send (AnsiEscapeSequenceRequest r)
  48. {
  49. Interlocked.Increment(ref sent);
  50. _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now);
  51. parser.ExpectResponse (r.Terminator,r.ResponseReceived);
  52. r.Send ();
  53. }
  54. public bool CanSend (AnsiEscapeSequenceRequest r)
  55. {
  56. if (ShouldThrottle (r))
  57. {
  58. return false;
  59. }
  60. return !parser.IsExpecting (r.Terminator);
  61. }
  62. private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
  63. {
  64. if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
  65. {
  66. return DateTime.Now - value < _throttle;
  67. }
  68. return false;
  69. }
  70. }
  71. internal abstract class AnsiResponseParserBase : IAnsiResponseParser
  72. {
  73. protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
  74. private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
  75. // Current state of the parser
  76. public AnsiResponseParserState State
  77. {
  78. get => _state;
  79. protected set
  80. {
  81. StateChangedAt = DateTime.Now;
  82. _state = value;
  83. }
  84. }
  85. /// <summary>
  86. /// When <see cref="State"/> was last changed.
  87. /// </summary>
  88. public DateTime StateChangedAt { get; private set; } = DateTime.Now;
  89. protected readonly HashSet<char> _knownTerminators = new ();
  90. public AnsiResponseParserBase ()
  91. {
  92. // These all are valid terminators on ansi responses,
  93. // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
  94. _knownTerminators.Add ('@');
  95. _knownTerminators.Add ('A');
  96. _knownTerminators.Add ('B');
  97. _knownTerminators.Add ('C');
  98. _knownTerminators.Add ('D');
  99. _knownTerminators.Add ('E');
  100. _knownTerminators.Add ('F');
  101. _knownTerminators.Add ('G');
  102. _knownTerminators.Add ('G');
  103. _knownTerminators.Add ('H');
  104. _knownTerminators.Add ('I');
  105. _knownTerminators.Add ('J');
  106. _knownTerminators.Add ('K');
  107. _knownTerminators.Add ('L');
  108. _knownTerminators.Add ('M');
  109. // No - N or O
  110. _knownTerminators.Add ('P');
  111. _knownTerminators.Add ('Q');
  112. _knownTerminators.Add ('R');
  113. _knownTerminators.Add ('S');
  114. _knownTerminators.Add ('T');
  115. _knownTerminators.Add ('W');
  116. _knownTerminators.Add ('X');
  117. _knownTerminators.Add ('Z');
  118. _knownTerminators.Add ('^');
  119. _knownTerminators.Add ('`');
  120. _knownTerminators.Add ('~');
  121. _knownTerminators.Add ('a');
  122. _knownTerminators.Add ('b');
  123. _knownTerminators.Add ('c');
  124. _knownTerminators.Add ('d');
  125. _knownTerminators.Add ('e');
  126. _knownTerminators.Add ('f');
  127. _knownTerminators.Add ('g');
  128. _knownTerminators.Add ('h');
  129. _knownTerminators.Add ('i');
  130. _knownTerminators.Add ('l');
  131. _knownTerminators.Add ('m');
  132. _knownTerminators.Add ('n');
  133. _knownTerminators.Add ('p');
  134. _knownTerminators.Add ('q');
  135. _knownTerminators.Add ('r');
  136. _knownTerminators.Add ('s');
  137. _knownTerminators.Add ('t');
  138. _knownTerminators.Add ('u');
  139. _knownTerminators.Add ('v');
  140. _knownTerminators.Add ('w');
  141. _knownTerminators.Add ('x');
  142. _knownTerminators.Add ('y');
  143. _knownTerminators.Add ('z');
  144. }
  145. protected void ResetState ()
  146. {
  147. State = AnsiResponseParserState.Normal;
  148. ClearHeld ();
  149. }
  150. public abstract void ClearHeld ();
  151. protected abstract string HeldToString ();
  152. protected abstract IEnumerable<object> HeldToObjects ();
  153. protected abstract void AddToHeld (object o);
  154. /// <summary>
  155. /// Processes an input collection of objects <paramref name="inputLength"/> long.
  156. /// You must provide the indexers to return the objects and the action to append
  157. /// to output stream.
  158. /// </summary>
  159. /// <param name="getCharAtIndex">The character representation of element i of your input collection</param>
  160. /// <param name="getObjectAtIndex">The actual element in the collection (e.g. char or Tuple&lt;char,T&gt;)</param>
  161. /// <param name="appendOutput">
  162. /// Action to invoke when parser confirms an element of the current collection or a previous
  163. /// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder).
  164. /// </param>
  165. /// <param name="inputLength">The total number of elements in your collection</param>
  166. protected void ProcessInputBase (
  167. Func<int, char> getCharAtIndex,
  168. Func<int, object> getObjectAtIndex,
  169. Action<object> appendOutput,
  170. int inputLength
  171. )
  172. {
  173. var index = 0; // Tracks position in the input string
  174. while (index < inputLength)
  175. {
  176. char currentChar = getCharAtIndex (index);
  177. object currentObj = getObjectAtIndex (index);
  178. bool isEscape = currentChar == '\x1B';
  179. switch (State)
  180. {
  181. case AnsiResponseParserState.Normal:
  182. if (isEscape)
  183. {
  184. // Escape character detected, move to ExpectingBracket state
  185. State = AnsiResponseParserState.ExpectingBracket;
  186. AddToHeld (currentObj); // Hold the escape character
  187. }
  188. else
  189. {
  190. // Normal character, append to output
  191. appendOutput (currentObj);
  192. }
  193. break;
  194. case AnsiResponseParserState.ExpectingBracket:
  195. if (isEscape)
  196. {
  197. // Second escape so we must release first
  198. ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket);
  199. AddToHeld (currentObj); // Hold the new escape
  200. }
  201. else if (currentChar == '[')
  202. {
  203. // Detected '[', transition to InResponse state
  204. State = AnsiResponseParserState.InResponse;
  205. AddToHeld (currentObj); // Hold the '['
  206. }
  207. else
  208. {
  209. // Invalid sequence, release held characters and reset to Normal
  210. ReleaseHeld (appendOutput);
  211. appendOutput (currentObj); // Add current character
  212. }
  213. break;
  214. case AnsiResponseParserState.InResponse:
  215. AddToHeld (currentObj);
  216. // Check if the held content should be released
  217. if (ShouldReleaseHeldContent ())
  218. {
  219. ReleaseHeld (appendOutput);
  220. }
  221. break;
  222. }
  223. index++;
  224. }
  225. }
  226. private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
  227. {
  228. foreach (object o in HeldToObjects ())
  229. {
  230. appendOutput (o);
  231. }
  232. State = newState;
  233. ClearHeld ();
  234. }
  235. // Common response handler logic
  236. protected bool ShouldReleaseHeldContent ()
  237. {
  238. string cur = HeldToString ();
  239. // Check for expected responses
  240. (string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
  241. if (matchingResponse.response != null)
  242. {
  243. DispatchResponse (matchingResponse.response);
  244. expectedResponses.Remove (matchingResponse);
  245. return false;
  246. }
  247. if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
  248. {
  249. // Detected a response that was not expected
  250. return true;
  251. }
  252. return false; // Continue accumulating
  253. }
  254. protected void DispatchResponse (Action<string> response)
  255. {
  256. response?.Invoke (HeldToString ());
  257. ResetState ();
  258. }
  259. /// <summary>
  260. /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is
  261. /// completed.
  262. /// </summary>
  263. public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
  264. /// <inheritdoc />
  265. public bool IsExpecting (string requestTerminator)
  266. {
  267. // If any of the new terminator matches any existing terminators characters it's a collision so true.
  268. return expectedResponses.Any (r => r.terminator.Intersect (requestTerminator).Any());
  269. }
  270. }
  271. internal class AnsiResponseParser<T> : AnsiResponseParserBase
  272. {
  273. private readonly List<Tuple<char, T>> held = new ();
  274. public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
  275. {
  276. List<Tuple<char, T>> output = new List<Tuple<char, T>> ();
  277. ProcessInputBase (
  278. i => input [i].Item1,
  279. i => input [i],
  280. c => output.Add ((Tuple<char, T>)c),
  281. input.Length);
  282. return output;
  283. }
  284. public IEnumerable<Tuple<char, T>> Release ()
  285. {
  286. foreach (Tuple<char, T> h in held.ToArray ())
  287. {
  288. yield return h;
  289. }
  290. ResetState ();
  291. }
  292. public override void ClearHeld () { held.Clear (); }
  293. protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
  294. protected override IEnumerable<object> HeldToObjects () { return held; }
  295. protected override void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
  296. }
  297. internal class AnsiResponseParser : AnsiResponseParserBase
  298. {
  299. private readonly StringBuilder held = new ();
  300. public string ProcessInput (string input)
  301. {
  302. var output = new StringBuilder ();
  303. ProcessInputBase (
  304. i => input [i],
  305. i => input [i], // For string there is no T so object is same as char
  306. c => output.Append ((char)c),
  307. input.Length);
  308. return output.ToString ();
  309. }
  310. public string Release ()
  311. {
  312. var output = held.ToString ();
  313. ResetState ();
  314. return output;
  315. }
  316. public override void ClearHeld () { held.Clear (); }
  317. protected override string HeldToString () { return held.ToString (); }
  318. protected override IEnumerable<object> HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); }
  319. protected override void AddToHeld (object o) { held.Append ((char)o); }
  320. }
  321. /// <summary>
  322. /// Describes an ongoing ANSI request sent to the console.
  323. /// Use <see cref="ResponseReceived"/> to handle the response
  324. /// when console answers the request.
  325. /// </summary>
  326. public class AnsiEscapeSequenceRequest
  327. {
  328. /// <summary>
  329. /// Request to send e.g. see
  330. /// <see>
  331. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
  332. /// </see>
  333. /// </summary>
  334. public required string Request { get; init; }
  335. /// <summary>
  336. /// Invoked when the console responds with an ANSI response code that matches the
  337. /// <see cref="Terminator"/>
  338. /// </summary>
  339. public Action<string> ResponseReceived;
  340. /// <summary>
  341. /// <para>
  342. /// The terminator that uniquely identifies the type of response as responded
  343. /// by the console. e.g. for
  344. /// <see>
  345. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
  346. /// </see>
  347. /// the terminator is
  348. /// <see>
  349. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
  350. /// </see>
  351. /// .
  352. /// </para>
  353. /// <para>
  354. /// After sending a request, the first response with matching terminator will be matched
  355. /// to the oldest outstanding request.
  356. /// </para>
  357. /// </summary>
  358. public required string Terminator { get; init; }
  359. /// <summary>
  360. /// Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
  361. /// Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
  362. /// sending many requests.
  363. /// </summary>
  364. public void Send ()
  365. {
  366. Application.Driver?.RawWrite (Request);
  367. }
  368. }