diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs index a051e6c8e..3722a8667 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -76,7 +76,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) { if (DateTime.Now - dt > _staleTimeout) { - parser.StopExpecting (withTerminator); + parser.StopExpecting (withTerminator,false); return true; } @@ -118,7 +118,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) private void Send (AnsiEscapeSequenceRequest r) { _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 (); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs new file mode 100644 index 000000000..d1245adcc --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Terminal.Gui; + +public record AnsiResponseExpectation (string Terminator, Action Response) +{ + public bool Matches (string cur) + { + return cur.EndsWith (Terminator); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index d93a900ac..9f1e6dcd4 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -4,17 +4,24 @@ using System.Runtime.ConstrainedExecution; namespace Terminal.Gui; + internal abstract class AnsiResponseParserBase : IAnsiResponseParser { /// /// Responses we are expecting to come in. /// - protected readonly List<(string terminator, Action response)> expectedResponses = new (); + protected readonly List expectedResponses = new (); /// /// Collection of responses that we . /// - protected readonly List<(string terminator, Action response)> lateResponses = new (); + protected readonly List lateResponses = new (); + + /// + /// Responses that you want to look out for that will come in continuously e.g. mouse events. + /// Key is the terminator. + /// + protected readonly List persistentExpectations = new (); private AnsiResponseParserState _state = AnsiResponseParserState.Normal; @@ -208,13 +215,28 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser string cur = HeldToString (); // 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; } // 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; } @@ -230,20 +252,24 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser return false; // Continue accumulating } - private bool MatchResponse (string cur, List<(string terminator, Action response)> collection, bool invokeCallback) + + private bool MatchResponse (string cur, List collection, bool invokeCallback, bool removeExpectation) { // 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) { - matchingResponse.response?.Invoke (HeldToString ()); + matchingResponse.Response?.Invoke (HeldToString ()); } ResetState (); - collection.Remove (matchingResponse); + + if (removeExpectation) + { + collection.Remove (matchingResponse); + } return true; } @@ -252,24 +278,41 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser } /// - public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } - - /// - public bool IsExpecting (string requestTerminator) + public void ExpectResponse (string terminator, Action response, bool persistent) { - // 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()); + if (persistent) + { + persistentExpectations.Add (new (terminator, response)); + } + else + { + expectedResponses.Add (new (terminator, response)); + } } /// - public void StopExpecting (string requestTerminator) + public bool IsExpecting (string terminator) { - var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray (); + // If any of the new terminator matches any existing terminators characters it's a collision so true. + return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any()); + } - foreach (var r in removed) + /// + public void StopExpecting (string terminator, bool persistent) + { + if (persistent) { - expectedResponses.Remove (r); - lateResponses.Add (r); + persistentExpectations.RemoveAll (r=>r.Matches (terminator)); + } + else + { + var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); + + foreach (var r in removed) + { + expectedResponses.Remove (r); + lateResponses.Add (r); + } } } } diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs index 4a6ee635b..d1246c661 100644 --- a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -19,16 +19,21 @@ public interface IAnsiResponseParser /// sent an ANSI request out). /// /// The terminator you expect to see on response. + /// if you want this to persist permanently + /// and be raised for every event matching the . /// Callback to invoke when the response is seen in console input. - void ExpectResponse (string terminator, Action response); + /// If trying to register a persistent request for a terminator + /// that already has one. + /// exists. + void ExpectResponse (string terminator, Action response, bool persistent); /// /// Returns true if there is an existing expectation (i.e. we are waiting a response - /// from console) for the given . + /// from console) for the given . /// - /// + /// /// - bool IsExpecting (string requestTerminator); + bool IsExpecting (string terminator); /// /// 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. /// /// - void StopExpecting (string requestTerminator); -} + /// if you want to remove a persistent + /// request listener. + void StopExpecting (string requestTerminator, bool persistent); +} \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index e1b3a5b24..fa7fac4f7 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -27,8 +27,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output) int i = 0; // 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 AssertConsumed (ansiStream, ref i); // Esc @@ -118,8 +118,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output) string response2 = string.Empty; // 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 StringBuilder actualOutput1 = new StringBuilder (); @@ -225,7 +225,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output) 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) { @@ -326,13 +326,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output) string? responseA = 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 - p.StopExpecting ("z"); + p.StopExpecting ("z", false); // 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 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 [] StringToBatch (string batch) { 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) { var c2 = ansiStream [i];