AnsiEscapeSequenceRequest.cs 5.8 KB

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