Browse Source

Merge branch 'ansi-parser' into ansi-parser-net-driver

tznind 9 months ago
parent
commit
d5095a2302

+ 56 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs

@@ -196,13 +196,38 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         // then we can release it back to input processing stream
         // then we can release it back to input processing stream
         if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
         if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
         {
         {
-            // Detected a response that was not expected
+            // We have found a terminator so bail
+            State = AnsiResponseParserState.Normal;
+
+            // Maybe swallow anyway if user has custom delegate
+            var swallow =  ShouldSwallowUnexpectedResponse ();
+
+            if (swallow)
+            {
+                heldContent.ClearHeld ();
+                // Do not send back to input stream
+                return false;
+            }
+
+            // Do release back to input stream
             return true;
             return true;
         }
         }
 
 
         return false; // Continue accumulating
         return false; // Continue accumulating
     }
     }
 
 
+    /// <summary>
+    /// <para>
+    /// When overriden in a derived class, indicates whether the unexpected response
+    /// currently in <see cref="heldContent"/> should be released or swallowed.
+    /// Use this to enable default event for escape codes.
+    /// </para>
+    /// 
+    /// <remarks>Note this is only called for complete responses.
+    /// Based on <see cref="_knownTerminators"/></remarks>
+    /// </summary>
+    /// <returns></returns>
+    protected abstract bool ShouldSwallowUnexpectedResponse ();
 
 
     private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
     private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
     {
     {
@@ -272,6 +297,11 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
 {
 {
     public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
     public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
 
 
+
+    /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
+    public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = (_) => false;
+
+
     public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
     public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
     {
     {
         List<Tuple<char, T>> output = new List<Tuple<char, T>> ();
         List<Tuple<char, T>> output = new List<Tuple<char, T>> ();
@@ -318,10 +348,29 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
             expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
             expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
         }
         }
     }
     }
+
+    /// <inheritdoc />
+    protected override bool ShouldSwallowUnexpectedResponse ()
+    {
+        return UnexpectedResponseHandler.Invoke (HeldToEnumerable ());
+    }
 }
 }
 
 
 internal class AnsiResponseParser : AnsiResponseParserBase
 internal class AnsiResponseParser : AnsiResponseParserBase
 {
 {
+    /// <summary>
+    /// <para>
+    /// Delegate for handling unrecognized escape codes. Default behaviour
+    /// is to return <see langword="false"/> which simply releases the
+    /// characters back to input stream for downstream processing.
+    /// </para>
+    /// <para>
+    /// Implement a method to handle if you want and return <see langword="true"/> if you want the
+    /// keystrokes 'swallowed' (i.e. not returned to input stream).
+    /// </para>
+    /// </summary>
+    public Func<string, bool> UnknownResponseHandler { get; set; } = (_) => false;
+
     public AnsiResponseParser () : base (new StringHeld ()) { }
     public AnsiResponseParser () : base (new StringHeld ()) { }
 
 
     public string ProcessInput (string input)
     public string ProcessInput (string input)
@@ -344,4 +393,10 @@ internal class AnsiResponseParser : AnsiResponseParserBase
 
 
         return output;
         return output;
     }
     }
+
+    /// <inheritdoc />
+    protected override bool ShouldSwallowUnexpectedResponse ()
+    {
+        return UnknownResponseHandler.Invoke (heldContent.HeldToString ());
+    }
 }
 }

+ 114 - 0
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -1,6 +1,7 @@
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
+using System.Linq;
 using System.Text;
 using System.Text;
 using Xunit.Abstractions;
 using Xunit.Abstractions;
 
 
@@ -414,6 +415,71 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
         Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
     }
     }
 
 
