Browse Source

Add AnsiEscapeSequenceRequest and AnsiEscapeSequenceResponse classes.

BDisp 10 months ago
parent
commit
c89a9c8dfb

+ 163 - 0
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs

@@ -0,0 +1,163 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes an ongoing ANSI request sent to the console.
+///     Use <see cref="ResponseReceived"/> to handle the response
+///     when console answers the request.
+/// </summary>
+public class AnsiEscapeSequenceRequest
+{
+    /// <summary>
+    ///     Execute an ANSI escape sequence escape which may return a response or error.
+    /// </summary>
+    /// <param name="ansiRequest">The ANSI escape sequence to request.</param>
+    /// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator and value.</returns>
+    public static AnsiEscapeSequenceResponse ExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
+    {
+        var response = new StringBuilder ();
+        var error = new StringBuilder ();
+        var savedIsReportingMouseMoves = false;
+
+        try
+        {
+            switch (Application.Driver)
+            {
+                case NetDriver netDriver:
+                    savedIsReportingMouseMoves = netDriver.IsReportingMouseMoves;
+
+                    if (savedIsReportingMouseMoves)
+                    {
+                        netDriver.StopReportingMouseMoves ();
+                    }
+
+                    break;
+                case CursesDriver cursesDriver:
+                    savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves;
+
+                    if (savedIsReportingMouseMoves)
+                    {
+                        cursesDriver.StopReportingMouseMoves ();
+                    }
+
+                    break;
+            }
+
+            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 netDriver:
+                        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 = response.ToString () [^1].ToString (), Value = values [0] };
+
+        // Invoke the event if it's subscribed
+        ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse);
+
+        return ansiResponse;
+    }
+
+    /// <summary>
+    ///     Request to send e.g. see
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///     </see>
+    /// </summary>
+    public required string Request { get; init; }
+
+    /// <summary>
+    ///     Invoked when the console responds with an ANSI response code that matches the
+    ///     <see cref="Terminator"/>
+    /// </summary>
+    public event EventHandler<AnsiEscapeSequenceResponse>? ResponseReceived;
+
+    /// <summary>
+    ///     <para>
+    ///         The terminator that uniquely identifies the type of response as responded
+    ///         by the console. e.g. for
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///         </see>
+    ///         the terminator is
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
+    ///         </see>
+    ///         .
+    ///     </para>
+    ///     <para>
+    ///         After sending a request, the first response with matching terminator will be matched
+    ///         to the oldest outstanding request.
+    ///     </para>
+    /// </summary>
+    public required string Terminator { get; init; }
+
+    /// <summary>
+    ///     The value expected in the response e.g.
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
+    ///     </see>
+    ///     which will have a 't' as terminator but also other different request may return the same terminator with a
+    ///     different value.
+    /// </summary>
+    public string? Value { get; init; }
+}

+ 53 - 0
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs

@@ -0,0 +1,53 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes a finished ANSI received from the console.
+/// </summary>
+public class AnsiEscapeSequenceResponse
+{
+    /// <summary>
+    ///     Error received from e.g. see
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///     </see>
+    /// </summary>
+    public required string Error { get; init; }
+
+    /// <summary>
+    ///     Response received from e.g. see
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///     </see>
+    ///     .
+    /// </summary>
+    public required string Response { get; init; }
+
+    /// <summary>
+    ///     <para>
+    ///         The terminator that uniquely identifies the type of response as responded
+    ///         by the console. e.g. for
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///         </see>
+    ///         the terminator is
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
+    ///         </see>
+    ///     </para>
+    ///     <para>
+    ///         The received terminator must match to the terminator sent by the request.
+    ///     </para>
+    /// </summary>
+    public required string Terminator { get; init; }
+
+    /// <summary>
+    ///     The value expected in the response e.g.
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
+    ///     </see>
+    ///     which will have a 't' as terminator but also other different request may return the same terminator with a
+    ///     different value.
+    /// </summary>
+    public string? Value { get; init; }
+}

+ 6 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -177,11 +177,15 @@ internal class CursesDriver : ConsoleDriver
         return true;
         return true;
     }
     }
 
 
+    public bool IsReportingMouseMoves { get; private set; }
+
     public void StartReportingMouseMoves ()
     public void StartReportingMouseMoves ()
     {
     {
         if (!RunningUnitTests)
         if (!RunningUnitTests)
         {
         {
             Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
             Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+
+            IsReportingMouseMoves = true;
         }
         }
     }
     }
 
 
