diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 97c294de7..1c5263d31 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -103,57 +103,70 @@ namespace Terminal.Gui; public abstract void ClearHeld (); protected abstract string HeldToString (); - protected abstract void AddToHeld (char c); + protected abstract IEnumerable HeldToObjects (); + protected abstract void AddToHeld (object o); // Base method for processing input - public void ProcessInputBase (Func getCharAtIndex, Action appendOutput, int inputLength) + public void ProcessInputBase ( + Func getCharAtIndex, + Func getObjectAtIndex, + Action appendOutput, + int inputLength) { var index = 0; // Tracks position in the input string while (index < inputLength) { var currentChar = getCharAtIndex (index); + var currentObj = getObjectAtIndex (index); + + bool isEscape = currentChar == '\x1B'; switch (State) { case ParserState.Normal: - if (currentChar == '\x1B') + if (isEscape) { // Escape character detected, move to ExpectingBracket state State = ParserState.ExpectingBracket; - AddToHeld (currentChar); // Hold the escape character + AddToHeld (currentObj); // Hold the escape character } else { // Normal character, append to output - appendOutput (currentChar); + appendOutput (currentObj); } break; case ParserState.ExpectingBracket: + if (isEscape) + { + // Second escape so we must release first + ReleaseHeld (appendOutput, ParserState.ExpectingBracket); + AddToHeld (currentObj); // Hold the new escape + } + else if (currentChar == '[') { // Detected '[', transition to InResponse state State = ParserState.InResponse; - AddToHeld (currentChar); // Hold the '[' + AddToHeld (currentObj); // Hold the '[' } else { // Invalid sequence, release held characters and reset to Normal ReleaseHeld (appendOutput); - appendOutput (currentChar); // Add current character - ResetState (); + appendOutput (currentObj); // Add current character } break; case ParserState.InResponse: - AddToHeld (currentChar); + AddToHeld (currentObj); // Check if the held content should be released if (ShouldReleaseHeldContent ()) { ReleaseHeld (appendOutput); - ResetState (); // Exit response mode and reset } break; } @@ -162,13 +175,17 @@ namespace Terminal.Gui; } } - private void ReleaseHeld (Action appendOutput) + + private void ReleaseHeld (Action appendOutput, ParserState newState = ParserState.Normal) { - foreach (var c in HeldToString ()) + foreach (var o in HeldToObjects ()) { - appendOutput (c); + appendOutput (o); } - } + + State = newState; + ClearHeld (); + } // Common response handler logic protected bool ShouldReleaseHeldContent () @@ -214,7 +231,11 @@ namespace Terminal.Gui; public IEnumerable> ProcessInput (params Tuple [] input) { var output = new List> (); - ProcessInputBase (i => input [i].Item1, c => output.Add (new Tuple (c, input [0].Item2)), input.Length); + ProcessInputBase ( + i => input [i].Item1, + i => input [i], + c => output.Add ((Tuple)c), + input.Length); return output; } @@ -231,7 +252,9 @@ namespace Terminal.Gui; protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ()); - protected override void AddToHeld (char c) => held.Add (new Tuple (c, default!)); + protected override IEnumerable HeldToObjects () => held; + + protected override void AddToHeld (object o) => held.Add ((Tuple)o); } @@ -243,7 +266,11 @@ namespace Terminal.Gui; public string ProcessInput (string input) { var output = new StringBuilder (); - ProcessInputBase (i => input [i], c => output.Append (c), input.Length); + ProcessInputBase ( + i => input [i], + i => input [i], // For string there is no T so object is same as char + c => output.Append ((char)c), + input.Length); return output.ToString (); } public string Release () @@ -257,5 +284,6 @@ namespace Terminal.Gui; protected override string HeldToString () => held.ToString (); - protected override void AddToHeld (char c) => held.Append (c); + protected override IEnumerable HeldToObjects () => held.ToString().Select(c => (object) c).ToArray (); + protected override void AddToHeld (object o) => held.Append ((char)o); } \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 17198ddcb..7f0804299 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -8,6 +8,11 @@ public class AnsiResponseParserTests (ITestOutputHelper output) AnsiResponseParser _parser1 = new AnsiResponseParser (); AnsiResponseParser _parser2 = new AnsiResponseParser (); + /// + /// Used for the T value in batches that are passed to the AnsiResponseParser<int> (parser1) + /// + private int tIndex = 0; + [Fact] public void TestInputProcessing () { @@ -109,6 +114,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output) foreach (var batchSet in permutations) { + tIndex = 0; string response1 = string.Empty; string response2 = string.Empty; @@ -140,9 +146,47 @@ public class AnsiResponseParserTests (ITestOutputHelper output) output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" ); } + [Fact] + public void ReleasesEscapeAfterTimeout () + { + string input = "\x1B"; + int i = 0; + + // Esc on its own looks like it might be an esc sequence so should be consumed + AssertConsumed (input,ref i); + + // We should know when the state changed + Assert.Equal (ParserState.ExpectingBracket, _parser1.State); + Assert.Equal (ParserState.ExpectingBracket, _parser2.State); + + Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date); + Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date); + + AssertManualReleaseIs (input); + } + + + [Fact] + public void TwoExcapesInARow () + { + // Example user presses Esc key then a DAR comes in + string input = "\x1B\x1B"; + int i = 0; + + // First Esc gets grabbed + AssertConsumed (input, ref i); + + // Upon getting the second Esc we should release the first + AssertReleased (input, ref i, "\x1B",0); + + // Assume 50ms or something has passed, lets force release as no new content + // It should be the second escape that gets released (i.e. index 1) + AssertManualReleaseIs (input,1); + } + private Tuple [] StringToBatch (string batch) { - return batch.Select ((k, i) => Tuple.Create (k, i)).ToArray (); + return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); } public static IEnumerable GetBatchPermutations (string input, int maxDepth = 3) @@ -207,15 +251,21 @@ public class AnsiResponseParserTests (ITestOutputHelper output) Assert.Empty (_parser1.ProcessInput(c1)); Assert.Empty (_parser2.ProcessInput (c2.ToString())); } - private void AssertReleased (string ansiStream, ref int i, string expectedRelease) + + private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int[] expectedTValues) { var c2 = ansiStream [i]; var c1 = NextChar (ansiStream, ref i); // Parser realizes it has grabbed content that does not belong to an outstanding request // Parser returns false to indicate to continue - Assert.Equal(expectedRelease,BatchToString(_parser1.ProcessInput (c1))); + var released1 = _parser1.ProcessInput (c1).ToArray (); + Assert.Equal (expectedRelease, BatchToString (released1)); + if (expectedTValues.Length > 0) + { + Assert.True (expectedTValues.SequenceEqual (released1.Select (kv=>kv.Item2))); + } Assert.Equal (expectedRelease, _parser2.ProcessInput (c2.ToString ())); } @@ -229,4 +279,21 @@ public class AnsiResponseParserTests (ITestOutputHelper output) { return StringToBatch(ansiStream [i++].ToString()); } + private void AssertManualReleaseIs (string expectedRelease, params int [] expectedTValues) + { + + // Consumer is responsible for determining this based on e.g. after 50ms + var released1 = _parser1.Release ().ToArray (); + Assert.Equal (expectedRelease, BatchToString (released1)); + + if (expectedTValues.Length > 0) + { + Assert.True (expectedTValues.SequenceEqual (released1.Select (kv => kv.Item2))); + } + + Assert.Equal (expectedRelease, _parser2.Release ()); + + Assert.Equal (ParserState.Normal, _parser1.State); + Assert.Equal (ParserState.Normal, _parser2.State); + } }