Browse Source

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

tznind 9 months ago
parent
commit
057aeea1ac

+ 9 - 10
Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs → Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs

@@ -1,12 +1,11 @@
 #nullable enable
 using System.Collections.Concurrent;
-using System.Diagnostics;
 
 namespace Terminal.Gui;
 
-public class AnsiRequestScheduler(IAnsiResponseParser parser)
+public class AnsiRequestScheduler (IAnsiResponseParser parser)
 {
-    private readonly List<Tuple<AnsiEscapeSequenceRequest,DateTime>> _requests = new  ();
+    private readonly List<Tuple<AnsiEscapeSequenceRequest, DateTime>> _requests = new ();
 
     /// <summary>
     ///<para>
@@ -38,10 +37,10 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
     /// </summary>
     /// <param name="request"></param>
     /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
-    public bool SendOrSchedule (AnsiEscapeSequenceRequest request )
+    public bool SendOrSchedule (AnsiEscapeSequenceRequest request)
     {
 
-        if (CanSend(request, out var reason))
+        if (CanSend (request, out var reason))
         {
             Send (request);
             return true;
@@ -59,7 +58,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
             }
         }
 
-        _requests.Add (Tuple.Create(request,DateTime.Now));
+        _requests.Add (Tuple.Create (request, DateTime.Now));
         return false;
     }
 
@@ -76,7 +75,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
         {
             if (DateTime.Now - dt > _staleTimeout)
             {
-                parser.StopExpecting (withTerminator,false);
+                parser.StopExpecting (withTerminator, false);
 
                 return true;
             }
@@ -102,7 +101,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
             return false;
         }
 
-        var opportunity = _requests.FirstOrDefault (r=>CanSend(r.Item1, out _));
+        var opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _));
 
         if (opportunity != null)
         {
@@ -117,8 +116,8 @@ 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,false);
+        _lastSend.AddOrUpdate (r.Terminator, (s) => DateTime.Now, (s, v) => DateTime.Now);
+        parser.ExpectResponse (r.Terminator, r.ResponseReceived, false);
         r.Send ();
     }
 

+ 2 - 2
Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs → Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs

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

+ 49 - 42
Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs → Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs

@@ -1,7 +1,5 @@
 #nullable enable
 
-using System.Runtime.ConstrainedExecution;
-
 namespace Terminal.Gui;
 
 
@@ -36,6 +34,8 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         }
     }
 
+    protected readonly IHeld heldContent;
+
     /// <summary>
     ///     When <see cref="State"/> was last changed.
     /// </summary>
@@ -55,18 +55,17 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
     });
 
+    protected AnsiResponseParserBase (IHeld heldContent)
+    {
+        this.heldContent = heldContent;
+    }
 
     protected void ResetState ()
     {
         State = AnsiResponseParserState.Normal;
-        ClearHeld ();
+        heldContent.ClearHeld ();
     }
 
-    public abstract void ClearHeld ();
-    protected abstract string HeldToString ();
-    protected abstract IEnumerable<object> HeldToObjects ();
-    protected abstract void AddToHeld (object o);
-
     /// <summary>
     ///     Processes an input collection of objects <paramref name="inputLength"/> long.
     ///     You must provide the indexers to return the objects and the action to append
@@ -102,7 +101,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                     {
                         // Escape character detected, move to ExpectingBracket state
                         State = AnsiResponseParserState.ExpectingBracket;
-                        AddToHeld (currentObj); // Hold the escape character
+                        heldContent.AddToHeld (currentObj); // Hold the escape character
                     }
                     else
                     {
@@ -117,13 +116,13 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                     {
                         // Second escape so we must release first
                         ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket);
-                        AddToHeld (currentObj); // Hold the new escape
+                        heldContent.AddToHeld (currentObj); // Hold the new escape
                     }
                     else if (currentChar == '[')
                     {
                         // Detected '[', transition to InResponse state
                         State = AnsiResponseParserState.InResponse;
-                        AddToHeld (currentObj); // Hold the '['
+                        heldContent.AddToHeld (currentObj); // Hold the '['
                     }
                     else
                     {
@@ -135,7 +134,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                     break;
 
                 case AnsiResponseParserState.InResponse:
-                    AddToHeld (currentObj);
+                    heldContent.AddToHeld (currentObj);
 
                     // Check if the held content should be released
                     if (ShouldReleaseHeldContent ())
@@ -152,25 +151,25 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
     private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
     {
-        foreach (object o in HeldToObjects ())
+        foreach (object o in heldContent.HeldToObjects ())
         {
             appendOutput (o);
         }
 
         State = newState;
-        ClearHeld ();
+        heldContent.ClearHeld ();
     }
 
     // Common response handler logic
     protected bool ShouldReleaseHeldContent ()
     {
-        string cur = HeldToString ();
+        string cur = heldContent.HeldToString ();
 
         // Look for an expected response for what is accumulated so far (since Esc)
         if (MatchResponse (cur,
                            expectedResponses,
                            invokeCallback: true,
-                           removeExpectation:true))
+                           removeExpectation: true))
         {
             return false;
         }