@@ -190,6 +194,8 @@ internal class CursesDriver : ConsoleDriver
         if (!RunningUnitTests)
         if (!RunningUnitTests)
         {
         {
             Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
             Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+            IsReportingMouseMoves = false;
         }
         }
     }
     }
 
 

+ 9 - 110
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -304,91 +304,6 @@ public static class EscSeqUtils
     }
     }
 
 
     #nullable enable
     #nullable enable
-    /// <summary>
-    /// Execute an ANSI escape sequence escape which may return a response or error.
-    /// </summary>
-    /// <param name="ansiRequest">The ANSI escape sequence to request.</param>
-    /// <returns>A tuple with the response and error.</returns>
-    public static (string response, string error) ExecuteAnsiRequest (string ansiRequest)
-    {
-        var response = new StringBuilder ();
-        var error = new StringBuilder ();
-        var foundEscapeSequence = false;
-
-        try
-        {
-            switch (Application.Driver)
-            {
-                case NetDriver netDriver:
-                    netDriver.StopReportingMouseMoves ();
-
-                    break;
-                case CursesDriver cursesDriver:
-                    cursesDriver.StopReportingMouseMoves ();
-
-                    break;
-            }
-
-            Thread.Sleep (100); // Allow time for mouse stopping
-
-            // Flush the input buffer to avoid reading stale input
-            while (Console.KeyAvailable)
-            {
-                Console.ReadKey (true);
-            }
-
-            // Send the ANSI escape sequence
-            Console.Write (ansiRequest);
-            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 another \u001B is encountered
-            while (Console.KeyAvailable)
-            {
-                // Peek the next key
-                ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console
-
-                if (keyInfo.KeyChar == '\u001B') // Check if the key is Escape (ANSI escape sequence starts)
-                {
-                    if (foundEscapeSequence)
-                    {
-                        // If we already found one \u001B, break out of the loop when another is found
-                        break;
-                    }
-                    else
-                    {
-                        foundEscapeSequence = true; // Mark that we've encountered the first escape sequence
-                    }
-                }
-
-                // Append the current key to the response
-                response.Append (keyInfo.KeyChar);
-            }
-        }
-        catch (Exception ex)
-        {
-            error.AppendLine ($"Error executing ANSI request: {ex.Message}");
-        }
-        finally
-        {
-            switch (Application.Driver)
-            {
-                case NetDriver netDriver:
-                    netDriver.StartReportingMouseMoves ();
-
-                    break;
-                case CursesDriver cursesDriver:
-                    cursesDriver.StartReportingMouseMoves ();
-
-                    break;
-            }
-        }
-
-        return (response.ToString (), error.ToString ());
-    }
-
     /// <summary>
     /// <summary>
     ///     Gets the c1Control used in the called escape sequence.
     ///     Gets the c1Control used in the called escape sequence.
     /// </summary>
     /// </summary>
@@ -1399,15 +1314,11 @@ public static class EscSeqUtils
     #region Requests
     #region Requests
 
 
     /// <summary>
     /// <summary>
-    ///     ESC [ 6 n - Request Cursor Position Report (CPR)
-    ///     https://terminalguide.namepad.de/seq/csi_sn-6/
+    ///     ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
+    ///     https://terminalguide.namepad.de/seq/csi_sn__p-6/
+    ///     The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) ; 1 R
     /// </summary>
     /// </summary>
-    public static readonly string CSI_RequestCursorPositionReport = CSI + "6n";
-
-    /// <summary>
-    ///     The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
-    /// </summary>
-    public const string CSI_RequestCursorPositionReport_Terminator = "R";
+    public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
 
 
     /// <summary>
     /// <summary>
     ///     ESC [ 0 c - Send Device Attributes (Primary DA)
     ///     ESC [ 0 c - Send Device Attributes (Primary DA)
@@ -1426,37 +1337,25 @@ public static class EscSeqUtils
     ///     28 = Rectangular area operations
     ///     28 = Rectangular area operations
     ///     32 = Text macros
     ///     32 = Text macros
     ///     42 = ISO Latin-2 character set
     ///     42 = ISO Latin-2 character set
+    ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
+    ///     <see cref="CSI_SendDeviceAttributes2"/>
     /// </summary>
     /// </summary>
