Browse Source

Support for persistent expectations of responses

tznind 9 months ago
parent
commit
eaddbc6c1d

+ 2 - 2
Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs

@@ -76,7 +76,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
         {
         {
             if (DateTime.Now - dt > _staleTimeout)
             if (DateTime.Now - dt > _staleTimeout)
             {
             {
-                parser.StopExpecting (withTerminator);
+                parser.StopExpecting (withTerminator,false);
 
 
                 return true;
                 return true;
             }
             }
@@ -118,7 +118,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
     private void Send (AnsiEscapeSequenceRequest r)
     private void Send (AnsiEscapeSequenceRequest r)
     {
     {
         _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now);
         _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now);
-        parser.ExpectResponse (r.Terminator,r.ResponseReceived);
+        parser.ExpectResponse (r.Terminator,r.ResponseReceived,false);
         r.Send ();
         r.Send ();
     }
     }
 
 

+ 10 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs

@@ -0,0 +1,10 @@
+#nullable enable
+namespace Terminal.Gui;
+
+public record AnsiResponseExpectation (string Terminator, Action<string> Response)
+{
+    public bool Matches (string cur)
+    {
+        return cur.EndsWith (Terminator);
+    }
+}

+ 62 - 19
Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs

@@ -4,17 +4,24 @@ using System.Runtime.ConstrainedExecution;
 
 
 namespace Terminal.Gui;
 namespace Terminal.Gui;
 
 
+
 internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 {
 {
     /// <summary>
     /// <summary>
     /// Responses we are expecting to come in.
     /// Responses we are expecting to come in.
     /// </summary>
     /// </summary>
-    protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
+    protected readonly List<AnsiResponseExpectation> expectedResponses = new ();
 
 
     /// <summary>
     /// <summary>
     /// Collection of responses that we <see cref="StopExpecting"/>.
     /// Collection of responses that we <see cref="StopExpecting"/>.
     /// </summary>
     /// </summary>
-    protected readonly List<(string terminator, Action<string> response)> lateResponses = new ();
+    protected readonly List<AnsiResponseExpectation> lateResponses = new ();
+
+    /// <summary>
+    /// Responses that you want to look out for that will come in continuously e.g. mouse events.
+    /// Key is the terminator.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> persistentExpectations = new ();
 
 
     private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
     private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
 
 
@@ -208,13 +215,28 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         string cur = HeldToString ();
         string cur = HeldToString ();
 
 
         // Look for an expected response for what is accumulated so far (since Esc)
         // Look for an expected response for what is accumulated so far (since Esc)
-        if (MatchResponse (cur, expectedResponses, true))
+        if (MatchResponse (cur,
+                           expectedResponses,
+                           invokeCallback: true,
+                           removeExpectation:true))
         {
         {
             return false;
             return false;
         }
         }
 
 
         // Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
         // Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
-        if (MatchResponse (cur, lateResponses, false))
+        if (MatchResponse (cur,
+                           lateResponses,
+                           invokeCallback: false,
+                           removeExpectation:true))
+        {
+            return false;
+        }
+
+        // Look for persistent requests
+        if (MatchResponse (cur,
+                           persistentExpectations,
+                           invokeCallback: true,
+                           removeExpectation:false))
         {
         {
             return false;
             return false;
         }
         }
@@ -230,20 +252,24 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         return false; // Continue accumulating
         return false; // Continue accumulating
     }
     }
 
 
-    private bool MatchResponse (string cur, List<(string terminator, Action<string> response)> collection, bool invokeCallback)
+
+    private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
     {
     {
         // Check for expected responses
         // Check for expected responses
-        var matchingResponse = collection.FirstOrDefault (r => cur.EndsWith (r.terminator));
+        var matchingResponse = collection.FirstOrDefault (r => r.Matches(cur));
 
 
-        if (matchingResponse.response != null)
+        if (matchingResponse?.Response != null)
         {
         {
-
             if (invokeCallback)
             if (invokeCallback)
             {
             {
-                matchingResponse.response?.Invoke (HeldToString ());
+                matchingResponse.Response?.Invoke (HeldToString ());
             }
             }
             ResetState ();
             ResetState ();
-            collection.Remove (matchingResponse);
+
+            if (removeExpectation)
+            {
+                collection.Remove (matchingResponse);
+            }
 
 
             return true;
             return true;
         }
         }
@@ -252,24 +278,41 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     }
     }
 
 
     /// <inheritdoc />
     /// <inheritdoc />
