Allow attatching arbitrary metadata to each char handled by AnsiResponseParser

This commit is contained in:
tznind
2024-10-12 09:01:30 +01:00
parent fa15389e9c
commit d1669a50b8
3 changed files with 66 additions and 29 deletions

View File

@@ -2,9 +2,9 @@
namespace Terminal.Gui;
internal class AnsiResponseParser
internal class AnsiResponseParser<T>
{
private readonly StringBuilder held = new ();
private readonly List<Tuple<char,T>> held = new ();
private readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
// Enum to manage the parser's state
@@ -104,47 +104,47 @@ internal class AnsiResponseParser
/// Returns what should be passed on to any downstream input processing
/// (i.e., removes expected ANSI responses from the input stream).
/// </summary>
public string ProcessInput (string input)
public IEnumerable<Tuple<char,T>> ProcessInput (params Tuple<char,T>[] input)
{
var output = new StringBuilder (); // Holds characters that should pass through
var output = new List<Tuple<char, T>> (); // Holds characters that should pass through
var index = 0; // Tracks position in the input string
while (index < input.Length)
{
char currentChar = input [index];
var currentChar = input [index];
switch (currentState)
{
case ParserState.Normal:
if (currentChar == '\x1B')
if (currentChar.Item1 == '\x1B')
{
// Escape character detected, move to ExpectingBracket state
currentState = ParserState.ExpectingBracket;
held.Append (currentChar); // Hold the escape character
held.Add (currentChar); // Hold the escape character
index++;
}
else
{
// Normal character, append to output
output.Append (currentChar);
output.Add (currentChar);
index++;
}
break;
case ParserState.ExpectingBracket:
if (currentChar == '[')
if (currentChar.Item1 == '[')
{
// Detected '[' , transition to InResponse state
currentState = ParserState.InResponse;
held.Append (currentChar); // Hold the '['
held.Add (currentChar); // Hold the '['
index++;
}
else
{
// Invalid sequence, release held characters and reset to Normal
output.Append (held.ToString ());
output.Append (currentChar); // Add current character
output.AddRange (held);
output.Add (currentChar); // Add current character
ResetState ();
index++;
}
@@ -152,14 +152,14 @@ internal class AnsiResponseParser
break;
case ParserState.InResponse:
held.Append (currentChar);
held.Add (currentChar);
// Check if the held content should be released
string handled = HandleHeldContent ();
var handled = HandleHeldContent ();
if (!string.IsNullOrEmpty (handled))
if (handled != null)
{
output.Append (handled);
output.AddRange (handled);
ResetState (); // Exit response mode and reset
}
@@ -169,7 +169,7 @@ internal class AnsiResponseParser
}
}
return output.ToString (); // Return all characters that passed through
return output; // Return all characters that passed through
}
/// <summary>
@@ -185,9 +185,9 @@ internal class AnsiResponseParser
/// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected
/// response.
/// </summary>
private string HandleHeldContent ()
private IEnumerable<Tuple<char,T>>? HandleHeldContent ()
{
var cur = held.ToString ();
string cur = HeldToString ();
// Check for expected responses
(string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
@@ -197,25 +197,30 @@ internal class AnsiResponseParser
DispatchResponse (matchingResponse.response);
expectedResponses.Remove (matchingResponse);
return string.Empty;
return null;
}
if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
{
// Detected a response that we were not expecting
return cur;
return held;
}
// Add more cases here for other standard sequences (like arrow keys, function keys, etc.)
// If no match, continue accumulating characters
return string.Empty;
return null;
}
private string HeldToString ()
{
return new string (held.Select (h => h.Item1).ToArray ());
}
private void DispatchResponse (Action<string> response)
{
// If it matches the expected response, invoke the callback and return nothing for output
response?.Invoke (held.ToString ());
response?.Invoke (HeldToString ());
ResetState ();
}

View File

@@ -1443,11 +1443,30 @@ internal class WindowsDriver : ConsoleDriver
#endif
WinConsole?.SetInitialCursorVisibility ();
// Send DAR
WinConsole?.WriteANSI (EscSeqUtils.CSI_SendDeviceAttributes);
Parser.ExpectResponse (EscSeqUtils.CSI_ReportDeviceAttributes_Terminator,
(e) =>
{
// TODO: do something with this
});
return new MainLoop (_mainLoopDriver);
}
private bool firstTime = true;
public AnsiResponseParser<WindowsConsole.InputRecord> Parser { get; set; } = new ();
internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
{
if (firstTime)
{
WinConsole?.WriteANSI (EscSeqUtils.CSI_SendDeviceAttributes);
}
switch (inputEvent.EventType)
{
case WindowsConsole.EventType.Key:
@@ -1470,6 +1489,7 @@ internal class WindowsDriver : ConsoleDriver
break;
}
if (inputEvent.KeyEvent.bKeyDown)
{
// Avoid sending repeat key down events

View File

@@ -6,7 +6,7 @@ using Xunit.Abstractions;
namespace UnitTests.ConsoleDrivers;
public class AnsiResponseParserTests (ITestOutputHelper output)
{
AnsiResponseParser _parser = new AnsiResponseParser ();
AnsiResponseParser<int> _parser = new AnsiResponseParser<int> ();
[Fact]
public void TestInputProcessing ()
@@ -114,7 +114,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
foreach (var batch in batchSet)
{
actualOutput.Append (_parser.ProcessInput (batch));
var output = _parser.ProcessInput (StringToBatch (batch));
actualOutput.Append (BatchToString (output));
}
// Assert the final output minus the expected response
@@ -126,6 +127,11 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" );
}
private Tuple<char, int> [] StringToBatch (string batch)
{
return batch.Select ((k, i) => Tuple.Create (k, i)).ToArray ();
}
public static IEnumerable<string []> GetBatchPermutations (string input, int maxDepth = 3)
{
// Call the recursive method to generate batches with an initial depth of 0
@@ -173,7 +179,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
// Parser does not grab this key (i.e. driver can continue with regular operations)
Assert.Equal ( c,_parser.ProcessInput (c));
Assert.Equal (expected,c.Single());
Assert.Equal (expected,c.Single().Item1);
}
private void AssertConsumed (string ansiStream, ref int i)
{
@@ -187,10 +193,16 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
// Parser realizes it has grabbed content that does not belong to an outstanding request
// Parser returns false to indicate to continue
Assert.Equal(expectedRelease,_parser.ProcessInput (c));
Assert.Equal(expectedRelease,BatchToString(_parser.ProcessInput (c)));
}
private string NextChar (string ansiStream, ref int i)
private string BatchToString (IEnumerable<Tuple<char, int>> processInput)
{
return ansiStream [i++].ToString();
return new string(processInput.Select (a=>a.Item1).ToArray ());
}
private Tuple<char,int>[] NextChar (string ansiStream, ref int i)
{
return StringToBatch(ansiStream [i++].ToString());
}
}