+    [Fact]
+    public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo ()
+    {
+        int i = 0;
+
+        // Swallow all unknown escape codes
+        _parser1.UnexpectedResponseHandler = _ => true;
+        _parser2.UnknownResponseHandler = _ => true;
+
+
+        AssertReleased (
+                        "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
+                        "Just test",
+                        0,
+                        1,
+                        2,
+                        3,
+                        4,
+                        5,
+                        6,
+                        28,
+                        29);
+    }
+
+    [Fact]
+    public void UnknownResponses_ParameterShouldMatch ()
+    {
+        int i = 0;
+
+        // Track unknown responses passed to the UnexpectedResponseHandler
+        var unknownResponses = new List<string> ();
+
+        // Set up the UnexpectedResponseHandler to log each unknown response
+        _parser1.UnexpectedResponseHandler = r1 =>
+                                          {
+                                              unknownResponses.Add (BatchToString (r1));
+                                              return true; // Return true to swallow unknown responses
+                                          };
+
+        _parser2.UnknownResponseHandler = r2 =>
+                                          {
+                                              // parsers should be agreeing on what these responses are!
+                                              Assert.Equal(unknownResponses.Last(),r2);
+                                              return true; // Return true to swallow unknown responses
+                                          };
+
+        // Input with known and unknown responses
+        AssertReleased (
+                        "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
+                        "Just test");
+
+        // Expected unknown responses (ANSI sequences that are unknown)
+        var expectedUnknownResponses = new List<string>
+        {
+            "\u001b[<0;0;0M",
+            "\u001b[3c",
+            "\u001b[2c",
+            "\u001b[4c"
+        };
+
+        // Assert that the UnexpectedResponseHandler was called with the correct unknown responses
+        Assert.Equal (expectedUnknownResponses.Count, unknownResponses.Count);
+        Assert.Equal (expectedUnknownResponses, unknownResponses);
+    }
+
     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 ();
@@ -480,6 +546,54 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         Assert.Empty (_parser2.ProcessInput (c2.ToString()));
         Assert.Empty (_parser2.ProcessInput (c2.ToString()));
     }
     }
 
 
+    /// <summary>
+    /// Overload that fully exhausts <paramref name="ansiStream"/> and asserts
+    /// that the final released content across whole processing is <paramref name="expectedRelease"/>
+    /// </summary>
+    /// <param name="ansiStream"></param>
+    /// <param name="expectedRelease"></param>
+    /// <param name="expectedTValues"></param>
+    private void AssertReleased (string ansiStream, string expectedRelease, params int [] expectedTValues)
+    {
+        var sb = new StringBuilder ();
+        var tValues = new List<int> ();
+
+        int i = 0;
+
+        while (i < ansiStream.Length)
+        {
+            var c2 = ansiStream [i];
+            var c1 = NextChar (ansiStream, ref i);
+
+            var released1 = _parser1.ProcessInput (c1).ToArray ();
+            tValues.AddRange(released1.Select (kv => kv.Item2));
+
+
+            var released2 = _parser2.ProcessInput (c2.ToString ());
+
+            // Both parsers should have same chars so release chars consistently with each other
+            Assert.Equal (BatchToString(released1),released2);
+
+            sb.Append (released2);
+        }
+
+        Assert.Equal (expectedRelease, sb.ToString());
+
+        if (expectedTValues.Length > 0)
+        {
+            Assert.True (expectedTValues.SequenceEqual (tValues));
+        }
+    }
+
+    /// <summary>
+    /// Asserts that <paramref name="i"/> index of <see cref="ansiStream"/> when consumed will release
+    /// <paramref name="expectedRelease"/>. Results in implicit increment of <paramref name="i"/>.
+    /// <remarks>Note that this does NOT iteratively consume all the stream, only 1 char at <paramref name="i"/></remarks>
+    /// </summary>
+    /// <param name="ansiStream"></param>
+    /// <param name="i"></param>
+    /// <param name="expectedRelease"></param>
+    /// <param name="expectedTValues"></param>
     private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int[] expectedTValues)
     private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int[] expectedTValues)
     {
     {
         var c2 = ansiStream [i];
         var c2 = ansiStream [i];