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

This commit is contained in:
tznind
2024-10-26 19:41:08 +01:00
8 changed files with 165 additions and 55 deletions

View File

@@ -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 ();
}

View File

@@ -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);
}
}
}

View File

@@ -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 (); }
private IEnumerable<Tuple<char, T>> HeldToEnumerable ()
{
return (IEnumerable<Tuple<char, T>>)heldContent.HeldToObjects ();
}
protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
protected override IEnumerable<object> HeldToObjects () { return held; }
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); }
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 ();