-    public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
+    public void ExpectResponse (string terminator, Action<string> response, bool persistent)
+    {
+        if (persistent)
+        {
+            persistentExpectations.Add (new (terminator, response));
+        }
+        else
+        {
+            expectedResponses.Add (new (terminator, response));
+        }
+    }
 
 
     /// <inheritdoc />
     /// <inheritdoc />
-    public bool IsExpecting (string requestTerminator)
+    public bool IsExpecting (string terminator)
     {
     {
         // If any of the new terminator matches any existing terminators characters it's a collision so true.
         // If any of the new terminator matches any existing terminators characters it's a collision so true.
-        return expectedResponses.Any (r => r.terminator.Intersect (requestTerminator).Any());
+        return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any());
     }
     }
 
 
     /// <inheritdoc />
     /// <inheritdoc />
-    public void StopExpecting (string requestTerminator)
+    public void StopExpecting (string terminator, bool persistent)
     {
     {
-        var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray ();
-
-        foreach (var r in removed)
+        if (persistent)
+        {
+            persistentExpectations.RemoveAll (r=>r.Matches (terminator));
+        }
+        else
         {
         {
-            expectedResponses.Remove (r);
-            lateResponses.Add (r);
+            var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
+
+            foreach (var r in removed)
+            {
+                expectedResponses.Remove (r);
+                lateResponses.Add (r);
+            }
         }
         }
     }
     }
 }
 }

+ 13 - 6
Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs

@@ -19,16 +19,21 @@ public interface IAnsiResponseParser
     /// sent an ANSI request out).
     /// sent an ANSI request out).
     /// </summary>
     /// </summary>
     /// <param name="terminator">The terminator you expect to see on response.</param>
     /// <param name="terminator">The terminator you expect to see on response.</param>
+    /// <param name="persistent"><see langword="true"/> if you want this to persist permanently
+    /// and be raised for every event matching the <paramref name="terminator"/>.</param>
     /// <param name="response">Callback to invoke when the response is seen in console input.</param>
     /// <param name="response">Callback to invoke when the response is seen in console input.</param>
-    void ExpectResponse (string terminator, Action<string> response);
+    /// <exception cref="ArgumentException">If trying to register a persistent request for a terminator
+    /// that already has one.
+    /// exists.</exception>
+    void ExpectResponse (string terminator, Action<string> response, bool persistent);
 
 
     /// <summary>
     /// <summary>
     /// Returns true if there is an existing expectation (i.e. we are waiting a response
     /// Returns true if there is an existing expectation (i.e. we are waiting a response
-    /// from console) for the given <paramref name="requestTerminator"/>.
+    /// from console) for the given <paramref name="terminator"/>.
     /// </summary>
     /// </summary>
-    /// <param name="requestTerminator"></param>
+    /// <param name="terminator"></param>
     /// <returns></returns>
     /// <returns></returns>
-    bool IsExpecting (string requestTerminator);
+    bool IsExpecting (string terminator);
 
 
     /// <summary>
     /// <summary>
     /// Removes callback and expectation that we will get a response for the
     /// Removes callback and expectation that we will get a response for the
@@ -36,5 +41,7 @@ public interface IAnsiResponseParser
     /// requests e.g. if you want to send a different one with the same terminator.
     /// requests e.g. if you want to send a different one with the same terminator.
     /// </summary>
     /// </summary>
     /// <param name="requestTerminator"></param>
     /// <param name="requestTerminator"></param>
-    void StopExpecting (string requestTerminator);
-}
+    /// <param name="persistent"><see langword="true"/> if you want to remove a persistent
+    /// request listener.</param>
+    void StopExpecting (string requestTerminator, bool persistent);
+}

+ 31 - 10
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -27,8 +27,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         int i = 0;
         int i = 0;
 
 
         // Imagine that we are expecting a DAR
         // Imagine that we are expecting a DAR