-    public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
+    public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
 
 
     /// <summary>
     /// <summary>
     ///     ESC [ > 0 c - Send Device Attributes (Secondary DA)
     ///     ESC [ > 0 c - Send Device Attributes (Secondary DA)
     ///     Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
     ///     Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
-    /// </summary>
-    public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
-
-    /// <summary>
     ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
     ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
     ///     <see cref="CSI_SendDeviceAttributes2"/>
     ///     <see cref="CSI_SendDeviceAttributes2"/>
     /// </summary>
     /// </summary>
-    public const string CSI_ReportDeviceAttributes_Terminator = "c";
+    public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
 
 
     /// <summary>
     /// <summary>
     ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
     ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
     ///     https://terminalguide.namepad.de/seq/csi_st-18/
     ///     https://terminalguide.namepad.de/seq/csi_st-18/
-    /// </summary>
-    public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
-
-    /// <summary>
     ///     The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
     ///     The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
     /// </summary>
     /// </summary>
-    public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
-
-    /// <summary>
-    ///     The value of the response to <see cref="CSI_ReportTerminalSizeInChars"/> indicating value 1 and 2 are the terminal
-    ///     size in chars.
-    /// </summary>
-    public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
+    public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
 
 
     #endregion
     #endregion
 }
 }

+ 44 - 42
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -591,52 +591,48 @@ internal class NetEvents : IDisposable
 
 
     private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
     private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
     {
     {
-        switch (terminating)
-        {
-            // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
-            case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
-                var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
-
-                if (_lastCursorPosition.Y != point.Y)
-                {
-                    _lastCursorPosition = point;
-                    var eventType = EventType.WindowPosition;
-                    var winPositionEv = new WindowPositionEvent { CursorPosition = point };
+        if (terminating ==
 
 
-                    _inputQueue.Enqueue (
-                                         new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
-                                        );
-                }
-                else
-                {
-                    return;
-                }
-
-                break;
-
-            case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
-                switch (values [0])
-                {
-                    case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
-                        EnqueueWindowSizeEvent (
-                                                Math.Max (int.Parse (values [1]), 0),
-                                                Math.Max (int.Parse (values [2]), 0),
-                                                Math.Max (int.Parse (values [1]), 0),
-                                                Math.Max (int.Parse (values [2]), 0)
-                                               );
-
-                        break;
-                    default:
-                        EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+            // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
+            // The observation is correct because the response isn't immediate and this is useless
+            EscSeqUtils.CSI_RequestCursorPositionReport.Terminator)
+        {
+            var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
 
 
-                        break;
-                }
+            if (_lastCursorPosition.Y != point.Y)
+            {
+                _lastCursorPosition = point;
+                var eventType = EventType.WindowPosition;
+                var winPositionEv = new WindowPositionEvent { CursorPosition = point };
 
 
-                break;
-            default:
+                _inputQueue.Enqueue (
+                                     new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
+                                    );
+            }
+            else
+            {
+                return;
+            }
+        }
+        else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator)
+        {
+            if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value)
+            {
+                EnqueueWindowSizeEvent (
+                                        Math.Max (int.Parse (values [1]), 0),
+                                        Math.Max (int.Parse (values [2]), 0),
+                                        Math.Max (int.Parse (values [1]), 0),
+                                        Math.Max (int.Parse (values [2]), 0)
+                                       );
+            }
+            else
+            {
                 EnqueueRequestResponseEvent (c1Control, code, values, terminating);
                 EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-
-                break;
+            }
+        }
+        else
+        {
+            EnqueueRequestResponseEvent (c1Control, code, values, terminating);
         }
         }
 
 
         _inputReady.Set ();
         _inputReady.Set ();
@@ -1377,11 +1373,15 @@ internal class NetDriver : ConsoleDriver
 
 
     #region Mouse Handling
     #region Mouse Handling
 
 
+    public bool IsReportingMouseMoves { get; private set; }
+
     public void StartReportingMouseMoves ()
     public void StartReportingMouseMoves ()
     {
     {
         if (!RunningUnitTests)
         if (!RunningUnitTests)
         {
         {
             Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
             Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+
+            IsReportingMouseMoves = true;
         }
         }
     }
     }
 
 
@@ -1390,6 +1390,8 @@ internal class NetDriver : ConsoleDriver
         if (!RunningUnitTests)
         if (!RunningUnitTests)
         {
         {
             Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
             Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+            IsReportingMouseMoves = false;
         }
         }
     }
     }