mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 16:59:35 +01:00
Support for persistent expectations of responses
This commit is contained in:
@@ -76,7 +76,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser)
|
||||
{
|
||||
if (DateTime.Now - dt > _staleTimeout)
|
||||
{
|
||||
parser.StopExpecting (withTerminator);
|
||||
parser.StopExpecting (withTerminator,false);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ 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);
|
||||
parser.ExpectResponse (r.Terminator,r.ResponseReceived,false);
|
||||
r.Send ();
|
||||
}
|
||||
|
||||
|
||||
10
Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs
Normal file
10
Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui;
|
||||
|
||||
public record AnsiResponseExpectation (string Terminator, Action<string> Response)
|
||||
{
|
||||
public bool Matches (string cur)
|
||||
{
|
||||
return cur.EndsWith (Terminator);
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,24 @@ using System.Runtime.ConstrainedExecution;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
|
||||
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Responses we are expecting to come in.
|
||||
/// </summary>
|
||||
protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
|
||||
protected readonly List<AnsiResponseExpectation> expectedResponses = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Collection of responses that we <see cref="StopExpecting"/>.
|
||||
/// </summary>
|
||||
protected readonly List<(string terminator, Action<string> response)> lateResponses = new ();
|
||||
protected readonly List<AnsiResponseExpectation> lateResponses = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Responses that you want to look out for that will come in continuously e.g. mouse events.
|
||||
/// Key is the terminator.
|
||||
/// </summary>
|
||||
protected readonly List<AnsiResponseExpectation> persistentExpectations = new ();
|
||||
|
||||
private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
|
||||
|
||||
@@ -208,13 +215,28 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
string cur = HeldToString ();
|
||||
|
||||
// Look for an expected response for what is accumulated so far (since Esc)
|
||||
if (MatchResponse (cur, expectedResponses, true))
|
||||
if (MatchResponse (cur,
|
||||
expectedResponses,
|
||||
invokeCallback: true,
|
||||
removeExpectation:true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
|
||||
if (MatchResponse (cur, lateResponses, false))
|
||||
if (MatchResponse (cur,
|
||||
lateResponses,
|
||||
invokeCallback: false,
|
||||
removeExpectation:true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for persistent requests
|
||||
if (MatchResponse (cur,
|
||||
persistentExpectations,
|
||||
invokeCallback: true,
|
||||
removeExpectation:false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -230,20 +252,24 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
return false; // Continue accumulating
|
||||
}
|
||||
|
||||
private bool MatchResponse (string cur, List<(string terminator, Action<string> response)> collection, bool invokeCallback)
|
||||
|
||||
private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
|
||||
{
|
||||
// Check for expected responses
|
||||
var matchingResponse = collection.FirstOrDefault (r => cur.EndsWith (r.terminator));
|
||||
var matchingResponse = collection.FirstOrDefault (r => r.Matches(cur));
|
||||
|
||||
if (matchingResponse.response != null)
|
||||
if (matchingResponse?.Response != null)
|
||||
{
|
||||
|
||||
if (invokeCallback)
|
||||
{
|
||||
matchingResponse.response?.Invoke (HeldToString ());
|
||||
matchingResponse.Response?.Invoke (HeldToString ());
|
||||
}
|
||||
ResetState ();
|
||||
collection.Remove (matchingResponse);
|
||||
|
||||
if (removeExpectation)
|
||||
{
|
||||
collection.Remove (matchingResponse);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -252,24 +278,41 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsExpecting (string requestTerminator)
|
||||
public void ExpectResponse (string terminator, Action<string> response, bool persistent)
|
||||
{
|
||||
// If any of the new terminator matches any existing terminators characters it's a collision so true.
|
||||
return expectedResponses.Any (r => r.terminator.Intersect (requestTerminator).Any());
|
||||
if (persistent)
|
||||
{
|
||||
persistentExpectations.Add (new (terminator, response));
|
||||
}
|
||||
else
|
||||
{
|
||||
expectedResponses.Add (new (terminator, response));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopExpecting (string requestTerminator)
|
||||
public bool IsExpecting (string terminator)
|
||||
{
|
||||
var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray ();
|
||||
// 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());
|
||||
}
|
||||
|
||||
foreach (var r in removed)
|
||||
/// <inheritdoc />
|
||||
public void StopExpecting (string terminator, bool persistent)
|
||||
{
|
||||
if (persistent)
|
||||
{
|
||||
expectedResponses.Remove (r);
|
||||
lateResponses.Add (r);
|
||||
persistentExpectations.RemoveAll (r=>r.Matches (terminator));
|
||||
}
|
||||
else
|
||||
{
|
||||
var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
|
||||
|
||||
foreach (var r in removed)
|
||||
{
|
||||
expectedResponses.Remove (r);
|
||||
lateResponses.Add (r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,21 @@ public interface IAnsiResponseParser
|
||||
/// sent an ANSI request out).
|
||||
/// </summary>
|
||||
/// <param name="terminator">The terminator you expect to see on response.</param>
|
||||
/// <param name="persistent"><see langword="true"/> if you want this to persist permanently
|
||||
/// and be raised for every event matching the <paramref name="terminator"/>.</param>
|
||||
/// <param name="response">Callback to invoke when the response is seen in console input.</param>
|
||||
void ExpectResponse (string terminator, Action<string> response);
|
||||
/// <exception cref="ArgumentException">If trying to register a persistent request for a terminator
|
||||
/// that already has one.
|
||||
/// exists.</exception>
|
||||
void ExpectResponse (string terminator, Action<string> response, bool persistent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if there is an existing expectation (i.e. we are waiting a response
|
||||
/// from console) for the given <paramref name="requestTerminator"/>.
|
||||
/// from console) for the given <paramref name="terminator"/>.
|
||||
/// </summary>
|
||||
/// <param name="requestTerminator"></param>
|
||||
/// <param name="terminator"></param>
|
||||
/// <returns></returns>
|
||||
bool IsExpecting (string requestTerminator);
|
||||
bool IsExpecting (string terminator);
|
||||
|
||||
/// <summary>
|
||||
/// Removes callback and expectation that we will get a response for the
|
||||
@@ -36,5 +41,7 @@ public interface IAnsiResponseParser
|
||||
/// requests e.g. if you want to send a different one with the same terminator.
|
||||
/// </summary>
|
||||
/// <param name="requestTerminator"></param>
|
||||
void StopExpecting (string requestTerminator);
|
||||
}
|
||||
/// <param name="persistent"><see langword="true"/> if you want to remove a persistent
|
||||
/// request listener.</param>
|
||||
void StopExpecting (string requestTerminator, bool persistent);
|
||||
}
|
||||
@@ -27,8 +27,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
int i = 0;
|
||||
|
||||
// Imagine that we are expecting a DAR
|
||||
_parser1.ExpectResponse ("c",(s)=> response1 = s);
|
||||
_parser2.ExpectResponse ("c", (s) => response2 = s);
|
||||
_parser1.ExpectResponse ("c",(s)=> response1 = s, false);
|
||||
_parser2.ExpectResponse ("c", (s) => response2 = s , false);
|
||||
|
||||
// First char is Escape which we must consume incase what follows is the DAR
|
||||
AssertConsumed (ansiStream, ref i); // Esc
|
||||
@@ -118,8 +118,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
string response2 = string.Empty;
|
||||
|
||||
// Register the expected response with the given terminator
|
||||
_parser1.ExpectResponse (expectedTerminator, s => response1 = s);
|
||||
_parser2.ExpectResponse (expectedTerminator, s => response2 = s);
|
||||
_parser1.ExpectResponse (expectedTerminator, s => response1 = s, false);
|
||||
_parser2.ExpectResponse (expectedTerminator, s => response2 = s, false);
|
||||
|
||||
// Process the input
|
||||
StringBuilder actualOutput1 = new StringBuilder ();
|
||||
@@ -225,7 +225,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
|
||||
if (terminator.HasValue)
|
||||
{
|
||||
parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s);
|
||||
parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s, false);
|
||||
}
|
||||
foreach (var state in expectedStates)
|
||||
{
|
||||
@@ -326,13 +326,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
string? responseA = null;
|
||||
string? responseB = null;
|
||||
|
||||
p.ExpectResponse ("z",(r)=>responseA=r);
|
||||
p.ExpectResponse ("z",(r)=>responseA=r, false);
|
||||
|
||||
// Some time goes by without us seeing a response
|
||||
p.StopExpecting ("z");
|
||||
p.StopExpecting ("z", false);
|
||||
|
||||
// Send our new request
|
||||
p.ExpectResponse ("z", (r) => responseB = r);
|
||||
p.ExpectResponse ("z", (r) => responseB = r, false);
|
||||
|
||||
// Because we gave up on getting A, we should expect the response to be to our new request
|
||||
Assert.Empty(p.ProcessInput ("\u001b[<1;2z"));
|
||||
@@ -351,6 +351,29 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestPersistentResponses ()
|
||||
{
|
||||
var p = new AnsiResponseParser ();
|
||||
|
||||
int m = 0;
|
||||
int M = 1;
|
||||
|
||||
p.ExpectResponse ("m", _ => m++, true);
|
||||
p.ExpectResponse ("M", _ => M++, true);
|
||||
|
||||
// Act - Feed input strings containing ANSI sequences
|
||||
p.ProcessInput ("\u001b[<0;10;10m"); // Should match and increment `m`
|
||||
p.ProcessInput ("\u001b[<0;20;20m"); // Should match and increment `m`
|
||||
p.ProcessInput ("\u001b[<0;30;30M"); // Should match and increment `M`
|
||||
p.ProcessInput ("\u001b[<0;40;40M"); // Should match and increment `M`
|
||||
p.ProcessInput ("\u001b[<0;50;50M"); // Should match and increment `M`
|
||||
|
||||
// Assert - Verify that counters reflect the expected counts of each terminator
|
||||
Assert.Equal (2, m); // Expected two `m` responses
|
||||
Assert.Equal (4, M); // Expected three `M` responses plus the initial value of 1
|
||||
}
|
||||
|
||||
private Tuple<char, int> [] StringToBatch (string batch)
|
||||
{
|
||||
return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
|
||||
@@ -395,8 +418,6 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void AssertIgnored (string ansiStream,char expected, ref int i)
|
||||
{
|
||||
var c2 = ansiStream [i];
|
||||
|
||||
Reference in New Issue
Block a user