Browse Source

Add UnknownResponseHandler

tznind 9 months ago
parent
commit
acdd483cb2

+ 47 - 2
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs

@@ -196,13 +196,28 @@ 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
-            return true;
+            // We have found a terminator so bail
+            State = AnsiResponseParserState.Normal;
+
+            // Maybe swallow anyway if user has custom delegate
+            return ShouldReleaseUnexpectedResponse ();
         }
         }
 
 
         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 ShouldReleaseUnexpectedResponse ();
 
 
     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 +287,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> UnknownResponseHandler { 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 +338,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 ShouldReleaseUnexpectedResponse ()
+    {
+        return !UnknownResponseHandler.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 +383,10 @@ internal class AnsiResponseParser : AnsiResponseParserBase
 
 
         return output;
         return output;
     }
     }
+
+    /// <inheritdoc />
+    protected override bool ShouldReleaseUnexpectedResponse ()
+    {
+        return !UnknownResponseHandler.Invoke (heldContent.ToString () ?? string.Empty);
+    }
 }
 }

+ 73 - 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,30 @@ 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.UnknownResponseHandler = _ => 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);
+    }
+
     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 +505,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];