mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-02 01:03:29 +01:00
Merge branch 'ansi-parser' into ansi-parser-net-driver
This commit is contained in:
@@ -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 ();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
33
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
Normal file
33
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
Normal 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);
|
||||
}
|
||||
15
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
Normal file
15
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
Normal 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);
|
||||
}
|
||||
@@ -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 ();
|
||||
|
||||
Reference in New Issue
Block a user