#nullable enable namespace Terminal.Gui; /// /// Describes an ongoing ANSI request sent to the console. /// Use to handle the response /// when console answers the request. /// public class AnsiEscapeSequenceRequest { /// /// Request to send e.g. see /// /// EscSeqUtils.CSI_SendDeviceAttributes.Request /// /// public required string Request { get; init; } /// /// Invoked when the console responds with an ANSI response code that matches the /// /// public event EventHandler? ResponseReceived; /// /// /// The terminator that uniquely identifies the type of response as responded /// by the console. e.g. for /// /// EscSeqUtils.CSI_SendDeviceAttributes.Request /// /// the terminator is /// /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator /// /// . /// /// /// After sending a request, the first response with matching terminator will be matched /// to the oldest outstanding request. /// /// public required string Terminator { get; init; } /// /// Execute an ANSI escape sequence escape which may return a response or error. /// /// The ANSI escape sequence to request. /// /// When this method returns , an object containing the response with an empty /// error. /// /// A with the response, error, terminator and value. public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result) { var response = new StringBuilder (); var error = new StringBuilder (); var savedIsReportingMouseMoves = false; NetDriver? netDriver = null; try { switch (Application.Driver) { case NetDriver: netDriver = Application.Driver as NetDriver; savedIsReportingMouseMoves = netDriver!.IsReportingMouseMoves; if (savedIsReportingMouseMoves) { netDriver.StopReportingMouseMoves (); } while (Console.KeyAvailable) { netDriver._mainLoopDriver._netEvents._waitForStart.Set (); netDriver._mainLoopDriver._netEvents._waitForStart.Reset (); netDriver._mainLoopDriver._netEvents._forceRead = true; } netDriver._mainLoopDriver._netEvents._forceRead = false; break; case CursesDriver cursesDriver: savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves; if (savedIsReportingMouseMoves) { cursesDriver.StopReportingMouseMoves (); } break; } if (netDriver is { }) { NetEvents._suspendRead = true; } else { Thread.Sleep (100); // Allow time for mouse stopping and to flush the input buffer // Flush the input buffer to avoid reading stale input while (Console.KeyAvailable) { Console.ReadKey (true); } } // Send the ANSI escape sequence Console.Write (ansiRequest.Request); Console.Out.Flush (); // Ensure the request is sent // Read the response from stdin (response should come back as input) Thread.Sleep (100); // Allow time for the terminal to respond // Read input until no more characters are available or the terminator is encountered while (Console.KeyAvailable) { // Peek the next key ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console // Append the current key to the response response.Append (keyInfo.KeyChar); if (keyInfo.KeyChar == ansiRequest.Terminator [^1]) // Check if the key is terminator (ANSI escape sequence ends) { // Break out of the loop when terminator is found break; } } if (!response.ToString ().EndsWith (ansiRequest.Terminator [^1])) { throw new InvalidOperationException ($"Terminator doesn't ends with: '{ansiRequest.Terminator [^1]}'"); } } catch (Exception ex) { error.AppendLine ($"Error executing ANSI request: {ex.Message}"); } finally { if (savedIsReportingMouseMoves) { switch (Application.Driver) { case NetDriver: NetEvents._suspendRead = false; netDriver!.StartReportingMouseMoves (); break; case CursesDriver cursesDriver: cursesDriver.StartReportingMouseMoves (); break; } } } var values = new string? [] { null }; if (string.IsNullOrEmpty (error.ToString ())) { (string? c1Control, string? code, values, string? terminator) = EscSeqUtils.GetEscapeResult (response.ToString ().ToCharArray ()); } AnsiEscapeSequenceResponse ansiResponse = new () { Response = response.ToString (), Error = error.ToString (), Terminator = string.IsNullOrEmpty (response.ToString ()) ? "" : response.ToString () [^1].ToString (), Value = values [0] }; // Invoke the event if it's subscribed ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse); result = ansiResponse; return string.IsNullOrWhiteSpace (result.Error) && !string.IsNullOrWhiteSpace (result.Response); } /// /// The value expected in the response e.g. /// /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value /// /// which will have a 't' as terminator but also other different request may return the same terminator with a /// different value. /// public string? Value { get; init; } }