AnsiEscapeSequenceRequest.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Describes an ongoing ANSI request sent to the console.
  5. /// Use <see cref="ResponseReceived"/> to handle the response
  6. /// when console answers the request.
  7. /// </summary>
  8. public class AnsiEscapeSequenceRequest
  9. {
  10. /// <summary>
  11. /// Request to send e.g. see
  12. /// <see>
  13. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
  14. /// </see>
  15. /// </summary>
  16. public required string Request { get; init; }
  17. /// <summary>
  18. /// Invoked when the console responds with an ANSI response code that matches the
  19. /// <see cref="Terminator"/>
  20. /// </summary>
  21. public event EventHandler<AnsiEscapeSequenceResponse>? ResponseReceived;
  22. /// <summary>
  23. /// <para>
  24. /// The terminator that uniquely identifies the type of response as responded
  25. /// by the console. e.g. for
  26. /// <see>
  27. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
  28. /// </see>
  29. /// the terminator is
  30. /// <see>
  31. /// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
  32. /// </see>
  33. /// .
  34. /// </para>
  35. /// <para>
  36. /// After sending a request, the first response with matching terminator will be matched
  37. /// to the oldest outstanding request.
  38. /// </para>
  39. /// </summary>
  40. public required string Terminator { get; init; }
  41. /// <summary>
  42. /// Execute an ANSI escape sequence escape which may return a response or error.
  43. /// </summary>
  44. /// <param name="ansiRequest">The ANSI escape sequence to request.</param>
  45. /// <param name="result">
  46. /// When this method returns <see langword="true"/>, an object containing the response with an empty
  47. /// error.
  48. /// </param>
  49. /// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator and value.</returns>
  50. public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
  51. {
  52. var response = new StringBuilder ();
  53. var error = new StringBuilder ();
  54. var savedIsReportingMouseMoves = false;
  55. NetDriver? netDriver = null;
  56. try
  57. {
  58. switch (Application.Driver)
  59. {
  60. case NetDriver:
  61. netDriver = Application.Driver as NetDriver;
  62. savedIsReportingMouseMoves = netDriver!.IsReportingMouseMoves;
  63. if (savedIsReportingMouseMoves)
  64. {
  65. netDriver.StopReportingMouseMoves ();
  66. }
  67. while (Console.KeyAvailable)
  68. {
  69. netDriver._mainLoopDriver._netEvents._waitForStart.Set ();
  70. netDriver._mainLoopDriver._netEvents._waitForStart.Reset ();
  71. netDriver._mainLoopDriver._netEvents._forceRead = true;
  72. }
  73. netDriver._mainLoopDriver._netEvents._forceRead = false;
  74. break;
  75. case CursesDriver cursesDriver:
  76. savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves;
  77. if (savedIsReportingMouseMoves)
  78. {
  79. cursesDriver.StopReportingMouseMoves ();
  80. }
  81. break;
  82. }
  83. if (netDriver is { })
  84. {
  85. NetEvents._suspendRead = true;
  86. }
  87. else
  88. {
  89. Thread.Sleep (100); // Allow time for mouse stopping and to flush the input buffer
  90. // Flush the input buffer to avoid reading stale input
  91. while (Console.KeyAvailable)
  92. {
  93. Console.ReadKey (true);
  94. }
  95. }
  96. // Send the ANSI escape sequence
  97. Console.Write (ansiRequest.Request);
  98. Console.Out.Flush (); // Ensure the request is sent
  99. // Read the response from stdin (response should come back as input)
  100. Thread.Sleep (100); // Allow time for the terminal to respond
  101. // Read input until no more characters are available or the terminator is encountered
  102. while (Console.KeyAvailable)
  103. {
  104. // Peek the next key
  105. ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console
  106. // Append the current key to the response
  107. response.Append (keyInfo.KeyChar);
  108. if (keyInfo.KeyChar == ansiRequest.Terminator [^1]) // Check if the key is terminator (ANSI escape sequence ends)
  109. {
  110. // Break out of the loop when terminator is found
  111. break;
  112. }
  113. }
  114. if (!response.ToString ().EndsWith (ansiRequest.Terminator [^1]))
  115. {
  116. throw new InvalidOperationException ($"Terminator doesn't ends with: '{ansiRequest.Terminator [^1]}'");
  117. }
  118. }
  119. catch (Exception ex)
  120. {
  121. error.AppendLine ($"Error executing ANSI request: {ex.Message}");
  122. }
  123. finally
  124. {
  125. if (savedIsReportingMouseMoves)
  126. {
  127. switch (Application.Driver)
  128. {
  129. case NetDriver:
  130. NetEvents._suspendRead = false;
  131. netDriver!.StartReportingMouseMoves ();
  132. break;
  133. case CursesDriver cursesDriver:
  134. cursesDriver.StartReportingMouseMoves ();
  135. break;
  136. }
  137. }
  138. }
  139. var values = new string? [] { null };
  140. if (string.IsNullOrEmpty (error.ToString ()))
  141. {
  142. (string? c1Control, string? code, values, string? terminator) = EscSeqUtils.GetEscapeResult (response.ToString ().ToCharArray ());
  143. }
  144. AnsiEscapeSequenceResponse ansiResponse = new ()
  145. {
  146. Response = response.ToString (), Error = error.ToString (),
  147. Terminator = string.IsNullOrEmpty (response.ToString ()) ? "" : response.ToString () [^1].ToString (), Value = values [0]
  148. };
  149. // Invoke the event if it's subscribed
  150. ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse);
  151. result = ansiResponse;
  152. return string.IsNullOrWhiteSpace (result.Error) && !string.IsNullOrWhiteSpace (result.Response);
  153. }
  154. /// <summary>
  155. /// The value expected in the response e.g.
  156. /// <see>
  157. /// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
  158. /// </see>
  159. /// which will have a 't' as terminator but also other different request may return the same terminator with a
  160. /// different value.
  161. /// </summary>
  162. public string? Value { get; init; }
  163. }