-        _parser1.ExpectResponse ("c",(s)=> response1 = s);
-        _parser2.ExpectResponse ("c", (s) => response2 = s);
+        _parser1.ExpectResponse ("c",(s)=> response1 = s, false);
+        _parser2.ExpectResponse ("c", (s) => response2 = s , false);
 
 
         // First char is Escape which we must consume incase what follows is the DAR
         // First char is Escape which we must consume incase what follows is the DAR
         AssertConsumed (ansiStream, ref i); // Esc
         AssertConsumed (ansiStream, ref i); // Esc
@@ -118,8 +118,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
             string response2 = string.Empty;
             string response2 = string.Empty;
 
 
             // Register the expected response with the given terminator
             // Register the expected response with the given terminator
-            _parser1.ExpectResponse (expectedTerminator, s => response1 = s);
-            _parser2.ExpectResponse (expectedTerminator, s => response2 = s);
+            _parser1.ExpectResponse (expectedTerminator, s => response1 = s, false);
+            _parser2.ExpectResponse (expectedTerminator, s => response2 = s, false);
 
 
             // Process the input
             // Process the input
             StringBuilder actualOutput1 = new StringBuilder ();
             StringBuilder actualOutput1 = new StringBuilder ();
@@ -225,7 +225,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
 
 
         if (terminator.HasValue)
         if (terminator.HasValue)
         {
         {
-            parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s);
+            parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s, false);
         }
         }
         foreach (var state in expectedStates)
         foreach (var state in expectedStates)
         {
         {
@@ -326,13 +326,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         string? responseA = null;
         string? responseA = null;
         string? responseB = null;
         string? responseB = null;
 
 
-        p.ExpectResponse ("z",(r)=>responseA=r);
+        p.ExpectResponse ("z",(r)=>responseA=r, false);
 
 
         // Some time goes by without us seeing a response
         // Some time goes by without us seeing a response
-        p.StopExpecting ("z");
+        p.StopExpecting ("z", false);
 
 
         // Send our new request
         // Send our new request
-        p.ExpectResponse ("z", (r) => responseB = r);
+        p.ExpectResponse ("z", (r) => responseB = r, false);
 
 
         // Because we gave up on getting A, we should expect the response to be to our new request
         // Because we gave up on getting A, we should expect the response to be to our new request
         Assert.Empty(p.ProcessInput ("\u001b[<1;2z"));
         Assert.Empty(p.ProcessInput ("\u001b[<1;2z"));
@@ -351,6 +351,29 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
 
 
     }
     }
 
 
+    [Fact]
+    public void TestPersistentResponses ()
+    {
+        var p = new AnsiResponseParser ();
+
+        int m = 0;
+        int M = 1;
+
+        p.ExpectResponse ("m", _ => m++, true);
+        p.ExpectResponse ("M", _ => M++, true);
+
+        // Act - Feed input strings containing ANSI sequences
+        p.ProcessInput ("\u001b[<0;10;10m");  // Should match and increment `m`
+        p.ProcessInput ("\u001b[<0;20;20m");  // Should match and increment `m`
+        p.ProcessInput ("\u001b[<0;30;30M");  // Should match and increment `M`
+        p.ProcessInput ("\u001b[<0;40;40M");  // Should match and increment `M`
+        p.ProcessInput ("\u001b[<0;50;50M");  // Should match and increment `M`
+
+        // Assert - Verify that counters reflect the expected counts of each terminator
+        Assert.Equal (2, m);  // Expected two `m` responses
+        Assert.Equal (4, M);  // Expected three `M` responses plus the initial value of 1
+    }
+
     private Tuple<char, int> [] StringToBatch (string batch)
     private Tuple<char, int> [] StringToBatch (string batch)
     {
     {
         return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
         return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
@@ -395,8 +418,6 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         }
         }
     }
     }
 
 
-
-
     private void AssertIgnored (string ansiStream,char expected, ref int i)
     private void AssertIgnored (string ansiStream,char expected, ref int i)
     {
     {
         var c2 = ansiStream [i];
         var c2 = ansiStream [i];