AnsiEscapeSequenceRequest.cs 6.6 KB

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