@@ -179,7 +178,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         if (MatchResponse (cur,
                            lateResponses,
                            invokeCallback: false,
-                           removeExpectation:true))
+                           removeExpectation: true))
         {
             return false;
         }
@@ -188,7 +187,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         if (MatchResponse (cur,
                            persistentExpectations,
                            invokeCallback: true,
-                           removeExpectation:false))
+                           removeExpectation: false))
         {
             return false;
         }
@@ -208,13 +207,13 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
     {
         // Check for expected responses
-        var matchingResponse = collection.FirstOrDefault (r => r.Matches(cur));
+        var matchingResponse = collection.FirstOrDefault (r => r.Matches (cur));
 
         if (matchingResponse?.Response != null)
         {
             if (invokeCallback)
             {
-                matchingResponse.Response?.Invoke (HeldToString ());
+                matchingResponse.Response.Invoke (heldContent);
             }
             ResetState ();
 
@@ -234,11 +233,11 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     {
         if (persistent)
         {
-            persistentExpectations.Add (new (terminator, response));
+            persistentExpectations.Add (new (terminator, (h)=>response.Invoke (h.HeldToString ())));
         }
         else
         {
-            expectedResponses.Add (new (terminator, response));
+            expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ())));
         }
     }
 
@@ -246,7 +245,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     public bool IsExpecting (string terminator)
     {
         // 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());
+        return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any ());
     }
 
     /// <inheritdoc />
@@ -254,7 +253,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     {
         if (persistent)
         {
-            persistentExpectations.RemoveAll (r=>r.Matches (terminator));
+            persistentExpectations.RemoveAll (r => r.Matches (terminator));
         }
         else
         {
@@ -271,7 +270,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
 internal class AnsiResponseParser<T> : AnsiResponseParserBase
 {
-    private readonly List<Tuple<char, T>> held = new ();
+    public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
 
     public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
     {
@@ -288,7 +287,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
 
     public IEnumerable<Tuple<char, T>> Release ()
     {
-        foreach (Tuple<char, T> h in held.ToArray ())
+        foreach (Tuple<char, T> h in HeldToEnumerable())
         {
             yield return h;
         }
@@ -296,18 +295,34 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
         ResetState ();
     }
 
-    public override void ClearHeld () { held.Clear (); }
-
-    protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
-
-    protected override IEnumerable<object> HeldToObjects () { return held; }
+    private IEnumerable<Tuple<char, T>> HeldToEnumerable ()
+    {
+        return (IEnumerable<Tuple<char, T>>)heldContent.HeldToObjects ();
+    }
 
-    protected override void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
+    /// <summary>
+    /// 'Overload' for specifying an expectation that requires the metadata as well as characters. Has
+    /// a unique name because otherwise most lamdas will give ambiguous overload errors.
+    /// </summary>
+    /// <param name="terminator"></param>
+    /// <param name="response"></param>
+    /// <param name="persistent"></param>
+    public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char,T>>> response, bool persistent)
+    {
+        if (persistent)
+        {
+            persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
+        }
+        else
+        {
+            expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
+        }
+    }
 }
 
 internal class AnsiResponseParser : AnsiResponseParserBase
 {
-    private readonly StringBuilder held = new ();
+    public AnsiResponseParser () : base (new StringHeld ()) { }
 
     public string ProcessInput (string input)
     {
@@ -324,17 +339,9 @@ internal class AnsiResponseParser : AnsiResponseParserBase
 
     public string Release ()
     {
-        var output = held.ToString ();
+        var output = heldContent.HeldToString ();
         ResetState ();
 
         return output;
     }
-
-    public override void ClearHeld () { held.Clear (); }
-
-    protected override string HeldToString () { return held.ToString (); }
-
-    protected override IEnumerable<object> HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); }
-
-    protected override void AddToHeld (object o) { held.Append ((char)o); }
 }

+ 16 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs

@@ -0,0 +1,16 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+/// Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{T}"/>
+/// </summary>
+/// <typeparam name="T"></typeparam>
+internal class GenericHeld<T> : IHeld
+{
+    private readonly List<Tuple<char, T>> held = new ();
+
+    public void ClearHeld () => held.Clear ();
+    public string HeldToString () => new (held.Select (h => h.Item1).ToArray ());
+    public IEnumerable<object> HeldToObjects () => held;
+    public void AddToHeld (object o) => held.Add ((Tuple<char, T>)o);
+}

+ 0 - 0
Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs → Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs


+ 33 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs

@@ -0,0 +1,33 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+/// Describes a sequence of chars (and optionally T metadata) accumulated
+/// by an <see cref="IAnsiResponseParser"/>
+/// </summary>
+internal interface IHeld
+{
+    /// <summary>
+    /// Clears all held objects
+    /// </summary>
+    void ClearHeld ();
+
+    /// <summary>
+    /// Returns string representation of the held objects
+    /// </summary>
+    /// <returns></returns>
+    string HeldToString ();
+
+    /// <summary>
+    /// Returns the collection objects directly e.g. <see langword="char"/>
+    /// or <see cref="Tuple"/> <see langword="char"/> + metadata T
+    /// </summary>
+    /// <returns></returns>
+    IEnumerable<object> HeldToObjects ();
+
+    /// <summary>
+    /// Adds the given object to the collection.
+    /// </summary>
+    /// <param name="o"></param>
+    void AddToHeld (object o);
+}

+ 15 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs

@@ -0,0 +1,15 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+/// Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser"/>
+/// </summary>
+internal class StringHeld : IHeld
+{
+    private readonly StringBuilder held = new ();
+
+    public void ClearHeld () => held.Clear ();
+    public string HeldToString () => held.ToString ();
+    public IEnumerable<object> HeldToObjects () => held.ToString ().Select (c => (object)c);
+    public void AddToHeld (object o) => held.Append ((char)o);
+}

+ 41 - 1
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -1,4 +1,6 @@
-using System.Diagnostics;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.Text;
 using Xunit.Abstractions;
 
@@ -374,6 +376,44 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
         Assert.Equal (4, M);  // Expected three `M` responses plus the initial value of 1
     }
 
+    [Fact]
+    public void TestPersistentResponses_WithMetadata ()
+    {
+        var p = new AnsiResponseParser<int> ();
+
+        int m = 0;
+
+        var result = new List<Tuple<char,int>> ();
+
+        p.ExpectResponseT ("m", (r) =>
+                               {
+                                   result = r.ToList ();
+                                   m++;
+                               }, true);
+
+        // Act - Feed input strings containing ANSI sequences
+        p.ProcessInput (StringToBatch("\u001b[<0;10;10m"));  // Should match and increment `m`
+
+        // Prepare expected result: 
+        var expected = new List<Tuple<char, int>>
+        {
+            Tuple.Create('\u001b', 0), // Escape character
+            Tuple.Create('[', 1),
+            Tuple.Create('<', 2),
+            Tuple.Create('0', 3),
+            Tuple.Create(';', 4),
+            Tuple.Create('1', 5),
+            Tuple.Create('0', 6),
+            Tuple.Create(';', 7),
+            Tuple.Create('1', 8),
+            Tuple.Create('0', 9),
+            Tuple.Create('m', 10)
+        };
+
+        Assert.Equal (expected.Count, result.Count); // Ensure the count is as expected
+        Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
+    }
+
     private Tuple<char, int> [] StringToBatch (string batch)
     {
         return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();