From ac09a6add486562205ad812eef36a9f1aa97d1a2 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 13:30:53 +0100 Subject: [PATCH 01/77] Initial test to define the task of the parser --- .../ConsoleDrivers/AnsiResponseParser.cs | 20 +++++ .../ConsoleDrivers/AnsiResponseParserTests.cs | 76 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs create mode 100644 UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs new file mode 100644 index 0000000000..fd4218bddf --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Terminal.Gui; +class AnsiResponseParser +{ + + public bool ConsumeInput (char character, out string? released) + { + // if character is escape + + // start consuming till we see terminator + + released = null; + return false; + } + + public void ExpectResponse (char terminator, Action response) + { + } +} diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs new file mode 100644 index 0000000000..79f0bbba48 --- /dev/null +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -0,0 +1,76 @@ +namespace UnitTests.ConsoleDrivers; +public class AnsiResponseParserTests +{ + AnsiResponseParser _parser = new AnsiResponseParser (); + + [Fact] + public void TestInputProcessing () + { + string ansiStream = "\x1B[<0;10;20M" + // ANSI escape for mouse move at (10, 20) + "Hello" + // User types "Hello" + "\x1B[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR) + + + string? response = null; + + int i = 0; + + // Imagine that we are expecting a DAR + _parser.ExpectResponse ('c',(s)=> response = s); + + // First char is Escape which we must consume incase what follows is the DAR + AssertConsumed (ansiStream, ref i); // Esc + + for (int c = 0; c < "[<0;10;20".Length; c++) + { + AssertConsumed (ansiStream, ref i); + } + + // We see the M terminator + AssertReleased (ansiStream, ref i, "\x1B[<0;10;20M"); + + // Regular user typing + for (int c = 0; c < "Hello".Length; c++) + { + AssertIgnored (ansiStream, ref i); + } + + // Now we have entered the actual DAR we should be consuming these + for (int c = 0; c < "\x1B [0".Length; c++) + { + AssertConsumed (ansiStream, ref i); + } + + // Consume the terminator 'c' and expect this to call the above event + Assert.Null (response); + AssertConsumed (ansiStream, ref i); + Assert.NotNull (response); + Assert.Equal ("\u001b[0c", response); + } + + private void AssertIgnored (string ansiStream, ref int i) + { + // Parser does not grab this key (i.e. driver can continue with regular operations) + Assert.False (_parser.ConsumeInput (NextChar (ansiStream, ref i), out var released)); + Assert.Null (released); + } + private void AssertConsumed (string ansiStream, ref int i) + { + // Parser grabs this key + Assert.True (_parser.ConsumeInput( NextChar (ansiStream, ref i), out var released)); + Assert.Null (released); + } + private void AssertReleased (string ansiStream, ref int i, string expectedRelease) + { + // Parser realizes it has grabbed content that does not belong to an outstanding request + // Parser returns false to indicate to continue + Assert.False(_parser.ConsumeInput (NextChar (ansiStream,ref i), out var released)); + + // Parser releases all the grabbed content back to the driver + Assert.Equal ( released,expectedRelease); + } + private char NextChar (string ansiStream, ref int i) + { + return ansiStream [i++]; + } +} From 7f17f5e8133db7af17894d268b4a4caeea3eb637 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 13:37:25 +0100 Subject: [PATCH 02/77] Add example responses we might see --- .../ConsoleDrivers/AnsiResponseParser.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index fd4218bddf..31aed18eea 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -4,6 +4,26 @@ namespace Terminal.Gui; class AnsiResponseParser { + /* + * ANSI Input Sequences + * + * \x1B[A // Up Arrow key pressed + * \x1B[B // Down Arrow key pressed + * \x1B[C // Right Arrow key pressed + * \x1B[D // Left Arrow key pressed + * \x1B[3~ // Delete key pressed + * \x1B[2~ // Insert key pressed + * \x1B[5~ // Page Up key pressed + * \x1B[6~ // Page Down key pressed + * \x1B[1;5D // Ctrl + Left Arrow + * \x1B[1;5C // Ctrl + Right Arrow + * \x1B[0;10;20M // Mouse button pressed at position (10, 20) + * \x1B[0c // Device Attributes Response (e.g., terminal identification) + */ + + private bool inResponse = false; + + public bool ConsumeInput (char character, out string? released) { // if character is escape From 22e612e4ac9ac90aed9f6b40845d70e8c290b628 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 18:01:20 +0100 Subject: [PATCH 03/77] Explore parser more --- .../ConsoleDrivers/AnsiResponseParser.cs | 63 +++++++++++++++++-- .../ConsoleDrivers/AnsiResponseParserTests.cs | 22 +++---- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 31aed18eea..3ac5b4d683 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Diagnostics; + namespace Terminal.Gui; class AnsiResponseParser { @@ -23,18 +25,69 @@ class AnsiResponseParser private bool inResponse = false; + private StringBuilder held = new StringBuilder(); - public bool ConsumeInput (char character, out string? released) + /// + /// + /// Processes input which may be a single character or multiple. + /// Returns what should be passed on to any downstream input processing + /// (i.e. removes expected Ansi responses from the input stream + /// + /// + /// This method is designed to be called iteratively and as such may + /// return more characters than were passed in depending on previous + /// calls (e.g. if it was in the middle of an unrelated ANSI response. + /// + /// + /// + public string ProcessInput (string input) { + + if (inResponse) + { + if (currentTerminator != null && input.StartsWith (currentTerminator)) + { + // Consume terminator and release the event + held.Append (currentTerminator); + currentResponse?.Invoke (held.ToString()); + + // clear the state + held.Clear (); + currentResponse = null; + + // recurse + return ProcessInput (input.Substring (currentTerminator.Length)); + } + + // we are in a response but have not reached terminator yet + held.Append (input [0]); + return ProcessInput (input.Substring (1)); + } + + // if character is escape + if (input.StartsWith ('\x1B')) + { + // We shouldn't get an escape in the middle of a response - TODO: figure out how to handle that + Debug.Assert (!inResponse); - // start consuming till we see terminator - released = null; - return false; + // consume the escape + held.Append (input [0]); + inResponse = true; + return ProcessInput (input.Substring (1)); + } + + return input[0] + ProcessInput (input.Substring (1)); } - public void ExpectResponse (char terminator, Action response) + private string? currentTerminator = null; + private Action? currentResponse = null; + + public void ExpectResponse (string terminator, Action response) { + currentTerminator = terminator; + currentResponse = response; + } } diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 79f0bbba48..ab72288b1e 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -16,7 +16,7 @@ public void TestInputProcessing () int i = 0; // Imagine that we are expecting a DAR - _parser.ExpectResponse ('c',(s)=> response = s); + _parser.ExpectResponse ("c",(s)=> response = s); // First char is Escape which we must consume incase what follows is the DAR AssertConsumed (ansiStream, ref i); // Esc @@ -50,27 +50,27 @@ public void TestInputProcessing () private void AssertIgnored (string ansiStream, ref int i) { + var c = NextChar (ansiStream, ref i); + // Parser does not grab this key (i.e. driver can continue with regular operations) - Assert.False (_parser.ConsumeInput (NextChar (ansiStream, ref i), out var released)); - Assert.Null (released); + Assert.Equal ( c,_parser.ProcessInput (c)); } private void AssertConsumed (string ansiStream, ref int i) { // Parser grabs this key - Assert.True (_parser.ConsumeInput( NextChar (ansiStream, ref i), out var released)); - Assert.Null (released); + var c = NextChar (ansiStream, ref i); + Assert.Empty (_parser.ProcessInput(c)); } private void AssertReleased (string ansiStream, ref int i, string expectedRelease) { + var c = 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.False(_parser.ConsumeInput (NextChar (ansiStream,ref i), out var released)); - - // Parser releases all the grabbed content back to the driver - Assert.Equal ( released,expectedRelease); + Assert.Equal(expectedRelease,_parser.ProcessInput (c)); } - private char NextChar (string ansiStream, ref int i) + private string NextChar (string ansiStream, ref int i) { - return ansiStream [i++]; + return ansiStream [i++].ToString(); } } From 75ec58907331720962abd24d27a314ceeed6966f Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 18:17:35 +0100 Subject: [PATCH 04/77] prepare to handle more rules --- .../ConsoleDrivers/AnsiResponseParser.cs | 112 ++++++++++++------ 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 3ac5b4d683..721913d2a5 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -1,10 +1,18 @@ #nullable enable using System.Diagnostics; +using System.Text; namespace Terminal.Gui; class AnsiResponseParser { + private bool inResponse = false; + private StringBuilder held = new StringBuilder (); + private string? currentTerminator = null; + private Action? currentResponse = null; + + + private List> _ignorers = new (); /* * ANSI Input Sequences @@ -23,71 +31,97 @@ class AnsiResponseParser * \x1B[0c // Device Attributes Response (e.g., terminal identification) */ - private bool inResponse = false; - - private StringBuilder held = new StringBuilder(); + public AnsiResponseParser () + { + // How to spot when you have entered and left an AnsiResponse but not the one we are looking for + _ignorers.Add (s=>s.StartsWith ("\x1B[<") && s.EndsWith ("M")); + } /// - /// /// Processes input which may be a single character or multiple. /// Returns what should be passed on to any downstream input processing - /// (i.e. removes expected Ansi responses from the input stream - /// - /// - /// This method is designed to be called iteratively and as such may - /// return more characters than were passed in depending on previous - /// calls (e.g. if it was in the middle of an unrelated ANSI response. + /// (i.e., removes expected ANSI responses from the input stream). /// - /// - /// public string ProcessInput (string input) { + StringBuilder output = new StringBuilder (); // Holds characters that should pass through + int index = 0; // Tracks position in the input string - if (inResponse) + while (index < input.Length) { - if (currentTerminator != null && input.StartsWith (currentTerminator)) - { - // Consume terminator and release the event - held.Append (currentTerminator); - currentResponse?.Invoke (held.ToString()); + char currentChar = input [index]; - // clear the state - held.Clear (); - currentResponse = null; + if (inResponse) + { + // If we are in a response, accumulate characters in `held` + held.Append (currentChar); + + // Handle the current content in `held` + var handled = HandleHeldContent (); + if (!string.IsNullOrEmpty (handled)) + { + // If content is ready to be released, append it to output and reset state + output.Append (handled); + inResponse = false; + held.Clear (); + } + + index++; + continue; + } - // recurse - return ProcessInput (input.Substring (currentTerminator.Length)); + // If character is the start of an escape sequence + if (currentChar == '\x1B') + { + // Start capturing the ANSI response sequence + inResponse = true; + held.Append (currentChar); + index++; + continue; } - // we are in a response but have not reached terminator yet - held.Append (input [0]); - return ProcessInput (input.Substring (1)); + // If not in an ANSI response, pass the character through as regular input + output.Append (currentChar); + index++; } + // Return characters that should pass through as regular input + return output.ToString (); + } - // if character is escape - if (input.StartsWith ('\x1B')) + /// + /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected response. + /// + private string HandleHeldContent () + { + // If we're expecting a specific terminator, check if the content matches + if (currentTerminator != null && held.ToString ().EndsWith (currentTerminator)) { - // We shouldn't get an escape in the middle of a response - TODO: figure out how to handle that - Debug.Assert (!inResponse); - + // If it matches the expected response, invoke the callback and return nothing for output + currentResponse?.Invoke (held.ToString ()); + return string.Empty; + } - // consume the escape - held.Append (input [0]); - inResponse = true; - return ProcessInput (input.Substring (1)); + // Handle common ANSI sequences (such as mouse input or arrow keys) + if (_ignorers.Any(m=>m.Invoke (held.ToString()))) + { + // Detected mouse input, release it without triggering the delegate + return held.ToString (); } - return input[0] + ProcessInput (input.Substring (1)); + // Add more cases here for other standard sequences (like arrow keys, function keys, etc.) + + // If no match, continue accumulating characters + return string.Empty; } - private string? currentTerminator = null; - private Action? currentResponse = null; + /// + /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed. + /// public void ExpectResponse (string terminator, Action response) { currentTerminator = terminator; currentResponse = response; - } } From a0c33632104cf96cb0a242a57c3f77f80ce62ca2 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 18:39:31 +0100 Subject: [PATCH 05/77] Update handling to have state --- .../ConsoleDrivers/AnsiResponseParser.cs | 117 +++++++++++++----- .../ConsoleDrivers/AnsiResponseParserTests.cs | 7 +- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 721913d2a5..83133c624b 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -14,6 +14,17 @@ class AnsiResponseParser private List> _ignorers = new (); + // Enum to manage the parser's state + private enum ParserState + { + Normal, + ExpectingBracket, + InResponse + } + + // Current state of the parser + private ParserState currentState = ParserState.Normal; + /* * ANSI Input Sequences * @@ -33,10 +44,20 @@ class AnsiResponseParser public AnsiResponseParser () { - // How to spot when you have entered and left an AnsiResponse but not the one we are looking for - _ignorers.Add (s=>s.StartsWith ("\x1B[<") && s.EndsWith ("M")); + // Add more common ANSI sequences to be ignored + _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event + _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("A")); // Up arrow + _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("B")); // Down arrow + _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("C")); // Right arrow + _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("D")); // Left arrow + _ignorers.Add (s => s.StartsWith ("\x1B[3~")); // Delete + _ignorers.Add (s => s.StartsWith ("\x1B[5~")); // Page Up + _ignorers.Add (s => s.StartsWith ("\x1B[6~")); // Page Down + _ignorers.Add (s => s.StartsWith ("\x1B[2~")); // Insert + // Add more if necessary } + /// /// Processes input which may be a single character or multiple. /// Returns what should be passed on to any downstream input processing @@ -51,42 +72,71 @@ public string ProcessInput (string input) { char currentChar = input [index]; - if (inResponse) + switch (currentState) { - // If we are in a response, accumulate characters in `held` - held.Append (currentChar); - - // Handle the current content in `held` - var handled = HandleHeldContent (); - if (!string.IsNullOrEmpty (handled)) - { - // If content is ready to be released, append it to output and reset state - output.Append (handled); - inResponse = false; - held.Clear (); - } - - index++; - continue; + case ParserState.Normal: + if (currentChar == '\x1B') + { + // Escape character detected, move to ExpectingBracket state + currentState = ParserState.ExpectingBracket; + held.Append (currentChar); // Hold the escape character + index++; + } + else + { + // Normal character, append to output + output.Append (currentChar); + index++; + } + break; + + case ParserState.ExpectingBracket: + if (currentChar == '[' || currentChar == ']') + { + // Detected '[' or ']', transition to InResponse state + currentState = ParserState.InResponse; + held.Append (currentChar); // Hold the '[' or ']' + index++; + } + else + { + // Invalid sequence, release held characters and reset to Normal + output.Append (held.ToString ()); + output.Append (currentChar); // Add current character + ResetState (); + index++; + } + break; + + case ParserState.InResponse: + held.Append (currentChar); + + // Check if the held content should be released + var handled = HandleHeldContent (); + if (!string.IsNullOrEmpty (handled)) + { + output.Append (handled); + ResetState (); // Exit response mode and reset + } + + index++; + break; } + } - // If character is the start of an escape sequence - if (currentChar == '\x1B') - { - // Start capturing the ANSI response sequence - inResponse = true; - held.Append (currentChar); - index++; - continue; - } + return output.ToString (); // Return all characters that passed through + } - // If not in an ANSI response, pass the character through as regular input - output.Append (currentChar); - index++; - } - // Return characters that should pass through as regular input - return output.ToString (); + /// + /// Resets the parser's state when a response is handled or finished. + /// + private void ResetState () + { + currentState = ParserState.Normal; + held.Clear (); + currentTerminator = null; + currentResponse = null; } /// @@ -124,4 +174,5 @@ public void ExpectResponse (string terminator, Action response) currentTerminator = terminator; currentResponse = response; } + } diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index ab72288b1e..bd538d01eb 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -32,11 +32,11 @@ public void TestInputProcessing () // Regular user typing for (int c = 0; c < "Hello".Length; c++) { - AssertIgnored (ansiStream, ref i); + AssertIgnored (ansiStream,"Hello"[c], ref i); } // Now we have entered the actual DAR we should be consuming these - for (int c = 0; c < "\x1B [0".Length; c++) + for (int c = 0; c < "\x1B[0".Length; c++) { AssertConsumed (ansiStream, ref i); } @@ -48,12 +48,13 @@ public void TestInputProcessing () Assert.Equal ("\u001b[0c", response); } - private void AssertIgnored (string ansiStream, ref int i) + private void AssertIgnored (string ansiStream,char expected, ref int i) { var c = NextChar (ansiStream, ref i); // 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()); } private void AssertConsumed (string ansiStream, ref int i) { From 6cef9d72fe1c50e5b257ed30c1726fd728aaafa8 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 18:51:56 +0100 Subject: [PATCH 06/77] Fix ResetState and add DispatchResponse --- Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 83133c624b..eef5525869 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -135,8 +135,6 @@ private void ResetState () { currentState = ParserState.Normal; held.Clear (); - currentTerminator = null; - currentResponse = null; } /// @@ -147,8 +145,7 @@ private string HandleHeldContent () // If we're expecting a specific terminator, check if the content matches if (currentTerminator != null && held.ToString ().EndsWith (currentTerminator)) { - // If it matches the expected response, invoke the callback and return nothing for output - currentResponse?.Invoke (held.ToString ()); + DispatchResponse (); return string.Empty; } @@ -165,6 +162,14 @@ private string HandleHeldContent () return string.Empty; } + private void DispatchResponse () + { + // If it matches the expected response, invoke the callback and return nothing for output + currentResponse?.Invoke (held.ToString ()); + currentResponse = null; + currentTerminator = null; + ResetState (); + } /// /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed. From 390f2d01a4f6b909e5bbf6f18d9110f55335f8d5 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 19:37:09 +0100 Subject: [PATCH 07/77] Change to listing known terminators according to CSI spec --- .../ConsoleDrivers/AnsiResponseParser.cs | 140 +++++++++++++----- 1 file changed, 100 insertions(+), 40 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index eef5525869..92b9d23046 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -1,18 +1,14 @@ #nullable enable -using System.Diagnostics; -using System.Text; - namespace Terminal.Gui; -class AnsiResponseParser -{ - private bool inResponse = false; - private StringBuilder held = new StringBuilder (); - private string? currentTerminator = null; - private Action? currentResponse = null; +internal class AnsiResponseParser +{ + private readonly StringBuilder held = new (); + private string? currentTerminator; + private Action? currentResponse; - private List> _ignorers = new (); + private readonly List> _ignorers = new (); // Enum to manage the parser's state private enum ParserState @@ -24,6 +20,7 @@ private enum ParserState // Current state of the parser private ParserState currentState = ParserState.Normal; + private HashSet _knownTerminators = new HashSet (); /* * ANSI Input Sequences @@ -44,29 +41,79 @@ private enum ParserState public AnsiResponseParser () { + // These all are valid terminators on ansi responses, + // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s + _knownTerminators.Add ("@"); + _knownTerminators.Add ("A"); + _knownTerminators.Add ("B"); + _knownTerminators.Add ("C"); + _knownTerminators.Add ("D"); + _knownTerminators.Add ("E"); + _knownTerminators.Add ("F"); + _knownTerminators.Add ("G"); + _knownTerminators.Add ("G"); + _knownTerminators.Add ("H"); + _knownTerminators.Add ("I"); + _knownTerminators.Add ("J"); + _knownTerminators.Add ("K"); + _knownTerminators.Add ("L"); + _knownTerminators.Add ("M"); + // No - N or O + _knownTerminators.Add ("P"); + _knownTerminators.Add ("Q"); + _knownTerminators.Add ("R"); + _knownTerminators.Add ("S"); + _knownTerminators.Add ("T"); + _knownTerminators.Add ("W"); + _knownTerminators.Add ("X"); + _knownTerminators.Add ("Z"); + + _knownTerminators.Add ("^"); + _knownTerminators.Add ("`"); + _knownTerminators.Add ("~"); + + _knownTerminators.Add ("a"); + _knownTerminators.Add ("b"); + _knownTerminators.Add ("c"); + _knownTerminators.Add ("d"); + _knownTerminators.Add ("e"); + _knownTerminators.Add ("f"); + _knownTerminators.Add ("g"); + _knownTerminators.Add ("h"); + _knownTerminators.Add ("i"); + + + _knownTerminators.Add ("l"); + _knownTerminators.Add ("m"); + _knownTerminators.Add ("n"); + + _knownTerminators.Add ("p"); + _knownTerminators.Add ("q"); + _knownTerminators.Add ("r"); + _knownTerminators.Add ("s"); + _knownTerminators.Add ("t"); + _knownTerminators.Add ("u"); + _knownTerminators.Add ("v"); + _knownTerminators.Add ("w"); + _knownTerminators.Add ("x"); + _knownTerminators.Add ("y"); + _knownTerminators.Add ("z"); + // Add more common ANSI sequences to be ignored - _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event - _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("A")); // Up arrow - _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("B")); // Down arrow - _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("C")); // Right arrow - _ignorers.Add (s => s.StartsWith ("\x1B[") && s.EndsWith ("D")); // Left arrow - _ignorers.Add (s => s.StartsWith ("\x1B[3~")); // Delete - _ignorers.Add (s => s.StartsWith ("\x1B[5~")); // Page Up - _ignorers.Add (s => s.StartsWith ("\x1B[6~")); // Page Down - _ignorers.Add (s => s.StartsWith ("\x1B[2~")); // Insert + _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event + // Add more if necessary } - /// - /// Processes input which may be a single character or multiple. - /// Returns what should be passed on to any downstream input processing - /// (i.e., removes expected ANSI responses from the input stream). + /// Processes input which may be a single character or multiple. + /// Returns what should be passed on to any downstream input processing + /// (i.e., removes expected ANSI responses from the input stream). /// public string ProcessInput (string input) { - StringBuilder output = new StringBuilder (); // Holds characters that should pass through - int index = 0; // Tracks position in the input string + var output = new StringBuilder (); // Holds characters that should pass through + var index = 0; // Tracks position in the input string while (index < input.Length) { @@ -79,7 +126,7 @@ public string ProcessInput (string input) { // Escape character detected, move to ExpectingBracket state currentState = ParserState.ExpectingBracket; - held.Append (currentChar); // Hold the escape character + held.Append (currentChar); // Hold the escape character index++; } else @@ -88,48 +135,51 @@ public string ProcessInput (string input) output.Append (currentChar); index++; } + break; case ParserState.ExpectingBracket: - if (currentChar == '[' || currentChar == ']') + if (currentChar == '[' ) { - // Detected '[' or ']', transition to InResponse state + // Detected '[' , transition to InResponse state currentState = ParserState.InResponse; - held.Append (currentChar); // Hold the '[' or ']' + held.Append (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.Append (currentChar); // Add current character ResetState (); index++; } + break; case ParserState.InResponse: held.Append (currentChar); // Check if the held content should be released - var handled = HandleHeldContent (); + string handled = HandleHeldContent (); + if (!string.IsNullOrEmpty (handled)) { output.Append (handled); - ResetState (); // Exit response mode and reset + ResetState (); // Exit response mode and reset } index++; + break; } } - return output.ToString (); // Return all characters that passed through + return output.ToString (); // Return all characters that passed through } - /// - /// Resets the parser's state when a response is handled or finished. + /// Resets the parser's state when a response is handled or finished. /// private void ResetState () { @@ -138,19 +188,29 @@ private void ResetState () } /// - /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected response. + /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected + /// response. /// private string HandleHeldContent () { + var cur = held.ToString (); // If we're expecting a specific terminator, check if the content matches - if (currentTerminator != null && held.ToString ().EndsWith (currentTerminator)) + if (currentTerminator != null && cur.EndsWith (currentTerminator)) { DispatchResponse (); + return string.Empty; } + + if (_knownTerminators.Any (cur.EndsWith) && cur.StartsWith (EscSeqUtils.CSI)) + { + // Detected a response that we were not expecting + return held.ToString (); + } + // Handle common ANSI sequences (such as mouse input or arrow keys) - if (_ignorers.Any(m=>m.Invoke (held.ToString()))) + if (_ignorers.Any (m => m.Invoke (held.ToString ()))) { // Detected mouse input, release it without triggering the delegate return held.ToString (); @@ -172,12 +232,12 @@ private void DispatchResponse () } /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed. + /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is + /// completed. /// public void ExpectResponse (string terminator, Action response) { currentTerminator = terminator; currentResponse = response; } - } From af2ea009c46183db4f5f6611941ecb01716d602c Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 19:50:33 +0100 Subject: [PATCH 08/77] Allow multiple expected responses at once --- .../ConsoleDrivers/AnsiResponseParser.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 92b9d23046..68fe3391a2 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -5,8 +5,7 @@ namespace Terminal.Gui; internal class AnsiResponseParser { private readonly StringBuilder held = new (); - private string? currentTerminator; - private Action? currentResponse; + private readonly List<(string terminator, Action response)> expectedResponses = new (); private readonly List> _ignorers = new (); @@ -20,7 +19,7 @@ private enum ParserState // Current state of the parser private ParserState currentState = ParserState.Normal; - private HashSet _knownTerminators = new HashSet (); + private readonly HashSet _knownTerminators = new (); /* * ANSI Input Sequences @@ -58,6 +57,7 @@ public AnsiResponseParser () _knownTerminators.Add ("K"); _knownTerminators.Add ("L"); _knownTerminators.Add ("M"); + // No - N or O _knownTerminators.Add ("P"); _knownTerminators.Add ("Q"); @@ -82,7 +82,6 @@ public AnsiResponseParser () _knownTerminators.Add ("h"); _knownTerminators.Add ("i"); - _knownTerminators.Add ("l"); _knownTerminators.Add ("m"); _knownTerminators.Add ("n"); @@ -139,7 +138,7 @@ public string ProcessInput (string input) break; case ParserState.ExpectingBracket: - if (currentChar == '[' ) + if (currentChar == '[') { // Detected '[' , transition to InResponse state currentState = ParserState.InResponse; @@ -194,15 +193,18 @@ private void ResetState () private string HandleHeldContent () { var cur = held.ToString (); - // If we're expecting a specific terminator, check if the content matches - if (currentTerminator != null && cur.EndsWith (currentTerminator)) + + // Check for expected responses + (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); + + if (matchingResponse.response != null) { - DispatchResponse (); + DispatchResponse (matchingResponse.response); + expectedResponses.Remove (matchingResponse); return string.Empty; } - if (_knownTerminators.Any (cur.EndsWith) && cur.StartsWith (EscSeqUtils.CSI)) { // Detected a response that we were not expecting @@ -222,12 +224,10 @@ private string HandleHeldContent () return string.Empty; } - private void DispatchResponse () + private void DispatchResponse (Action response) { // If it matches the expected response, invoke the callback and return nothing for output - currentResponse?.Invoke (held.ToString ()); - currentResponse = null; - currentTerminator = null; + response?.Invoke (held.ToString ()); ResetState (); } @@ -235,9 +235,5 @@ private void DispatchResponse () /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is /// completed. /// - public void ExpectResponse (string terminator, Action response) - { - currentTerminator = terminator; - currentResponse = response; - } + public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } } From 3af5c6a4fb16480240fc5fce2c78bad2ea42b1e8 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 20:18:19 +0100 Subject: [PATCH 09/77] Test all permutations of the input stream --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index bd538d01eb..495e811e8a 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,5 +1,10 @@ -namespace UnitTests.ConsoleDrivers; -public class AnsiResponseParserTests +using System.Diagnostics; +using System.Text; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using Xunit.Abstractions; + +namespace UnitTests.ConsoleDrivers; +public class AnsiResponseParserTests (ITestOutputHelper output) { AnsiResponseParser _parser = new AnsiResponseParser (); @@ -48,6 +53,80 @@ public void TestInputProcessing () Assert.Equal ("\u001b[0c", response); } + + + [Theory] + [InlineData ("\x1B[<0;10;20MHello\x1B[0c", "c", "\u001b[0c", "\x1B[<0;10;20MHello")] + [InlineData ("\x1B[<1;15;25MWorld\x1B[1c", "c", "\u001b[1c", "\x1B[<1;15;25MWorld")] + // Add more test cases here... + public void TestInputSequences (string ansiStream, string expectedTerminator, string expectedResponse, string expectedOutput) + { + var swGenBatches = Stopwatch.StartNew (); + int tests = 0; + + var permutations = GetBatchPermutations (ansiStream).ToArray (); + + swGenBatches.Stop (); + var swRunTest = Stopwatch.StartNew (); + + foreach (var batchSet in permutations) + { + string? response = null; + + // Register the expected response with the given terminator + _parser.ExpectResponse (expectedTerminator, s => response = s); + + // Process the input + StringBuilder actualOutput = new StringBuilder (); + + foreach (var batch in batchSet) + { + actualOutput.Append (_parser.ProcessInput (batch)); + } + + // Assert the final output minus the expected response + Assert.Equal (expectedOutput, actualOutput.ToString()); + Assert.Equal (expectedResponse, response); + tests++; + } + + output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" ); + } + + public static IEnumerable GetBatchPermutations (string input) + { + // Call the recursive method to generate batches + return GenerateBatches (input, 0); + } + + private static IEnumerable GenerateBatches (string input, int start) + { + // If we have reached the end of the string, return an empty list + if (start >= input.Length) + { + yield return new string [0]; + yield break; + } + + // Iterate over the input string to create batches + for (int i = start + 1; i <= input.Length; i++) + { + // Take a batch from 'start' to 'i' + string batch = input.Substring (start, i - start); + + // Recursively get batches from the remaining substring + foreach (var remainingBatches in GenerateBatches (input, i)) + { + // Combine the current batch with the remaining batches + var result = new string [1 + remainingBatches.Length]; + result [0] = batch; + Array.Copy (remainingBatches, 0, result, 1, remainingBatches.Length); + yield return result; + } + } + } + + private void AssertIgnored (string ansiStream,char expected, ref int i) { var c = NextChar (ansiStream, ref i); From 964196d3b9fe26d61b40ea83fec859dfb67f8949 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 20:28:38 +0100 Subject: [PATCH 10/77] Change to hashmap char since all terminators we ignore are single character --- .../ConsoleDrivers/AnsiResponseParser.cs | 124 ++++++++---------- 1 file changed, 56 insertions(+), 68 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 68fe3391a2..9fe3b7350a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -7,8 +7,6 @@ internal class AnsiResponseParser private readonly StringBuilder held = new (); private readonly List<(string terminator, Action response)> expectedResponses = new (); - private readonly List> _ignorers = new (); - // Enum to manage the parser's state private enum ParserState { @@ -19,7 +17,7 @@ private enum ParserState // Current state of the parser private ParserState currentState = ParserState.Normal; - private readonly HashSet _knownTerminators = new (); + private readonly HashSet _knownTerminators = new (); /* * ANSI Input Sequences @@ -42,64 +40,61 @@ public AnsiResponseParser () { // These all are valid terminators on ansi responses, // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s - _knownTerminators.Add ("@"); - _knownTerminators.Add ("A"); - _knownTerminators.Add ("B"); - _knownTerminators.Add ("C"); - _knownTerminators.Add ("D"); - _knownTerminators.Add ("E"); - _knownTerminators.Add ("F"); - _knownTerminators.Add ("G"); - _knownTerminators.Add ("G"); - _knownTerminators.Add ("H"); - _knownTerminators.Add ("I"); - _knownTerminators.Add ("J"); - _knownTerminators.Add ("K"); - _knownTerminators.Add ("L"); - _knownTerminators.Add ("M"); + _knownTerminators.Add ('@'); + _knownTerminators.Add ('A'); + _knownTerminators.Add ('B'); + _knownTerminators.Add ('C'); + _knownTerminators.Add ('D'); + _knownTerminators.Add ('E'); + _knownTerminators.Add ('F'); + _knownTerminators.Add ('G'); + _knownTerminators.Add ('G'); + _knownTerminators.Add ('H'); + _knownTerminators.Add ('I'); + _knownTerminators.Add ('J'); + _knownTerminators.Add ('K'); + _knownTerminators.Add ('L'); + _knownTerminators.Add ('M'); // No - N or O - _knownTerminators.Add ("P"); - _knownTerminators.Add ("Q"); - _knownTerminators.Add ("R"); - _knownTerminators.Add ("S"); - _knownTerminators.Add ("T"); - _knownTerminators.Add ("W"); - _knownTerminators.Add ("X"); - _knownTerminators.Add ("Z"); - - _knownTerminators.Add ("^"); - _knownTerminators.Add ("`"); - _knownTerminators.Add ("~"); - - _knownTerminators.Add ("a"); - _knownTerminators.Add ("b"); - _knownTerminators.Add ("c"); - _knownTerminators.Add ("d"); - _knownTerminators.Add ("e"); - _knownTerminators.Add ("f"); - _knownTerminators.Add ("g"); - _knownTerminators.Add ("h"); - _knownTerminators.Add ("i"); - - _knownTerminators.Add ("l"); - _knownTerminators.Add ("m"); - _knownTerminators.Add ("n"); - - _knownTerminators.Add ("p"); - _knownTerminators.Add ("q"); - _knownTerminators.Add ("r"); - _knownTerminators.Add ("s"); - _knownTerminators.Add ("t"); - _knownTerminators.Add ("u"); - _knownTerminators.Add ("v"); - _knownTerminators.Add ("w"); - _knownTerminators.Add ("x"); - _knownTerminators.Add ("y"); - _knownTerminators.Add ("z"); - - // Add more common ANSI sequences to be ignored - _ignorers.Add (s => s.StartsWith ("\x1B[<") && s.EndsWith ("M")); // Mouse event + _knownTerminators.Add ('P'); + _knownTerminators.Add ('Q'); + _knownTerminators.Add ('R'); + _knownTerminators.Add ('S'); + _knownTerminators.Add ('T'); + _knownTerminators.Add ('W'); + _knownTerminators.Add ('X'); + _knownTerminators.Add ('Z'); + + _knownTerminators.Add ('^'); + _knownTerminators.Add ('`'); + _knownTerminators.Add ('~'); + + _knownTerminators.Add ('a'); + _knownTerminators.Add ('b'); + _knownTerminators.Add ('c'); + _knownTerminators.Add ('d'); + _knownTerminators.Add ('e'); + _knownTerminators.Add ('f'); + _knownTerminators.Add ('g'); + _knownTerminators.Add ('h'); + _knownTerminators.Add ('i'); + + _knownTerminators.Add ('l'); + _knownTerminators.Add ('m'); + _knownTerminators.Add ('n'); + + _knownTerminators.Add ('p'); + _knownTerminators.Add ('q'); + _knownTerminators.Add ('r'); + _knownTerminators.Add ('s'); + _knownTerminators.Add ('t'); + _knownTerminators.Add ('u'); + _knownTerminators.Add ('v'); + _knownTerminators.Add ('w'); + _knownTerminators.Add ('x'); + _knownTerminators.Add ('y'); + _knownTerminators.Add ('z'); // Add more if necessary } @@ -205,17 +200,10 @@ private string HandleHeldContent () return string.Empty; } - if (_knownTerminators.Any (cur.EndsWith) && cur.StartsWith (EscSeqUtils.CSI)) + if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) { // Detected a response that we were not expecting - return held.ToString (); - } - - // Handle common ANSI sequences (such as mouse input or arrow keys) - if (_ignorers.Any (m => m.Invoke (held.ToString ()))) - { - // Detected mouse input, release it without triggering the delegate - return held.ToString (); + return cur; } // Add more cases here for other standard sequences (like arrow keys, function keys, etc.) From b83219d82cb9c091453936aef205b1ab515c0524 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 11 Oct 2024 21:05:27 +0100 Subject: [PATCH 11/77] WIP - Add lots of tests, needs validation as expectations are wrong in many that are failing (AI) --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 495e811e8a..096dc0a2dc 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -50,28 +50,62 @@ public void TestInputProcessing () Assert.Null (response); AssertConsumed (ansiStream, ref i); Assert.NotNull (response); - Assert.Equal ("\u001b[0c", response); + Assert.Equal ("\x1B[0c", response); } - - [Theory] - [InlineData ("\x1B[<0;10;20MHello\x1B[0c", "c", "\u001b[0c", "\x1B[<0;10;20MHello")] - [InlineData ("\x1B[<1;15;25MWorld\x1B[1c", "c", "\u001b[1c", "\x1B[<1;15;25MWorld")] + [InlineData ("\x1B[<0;10;20MHi\x1B[0c", "c", "\x1B[0c", "\x1B[<0;10;20MHi")] + [InlineData ("\x1B[<1;15;25MYou\x1B[1c", "c", "\x1B[1c", "\x1B[<1;15;25MYou")] + [InlineData ("\x1B[0cHi\x1B[0c", "c", "\x1B[0c", "Hi\x1B[0c")] // Consume the first response but pass through the second + + [InlineData ("\x1B[<0;0;0MHe\x1B[3c", "c", "\x1B[3c", "\x1B[<0;0;0MHe")] // Short input + [InlineData ("\x1B[<0;1;2Da\x1B[0c\x1B[1c", "c", "\x1B[0c", "\x1B[<0;1;2Da\x1B[1c")] // Two responses, consume only the first + [InlineData ("\x1B[1;1M\x1B[3cAn", "c", "\x1B[3c", "\x1B[1;1MAn")] // Response with a preceding escape sequence + [InlineData ("hi\x1B[2c\x1B[<5;5;5m", "c", "\x1B[2c", "hi\x1B[<5;5;5m")] // Mixed normal and escape sequences + [InlineData ("\x1B[3c\x1B[4c\x1B[<0;0;0MIn", "c", "\x1B[0c", "\x1B[3c\x1B[4c\x1B[<0;0;0MIn")] // Multiple consecutive responses + [InlineData ("\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M\x1B[2c", "c", "\x1B[2c", "\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M")] // Interleaved responses + [InlineData ("\x1B[<0;1;1MHi\x1B[6c\x1B[2c\x1B[<1;0;0MT", "c", "\x1B[6c", "\x1B[<0;1;1MHi\x1B[2c\x1B[<1;0;0MT")] // Mixed input with multiple responses + [InlineData ("Te\x1B[<2;2;2M\x1B[7c", "c", "\x1B[7c", "Te\x1B[<2;2;2M")] // Text followed by escape sequence + [InlineData ("\x1B[0c\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT", "c", "\x1B[1;0M", "\x1B[<0;0;0M\x1B[3c\x1B[0cT")] // Multiple escape sequences, with expected response in between + [InlineData ("\x1B[0;0M\x1B[<0;0;0M\x1B[3cT\x1B[1c", "c", "\x1B[1c", "\x1B[0;0M\x1B[<0;0;0MT")] // Edge case with leading escape + [InlineData ("\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn\x1B[1c", "c", "\x1B[1c", "\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn")] // Multiple unexpected escape sequences + [InlineData ("\x1B[<5;5;5M\x1B[7cEx\x1B[8c", "c", "\x1B[8c", "\x1B[<5;5;5MEx")] // Extra sequences with no expected responses + + // Random characters and mixed inputs + [InlineData ("\x1B[<1;1;1MJJ\x1B[9c", "c", "\x1B[9c", "\x1B[<1;1;1MJJ")] // Mixed text + [InlineData ("Be\x1B[0cAf", "c", "\x1B[0c", "BeAf")] // Escape in the middle of the string + [InlineData ("\x1B[<0;0;0M\x1B[2cNot e", "c", "\x1B[2c", "\x1B[<0;0;0MNot e")] // Unexpected sequence followed by text + [InlineData ("Just te\x1B[<0;0;0M\x1B[3c\x1B[2c\x1B[4c", "c", "\x1B[4c", "Just te\x1B[<0;0;0M\x1B[3c\x1B[2c")] // Multiple unexpected responses + [InlineData ("\x1B[1;2;3M\x1B[0c\x1B[2;2M\x1B[0;0;0MTe", "c", "\x1B[0c", "\x1B[1;2;3M\x1B[2;2MTe")] // Multiple commands with responses + [InlineData ("\x1B[<3;3;3Mabc\x1B[4cde", "c", "\x1B[4c", "\x1B[<3;3;3Mabcde")] // Escape sequences mixed with regular text + + // Edge cases + [InlineData ("\x1B[0c\x1B[0c\x1B[0c", "c", "\x1B[0c", "\x1B[0c\x1B[0c")] // Multiple identical responses + [InlineData ("", "c", "", "")] // Empty input + [InlineData ("Normal", "c", "", "Normal")] // No escape sequences + [InlineData ("\x1B[<0;0;0M", "c", "", "\x1B[<0;0;0M")] // Escape sequence only + [InlineData ("\x1B[1;2;3M\x1B[0c", "c", "\x1B[0c", "\x1B[1;2;3M")] // Last response consumed + + [InlineData ("Inpu\x1B[0c\x1B[1;0;0M", "c", "\x1B[0c", "Inpu\x1B[1;0;0M")] // Single input followed by escape + [InlineData ("\x1B[2c\x1B[<5;6;7MDa", "c", "\x1B[2c", "\x1B[<5;6;7MDa")] // Multiple escape sequences followed by text + [InlineData ("\x1B[0cHi\x1B[1cGo", "c", "\x1B[1c", "HiGo")] // Normal text with multiple escape sequences + + [InlineData ("\x1B[<1;1;1MTe", "c", "\x1B[1c", "\x1B[<1;1;1MTe")] // Edge case of maximum length // Add more test cases here... public void TestInputSequences (string ansiStream, string expectedTerminator, string expectedResponse, string expectedOutput) { + var swGenBatches = Stopwatch.StartNew (); int tests = 0; - var permutations = GetBatchPermutations (ansiStream).ToArray (); + var permutations = GetBatchPermutations (ansiStream,7).ToArray (); swGenBatches.Stop (); var swRunTest = Stopwatch.StartNew (); foreach (var batchSet in permutations) { - string? response = null; + string response = string.Empty; // Register the expected response with the given terminator _parser.ExpectResponse (expectedTerminator, s => response = s); @@ -93,14 +127,20 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" ); } - public static IEnumerable GetBatchPermutations (string input) + public static IEnumerable GetBatchPermutations (string input, int maxDepth = 3) { - // Call the recursive method to generate batches - return GenerateBatches (input, 0); + // Call the recursive method to generate batches with an initial depth of 0 + return GenerateBatches (input, 0, maxDepth, 0); } - private static IEnumerable GenerateBatches (string input, int start) + private static IEnumerable GenerateBatches (string input, int start, int maxDepth, int currentDepth) { + // If we have reached the maximum recursion depth, return no results + if (currentDepth >= maxDepth) + { + yield break; // No more batches can be generated at this depth + } + // If we have reached the end of the string, return an empty list if (start >= input.Length) { @@ -114,8 +154,8 @@ private static IEnumerable GenerateBatches (string input, int start) // Take a batch from 'start' to 'i' string batch = input.Substring (start, i - start); - // Recursively get batches from the remaining substring - foreach (var remainingBatches in GenerateBatches (input, i)) + // Recursively get batches from the remaining substring, increasing the depth + foreach (var remainingBatches in GenerateBatches (input, i, maxDepth, currentDepth + 1)) { // Combine the current batch with the remaining batches var result = new string [1 + remainingBatches.Length]; @@ -127,6 +167,7 @@ private static IEnumerable GenerateBatches (string input, int start) } + private void AssertIgnored (string ansiStream,char expected, ref int i) { var c = NextChar (ansiStream, ref i); From fa15389e9c4b0fb70be914fad042e486c8ac452d Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 08:12:22 +0100 Subject: [PATCH 12/77] Fix unit test cases --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 096dc0a2dc..9850f5ed30 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -56,27 +56,26 @@ public void TestInputProcessing () [Theory] [InlineData ("\x1B[<0;10;20MHi\x1B[0c", "c", "\x1B[0c", "\x1B[<0;10;20MHi")] [InlineData ("\x1B[<1;15;25MYou\x1B[1c", "c", "\x1B[1c", "\x1B[<1;15;25MYou")] - [InlineData ("\x1B[0cHi\x1B[0c", "c", "\x1B[0c", "Hi\x1B[0c")] // Consume the first response but pass through the second - - [InlineData ("\x1B[<0;0;0MHe\x1B[3c", "c", "\x1B[3c", "\x1B[<0;0;0MHe")] // Short input - [InlineData ("\x1B[<0;1;2Da\x1B[0c\x1B[1c", "c", "\x1B[0c", "\x1B[<0;1;2Da\x1B[1c")] // Two responses, consume only the first - [InlineData ("\x1B[1;1M\x1B[3cAn", "c", "\x1B[3c", "\x1B[1;1MAn")] // Response with a preceding escape sequence - [InlineData ("hi\x1B[2c\x1B[<5;5;5m", "c", "\x1B[2c", "hi\x1B[<5;5;5m")] // Mixed normal and escape sequences - [InlineData ("\x1B[3c\x1B[4c\x1B[<0;0;0MIn", "c", "\x1B[0c", "\x1B[3c\x1B[4c\x1B[<0;0;0MIn")] // Multiple consecutive responses - [InlineData ("\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M\x1B[2c", "c", "\x1B[2c", "\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M")] // Interleaved responses - [InlineData ("\x1B[<0;1;1MHi\x1B[6c\x1B[2c\x1B[<1;0;0MT", "c", "\x1B[6c", "\x1B[<0;1;1MHi\x1B[2c\x1B[<1;0;0MT")] // Mixed input with multiple responses - [InlineData ("Te\x1B[<2;2;2M\x1B[7c", "c", "\x1B[7c", "Te\x1B[<2;2;2M")] // Text followed by escape sequence - [InlineData ("\x1B[0c\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT", "c", "\x1B[1;0M", "\x1B[<0;0;0M\x1B[3c\x1B[0cT")] // Multiple escape sequences, with expected response in between - [InlineData ("\x1B[0;0M\x1B[<0;0;0M\x1B[3cT\x1B[1c", "c", "\x1B[1c", "\x1B[0;0M\x1B[<0;0;0MT")] // Edge case with leading escape - [InlineData ("\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn\x1B[1c", "c", "\x1B[1c", "\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn")] // Multiple unexpected escape sequences - [InlineData ("\x1B[<5;5;5M\x1B[7cEx\x1B[8c", "c", "\x1B[8c", "\x1B[<5;5;5MEx")] // Extra sequences with no expected responses + [InlineData ("\x1B[0cHi\x1B[0c", "c", "\x1B[0c", "Hi\x1B[0c")] + [InlineData ("\x1B[<0;0;0MHe\x1B[3c", "c", "\x1B[3c", "\x1B[<0;0;0MHe")] + [InlineData ("\x1B[<0;1;2Da\x1B[0c\x1B[1c", "c", "\x1B[0c", "\x1B[<0;1;2Da\x1B[1c")] + [InlineData ("\x1B[1;1M\x1B[3cAn", "c", "\x1B[3c", "\x1B[1;1MAn")] + [InlineData ("hi\x1B[2c\x1B[<5;5;5m", "c", "\x1B[2c", "hi\x1B[<5;5;5m")] + [InlineData ("\x1B[3c\x1B[4c\x1B[<0;0;0MIn", "c", "\u001b[3c", "\u001b[4c\u001b[<0;0;0MIn")] + [InlineData ("\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M\x1B[2c", "c", "\x1B[0c", "\x1B[<1;2;3M\x1B[<1;2;3M\u001b[2c")] + [InlineData ("\x1B[<0;1;1MHi\x1B[6c\x1B[2c\x1B[<1;0;0MT", "c", "\x1B[6c", "\x1B[<0;1;1MHi\x1B[2c\x1B[<1;0;0MT")] + [InlineData ("Te\x1B[<2;2;2M\x1B[7c", "c", "\x1B[7c", "Te\x1B[<2;2;2M")] + [InlineData ("\x1B[0c\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT", "c", "\x1B[0c", "\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT")] + [InlineData ("\x1B[0;0M\x1B[<0;0;0M\x1B[3cT\x1B[1c", "c", "\u001b[3c", "\u001b[0;0M\u001b[<0;0;0MT\u001b[1c")] + [InlineData ("\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn\x1B[1c", "c", "\u001b[3c", "\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c")] + [InlineData ("\x1B[<5;5;5M\x1B[7cEx\x1B[8c", "c", "\x1B[7c", "\u001b[<5;5;5MEx\u001b[8c")] // Random characters and mixed inputs [InlineData ("\x1B[<1;1;1MJJ\x1B[9c", "c", "\x1B[9c", "\x1B[<1;1;1MJJ")] // Mixed text [InlineData ("Be\x1B[0cAf", "c", "\x1B[0c", "BeAf")] // Escape in the middle of the string [InlineData ("\x1B[<0;0;0M\x1B[2cNot e", "c", "\x1B[2c", "\x1B[<0;0;0MNot e")] // Unexpected sequence followed by text - [InlineData ("Just te\x1B[<0;0;0M\x1B[3c\x1B[2c\x1B[4c", "c", "\x1B[4c", "Just te\x1B[<0;0;0M\x1B[3c\x1B[2c")] // Multiple unexpected responses - [InlineData ("\x1B[1;2;3M\x1B[0c\x1B[2;2M\x1B[0;0;0MTe", "c", "\x1B[0c", "\x1B[1;2;3M\x1B[2;2MTe")] // Multiple commands with responses + [InlineData ("Just te\x1B[<0;0;0M\x1B[3c\x1B[2c\x1B[4c", "c", "\x1B[3c", "Just te\x1B[<0;0;0M\x1B[2c\x1B[4c")] // Multiple unexpected responses + [InlineData ("\x1B[1;2;3M\x1B[0c\x1B[2;2M\x1B[0;0;0MTe", "c", "\x1B[0c", "\x1B[1;2;3M\x1B[2;2M\x1B[0;0;0MTe")] // Multiple commands with responses [InlineData ("\x1B[<3;3;3Mabc\x1B[4cde", "c", "\x1B[4c", "\x1B[<3;3;3Mabcde")] // Escape sequences mixed with regular text // Edge cases @@ -88,9 +87,9 @@ public void TestInputProcessing () [InlineData ("Inpu\x1B[0c\x1B[1;0;0M", "c", "\x1B[0c", "Inpu\x1B[1;0;0M")] // Single input followed by escape [InlineData ("\x1B[2c\x1B[<5;6;7MDa", "c", "\x1B[2c", "\x1B[<5;6;7MDa")] // Multiple escape sequences followed by text - [InlineData ("\x1B[0cHi\x1B[1cGo", "c", "\x1B[1c", "HiGo")] // Normal text with multiple escape sequences + [InlineData ("\x1B[0cHi\x1B[1cGo", "c", "\x1B[0c", "Hi\u001b[1cGo")] // Normal text with multiple escape sequences - [InlineData ("\x1B[<1;1;1MTe", "c", "\x1B[1c", "\x1B[<1;1;1MTe")] // Edge case of maximum length + [InlineData ("\x1B[<1;1;1MTe", "c", "", "\x1B[<1;1;1MTe")] // Add more test cases here... public void TestInputSequences (string ansiStream, string expectedTerminator, string expectedResponse, string expectedOutput) { From d1669a50b817e24f13c69ece1833757592552620 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 09:01:30 +0100 Subject: [PATCH 13/77] Allow attatching arbitrary metadata to each char handled by AnsiResponseParser --- .../ConsoleDrivers/AnsiResponseParser.cs | 51 ++++++++++--------- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 20 ++++++++ .../ConsoleDrivers/AnsiResponseParserTests.cs | 24 ++++++--- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 9fe3b7350a..3967367bf5 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -2,9 +2,9 @@ namespace Terminal.Gui; -internal class AnsiResponseParser +internal class AnsiResponseParser { - private readonly StringBuilder held = new (); + private readonly List> held = new (); private readonly List<(string terminator, Action response)> expectedResponses = new (); // Enum to manage the parser's state @@ -104,47 +104,47 @@ public AnsiResponseParser () /// Returns what should be passed on to any downstream input processing /// (i.e., removes expected ANSI responses from the input stream). /// - public string ProcessInput (string input) + public IEnumerable> ProcessInput (params Tuple[] input) { - var output = new StringBuilder (); // Holds characters that should pass through + var output = new List> (); // 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 @@ public string ProcessInput (string input) 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 @@ public string ProcessInput (string input) } } - return output.ToString (); // Return all characters that passed through + return output; // Return all characters that passed through } /// @@ -185,9 +185,9 @@ private void ResetState () /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected /// response. /// - private string HandleHeldContent () + private IEnumerable>? HandleHeldContent () { - var cur = held.ToString (); + string cur = HeldToString (); // Check for expected responses (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); @@ -197,25 +197,30 @@ private string HandleHeldContent () 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 response) { // If it matches the expected response, invoke the callback and return nothing for output - response?.Invoke (held.ToString ()); + response?.Invoke (HeldToString ()); ResetState (); } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 2a9bef0810..b8bc01d5d3 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1443,11 +1443,30 @@ internal override MainLoop Init () #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 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 void ProcessInput (WindowsConsole.InputRecord inputEvent) break; } + if (inputEvent.KeyEvent.bKeyDown) { // Avoid sending repeat key down events diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 9850f5ed30..10e2dd9e15 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -6,7 +6,7 @@ namespace UnitTests.ConsoleDrivers; public class AnsiResponseParserTests (ITestOutputHelper output) { - AnsiResponseParser _parser = new AnsiResponseParser (); + AnsiResponseParser _parser = new AnsiResponseParser (); [Fact] public void TestInputProcessing () @@ -114,7 +114,8 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st 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 void TestInputSequences (string ansiStream, string expectedTerminator, st output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" ); } + private Tuple [] StringToBatch (string batch) + { + return batch.Select ((k, i) => Tuple.Create (k, i)).ToArray (); + } + public static IEnumerable GetBatchPermutations (string input, int maxDepth = 3) { // Call the recursive method to generate batches with an initial depth of 0 @@ -173,7 +179,7 @@ private void AssertIgnored (string ansiStream,char expected, ref int i) // 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 @@ private void AssertReleased (string ansiStream, ref int i, string expectedReleas // 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> processInput) + { + return new string(processInput.Select (a=>a.Item1).ToArray ()); + } + + private Tuple[] NextChar (string ansiStream, ref int i) { - return ansiStream [i++].ToString(); + return StringToBatch(ansiStream [i++].ToString()); } } From af3a80587f7d2c4a54a317690df62335ccb15067 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 09:27:00 +0100 Subject: [PATCH 14/77] Proof of concept DAR request --- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 37 ++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index b8bc01d5d3..77a4416925 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -15,6 +15,7 @@ #define HACK_CHECK_WINCHANGED +using System.Collections; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; @@ -1446,7 +1447,6 @@ internal override MainLoop Init () // Send DAR - WinConsole?.WriteANSI (EscSeqUtils.CSI_SendDeviceAttributes); Parser.ExpectResponse (EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, (e) => { @@ -1464,8 +1464,17 @@ internal void ProcessInput (WindowsConsole.InputRecord inputEvent) if (firstTime) { WinConsole?.WriteANSI (EscSeqUtils.CSI_SendDeviceAttributes); + firstTime = false; + } + foreach (var e in Parse (inputEvent)) + { + ProcessInputAfterParsing (e); } + } + + internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) + { switch (inputEvent.EventType) { @@ -1489,7 +1498,6 @@ internal void ProcessInput (WindowsConsole.InputRecord inputEvent) break; } - if (inputEvent.KeyEvent.bKeyDown) { // Avoid sending repeat key down events @@ -1540,6 +1548,31 @@ internal void ProcessInput (WindowsConsole.InputRecord inputEvent) } } + private IEnumerable Parse (WindowsConsole.InputRecord inputEvent) + { + if (inputEvent.EventType != WindowsConsole.EventType.Key) + { + yield return inputEvent; + yield break; + } + + // TODO: For now ignore key up events completely + if (!inputEvent.KeyEvent.bKeyDown) + { + yield break; + } + + // TODO: Esc on its own is a problem - need a minute delay i.e. if you get Esc but nothing after release it. + + // TODO: keydown/keyup badness + + foreach (Tuple output in + Parser.ProcessInput (Tuple.Create(inputEvent.KeyEvent.UnicodeChar,inputEvent))) + { + yield return output.Item2; + } + } + #if HACK_CHECK_WINCHANGED private void ChangeWin (object s, SizeChangedEventArgs e) { From 9f8fbc06983086c372cd1e4bc08e1f0b90ce55e4 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 10:51:22 +0100 Subject: [PATCH 15/77] Refactor so there is an abstract base parser --- .../ConsoleDrivers/AnsiResponseParser.cs | 166 ++++++++++-------- 1 file changed, 88 insertions(+), 78 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 3967367bf5..f8c54f8454 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -2,42 +2,40 @@ namespace Terminal.Gui; -internal class AnsiResponseParser -{ - private readonly List> held = new (); - private readonly List<(string terminator, Action response)> expectedResponses = new (); - - // Enum to manage the parser's state - private enum ParserState + // Enum to manage the parser's state + internal enum ParserState { Normal, ExpectingBracket, InResponse } +internal abstract class AnsiResponseParserBase +{ + protected readonly List<(string terminator, Action response)> expectedResponses = new (); + // Current state of the parser - private ParserState currentState = ParserState.Normal; - private readonly HashSet _knownTerminators = new (); - - /* - * ANSI Input Sequences - * - * \x1B[A // Up Arrow key pressed - * \x1B[B // Down Arrow key pressed - * \x1B[C // Right Arrow key pressed - * \x1B[D // Left Arrow key pressed - * \x1B[3~ // Delete key pressed - * \x1B[2~ // Insert key pressed - * \x1B[5~ // Page Up key pressed - * \x1B[6~ // Page Down key pressed - * \x1B[1;5D // Ctrl + Left Arrow - * \x1B[1;5C // Ctrl + Right Arrow - * \x1B[0;10;20M // Mouse button pressed at position (10, 20) - * \x1B[0c // Device Attributes Response (e.g., terminal identification) - */ - - public AnsiResponseParser () + private ParserState _state = ParserState.Normal; + public ParserState State + { + get => _state; + protected set + { + StateChangedAt = DateTime.Now; + _state = value; + } + } + + /// + /// When was last changed. + /// + public DateTime StateChangedAt { get; private set; } = DateTime.Now; + + protected readonly HashSet _knownTerminators = new (); + + public AnsiResponseParserBase () { + // These all are valid terminators on ansi responses, // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s _knownTerminators.Add ('@'); @@ -95,10 +93,61 @@ public AnsiResponseParser () _knownTerminators.Add ('x'); _knownTerminators.Add ('y'); _knownTerminators.Add ('z'); + } - // Add more if necessary + // Reset the parser's state + protected void ResetState () + { + State = ParserState.Normal; + ClearHeld (); + } + + public abstract void ClearHeld (); + + protected abstract string HeldToString (); + + protected void DispatchResponse (Action response) + { + response?.Invoke (HeldToString ()); + ResetState (); + } + + // Common response handler logic + protected bool ShouldReleaseHeldContent () + { + string cur = HeldToString (); + + // Check for expected responses + (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); + + if (matchingResponse.response != null) + { + DispatchResponse (matchingResponse.response); + expectedResponses.Remove (matchingResponse); + return false; + } + + if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) + { + // Detected a response that was not expected + return true; + } + + return false; // Continue accumulating } + /// + /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is + /// completed. + /// + public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } +} + + +internal class AnsiResponseParser : AnsiResponseParserBase +{ + private readonly List> held = new (); + /// /// Processes input which may be a single character or multiple. /// Returns what should be passed on to any downstream input processing @@ -113,13 +162,13 @@ public IEnumerable> ProcessInput (params Tuple[] input) { var currentChar = input [index]; - switch (currentState) + switch (State) { case ParserState.Normal: if (currentChar.Item1 == '\x1B') { // Escape character detected, move to ExpectingBracket state - currentState = ParserState.ExpectingBracket; + State = ParserState.ExpectingBracket; held.Add (currentChar); // Hold the escape character index++; } @@ -136,7 +185,7 @@ public IEnumerable> ProcessInput (params Tuple[] input) if (currentChar.Item1 == '[') { // Detected '[' , transition to InResponse state - currentState = ParserState.InResponse; + State = ParserState.InResponse; held.Add (currentChar); // Hold the '[' index++; } @@ -155,11 +204,10 @@ public IEnumerable> ProcessInput (params Tuple[] input) held.Add (currentChar); // Check if the held content should be released - var handled = HandleHeldContent (); - if (handled != null) + if (ShouldReleaseHeldContent ()) { - output.AddRange (handled); + output.AddRange (held); ResetState (); // Exit response mode and reset } @@ -177,56 +225,18 @@ public IEnumerable> ProcessInput (params Tuple[] input) /// private void ResetState () { - currentState = ParserState.Normal; + State = ParserState.Normal; held.Clear (); } - /// - /// Checks the current `held` content to decide whether it should be released, either as an expected or unexpected - /// response. - /// - private IEnumerable>? HandleHeldContent () + /// + public override void ClearHeld () { - string cur = HeldToString (); - - // Check for expected responses - (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); - - if (matchingResponse.response != null) - { - DispatchResponse (matchingResponse.response); - expectedResponses.Remove (matchingResponse); - - return null; - } - - if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) - { - // Detected a response that we were not expecting - return held; - } - - // Add more cases here for other standard sequences (like arrow keys, function keys, etc.) - - // If no match, continue accumulating characters - return null; + held.Clear (); } - private string HeldToString () + protected override string HeldToString () { return new string (held.Select (h => h.Item1).ToArray ()); } - - private void DispatchResponse (Action response) - { - // If it matches the expected response, invoke the callback and return nothing for output - response?.Invoke (HeldToString ()); - ResetState (); - } - - /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is - /// completed. - /// - public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } } From 4d542922086b48b2659b8d1c6c56224cb4cec7d2 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 11:07:15 +0100 Subject: [PATCH 16/77] Add string version --- .../ConsoleDrivers/AnsiResponseParser.cs | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index f8c54f8454..308c3fa434 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -204,7 +204,6 @@ public IEnumerable> ProcessInput (params Tuple[] input) held.Add (currentChar); // Check if the held content should be released - if (ShouldReleaseHeldContent ()) { output.AddRange (held); @@ -240,3 +239,102 @@ protected override string HeldToString () return new string (held.Select (h => h.Item1).ToArray ()); } } + + + + +internal class AnsiResponseParser : AnsiResponseParserBase +{ + private readonly StringBuilder held = new (); + + /// + /// Processes input which may be a single character or multiple. + /// Returns what should be passed on to any downstream input processing + /// (i.e., removes expected ANSI responses from the input stream). + /// + public string ProcessInput (string input) + { + var output = new StringBuilder (); // Holds characters that should pass through + var index = 0; // Tracks position in the input string + + while (index < input.Length) + { + var currentChar = input [index]; + + switch (State) + { + case ParserState.Normal: + if (currentChar == '\x1B') + { + // Escape character detected, move to ExpectingBracket state + State = ParserState.ExpectingBracket; + held.Append (currentChar); // Hold the escape character + index++; + } + else + { + // Normal character, append to output + output.Append (currentChar); + index++; + } + + break; + + case ParserState.ExpectingBracket: + if (currentChar == '[') + { + // Detected '[' , transition to InResponse state + State = ParserState.InResponse; + held.Append (currentChar); // Hold the '[' + index++; + } + else + { + // Invalid sequence, release held characters and reset to Normal + output.Append (held); + output.Append (currentChar); // Add current character + ResetState (); + index++; + } + + break; + + case ParserState.InResponse: + held.Append (currentChar); + + // Check if the held content should be released + if (ShouldReleaseHeldContent ()) + { + output.Append (held); + ResetState (); // Exit response mode and reset + } + + index++; + + break; + } + } + + return output.ToString(); // Return all characters that passed through + } + + /// + /// Resets the parser's state when a response is handled or finished. + /// + private void ResetState () + { + State = ParserState.Normal; + held.Clear (); + } + + /// + public override void ClearHeld () + { + held.Clear (); + } + + protected override string HeldToString () + { + return held.ToString (); + } +} From c99487f313401720105262539a23b84dc571d68e Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 11:22:33 +0100 Subject: [PATCH 17/77] Double up all tests so that generic and string versions are both tested --- .../ConsoleDrivers/AnsiResponseParser.cs | 368 +++++++----------- .../ConsoleDrivers/AnsiResponseParserTests.cs | 68 +++- 2 files changed, 183 insertions(+), 253 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 308c3fa434..22859f6165 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; - // Enum to manage the parser's state + // Enum to manage the parser's state internal enum ParserState { Normal, @@ -10,31 +10,31 @@ internal enum ParserState InResponse } -internal abstract class AnsiResponseParserBase -{ - protected readonly List<(string terminator, Action response)> expectedResponses = new (); - - // Current state of the parser - private ParserState _state = ParserState.Normal; - public ParserState State + internal abstract class AnsiResponseParserBase { - get => _state; - protected set + protected readonly List<(string terminator, Action response)> expectedResponses = new (); + private ParserState _state = ParserState.Normal; + + // Current state of the parser + public ParserState State { - StateChangedAt = DateTime.Now; - _state = value; + get => _state; + protected set + { + StateChangedAt = DateTime.Now; + _state = value; + } } - } - /// - /// When was last changed. - /// - public DateTime StateChangedAt { get; private set; } = DateTime.Now; + /// + /// When was last changed. + /// + public DateTime StateChangedAt { get; private set; } = DateTime.Now; - protected readonly HashSet _knownTerminators = new (); + protected readonly HashSet _knownTerminators = new (); - public AnsiResponseParserBase () - { + public AnsiResponseParserBase () + { // These all are valid terminators on ansi responses, // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s @@ -93,248 +93,152 @@ public AnsiResponseParserBase () _knownTerminators.Add ('x'); _knownTerminators.Add ('y'); _knownTerminators.Add ('z'); - } - - // Reset the parser's state - protected void ResetState () - { - State = ParserState.Normal; - ClearHeld (); - } - - public abstract void ClearHeld (); - - protected abstract string HeldToString (); - - protected void DispatchResponse (Action response) - { - response?.Invoke (HeldToString ()); - ResetState (); - } + } - // Common response handler logic - protected bool ShouldReleaseHeldContent () - { - string cur = HeldToString (); + protected void ResetState () + { + State = ParserState.Normal; + ClearHeld (); + } - // Check for expected responses - (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); + public abstract void ClearHeld (); + protected abstract string HeldToString (); + protected abstract void AddToHeld (char c); - if (matchingResponse.response != null) + // Base method for processing input + public void ProcessInputBase (Func getCharAtIndex, Action appendOutput, int inputLength) { - DispatchResponse (matchingResponse.response); - expectedResponses.Remove (matchingResponse); - return false; + var index = 0; // Tracks position in the input string + + while (index < inputLength) + { + var currentChar = getCharAtIndex (index); + + switch (State) + { + case ParserState.Normal: + if (currentChar == '\x1B') + { + // Escape character detected, move to ExpectingBracket state + State = ParserState.ExpectingBracket; + AddToHeld (currentChar); // Hold the escape character + } + else + { + // Normal character, append to output + appendOutput (currentChar); + } + break; + + case ParserState.ExpectingBracket: + if (currentChar == '[') + { + // Detected '[', transition to InResponse state + State = ParserState.InResponse; + AddToHeld (currentChar); // Hold the '[' + } + else + { + // Invalid sequence, release held characters and reset to Normal + ReleaseHeld (appendOutput); + appendOutput (currentChar); // Add current character + ResetState (); + } + break; + + case ParserState.InResponse: + AddToHeld (currentChar); + + // Check if the held content should be released + if (ShouldReleaseHeldContent ()) + { + ReleaseHeld (appendOutput); + ResetState (); // Exit response mode and reset + } + break; + } + + index++; + } } - if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) + private void ReleaseHeld (Action appendOutput) { - // Detected a response that was not expected - return true; + foreach (var c in HeldToString ()) + { + appendOutput (c); + } } - return false; // Continue accumulating - } - - /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is - /// completed. - /// - public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } -} - - -internal class AnsiResponseParser : AnsiResponseParserBase -{ - private readonly List> held = new (); + // Common response handler logic + protected bool ShouldReleaseHeldContent () + { + string cur = HeldToString (); - /// - /// Processes input which may be a single character or multiple. - /// Returns what should be passed on to any downstream input processing - /// (i.e., removes expected ANSI responses from the input stream). - /// - public IEnumerable> ProcessInput (params Tuple[] input) - { - var output = new List> (); // Holds characters that should pass through - var index = 0; // Tracks position in the input string + // Check for expected responses + (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); - while (index < input.Length) - { - var currentChar = input [index]; + if (matchingResponse.response != null) + { + DispatchResponse (matchingResponse.response); + expectedResponses.Remove (matchingResponse); + return false; + } - switch (State) + if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) { - case ParserState.Normal: - if (currentChar.Item1 == '\x1B') - { - // Escape character detected, move to ExpectingBracket state - State = ParserState.ExpectingBracket; - held.Add (currentChar); // Hold the escape character - index++; - } - else - { - // Normal character, append to output - output.Add (currentChar); - index++; - } - - break; - - case ParserState.ExpectingBracket: - if (currentChar.Item1 == '[') - { - // Detected '[' , transition to InResponse state - State = ParserState.InResponse; - held.Add (currentChar); // Hold the '[' - index++; - } - else - { - // Invalid sequence, release held characters and reset to Normal - output.AddRange (held); - output.Add (currentChar); // Add current character - ResetState (); - index++; - } - - break; - - case ParserState.InResponse: - held.Add (currentChar); - - // Check if the held content should be released - if (ShouldReleaseHeldContent ()) - { - output.AddRange (held); - ResetState (); // Exit response mode and reset - } - - index++; - - break; + // Detected a response that was not expected + return true; } + + return false; // Continue accumulating } - return output; // Return all characters that passed through - } - /// - /// Resets the parser's state when a response is handled or finished. - /// - private void ResetState () - { - State = ParserState.Normal; - held.Clear (); - } + protected void DispatchResponse (Action response) + { + response?.Invoke (HeldToString ()); + ResetState (); + } - /// - public override void ClearHeld () - { - held.Clear (); + /// + /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed. + /// + public void ExpectResponse (string terminator, Action response) => expectedResponses.Add ((terminator, response)); } - protected override string HeldToString () + internal class AnsiResponseParser : AnsiResponseParserBase { - return new string (held.Select (h => h.Item1).ToArray ()); - } -} + private readonly List> held = new (); + 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); + return output; + } + public override void ClearHeld () => held.Clear (); + protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ()); -internal class AnsiResponseParser : AnsiResponseParserBase -{ - private readonly StringBuilder held = new (); + protected override void AddToHeld (char c) => held.Add (new Tuple (c, default!)); + } - /// - /// Processes input which may be a single character or multiple. - /// Returns what should be passed on to any downstream input processing - /// (i.e., removes expected ANSI responses from the input stream). - /// - public string ProcessInput (string input) + internal class AnsiResponseParser : AnsiResponseParserBase { - var output = new StringBuilder (); // Holds characters that should pass through - var index = 0; // Tracks position in the input string + private readonly StringBuilder held = new (); - while (index < input.Length) + public string ProcessInput (string input) { - var currentChar = input [index]; - - switch (State) - { - case ParserState.Normal: - if (currentChar == '\x1B') - { - // Escape character detected, move to ExpectingBracket state - State = ParserState.ExpectingBracket; - held.Append (currentChar); // Hold the escape character - index++; - } - else - { - // Normal character, append to output - output.Append (currentChar); - index++; - } - - break; - - case ParserState.ExpectingBracket: - if (currentChar == '[') - { - // Detected '[' , transition to InResponse state - State = ParserState.InResponse; - held.Append (currentChar); // Hold the '[' - index++; - } - else - { - // Invalid sequence, release held characters and reset to Normal - output.Append (held); - output.Append (currentChar); // Add current character - ResetState (); - index++; - } - - break; - - case ParserState.InResponse: - held.Append (currentChar); - - // Check if the held content should be released - if (ShouldReleaseHeldContent ()) - { - output.Append (held); - ResetState (); // Exit response mode and reset - } - - index++; - - break; - } + var output = new StringBuilder (); + ProcessInputBase (i => input [i], c => output.Append (c), input.Length); + return output.ToString (); } - return output.ToString(); // Return all characters that passed through - } - - /// - /// Resets the parser's state when a response is handled or finished. - /// - private void ResetState () - { - State = ParserState.Normal; - held.Clear (); - } + public override void ClearHeld () => held.Clear (); - /// - public override void ClearHeld () - { - held.Clear (); - } + protected override string HeldToString () => held.ToString (); - protected override string HeldToString () - { - return held.ToString (); - } -} + protected override void AddToHeld (char c) => held.Append (c); + } \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 10e2dd9e15..01102ccc93 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,12 +1,14 @@ using System.Diagnostics; using System.Text; using Microsoft.VisualStudio.TestPlatform.Utilities; +using Terminal.Gui; using Xunit.Abstractions; namespace UnitTests.ConsoleDrivers; public class AnsiResponseParserTests (ITestOutputHelper output) { - AnsiResponseParser _parser = new AnsiResponseParser (); + AnsiResponseParser _parser1 = new AnsiResponseParser (); + AnsiResponseParser _parser2 = new AnsiResponseParser (); [Fact] public void TestInputProcessing () @@ -16,12 +18,14 @@ public void TestInputProcessing () "\x1B[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR) - string? response = null; + string? response1 = null; + string? response2 = null; int i = 0; // Imagine that we are expecting a DAR - _parser.ExpectResponse ("c",(s)=> response = s); + _parser1.ExpectResponse ("c",(s)=> response1 = s); + _parser2.ExpectResponse ("c", (s) => response2 = s); // First char is Escape which we must consume incase what follows is the DAR AssertConsumed (ansiStream, ref i); // Esc @@ -47,10 +51,13 @@ public void TestInputProcessing () } // Consume the terminator 'c' and expect this to call the above event - Assert.Null (response); + Assert.Null (response1); + Assert.Null (response1); AssertConsumed (ansiStream, ref i); - Assert.NotNull (response); - Assert.Equal ("\x1B[0c", response); + Assert.NotNull (response2); + Assert.Equal ("\x1B[0c", response2); + Assert.NotNull (response2); + Assert.Equal ("\x1B[0c", response2); } [Theory] @@ -97,30 +104,38 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st var swGenBatches = Stopwatch.StartNew (); int tests = 0; - var permutations = GetBatchPermutations (ansiStream,7).ToArray (); + var permutations = GetBatchPermutations (ansiStream,5).ToArray (); swGenBatches.Stop (); var swRunTest = Stopwatch.StartNew (); foreach (var batchSet in permutations) { - string response = string.Empty; + string response1 = string.Empty; + string response2 = string.Empty; // Register the expected response with the given terminator - _parser.ExpectResponse (expectedTerminator, s => response = s); + _parser1.ExpectResponse (expectedTerminator, s => response1 = s); + _parser2.ExpectResponse (expectedTerminator, s => response2 = s); // Process the input - StringBuilder actualOutput = new StringBuilder (); + StringBuilder actualOutput1 = new StringBuilder (); + StringBuilder actualOutput2 = new StringBuilder (); foreach (var batch in batchSet) { - var output = _parser.ProcessInput (StringToBatch (batch)); - actualOutput.Append (BatchToString (output)); + var output1 = _parser1.ProcessInput (StringToBatch (batch)); + actualOutput1.Append (BatchToString (output1)); + + var output2 = _parser2.ProcessInput (batch); + actualOutput2.Append (output2); } // Assert the final output minus the expected response - Assert.Equal (expectedOutput, actualOutput.ToString()); - Assert.Equal (expectedResponse, response); + Assert.Equal (expectedOutput, actualOutput1.ToString()); + Assert.Equal (expectedResponse, response1); + Assert.Equal (expectedOutput, actualOutput2.ToString ()); + Assert.Equal (expectedResponse, response2); tests++; } @@ -175,25 +190,36 @@ private static IEnumerable GenerateBatches (string input, int start, private void AssertIgnored (string ansiStream,char expected, ref int i) { - var c = NextChar (ansiStream, ref i); + var c2 = ansiStream [i]; + var c1 = NextChar (ansiStream, ref i); // 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().Item1); + Assert.Equal ( c1,_parser1.ProcessInput (c1)); + Assert.Equal (expected,c1.Single().Item1); + + Assert.Equal (c2, _parser2.ProcessInput (c2.ToString()).Single()); + Assert.Equal (expected, c2 ); } private void AssertConsumed (string ansiStream, ref int i) { // Parser grabs this key - var c = NextChar (ansiStream, ref i); - Assert.Empty (_parser.ProcessInput(c)); + var c2 = ansiStream [i]; + var c1 = NextChar (ansiStream, ref i); + + Assert.Empty (_parser1.ProcessInput(c1)); + Assert.Empty (_parser2.ProcessInput (c2.ToString())); } private void AssertReleased (string ansiStream, ref int i, string expectedRelease) { - var c = NextChar (ansiStream, ref i); + 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(_parser.ProcessInput (c))); + Assert.Equal(expectedRelease,BatchToString(_parser1.ProcessInput (c1))); + + + Assert.Equal (expectedRelease, _parser2.ProcessInput (c2.ToString ())); } private string BatchToString (IEnumerable> processInput) From dbfdffba7ad07affb1b90282564186c9da2926da Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 11:41:47 +0100 Subject: [PATCH 18/77] Give AnsiResponseParser a chance to release its content each iteration if it has been some time since we saw Esc --- .../ConsoleDrivers/AnsiResponseParser.cs | 19 ++++++++++++- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 28 +++++++++++++++++++ .../ConsoleDrivers/AnsiResponseParserTests.cs | 2 -- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 22859f6165..97c294de75 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -218,12 +218,23 @@ public IEnumerable> ProcessInput (params Tuple [] input) return output; } + public IEnumerable> Release () + { + foreach (var h in held) + { + yield return h; + } + ResetState (); + } + public override void ClearHeld () => held.Clear (); protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ()); protected override void AddToHeld (char c) => held.Add (new Tuple (c, default!)); - } + + +} internal class AnsiResponseParser : AnsiResponseParserBase { @@ -235,7 +246,13 @@ public string ProcessInput (string input) ProcessInputBase (i => input [i], c => output.Append (c), input.Length); return output.ToString (); } + public string Release () + { + var output = held.ToString (); + ResetState (); + return output; + } public override void ClearHeld () => held.Clear (); protected override string HeldToString () => held.ToString (); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 77a4416925..4a9a8360a9 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1457,6 +1457,11 @@ internal override MainLoop Init () } private bool firstTime = true; + + /// + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// + private TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); public AnsiResponseParser Parser { get; set; } = new (); internal void ProcessInput (WindowsConsole.InputRecord inputEvent) @@ -1566,6 +1571,11 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) // TODO: keydown/keyup badness + foreach (var i in ShouldRelease ()) + { + yield return i; + } + foreach (Tuple output in Parser.ProcessInput (Tuple.Create(inputEvent.KeyEvent.UnicodeChar,inputEvent))) { @@ -1573,6 +1583,19 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) } } + public IEnumerable ShouldRelease () + { + + if (Parser.State == ParserState.ExpectingBracket && + DateTime.Now - Parser.StateChangedAt > _escTimeout) + { + foreach (Tuple output in Parser.Release ()) + { + yield return output.Item2; + } + } + } + #if HACK_CHECK_WINCHANGED private void ChangeWin (object s, SizeChangedEventArgs e) { @@ -2276,6 +2299,11 @@ bool IMainLoopDriver.EventsPending () void IMainLoopDriver.Iteration () { + foreach(var i in ((WindowsDriver)_consoleDriver).ShouldRelease()) + { + ((WindowsDriver)_consoleDriver).ProcessInput (i); + } + while (_resultQueue.Count > 0) { WindowsConsole.InputRecord [] inputRecords = _resultQueue.Dequeue (); diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 01102ccc93..17198ddcb5 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,7 +1,5 @@ using System.Diagnostics; using System.Text; -using Microsoft.VisualStudio.TestPlatform.Utilities; -using Terminal.Gui; using Xunit.Abstractions; namespace UnitTests.ConsoleDrivers; From 16a787e1963b094653d5bfd8f153a623696ad86b Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 12:42:05 +0100 Subject: [PATCH 19/77] Fix base class dropping T all over the place --- .../ConsoleDrivers/AnsiResponseParser.cs | 64 +++++++++++----- .../ConsoleDrivers/AnsiResponseParserTests.cs | 73 ++++++++++++++++++- 2 files changed, 116 insertions(+), 21 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 97c294de75..1c5263d313 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -103,57 +103,70 @@ protected void ResetState () 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 @@ public void ProcessInputBase (Func getCharAtIndex, Action appen } } - 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 @@ internal class AnsiResponseParser : AnsiResponseParserBase 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 @@ public IEnumerable> Release () 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 @@ internal class AnsiResponseParser : AnsiResponseParserBase 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 @@ public string Release () 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 17198ddcb5..7f08042992 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 void TestInputSequences (string ansiStream, string expectedTerminator, st foreach (var batchSet in permutations) { + tIndex = 0; string response1 = string.Empty; string response2 = string.Empty; @@ -140,9 +146,47 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st 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 @@ private void AssertConsumed (string ansiStream, ref int i) 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 @@ private Tuple[] NextChar (string ansiStream, ref int i) { 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); + } } From 88922c2f43e5bc0d99ad6e6462ecd63959a362d6 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 12:45:49 +0100 Subject: [PATCH 20/77] Fix use case of 2 esc in a row --- UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 7f08042992..d1d319b54f 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -180,8 +180,9 @@ public void TwoExcapesInARow () 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); + AssertManualReleaseIs ("\x1B",1); } private Tuple [] StringToBatch (string batch) From 1877cc7f1be9006ae1d94fdfa0a93b3cf48db430 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 12:55:36 +0100 Subject: [PATCH 21/77] Add exploration of Esc + typing and standardize unicode to \u001b --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 105 +++++++++++------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index d1d319b54f..6535c978d3 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -16,9 +16,9 @@ public class AnsiResponseParserTests (ITestOutputHelper output) [Fact] public void TestInputProcessing () { - string ansiStream = "\x1B[<0;10;20M" + // ANSI escape for mouse move at (10, 20) + string ansiStream = "\u001b[<0;10;20M" + // ANSI escape for mouse move at (10, 20) "Hello" + // User types "Hello" - "\x1B[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR) + "\u001b[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR) string? response1 = null; @@ -39,7 +39,7 @@ public void TestInputProcessing () } // We see the M terminator - AssertReleased (ansiStream, ref i, "\x1B[<0;10;20M"); + AssertReleased (ansiStream, ref i, "\u001b[<0;10;20M"); // Regular user typing for (int c = 0; c < "Hello".Length; c++) @@ -48,7 +48,7 @@ public void TestInputProcessing () } // Now we have entered the actual DAR we should be consuming these - for (int c = 0; c < "\x1B[0".Length; c++) + for (int c = 0; c < "\u001b[0".Length; c++) { AssertConsumed (ansiStream, ref i); } @@ -58,48 +58,48 @@ public void TestInputProcessing () Assert.Null (response1); AssertConsumed (ansiStream, ref i); Assert.NotNull (response2); - Assert.Equal ("\x1B[0c", response2); + Assert.Equal ("\u001b[0c", response2); Assert.NotNull (response2); - Assert.Equal ("\x1B[0c", response2); + Assert.Equal ("\u001b[0c", response2); } [Theory] - [InlineData ("\x1B[<0;10;20MHi\x1B[0c", "c", "\x1B[0c", "\x1B[<0;10;20MHi")] - [InlineData ("\x1B[<1;15;25MYou\x1B[1c", "c", "\x1B[1c", "\x1B[<1;15;25MYou")] - [InlineData ("\x1B[0cHi\x1B[0c", "c", "\x1B[0c", "Hi\x1B[0c")] - [InlineData ("\x1B[<0;0;0MHe\x1B[3c", "c", "\x1B[3c", "\x1B[<0;0;0MHe")] - [InlineData ("\x1B[<0;1;2Da\x1B[0c\x1B[1c", "c", "\x1B[0c", "\x1B[<0;1;2Da\x1B[1c")] - [InlineData ("\x1B[1;1M\x1B[3cAn", "c", "\x1B[3c", "\x1B[1;1MAn")] - [InlineData ("hi\x1B[2c\x1B[<5;5;5m", "c", "\x1B[2c", "hi\x1B[<5;5;5m")] - [InlineData ("\x1B[3c\x1B[4c\x1B[<0;0;0MIn", "c", "\u001b[3c", "\u001b[4c\u001b[<0;0;0MIn")] - [InlineData ("\x1B[<1;2;3M\x1B[0c\x1B[<1;2;3M\x1B[2c", "c", "\x1B[0c", "\x1B[<1;2;3M\x1B[<1;2;3M\u001b[2c")] - [InlineData ("\x1B[<0;1;1MHi\x1B[6c\x1B[2c\x1B[<1;0;0MT", "c", "\x1B[6c", "\x1B[<0;1;1MHi\x1B[2c\x1B[<1;0;0MT")] - [InlineData ("Te\x1B[<2;2;2M\x1B[7c", "c", "\x1B[7c", "Te\x1B[<2;2;2M")] - [InlineData ("\x1B[0c\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT", "c", "\x1B[0c", "\x1B[<0;0;0M\x1B[3c\x1B[0c\x1B[1;0MT")] - [InlineData ("\x1B[0;0M\x1B[<0;0;0M\x1B[3cT\x1B[1c", "c", "\u001b[3c", "\u001b[0;0M\u001b[<0;0;0MT\u001b[1c")] - [InlineData ("\x1B[3c\x1B[<0;0;0M\x1B[0c\x1B[<1;1;1MIn\x1B[1c", "c", "\u001b[3c", "\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c")] - [InlineData ("\x1B[<5;5;5M\x1B[7cEx\x1B[8c", "c", "\x1B[7c", "\u001b[<5;5;5MEx\u001b[8c")] + [InlineData ("\u001b[<0;10;20MHi\u001b[0c", "c", "\u001b[0c", "\u001b[<0;10;20MHi")] + [InlineData ("\u001b[<1;15;25MYou\u001b[1c", "c", "\u001b[1c", "\u001b[<1;15;25MYou")] + [InlineData ("\u001b[0cHi\u001b[0c", "c", "\u001b[0c", "Hi\u001b[0c")] + [InlineData ("\u001b[<0;0;0MHe\u001b[3c", "c", "\u001b[3c", "\u001b[<0;0;0MHe")] + [InlineData ("\u001b[<0;1;2Da\u001b[0c\u001b[1c", "c", "\u001b[0c", "\u001b[<0;1;2Da\u001b[1c")] + [InlineData ("\u001b[1;1M\u001b[3cAn", "c", "\u001b[3c", "\u001b[1;1MAn")] + [InlineData ("hi\u001b[2c\u001b[<5;5;5m", "c", "\u001b[2c", "hi\u001b[<5;5;5m")] + [InlineData ("\u001b[3c\u001b[4c\u001b[<0;0;0MIn", "c", "\u001b[3c", "\u001b[4c\u001b[<0;0;0MIn")] + [InlineData ("\u001b[<1;2;3M\u001b[0c\u001b[<1;2;3M\u001b[2c", "c", "\u001b[0c", "\u001b[<1;2;3M\u001b[<1;2;3M\u001b[2c")] + [InlineData ("\u001b[<0;1;1MHi\u001b[6c\u001b[2c\u001b[<1;0;0MT", "c", "\u001b[6c", "\u001b[<0;1;1MHi\u001b[2c\u001b[<1;0;0MT")] + [InlineData ("Te\u001b[<2;2;2M\u001b[7c", "c", "\u001b[7c", "Te\u001b[<2;2;2M")] + [InlineData ("\u001b[0c\u001b[<0;0;0M\u001b[3c\u001b[0c\u001b[1;0MT", "c", "\u001b[0c", "\u001b[<0;0;0M\u001b[3c\u001b[0c\u001b[1;0MT")] + [InlineData ("\u001b[0;0M\u001b[<0;0;0M\u001b[3cT\u001b[1c", "c", "\u001b[3c", "\u001b[0;0M\u001b[<0;0;0MT\u001b[1c")] + [InlineData ("\u001b[3c\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c", "c", "\u001b[3c", "\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c")] + [InlineData ("\u001b[<5;5;5M\u001b[7cEx\u001b[8c", "c", "\u001b[7c", "\u001b[<5;5;5MEx\u001b[8c")] // Random characters and mixed inputs - [InlineData ("\x1B[<1;1;1MJJ\x1B[9c", "c", "\x1B[9c", "\x1B[<1;1;1MJJ")] // Mixed text - [InlineData ("Be\x1B[0cAf", "c", "\x1B[0c", "BeAf")] // Escape in the middle of the string - [InlineData ("\x1B[<0;0;0M\x1B[2cNot e", "c", "\x1B[2c", "\x1B[<0;0;0MNot e")] // Unexpected sequence followed by text - [InlineData ("Just te\x1B[<0;0;0M\x1B[3c\x1B[2c\x1B[4c", "c", "\x1B[3c", "Just te\x1B[<0;0;0M\x1B[2c\x1B[4c")] // Multiple unexpected responses - [InlineData ("\x1B[1;2;3M\x1B[0c\x1B[2;2M\x1B[0;0;0MTe", "c", "\x1B[0c", "\x1B[1;2;3M\x1B[2;2M\x1B[0;0;0MTe")] // Multiple commands with responses - [InlineData ("\x1B[<3;3;3Mabc\x1B[4cde", "c", "\x1B[4c", "\x1B[<3;3;3Mabcde")] // Escape sequences mixed with regular text + [InlineData ("\u001b[<1;1;1MJJ\u001b[9c", "c", "\u001b[9c", "\u001b[<1;1;1MJJ")] // Mixed text + [InlineData ("Be\u001b[0cAf", "c", "\u001b[0c", "BeAf")] // Escape in the middle of the string + [InlineData ("\u001b[<0;0;0M\u001b[2cNot e", "c", "\u001b[2c", "\u001b[<0;0;0MNot e")] // Unexpected sequence followed by text + [InlineData ("Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4c", "c", "\u001b[3c", "Just te\u001b[<0;0;0M\u001b[2c\u001b[4c")] // Multiple unexpected responses + [InlineData ("\u001b[1;2;3M\u001b[0c\u001b[2;2M\u001b[0;0;0MTe", "c", "\u001b[0c", "\u001b[1;2;3M\u001b[2;2M\u001b[0;0;0MTe")] // Multiple commands with responses + [InlineData ("\u001b[<3;3;3Mabc\u001b[4cde", "c", "\u001b[4c", "\u001b[<3;3;3Mabcde")] // Escape sequences mixed with regular text // Edge cases - [InlineData ("\x1B[0c\x1B[0c\x1B[0c", "c", "\x1B[0c", "\x1B[0c\x1B[0c")] // Multiple identical responses + [InlineData ("\u001b[0c\u001b[0c\u001b[0c", "c", "\u001b[0c", "\u001b[0c\u001b[0c")] // Multiple identical responses [InlineData ("", "c", "", "")] // Empty input [InlineData ("Normal", "c", "", "Normal")] // No escape sequences - [InlineData ("\x1B[<0;0;0M", "c", "", "\x1B[<0;0;0M")] // Escape sequence only - [InlineData ("\x1B[1;2;3M\x1B[0c", "c", "\x1B[0c", "\x1B[1;2;3M")] // Last response consumed + [InlineData ("\u001b[<0;0;0M", "c", "", "\u001b[<0;0;0M")] // Escape sequence only + [InlineData ("\u001b[1;2;3M\u001b[0c", "c", "\u001b[0c", "\u001b[1;2;3M")] // Last response consumed - [InlineData ("Inpu\x1B[0c\x1B[1;0;0M", "c", "\x1B[0c", "Inpu\x1B[1;0;0M")] // Single input followed by escape - [InlineData ("\x1B[2c\x1B[<5;6;7MDa", "c", "\x1B[2c", "\x1B[<5;6;7MDa")] // Multiple escape sequences followed by text - [InlineData ("\x1B[0cHi\x1B[1cGo", "c", "\x1B[0c", "Hi\u001b[1cGo")] // Normal text with multiple escape sequences + [InlineData ("Inpu\u001b[0c\u001b[1;0;0M", "c", "\u001b[0c", "Inpu\u001b[1;0;0M")] // Single input followed by escape + [InlineData ("\u001b[2c\u001b[<5;6;7MDa", "c", "\u001b[2c", "\u001b[<5;6;7MDa")] // Multiple escape sequences followed by text + [InlineData ("\u001b[0cHi\u001b[1cGo", "c", "\u001b[0c", "Hi\u001b[1cGo")] // Normal text with multiple escape sequences - [InlineData ("\x1B[<1;1;1MTe", "c", "", "\x1B[<1;1;1MTe")] + [InlineData ("\u001b[<1;1;1MTe", "c", "", "\u001b[<1;1;1MTe")] // Add more test cases here... public void TestInputSequences (string ansiStream, string expectedTerminator, string expectedResponse, string expectedOutput) { @@ -149,7 +149,7 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st [Fact] public void ReleasesEscapeAfterTimeout () { - string input = "\x1B"; + string input = "\u001b"; int i = 0; // Esc on its own looks like it might be an esc sequence so should be consumed @@ -170,19 +170,48 @@ public void ReleasesEscapeAfterTimeout () public void TwoExcapesInARow () { // Example user presses Esc key then a DAR comes in - string input = "\x1B\x1B"; + string input = "\u001b\u001b"; 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); + AssertReleased (input, ref i, "\u001b",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 ("\x1B",1); + AssertManualReleaseIs ("\u001b",1); + } + + [Fact] + public void TwoExcapesInARowWithTextBetween () + { + // Example user presses Esc key and types at the speed of light (normally the consumer should be handling Esc timeout) + // then a DAR comes in. + string input = "\u001bfish\u001b"; + int i = 0; + + // First Esc gets grabbed + AssertConsumed (input, ref i); // Esc + Assert.Equal (ParserState.ExpectingBracket,_parser1.State); + Assert.Equal (ParserState.ExpectingBracket, _parser2.State); + + // Because next char is 'f' we do not see a bracket so release both + AssertReleased (input, ref i, "\u001bf", 0,1); // f + + Assert.Equal (ParserState.Normal, _parser1.State); + Assert.Equal (ParserState.Normal, _parser2.State); + + AssertReleased (input, ref i,"i",2); + AssertReleased (input, ref i, "s", 3); + AssertReleased (input, ref i, "h", 4); + + AssertConsumed (input, ref i); // Second Esc + + // Assume 50ms or something has passed, lets force release as no new content + AssertManualReleaseIs ("\u001b", 5); } private Tuple [] StringToBatch (string batch) From d642fb65757d27d0b572120bd7447bc303ae59eb Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 14:09:58 +0100 Subject: [PATCH 22/77] Add WindowsDriverKeyPairer --- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 45 ++++++----- .../ConsoleDrivers/WindowsDriverKeyPairer.cs | 74 +++++++++++++++++++ 2 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 4a9a8360a9..69544fcca6 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Terminal.Gui.ConsoleDrivers; using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; using static Terminal.Gui.SpinnerStyle; @@ -1462,7 +1463,7 @@ internal override MainLoop Init () /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// private TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); - public AnsiResponseParser Parser { get; set; } = new (); + public AnsiResponseParser Parser { get; set; } = new (); internal void ProcessInput (WindowsConsole.InputRecord inputEvent) { @@ -1553,6 +1554,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) } } + private WindowsDriverKeyPairer pairer = new WindowsDriverKeyPairer (); private IEnumerable Parse (WindowsConsole.InputRecord inputEvent) { if (inputEvent.EventType != WindowsConsole.EventType.Key) @@ -1561,26 +1563,36 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) yield break; } - // TODO: For now ignore key up events completely - if (!inputEvent.KeyEvent.bKeyDown) - { - yield break; - } - - // TODO: Esc on its own is a problem - need a minute delay i.e. if you get Esc but nothing after release it. - - // TODO: keydown/keyup badness + var pair = pairer.ProcessInput (inputEvent).ToArray (); foreach (var i in ShouldRelease ()) { yield return i; } - foreach (Tuple output in - Parser.ProcessInput (Tuple.Create(inputEvent.KeyEvent.UnicodeChar,inputEvent))) + foreach (var p in pair) { - yield return output.Item2; + // may be down/up + if (p.Length == 2) + { + var c = p [0].KeyEvent.UnicodeChar; + foreach (Tuple output in + Parser.ProcessInput (Tuple.Create(c,p))) + { + foreach (var r in output.Item2) + { + yield return r; + } + } + } + else + { + // environment doesn't support down/up + + // TODO: what we do in this situation? + } } + } public IEnumerable ShouldRelease () @@ -1589,11 +1601,10 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) if (Parser.State == ParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) { - foreach (Tuple output in Parser.Release ()) - { - yield return output.Item2; - } + return Parser.Release ().SelectMany (o => o.Item2); } + + return []; } #if HACK_CHECK_WINCHANGED diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs new file mode 100644 index 0000000000..b86feaba02 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs @@ -0,0 +1,74 @@ +using static Terminal.Gui.WindowsConsole; + +namespace Terminal.Gui.ConsoleDrivers; +class WindowsDriverKeyPairer +{ + private InputRecord? _heldDownEvent = null; // To hold the "down" event + + // Process a single input record at a time + public IEnumerable ProcessInput (InputRecord record) + { + // If it's a "down" event, store it as a held event + if (IsKeyDown (record)) + { + return HandleKeyDown (record); + } + // If it's an "up" event, try to match it with the held "down" event + else if (IsKeyUp (record)) + { + return HandleKeyUp (record); + } + else + { + // If it's not a key event, just pass it through + return new [] { new [] { record } }; + } + } + + private IEnumerable HandleKeyDown (InputRecord record) + { + // If we already have a held "down" event, release it (unmatched) + if (_heldDownEvent != null) + { + // Release the previous "down" event since there's a new "down" + var previousDown = _heldDownEvent.Value; + _heldDownEvent = record; // Hold the new "down" event + return new [] { new [] { previousDown } }; + } + + // Hold the new "down" event + _heldDownEvent = record; + return Enumerable.Empty (); + } + + private IEnumerable HandleKeyUp (InputRecord record) + { + // If we have a held "down" event that matches this "up" event, release both + if (_heldDownEvent != null && IsMatchingKey (record, _heldDownEvent.Value)) + { + var downEvent = _heldDownEvent.Value; + _heldDownEvent = null; // Clear the held event + return new [] { new [] { downEvent, record } }; + } + else + { + // No match, release the "up" event by itself + return new [] { new [] { record } }; + } + } + + private bool IsKeyDown (InputRecord record) + { + return record.KeyEvent.bKeyDown; + } + + private bool IsKeyUp (InputRecord record) + { + return !record.KeyEvent.bKeyDown; + } + + private bool IsMatchingKey (InputRecord upEvent, InputRecord downEvent) + { + return upEvent.KeyEvent.UnicodeChar == downEvent.KeyEvent.UnicodeChar; + } +} From 204d640f0bd847b8bd9a26faa64ebf93509c90a0 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 12 Oct 2024 14:54:11 +0100 Subject: [PATCH 23/77] WIP: Scenario --- .../ConsoleDrivers/AnsiResponseParser.cs | 7 ++- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 16 ++++++- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 8 ++++ Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 21 ++------ UICatalog/Scenarios/AnsiRequestsScenario.cs | 48 +++++++++++++++++++ 5 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs create mode 100644 UICatalog/Scenarios/AnsiRequestsScenario.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 1c5263d313..e16374c74c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -10,7 +10,12 @@ internal enum ParserState InResponse } - internal abstract class AnsiResponseParserBase + public interface IAnsiResponseParser + { + void ExpectResponse (string terminator, Action response); + } + + internal abstract class AnsiResponseParserBase : IAnsiResponseParser { protected readonly List<(string terminator, Action response)> expectedResponses = new (); private ParserState _state = ParserState.Normal; diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 9f2a454654..e4edfc183e 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -13,7 +13,7 @@ namespace Terminal.Gui; /// - that uses the .NET Console API - /// for unit testing. /// -public abstract class ConsoleDriver +public abstract class ConsoleDriver : IConsoleDriver { // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. @@ -609,6 +609,20 @@ public void OnMouseEvent (MouseEvent a) public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); #endregion + + /// + public virtual IAnsiResponseParser GetParser () + { + // TODO: implement in other drivers + throw new NotSupportedException (); + } + + /// + public virtual void RawWrite (string str) + { + // TODO: implement in other drivers + throw new NotSupportedException (); + } } /// diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs new file mode 100644 index 0000000000..582a1a6ab7 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Terminal.Gui; + +public interface IConsoleDriver +{ + IAnsiResponseParser GetParser (); + void RawWrite (string str); +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 69544fcca6..3bffdf9b20 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1164,6 +1164,11 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } } + /// + public override IAnsiResponseParser GetParser () => Parser; + + /// + public override void RawWrite (string str) => WinConsole?.WriteANSI (str); #region Not Implemented @@ -1446,19 +1451,9 @@ internal override MainLoop Init () WinConsole?.SetInitialCursorVisibility (); - - // Send DAR - Parser.ExpectResponse (EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, - (e) => - { - // TODO: do something with this - }); - return new MainLoop (_mainLoopDriver); } - private bool firstTime = true; - /// /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// @@ -1467,12 +1462,6 @@ internal override MainLoop Init () internal void ProcessInput (WindowsConsole.InputRecord inputEvent) { - if (firstTime) - { - WinConsole?.WriteANSI (EscSeqUtils.CSI_SendDeviceAttributes); - firstTime = false; - } - foreach (var e in Parse (inputEvent)) { ProcessInputAfterParsing (e); diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs new file mode 100644 index 0000000000..4658a643fa --- /dev/null +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -0,0 +1,48 @@ +using Terminal.Gui; + +namespace UICatalog.Scenarios; + + + +[ScenarioMetadata ("Ansi Requests", "Demonstration of how to send ansi requests.")] +[ScenarioCategory ("Colors")] +[ScenarioCategory ("Drawing")] +public class AnsiRequestsScenario : Scenario +{ + public override void Main () + { + Application.Init (); + var win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; + + var btn = new Button () + { + Text = "Send DAR", + Width = Dim.Auto () + }; + + + + var tv = new TextView () + { + Y = Pos.Bottom (btn), + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + btn.Accepting += (s, e) => + { + // Ask for device attributes (DAR) + var p = Application.Driver.GetParser (); + p.ExpectResponse ("c", (r) => tv.Text += r + '\n'); + Application.Driver.RawWrite (EscSeqUtils.CSI_SendDeviceAttributes); + + }; + + win.Add (tv); + win.Add (btn); + + Application.Run (win); + win.Dispose (); + Application.Shutdown (); + } +} \ No newline at end of file From 67b28925ccab546abe8905f27a84b29da56e8821 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 10:42:34 +0100 Subject: [PATCH 24/77] Change WindowsDriver to send down/up on key down and ignore key up events from win 32 api --- .../ConsoleDrivers/AnsiResponseParser.cs | 2 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 47 ++++-------- .../ConsoleDrivers/WindowsDriverKeyPairer.cs | 74 ------------------- 3 files changed, 14 insertions(+), 109 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index e16374c74c..5625f4a5ae 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -246,7 +246,7 @@ public IEnumerable> ProcessInput (params Tuple [] input) public IEnumerable> Release () { - foreach (var h in held) + foreach (var h in held.ToArray ()) { yield return h; } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 3bffdf9b20..6850749ced 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1458,7 +1458,7 @@ internal override MainLoop Init () /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// private TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); - public AnsiResponseParser Parser { get; set; } = new (); + public AnsiResponseParser Parser { get; set; } = new (); internal void ProcessInput (WindowsConsole.InputRecord inputEvent) { @@ -1493,15 +1493,9 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) break; } - if (inputEvent.KeyEvent.bKeyDown) - { - // Avoid sending repeat key down events - OnKeyDown (new Key (map)); - } - else - { - OnKeyUp (new Key (map)); - } + // This follows convention in NetDriver + OnKeyDown (new Key (map)); + OnKeyUp (new Key (map)); break; @@ -1543,7 +1537,6 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) } } - private WindowsDriverKeyPairer pairer = new WindowsDriverKeyPairer (); private IEnumerable Parse (WindowsConsole.InputRecord inputEvent) { if (inputEvent.EventType != WindowsConsole.EventType.Key) @@ -1552,36 +1545,22 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) yield break; } - var pair = pairer.ProcessInput (inputEvent).ToArray (); + // Swallow key up events - they are unreliable + if (!inputEvent.KeyEvent.bKeyDown) + { + yield break; + } foreach (var i in ShouldRelease ()) { yield return i; } - foreach (var p in pair) + foreach (Tuple output in + Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent))) { - // may be down/up - if (p.Length == 2) - { - var c = p [0].KeyEvent.UnicodeChar; - foreach (Tuple output in - Parser.ProcessInput (Tuple.Create(c,p))) - { - foreach (var r in output.Item2) - { - yield return r; - } - } - } - else - { - // environment doesn't support down/up - - // TODO: what we do in this situation? - } + yield return output.Item2; } - } public IEnumerable ShouldRelease () @@ -1590,7 +1569,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) if (Parser.State == ParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) { - return Parser.Release ().SelectMany (o => o.Item2); + return Parser.Release ().Select (o => o.Item2); } return []; diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs deleted file mode 100644 index b86feaba02..0000000000 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriverKeyPairer.cs +++ /dev/null @@ -1,74 +0,0 @@ -using static Terminal.Gui.WindowsConsole; - -namespace Terminal.Gui.ConsoleDrivers; -class WindowsDriverKeyPairer -{ - private InputRecord? _heldDownEvent = null; // To hold the "down" event - - // Process a single input record at a time - public IEnumerable ProcessInput (InputRecord record) - { - // If it's a "down" event, store it as a held event - if (IsKeyDown (record)) - { - return HandleKeyDown (record); - } - // If it's an "up" event, try to match it with the held "down" event - else if (IsKeyUp (record)) - { - return HandleKeyUp (record); - } - else - { - // If it's not a key event, just pass it through - return new [] { new [] { record } }; - } - } - - private IEnumerable HandleKeyDown (InputRecord record) - { - // If we already have a held "down" event, release it (unmatched) - if (_heldDownEvent != null) - { - // Release the previous "down" event since there's a new "down" - var previousDown = _heldDownEvent.Value; - _heldDownEvent = record; // Hold the new "down" event - return new [] { new [] { previousDown } }; - } - - // Hold the new "down" event - _heldDownEvent = record; - return Enumerable.Empty (); - } - - private IEnumerable HandleKeyUp (InputRecord record) - { - // If we have a held "down" event that matches this "up" event, release both - if (_heldDownEvent != null && IsMatchingKey (record, _heldDownEvent.Value)) - { - var downEvent = _heldDownEvent.Value; - _heldDownEvent = null; // Clear the held event - return new [] { new [] { downEvent, record } }; - } - else - { - // No match, release the "up" event by itself - return new [] { new [] { record } }; - } - } - - private bool IsKeyDown (InputRecord record) - { - return record.KeyEvent.bKeyDown; - } - - private bool IsKeyUp (InputRecord record) - { - return !record.KeyEvent.bKeyDown; - } - - private bool IsMatchingKey (InputRecord upEvent, InputRecord downEvent) - { - return upEvent.KeyEvent.UnicodeChar == downEvent.KeyEvent.UnicodeChar; - } -} From 30ad592cd3fe35eff52df3d555704a6a9c198807 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 11:09:11 +0100 Subject: [PATCH 25/77] Investigate adding to CursesDriver --- .../ConsoleDrivers/CursesDriver/CursesDriver.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 0b11949a94..4502e8186d 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -193,6 +193,12 @@ public void StopReportingMouseMoves () } } + /// + public override void RawWrite (string str) + { + Console.Out.Write (str); + } + public override void Suspend () { StopReportingMouseMoves (); @@ -577,6 +583,10 @@ internal override MainLoop Init () return new MainLoop (_mainLoopDriver); } + private AnsiResponseParser Parser { get; set; } = new (); + /// + public override IAnsiResponseParser GetParser () => Parser; + internal void ProcessInput () { int wch; From ac90ed9de67c2abd1520e91445992b1138bf63e3 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 13:32:17 +0100 Subject: [PATCH 26/77] AnsiParser working with CursesDriver and WindowsDriver --- .../ConsoleDrivers/CursesDriver/CursesDriver.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 4502e8186d..5ca14dc4e5 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -583,10 +583,11 @@ internal override MainLoop Init () return new MainLoop (_mainLoopDriver); } - private AnsiResponseParser Parser { get; set; } = new (); + private AnsiResponseParser Parser { get; set; } = new (); /// public override IAnsiResponseParser GetParser () => Parser; + private List seen = new List (); internal void ProcessInput () { int wch; @@ -600,6 +601,8 @@ internal void ProcessInput () var k = KeyCode.Null; + seen.Add (wch); + if (code == Curses.KEY_CODE_YES) { while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize) @@ -885,6 +888,14 @@ ref ConsoleKeyInfo [] cki } else { + if (cki != null) + { + foreach (var c in cki) + { + Parser.ProcessInput (c.KeyChar.ToString()); + } + } + k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo); keyEventArgs = new Key (k); OnKeyDown (keyEventArgs); From effe1991e34ab444e29456c12e397730135d834f Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 13:36:42 +0100 Subject: [PATCH 27/77] Remove 'seen' debug aid --- Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 5ca14dc4e5..714e39bde5 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -587,7 +587,6 @@ internal override MainLoop Init () /// public override IAnsiResponseParser GetParser () => Parser; - private List seen = new List (); internal void ProcessInput () { int wch; @@ -601,8 +600,6 @@ internal void ProcessInput () var k = KeyCode.Null; - seen.Add (wch); - if (code == Curses.KEY_CODE_YES) { while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize) From 563eace4877e6992a40655f01546ec33e4ac147a Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 13:54:39 +0100 Subject: [PATCH 28/77] Mostly working for NetDriver --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 1298c1c357..57f58297cf 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -407,6 +407,8 @@ private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight return true; } + public AnsiResponseParser Parser { get; private set; } = new (); + // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) private void ProcessRequestResponse ( ref ConsoleKeyInfo newConsoleKeyInfo, @@ -415,6 +417,16 @@ private void ProcessRequestResponse ( ref ConsoleModifiers mod ) { + if (cki != null) + { + // If the response is fully consumed by parser + if(cki.Length > 1 && string.IsNullOrEmpty(Parser.ProcessInput (new string(cki.Select (k=>k.KeyChar).ToArray ())))) + { + // Lets not double process + return; + } + } + // isMouse is true if it's CSI<, false otherwise EscSeqUtils.DecodeEscSeq ( EscSeqRequests, @@ -1036,6 +1048,15 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out } } + /// + public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + + /// + public override void RawWrite (string str) + { + Console.Write (str); + } + internal override void End () { if (IsWinPlatform) From aa1c9bf286df7b8061a46e6ee4d655d6122812f0 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 13 Oct 2024 14:07:24 +0100 Subject: [PATCH 29/77] Add some comments to main parser method and mark as protected --- Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 5625f4a5ae..624213eb44 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -111,8 +111,17 @@ protected void ResetState () protected abstract IEnumerable HeldToObjects (); protected abstract void AddToHeld (object o); - // Base method for processing input - public void ProcessInputBase ( + /// + /// Processes an input collection of objects long. + /// You must provide the indexers to return the objects and the action to append + /// to output stream. + /// + /// The character representation of element i of your input collection + /// The actual element in the collection (e.g. char or Tuple<char,T>) + /// Action to invoke when parser confirms an element of the current collection or a previous + /// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder). + /// The total number of elements in your collection + protected void ProcessInputBase ( Func getCharAtIndex, Func getObjectAtIndex, Action appendOutput, From 53924da7d3e59e37d0204815540e3dd423bbfd27 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 20 Oct 2024 11:43:26 +0100 Subject: [PATCH 30/77] Add test for making step by step assertions on parser state after each char is fed in --- .../ConsoleDrivers/AnsiResponseParser.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 109 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 624213eb44..b8de3d9ecf 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui; // Enum to manage the parser's state - internal enum ParserState + public enum ParserState { Normal, ExpectingBracket, diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 6535c978d3..b80be5ae33 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.CommandLine.Parsing; +using System.Diagnostics; using System.Text; using Xunit.Abstractions; @@ -103,7 +104,6 @@ public void TestInputProcessing () // Add more test cases here... public void TestInputSequences (string ansiStream, string expectedTerminator, string expectedResponse, string expectedOutput) { - var swGenBatches = Stopwatch.StartNew (); int tests = 0; @@ -146,6 +146,111 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" ); } + public static IEnumerable TestInputSequencesExact_Cases () + { + yield return + [ + "Esc Only", + null, + new [] + { + new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty) + } + ]; + + yield return + [ + "Esc Hi with intermediate", + 'c', + new [] + { + new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), + new StepExpectation ('H',ParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars + new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), + new StepExpectation ('[',ParserState.InResponse,string.Empty), + new StepExpectation ('0',ParserState.InResponse,string.Empty), + new StepExpectation ('c',ParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response + new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), + } + ]; + } + + public class StepExpectation () + { + /// + /// The input character to feed into the parser at this step of the test + /// + public char Input { get; } + + /// + /// What should the state of the parser be after the + /// is fed in. + /// + public ParserState ExpectedStateAfterOperation { get; } + + /// + /// If this step should release one or more characters, put them here. + /// + public string ExpectedRelease { get; } = string.Empty; + + /// + /// If this step should result in a completing of detection of ANSI response + /// then put the expected full response sequence here. + /// + public string ExpectedAnsiResponse { get; } = string.Empty; + + public StepExpectation ( + char input, + ParserState expectedStateAfterOperation, + string expectedRelease = "", + string expectedAnsiResponse = "") : this () + { + Input = input; + ExpectedStateAfterOperation = expectedStateAfterOperation; + ExpectedRelease = expectedRelease; + ExpectedAnsiResponse = expectedAnsiResponse; + } + + } + + + + [MemberData(nameof(TestInputSequencesExact_Cases))] + [Theory] + public void TestInputSequencesExact (string caseName, char? terminator, IEnumerable expectedStates) + { + output.WriteLine ("Running test case:" + caseName); + + var parser = new AnsiResponseParser (); + string? response = null; + + if (terminator.HasValue) + { + parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s); + } + foreach (var state in expectedStates) + { + // If we expect the response to be detected at this step + if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse)) + { + // Then before passing input it should be null + Assert.Null (response); + } + + var actual = parser.ProcessInput (state.Input.ToString ()); + + Assert.Equal (state.ExpectedRelease,actual); + Assert.Equal (state.ExpectedStateAfterOperation, parser.State); + + // If we expect the response to be detected at this step + if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse)) + { + // And after passing input it shuld be the expected value + Assert.Equal (state.ExpectedAnsiResponse, response); + } + } + } + [Fact] public void ReleasesEscapeAfterTimeout () { From e0415e6584b31f8354599bef2e8f7e9be3bb017c Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 20 Oct 2024 15:37:47 +0100 Subject: [PATCH 31/77] tidyup and streamline naming --- .../ConsoleDrivers/AnsiResponseParser.cs | 389 +++++++++--------- .../ConsoleDrivers/IAnsiResponseParser.cs | 8 + Terminal.Gui/ConsoleDrivers/ParserState.cs | 24 ++ Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 36 +- 5 files changed, 244 insertions(+), 215 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs create mode 100644 Terminal.Gui/ConsoleDrivers/ParserState.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index b8de3d9ecf..f6a2403d1f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -2,45 +2,31 @@ namespace Terminal.Gui; - // Enum to manage the parser's state - public enum ParserState - { - Normal, - ExpectingBracket, - InResponse - } - - public interface IAnsiResponseParser - { - void ExpectResponse (string terminator, Action response); - } +internal abstract class AnsiResponseParserBase : IAnsiResponseParser +{ + protected readonly List<(string terminator, Action response)> expectedResponses = new (); + private AnsiResponseParserState _state = AnsiResponseParserState.Normal; - internal abstract class AnsiResponseParserBase : IAnsiResponseParser + // Current state of the parser + public AnsiResponseParserState State { - protected readonly List<(string terminator, Action response)> expectedResponses = new (); - private ParserState _state = ParserState.Normal; - - // Current state of the parser - public ParserState State + get => _state; + protected set { - get => _state; - protected set - { - StateChangedAt = DateTime.Now; - _state = value; - } + StateChangedAt = DateTime.Now; + _state = value; } + } - /// - /// When was last changed. - /// - public DateTime StateChangedAt { get; private set; } = DateTime.Now; - - protected readonly HashSet _knownTerminators = new (); + /// + /// When was last changed. + /// + public DateTime StateChangedAt { get; private set; } = DateTime.Now; - public AnsiResponseParserBase () - { + protected readonly HashSet _knownTerminators = new (); + public AnsiResponseParserBase () + { // These all are valid terminators on ansi responses, // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s _knownTerminators.Add ('@'); @@ -98,206 +84,217 @@ public AnsiResponseParserBase () _knownTerminators.Add ('x'); _knownTerminators.Add ('y'); _knownTerminators.Add ('z'); - } + } - protected void ResetState () - { - State = ParserState.Normal; - ClearHeld (); - } + protected void ResetState () + { + State = AnsiResponseParserState.Normal; + ClearHeld (); + } - public abstract void ClearHeld (); - protected abstract string HeldToString (); - protected abstract IEnumerable HeldToObjects (); - protected abstract void AddToHeld (object o); - - /// - /// Processes an input collection of objects long. - /// You must provide the indexers to return the objects and the action to append - /// to output stream. - /// - /// The character representation of element i of your input collection - /// The actual element in the collection (e.g. char or Tuple<char,T>) - /// Action to invoke when parser confirms an element of the current collection or a previous - /// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder). - /// The total number of elements in your collection - protected void ProcessInputBase ( - Func getCharAtIndex, - Func getObjectAtIndex, - Action appendOutput, - int inputLength) - { - var index = 0; // Tracks position in the input string + public abstract void ClearHeld (); + protected abstract string HeldToString (); + protected abstract IEnumerable HeldToObjects (); + protected abstract void AddToHeld (object o); + + /// + /// Processes an input collection of objects long. + /// You must provide the indexers to return the objects and the action to append + /// to output stream. + /// + /// The character representation of element i of your input collection + /// The actual element in the collection (e.g. char or Tuple<char,T>) + /// + /// Action to invoke when parser confirms an element of the current collection or a previous + /// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder). + /// + /// The total number of elements in your collection + protected 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 (isEscape) - { - // Escape character detected, move to ExpectingBracket state - State = ParserState.ExpectingBracket; - AddToHeld (currentObj); // Hold the escape character - } - else - { - // Normal character, append to output - 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 (currentObj); // Hold the '[' - } - else - { - // Invalid sequence, release held characters and reset to Normal - ReleaseHeld (appendOutput); - appendOutput (currentObj); // Add current character - } - break; - - case ParserState.InResponse: - AddToHeld (currentObj); - - // Check if the held content should be released - if (ShouldReleaseHeldContent ()) - { - ReleaseHeld (appendOutput); - } - break; - } - - index++; - } - } + while (index < inputLength) + { + char currentChar = getCharAtIndex (index); + object currentObj = getObjectAtIndex (index); + bool isEscape = currentChar == '\x1B'; - private void ReleaseHeld (Action appendOutput, ParserState newState = ParserState.Normal) - { - foreach (var o in HeldToObjects ()) + switch (State) { - appendOutput (o); + case AnsiResponseParserState.Normal: + if (isEscape) + { + // Escape character detected, move to ExpectingBracket state + State = AnsiResponseParserState.ExpectingBracket; + AddToHeld (currentObj); // Hold the escape character + } + else + { + // Normal character, append to output + appendOutput (currentObj); + } + + break; + + case AnsiResponseParserState.ExpectingBracket: + if (isEscape) + { + // Second escape so we must release first + ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket); + AddToHeld (currentObj); // Hold the new escape + } + else if (currentChar == '[') + { + // Detected '[', transition to InResponse state + State = AnsiResponseParserState.InResponse; + AddToHeld (currentObj); // Hold the '[' + } + else + { + // Invalid sequence, release held characters and reset to Normal + ReleaseHeld (appendOutput); + appendOutput (currentObj); // Add current character + } + + break; + + case AnsiResponseParserState.InResponse: + AddToHeld (currentObj); + + // Check if the held content should be released + if (ShouldReleaseHeldContent ()) + { + ReleaseHeld (appendOutput); + } + + break; } - State = newState; - ClearHeld (); + index++; + } } - // Common response handler logic - protected bool ShouldReleaseHeldContent () + private void ReleaseHeld (Action appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal) + { + foreach (object o in HeldToObjects ()) { - string cur = HeldToString (); + appendOutput (o); + } - // Check for expected responses - (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); + State = newState; + ClearHeld (); + } - if (matchingResponse.response != null) - { - DispatchResponse (matchingResponse.response); - expectedResponses.Remove (matchingResponse); - return false; - } + // Common response handler logic + protected bool ShouldReleaseHeldContent () + { + string cur = HeldToString (); - if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) - { - // Detected a response that was not expected - return true; - } + // Check for expected responses + (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); - return false; // Continue accumulating - } + if (matchingResponse.response != null) + { + DispatchResponse (matchingResponse.response); + expectedResponses.Remove (matchingResponse); + return false; + } - protected void DispatchResponse (Action response) + if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) { - response?.Invoke (HeldToString ()); - ResetState (); + // Detected a response that was not expected + return true; } - /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed. - /// - public void ExpectResponse (string terminator, Action response) => expectedResponses.Add ((terminator, response)); + return false; // Continue accumulating } - internal class AnsiResponseParser : AnsiResponseParserBase + protected void DispatchResponse (Action response) { - private readonly List> held = new (); + response?.Invoke (HeldToString ()); + ResetState (); + } - public IEnumerable> ProcessInput (params Tuple [] input) - { - var output = new List> (); - ProcessInputBase ( - i => input [i].Item1, - i => input [i], - c => output.Add ((Tuple)c), - input.Length); - return output; - } + /// + /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is + /// completed. + /// + public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } +} - public IEnumerable> Release () +internal class AnsiResponseParser : AnsiResponseParserBase +{ + private readonly List> held = new (); + + public IEnumerable> ProcessInput (params Tuple [] input) + { + List> output = new List> (); + + ProcessInputBase ( + i => input [i].Item1, + i => input [i], + c => output.Add ((Tuple)c), + input.Length); + + return output; + } + + public IEnumerable> Release () + { + foreach (Tuple h in held.ToArray ()) { - foreach (var h in held.ToArray ()) - { - yield return h; - } - ResetState (); + yield return h; } - public override void ClearHeld () => held.Clear (); - - protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ()); + ResetState (); + } - protected override IEnumerable HeldToObjects () => held; + public override void ClearHeld () { held.Clear (); } - protected override void AddToHeld (object o) => held.Add ((Tuple)o); + protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); } + protected override IEnumerable HeldToObjects () { return held; } + protected override void AddToHeld (object o) { held.Add ((Tuple)o); } } - internal class AnsiResponseParser : AnsiResponseParserBase +internal class AnsiResponseParser : AnsiResponseParserBase +{ + private readonly StringBuilder held = new (); + + public string ProcessInput (string input) { - private readonly StringBuilder held = new (); + var output = new StringBuilder (); - public string ProcessInput (string input) - { - var output = new StringBuilder (); - 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 () - { - var output = held.ToString (); - ResetState (); + 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; - } - public override void ClearHeld () => held.Clear (); + return output.ToString (); + } + + public string Release () + { + var output = held.ToString (); + ResetState (); + + return output; + } + + public override void ClearHeld () { held.Clear (); } - protected override string HeldToString () => held.ToString (); + protected override string HeldToString () { return held.ToString (); } - 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 + protected override IEnumerable HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); } + + protected override void AddToHeld (object o) { held.Append ((char)o); } +} diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs new file mode 100644 index 0000000000..f82cf148ad --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Terminal.Gui; + +public interface IAnsiResponseParser +{ + AnsiResponseParserState State { get; } + void ExpectResponse (string terminator, Action response); +} diff --git a/Terminal.Gui/ConsoleDrivers/ParserState.cs b/Terminal.Gui/ConsoleDrivers/ParserState.cs new file mode 100644 index 0000000000..934b6eb3eb --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/ParserState.cs @@ -0,0 +1,24 @@ +namespace Terminal.Gui; + +/// +/// Describes the current state of an +/// +public enum AnsiResponseParserState +{ + /// + /// Parser is reading normal input e.g. keys typed by user. + /// + Normal, + + /// + /// Parser has encountered an Esc and is waiting to see if next + /// key(s) continue to form an Ansi escape sequence + /// + ExpectingBracket, + + /// + /// Parser has encountered Esc[ and considers that it is in the process + /// of reading an ANSI sequence. + /// + InResponse +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 3a6b2b3b78..0340ad8bd5 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1566,7 +1566,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) public IEnumerable ShouldRelease () { - if (Parser.State == ParserState.ExpectingBracket && + if (Parser.State == AnsiResponseParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) { return Parser.Release ().Select (o => o.Item2); diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index b80be5ae33..71b892b5f5 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -154,7 +154,7 @@ public static IEnumerable TestInputSequencesExact_Cases () null, new [] { - new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty) + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty) } ]; @@ -164,13 +164,13 @@ public static IEnumerable TestInputSequencesExact_Cases () 'c', new [] { - new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), - new StepExpectation ('H',ParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars - new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), - new StepExpectation ('[',ParserState.InResponse,string.Empty), - new StepExpectation ('0',ParserState.InResponse,string.Empty), - new StepExpectation ('c',ParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response - new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty), + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), + new StepExpectation ('H',AnsiResponseParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), + new StepExpectation ('[',AnsiResponseParserState.InResponse,string.Empty), + new StepExpectation ('0',AnsiResponseParserState.InResponse,string.Empty), + new StepExpectation ('c',AnsiResponseParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), } ]; } @@ -186,7 +186,7 @@ public class StepExpectation () /// What should the state of the parser be after the /// is fed in. /// - public ParserState ExpectedStateAfterOperation { get; } + public AnsiResponseParserState ExpectedStateAfterOperation { get; } /// /// If this step should release one or more characters, put them here. @@ -201,7 +201,7 @@ public class StepExpectation () public StepExpectation ( char input, - ParserState expectedStateAfterOperation, + AnsiResponseParserState expectedStateAfterOperation, string expectedRelease = "", string expectedAnsiResponse = "") : this () { @@ -261,8 +261,8 @@ public void ReleasesEscapeAfterTimeout () 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 (AnsiResponseParserState.ExpectingBracket, _parser1.State); + Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State); Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date); Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date); @@ -300,14 +300,14 @@ public void TwoExcapesInARowWithTextBetween () // First Esc gets grabbed AssertConsumed (input, ref i); // Esc - Assert.Equal (ParserState.ExpectingBracket,_parser1.State); - Assert.Equal (ParserState.ExpectingBracket, _parser2.State); + Assert.Equal (AnsiResponseParserState.ExpectingBracket,_parser1.State); + Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State); // Because next char is 'f' we do not see a bracket so release both AssertReleased (input, ref i, "\u001bf", 0,1); // f - Assert.Equal (ParserState.Normal, _parser1.State); - Assert.Equal (ParserState.Normal, _parser2.State); + Assert.Equal (AnsiResponseParserState.Normal, _parser1.State); + Assert.Equal (AnsiResponseParserState.Normal, _parser2.State); AssertReleased (input, ref i,"i",2); AssertReleased (input, ref i, "s", 3); @@ -428,7 +428,7 @@ private void AssertManualReleaseIs (string expectedRelease, params int [] expect Assert.Equal (expectedRelease, _parser2.Release ()); - Assert.Equal (ParserState.Normal, _parser1.State); - Assert.Equal (ParserState.Normal, _parser2.State); + Assert.Equal (AnsiResponseParserState.Normal, _parser1.State); + Assert.Equal (AnsiResponseParserState.Normal, _parser2.State); } } From db1977aa8509739644151831dfb2d7376fbb9726 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 20 Oct 2024 17:25:55 +0100 Subject: [PATCH 32/77] Add stress test for ansi requests --- UICatalog/Scenarios/AnsiRequestsScenario.cs | 196 ++++++++++++++++++-- 1 file changed, 178 insertions(+), 18 deletions(-) diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index 4658a643fa..eaf82ff0c1 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -1,4 +1,9 @@ -using Terminal.Gui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ColorHelper; +using Terminal.Gui; namespace UICatalog.Scenarios; @@ -9,40 +14,195 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Drawing")] public class AnsiRequestsScenario : Scenario { + private GraphView _graphView; + private Window _win; + + private DateTime start = DateTime.Now; + private ScatterSeries _sentSeries; + private ScatterSeries _answeredSeries; + + private List sends = new (); + private Dictionary answers = new (); + private Label _lblSummary; + public override void Main () { Application.Init (); - var win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; + _win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; - var btn = new Button () + var lbl = new Label () { - Text = "Send DAR", - Width = Dim.Auto () + Text = "This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends", + Height = 2, + Width = Dim.Fill() }; + Application.AddTimeout ( + TimeSpan.FromMilliseconds (1000), + () => + { + UpdateGraph (); + + UpdateResponses (); + return _win.DisposedCount == 0; + }); var tv = new TextView () { - Y = Pos.Bottom (btn), - Width = Dim.Fill(), + Y = Pos.Bottom (lbl), + Width = Dim.Percent (50), Height = Dim.Fill() }; - btn.Accepting += (s, e) => - { - // Ask for device attributes (DAR) - var p = Application.Driver.GetParser (); - p.ExpectResponse ("c", (r) => tv.Text += r + '\n'); - Application.Driver.RawWrite (EscSeqUtils.CSI_SendDeviceAttributes); - }; + var lblDar = new Label () + { + Y = Pos.Bottom (lbl), + X = Pos.Right (tv) + 1, + Text = "DAR per second", + }; + var cbDar = new NumericUpDown () + { + X = Pos.Right (lblDar), + Y = Pos.Bottom (lbl), + Value = 0, + }; + + cbDar.ValueChanging += (s, e) => + { + if (e.NewValue < 0 || e.NewValue > 20) + { + e.Cancel = true; + } + }; + _win.Add (cbDar); + + int lastSendTime = Environment.TickCount; + Application.AddTimeout ( + TimeSpan.FromMilliseconds (50), + () => + { + if (cbDar.Value > 0) + { + int interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds + int currentTime = Environment.TickCount; // Current system time in milliseconds - win.Add (tv); - win.Add (btn); + // Check if the time elapsed since the last send is greater than the interval + if (currentTime - lastSendTime >= interval) + { + SendDar (); // Send the request + lastSendTime = currentTime; // Update the last send time + } + } - Application.Run (win); - win.Dispose (); + return _win.DisposedCount == 0; + }); + + + _graphView = new GraphView () + { + Y = Pos.Bottom (cbDar), + X = Pos.Right (tv), + Width = Dim.Fill(), + Height = Dim.Fill(1) + }; + + _lblSummary = new Label () + { + Y = Pos.Bottom (_graphView), + X = Pos.Right (tv), + Width = Dim.Fill() + }; + + SetupGraph (); + + _win.Add (lbl); + _win.Add (lblDar); + _win.Add (cbDar); + _win.Add (tv); + _win.Add (_graphView); + _win.Add (_lblSummary); + + Application.Run (_win); + _win.Dispose (); Application.Shutdown (); } + + private void UpdateResponses () + { + _lblSummary.Text = GetSummary (); + _lblSummary.SetNeedsDisplay(); + } + + private string GetSummary () + { + if (answers.Count == 0) + { + return "No requests sent yet"; + } + + var last = answers.Last ().Value; + + var unique = answers.Values.Distinct ().Count (); + var total = answers.Count; + + return $"Last:{last} U:{unique} T:{total}"; + } + + private void SetupGraph () + { + + _graphView.Series.Add (_sentSeries = new ScatterSeries ()); + _graphView.Series.Add (_answeredSeries = new ScatterSeries ()); + + _sentSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightGreen, ColorName16.Black)); + _answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightCyan, ColorName16.Black)); + + // Todo: + // _graphView.Annotations.Add (_sentSeries new PathAnnotation {}); + + _graphView.CellSize = new PointF (1, 1); + _graphView.MarginBottom = 2; + _graphView.AxisX.Increment = 1; + _graphView.AxisX.Text = "Seconds"; + _graphView.GraphColor = new Attribute (Color.Green, Color.Black); + } + + private void UpdateGraph () + { + _sentSeries.Points = sends + .GroupBy (ToSeconds) + .Select (g => new PointF (g.Key, g.Count ())) + .ToList (); + + _answeredSeries.Points = answers.Keys + .GroupBy (ToSeconds) + .Select (g => new PointF (g.Key, g.Count ())) + .ToList (); + // _graphView.ScrollOffset = new PointF(,0); + _graphView.SetNeedsDisplay(); + + } + + private int ToSeconds (DateTime t) + { + return (int)(DateTime.Now - t).TotalSeconds; + } + + private void SendDar () + { + // Ask for device attributes (DAR) + var p = Application.Driver.GetParser (); + p.ExpectResponse ("c", HandleResponse); + Application.Driver.RawWrite (EscSeqUtils.CSI_SendDeviceAttributes); + sends.Add (DateTime.Now); + } + + private void HandleResponse (string response) + { + answers.Add (DateTime.Now,response); + } + + } \ No newline at end of file From d345fc01307a9321a2ebf92986d1fc9495e3916b Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 20 Oct 2024 20:30:03 +0100 Subject: [PATCH 33/77] Add AnsiRequestScheduler that prevents 2+ queries executing at once and has a global throttle --- .../ConsoleDrivers/AnsiResponseParser.cs | 144 ++++++++++++++++++ .../ConsoleDrivers/IAnsiResponseParser.cs | 8 + UICatalog/Scenarios/AnsiRequestsScenario.cs | 47 ++++-- 3 files changed, 183 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index f6a2403d1f..31a9ea35d7 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -1,7 +1,91 @@ #nullable enable +using System.Collections.Concurrent; +using System.Runtime.ConstrainedExecution; + namespace Terminal.Gui; +public class AnsiRequestScheduler(IAnsiResponseParser parser) +{ + public static int sent = 0; + public List Requsts = new (); + + private ConcurrentDictionary _lastSend = new (); + + private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); + + /// + /// Sends the immediately or queues it if there is already + /// an outstanding request for the given . + /// + /// + /// if request was sent immediately. if it was queued. + public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) + { + if (CanSend(request)) + { + Send (request); + + return true; + } + else + { + Requsts.Add (request); + return false; + } + } + + + /// + /// Identifies and runs any that can be sent based on the + /// current outstanding requests of the parser. + /// + /// if a request was found and run. + /// if no outstanding requests or all have existing outstanding requests underway in parser. + public bool RunSchedule () + { + var opportunity = Requsts.FirstOrDefault (CanSend); + + if (opportunity != null) + { + Requsts.Remove (opportunity); + Send (opportunity); + + return true; + } + + return false; + } + + private void Send (AnsiEscapeSequenceRequest r) + { + Interlocked.Increment(ref sent); + _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now); + parser.ExpectResponse (r.Terminator,r.ResponseReceived); + r.Send (); + } + + public bool CanSend (AnsiEscapeSequenceRequest r) + { + if (ShouldThrottle (r)) + { + return false; + } + + return !parser.IsExpecting (r.Terminator); + } + + private bool ShouldThrottle (AnsiEscapeSequenceRequest r) + { + if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) + { + return DateTime.Now - value < _throttle; + } + + return false; + } +} + internal abstract class AnsiResponseParserBase : IAnsiResponseParser { protected readonly List<(string terminator, Action response)> expectedResponses = new (); @@ -227,6 +311,13 @@ protected void DispatchResponse (Action response) /// completed. /// public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } + + /// + public bool IsExpecting (string requestTerminator) + { + // 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()); + } } internal class AnsiResponseParser : AnsiResponseParserBase @@ -298,3 +389,56 @@ public string Release () protected override void AddToHeld (object o) { held.Append ((char)o); } } + + +/// +/// Describes an ongoing ANSI request sent to the console. +/// Use to handle the response +/// when console answers the request. +/// +public class AnsiEscapeSequenceRequest +{ + /// + /// Request to send e.g. see + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// + public required string Request { get; init; } + + /// + /// Invoked when the console responds with an ANSI response code that matches the + /// + /// + public Action ResponseReceived; + + /// + /// + /// The terminator that uniquely identifies the type of response as responded + /// by the console. e.g. for + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// the terminator is + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator + /// + /// . + /// + /// + /// After sending a request, the first response with matching terminator will be matched + /// to the oldest outstanding request. + /// + /// + public required string Terminator { get; init; } + + /// + /// Sends the to the raw output stream of the current . + /// Only call this method from the main UI thread. You should use if + /// sending many requests. + /// + public void Send () + { + Application.Driver?.RawWrite (Request); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs index f82cf148ad..00eac8208c 100644 --- a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -5,4 +5,12 @@ public interface IAnsiResponseParser { AnsiResponseParserState State { get; } void ExpectResponse (string terminator, Action response); + + /// + /// Returns true if there is an existing expectation (i.e. we are waiting a response + /// from console) for the given . + /// + /// + /// + bool IsExpecting (string requestTerminator); } diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index eaf82ff0c1..8f53770e3f 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -24,10 +24,14 @@ public class AnsiRequestsScenario : Scenario private List sends = new (); private Dictionary answers = new (); private Label _lblSummary; + private AnsiRequestScheduler _scheduler; public override void Main () { Application.Init (); + + _scheduler = new AnsiRequestScheduler (Application.Driver.GetParser ()); + _win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; var lbl = new Label () @@ -45,7 +49,7 @@ public override void Main () UpdateResponses (); - return _win.DisposedCount == 0; + return true; }); var tv = new TextView () @@ -79,24 +83,32 @@ public override void Main () _win.Add (cbDar); int lastSendTime = Environment.TickCount; + object lockObj = new object (); Application.AddTimeout ( - TimeSpan.FromMilliseconds (50), + TimeSpan.FromMilliseconds (100), () => { - if (cbDar.Value > 0) + lock (lockObj) { - int interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds - int currentTime = Environment.TickCount; // Current system time in milliseconds - - // Check if the time elapsed since the last send is greater than the interval - if (currentTime - lastSendTime >= interval) + if (cbDar.Value > 0) { - SendDar (); // Send the request - lastSendTime = currentTime; // Update the last send time + int interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds + int currentTime = Environment.TickCount; // Current system time in milliseconds + + // Check if the time elapsed since the last send is greater than the interval + if (currentTime - lastSendTime >= interval) + { + SendDar (); // Send the request + lastSendTime = currentTime; // Update the last send time + } } + + // TODO: Scheduler probably should be part of core driver + // Also any that we didn't get a chance to send + _scheduler.RunSchedule(); } - return _win.DisposedCount == 0; + return true; }); @@ -157,7 +169,7 @@ private void SetupGraph () _graphView.Series.Add (_answeredSeries = new ScatterSeries ()); _sentSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightGreen, ColorName16.Black)); - _answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightCyan, ColorName16.Black)); + _answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightRed, ColorName16.Black)); // Todo: // _graphView.Annotations.Add (_sentSeries new PathAnnotation {}); @@ -192,10 +204,13 @@ private int ToSeconds (DateTime t) private void SendDar () { - // Ask for device attributes (DAR) - var p = Application.Driver.GetParser (); - p.ExpectResponse ("c", HandleResponse); - Application.Driver.RawWrite (EscSeqUtils.CSI_SendDeviceAttributes); + _scheduler.SendOrSchedule ( + new () + { + Request = EscSeqUtils.CSI_SendDeviceAttributes, + Terminator = EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, + ResponseReceived = HandleResponse + }); sends.Add (DateTime.Now); } From 5356bf4a69a6248ed847974a05544d236f3514cf Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 20 Oct 2024 20:34:29 +0100 Subject: [PATCH 34/77] Set scenario timeout to 50ms --- UICatalog/Scenarios/AnsiRequestsScenario.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index 8f53770e3f..b6da2093af 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -85,7 +85,7 @@ public override void Main () int lastSendTime = Environment.TickCount; object lockObj = new object (); Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), + TimeSpan.FromMilliseconds (50), () => { lock (lockObj) From 92648e5a4890b03a5d0d960d55e7ab75cddf73a2 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 03:14:06 +0100 Subject: [PATCH 35/77] typo and fix FakeDriver --- Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs | 10 +++++----- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 12 ++---------- Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs | 8 ++++++++ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 31a9ea35d7..37c6703cfb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -8,7 +8,7 @@ namespace Terminal.Gui; public class AnsiRequestScheduler(IAnsiResponseParser parser) { public static int sent = 0; - public List Requsts = new (); + public List Requests = new (); private ConcurrentDictionary _lastSend = new (); @@ -30,25 +30,25 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) } else { - Requsts.Add (request); + Requests.Add (request); return false; } } /// - /// Identifies and runs any that can be sent based on the + /// Identifies and runs any that can be sent based on the /// current outstanding requests of the parser. /// /// if a request was found and run. /// if no outstanding requests or all have existing outstanding requests underway in parser. public bool RunSchedule () { - var opportunity = Requsts.FirstOrDefault (CanSend); + var opportunity = Requests.FirstOrDefault (CanSend); if (opportunity != null) { - Requsts.Remove (opportunity); + Requests.Remove (opportunity); Send (opportunity); return true; diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 78126b4e49..bcaa685329 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -611,18 +611,10 @@ public void OnMouseEvent (MouseEventArgs a) #endregion /// - public virtual IAnsiResponseParser GetParser () - { - // TODO: implement in other drivers - throw new NotSupportedException (); - } + public abstract IAnsiResponseParser GetParser (); /// - public virtual void RawWrite (string str) - { - // TODO: implement in other drivers - throw new NotSupportedException (); - } + public abstract void RawWrite (string str); } /// diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 73c12959f4..a98c5ca91e 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -392,6 +392,14 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); } + private AnsiResponseParser _parser = new (); + + /// + public override IAnsiResponseParser GetParser () => _parser; + + /// + public override void RawWrite (string str) { throw new NotImplementedException (); } + public void SetBufferSize (int width, int height) { FakeConsole.SetBufferSize (width, height); From 50520240611c5aac93a3d6a19c1dabe94a6dc336 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 03:41:02 +0100 Subject: [PATCH 36/77] Make scheduler part of main loop and hide implementation details from public API --- Terminal.Gui/Application/MainLoop.cs | 7 +++++ .../ConsoleDrivers/AnsiResponseParser.cs | 11 ++++++- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 30 +++++++++++++++---- .../CursesDriver/CursesDriver.cs | 8 ++--- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 4 +-- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 8 ----- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 4 +-- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 14 ++++----- UICatalog/Scenarios/AnsiRequestsScenario.cs | 21 +++++-------- 9 files changed, 64 insertions(+), 43 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index ee4bba220c..bfd7429130 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -270,6 +270,8 @@ internal void RunIteration () } } + RunAnsiScheduler (); + MainLoopDriver.Iteration (); var runIdle = false; @@ -285,6 +287,11 @@ internal void RunIteration () } } + private void RunAnsiScheduler () + { + Application.Driver?.GetRequestScheduler ().RunSchedule (); + } + /// Stops the main loop driver and calls . Used only for unit tests. internal void Stop () { diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 37c6703cfb..3998af105f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -13,6 +13,7 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) private ConcurrentDictionary _lastSend = new (); private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); + private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); /// /// Sends the immediately or queues it if there is already @@ -35,15 +36,23 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) } } + private DateTime _lastRun = DateTime.Now; /// /// Identifies and runs any that can be sent based on the /// current outstanding requests of the parser. /// + /// Repeated requests to run the schedule over short period of time will be ignored. + /// Pass to override this behaviour and force evaluation of outstanding requests. /// if a request was found and run. /// if no outstanding requests or all have existing outstanding requests underway in parser. - public bool RunSchedule () + public bool RunSchedule (bool force = false) { + if (!force && DateTime.Now - _lastRun < _runScheduleThrottle) + { + return false; + } + var opportunity = Requests.FirstOrDefault (CanSend); if (opportunity != null) diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index bcaa685329..bc78c8317b 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -13,7 +13,7 @@ namespace Terminal.Gui; /// - that uses the .NET Console API - /// for unit testing. /// -public abstract class ConsoleDriver : IConsoleDriver +public abstract class ConsoleDriver { // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. @@ -610,11 +610,31 @@ public void OnMouseEvent (MouseEventArgs a) #endregion - /// - public abstract IAnsiResponseParser GetParser (); + private AnsiRequestScheduler? _scheduler; - /// - public abstract void RawWrite (string str); + /// + /// Queues the given for execution + /// + /// + public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) + { + GetRequestScheduler ().SendOrSchedule (request); + } + + protected abstract IAnsiResponseParser GetParser (); + + internal AnsiRequestScheduler GetRequestScheduler () + { + // Lazy initialization because GetParser is virtual + return _scheduler ??= new (GetParser ()); + } + + /// + /// Writes the given directly to the console (rather than the output + /// draw buffer). + /// + /// + internal abstract void RawWrite (string str); } /// diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index cd287d5b2a..647acd19e9 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -194,7 +194,7 @@ public void StopReportingMouseMoves () } /// - public override void RawWrite (string str) + internal override void RawWrite (string str) { Console.Out.Write (str); } @@ -583,9 +583,9 @@ internal override MainLoop Init () return new MainLoop (_mainLoopDriver); } - private AnsiResponseParser Parser { get; set; } = new (); + private readonly AnsiResponseParser _parser = new (); /// - public override IAnsiResponseParser GetParser () => Parser; + protected override IAnsiResponseParser GetParser () => _parser; internal void ProcessInput () { @@ -889,7 +889,7 @@ ref ConsoleKeyInfo [] cki { foreach (var c in cki) { - Parser.ProcessInput (c.KeyChar.ToString()); + _parser.ProcessInput (c.KeyChar.ToString()); } } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index a98c5ca91e..537d60adfa 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -395,10 +395,10 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al private AnsiResponseParser _parser = new (); /// - public override IAnsiResponseParser GetParser () => _parser; + protected override IAnsiResponseParser GetParser () => _parser; /// - public override void RawWrite (string str) { throw new NotImplementedException (); } + internal override void RawWrite (string str) { } public void SetBufferSize (int width, int height) { diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs deleted file mode 100644 index 582a1a6ab7..0000000000 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable enable -namespace Terminal.Gui; - -public interface IConsoleDriver -{ - IAnsiResponseParser GetParser (); - void RawWrite (string str); -} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 2e0e3ab6da..02541c967f 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1049,10 +1049,10 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out } /// - public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + protected override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; /// - public override void RawWrite (string str) + internal override void RawWrite (string str) { Console.Write (str); } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 0340ad8bd5..815a1325e9 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1165,10 +1165,10 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } /// - public override IAnsiResponseParser GetParser () => Parser; + protected override IAnsiResponseParser GetParser () => _parser; /// - public override void RawWrite (string str) => WinConsole?.WriteANSI (str); + internal override void RawWrite (string str) => WinConsole?.WriteANSI (str); #region Not Implemented @@ -1458,7 +1458,7 @@ internal override MainLoop Init () /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// private TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); - public AnsiResponseParser Parser { get; set; } = new (); + private AnsiResponseParser _parser = new (); internal void ProcessInput (WindowsConsole.InputRecord inputEvent) { @@ -1557,7 +1557,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) } foreach (Tuple output in - Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent))) + _parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent))) { yield return output.Item2; } @@ -1566,10 +1566,10 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) public IEnumerable ShouldRelease () { - if (Parser.State == AnsiResponseParserState.ExpectingBracket && - DateTime.Now - Parser.StateChangedAt > _escTimeout) + if (_parser.State == AnsiResponseParserState.ExpectingBracket && + DateTime.Now - _parser.StateChangedAt > _escTimeout) { - return Parser.Release ().Select (o => o.Item2); + return _parser.Release ().Select (o => o.Item2); } return []; diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index b6da2093af..ba10ab583b 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -24,14 +24,11 @@ public class AnsiRequestsScenario : Scenario private List sends = new (); private Dictionary answers = new (); private Label _lblSummary; - private AnsiRequestScheduler _scheduler; public override void Main () { Application.Init (); - _scheduler = new AnsiRequestScheduler (Application.Driver.GetParser ()); - _win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; var lbl = new Label () @@ -102,10 +99,6 @@ public override void Main () lastSendTime = currentTime; // Update the last send time } } - - // TODO: Scheduler probably should be part of core driver - // Also any that we didn't get a chance to send - _scheduler.RunSchedule(); } return true; @@ -204,13 +197,13 @@ private int ToSeconds (DateTime t) private void SendDar () { - _scheduler.SendOrSchedule ( - new () - { - Request = EscSeqUtils.CSI_SendDeviceAttributes, - Terminator = EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, - ResponseReceived = HandleResponse - }); + Application.Driver.QueueAnsiRequest ( + new () + { + Request = EscSeqUtils.CSI_SendDeviceAttributes, + Terminator = EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, + ResponseReceived = HandleResponse + }); sends.Add (DateTime.Now); } From 6de203290dd1c50e997976497c7472bc07bed998 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 03:54:21 +0100 Subject: [PATCH 37/77] Explain purpose of throttle and move to seperate class --- .../ConsoleDrivers/AnsiRequestScheduler.cs | 104 ++++++++++++++++++ .../ConsoleDrivers/AnsiResponseParser.cs | 91 --------------- 2 files changed, 104 insertions(+), 91 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs new file mode 100644 index 0000000000..cc825ca6ad --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -0,0 +1,104 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +public class AnsiRequestScheduler(IAnsiResponseParser parser) +{ + private readonly List _requests = new (); + + /// + /// + /// Dictionary where key is ansi request terminator and value is when we last sent a request for + /// this terminator. Combined with this prevents hammering the console + /// with too many requests in sequence which can cause console to freeze as there is no space for + /// regular screen drawing / mouse events etc to come in. + /// + /// + /// When user exceeds the throttle, new requests accumulate in (i.e. remain + /// queued). + /// + /// + private ConcurrentDictionary _lastSend = new (); + + private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); + private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); + + /// + /// Sends the immediately or queues it if there is already + /// an outstanding request for the given . + /// + /// + /// if request was sent immediately. if it was queued. + public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) + { + if (CanSend(request)) + { + Send (request); + + return true; + } + else + { + _requests.Add (request); + return false; + } + } + + private DateTime _lastRun = DateTime.Now; + + /// + /// Identifies and runs any that can be sent based on the + /// current outstanding requests of the parser. + /// + /// Repeated requests to run the schedule over short period of time will be ignored. + /// Pass to override this behaviour and force evaluation of outstanding requests. + /// if a request was found and run. + /// if no outstanding requests or all have existing outstanding requests underway in parser. + public bool RunSchedule (bool force = false) + { + if (!force && DateTime.Now - _lastRun < _runScheduleThrottle) + { + return false; + } + + var opportunity = _requests.FirstOrDefault (CanSend); + + if (opportunity != null) + { + _requests.Remove (opportunity); + Send (opportunity); + + return true; + } + + return false; + } + + private void Send (AnsiEscapeSequenceRequest r) + { + _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now); + parser.ExpectResponse (r.Terminator,r.ResponseReceived); + r.Send (); + } + + public bool CanSend (AnsiEscapeSequenceRequest r) + { + if (ShouldThrottle (r)) + { + return false; + } + + return !parser.IsExpecting (r.Terminator); + } + + private bool ShouldThrottle (AnsiEscapeSequenceRequest r) + { + if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) + { + return DateTime.Now - value < _throttle; + } + + return false; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 3998af105f..d0c43acc09 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -1,100 +1,9 @@ #nullable enable -using System.Collections.Concurrent; using System.Runtime.ConstrainedExecution; namespace Terminal.Gui; -public class AnsiRequestScheduler(IAnsiResponseParser parser) -{ - public static int sent = 0; - public List Requests = new (); - - private ConcurrentDictionary _lastSend = new (); - - private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); - private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); - - /// - /// Sends the immediately or queues it if there is already - /// an outstanding request for the given . - /// - /// - /// if request was sent immediately. if it was queued. - public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) - { - if (CanSend(request)) - { - Send (request); - - return true; - } - else - { - Requests.Add (request); - return false; - } - } - - private DateTime _lastRun = DateTime.Now; - - /// - /// Identifies and runs any that can be sent based on the - /// current outstanding requests of the parser. - /// - /// Repeated requests to run the schedule over short period of time will be ignored. - /// Pass to override this behaviour and force evaluation of outstanding requests. - /// if a request was found and run. - /// if no outstanding requests or all have existing outstanding requests underway in parser. - public bool RunSchedule (bool force = false) - { - if (!force && DateTime.Now - _lastRun < _runScheduleThrottle) - { - return false; - } - - var opportunity = Requests.FirstOrDefault (CanSend); - - if (opportunity != null) - { - Requests.Remove (opportunity); - Send (opportunity); - - return true; - } - - return false; - } - - private void Send (AnsiEscapeSequenceRequest r) - { - Interlocked.Increment(ref sent); - _lastSend.AddOrUpdate (r.Terminator,(s)=>DateTime.Now,(s,v)=>DateTime.Now); - parser.ExpectResponse (r.Terminator,r.ResponseReceived); - r.Send (); - } - - public bool CanSend (AnsiEscapeSequenceRequest r) - { - if (ShouldThrottle (r)) - { - return false; - } - - return !parser.IsExpecting (r.Terminator); - } - - private bool ShouldThrottle (AnsiEscapeSequenceRequest r) - { - if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) - { - return DateTime.Now - value < _throttle; - } - - return false; - } -} - internal abstract class AnsiResponseParserBase : IAnsiResponseParser { protected readonly List<(string terminator, Action response)> expectedResponses = new (); From 23af0d9fd35990c88f6eaf4401697e5152208813 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 04:24:13 +0100 Subject: [PATCH 38/77] Add evicting and xmldoc --- .../ConsoleDrivers/AnsiRequestScheduler.cs | 88 ++++++++++++++++--- .../ConsoleDrivers/AnsiResponseParser.cs | 11 ++- .../ConsoleDrivers/IAnsiResponseParser.cs | 24 +++++ 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs index cc825ca6ad..d6a5bd3bbc 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; public class AnsiRequestScheduler(IAnsiResponseParser parser) { - private readonly List _requests = new (); + private readonly List> _requests = new (); /// /// @@ -24,6 +24,13 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); + /// + /// If console has not responded to a request after this period of time, we assume that it is never going + /// to respond. Only affects when we try to send a new request with the same terminator - at which point + /// we tell the parser to stop expecting the old request and start expecting the new request. + /// + private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + /// /// Sends the immediately or queues it if there is already /// an outstanding request for the given . @@ -32,17 +39,51 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) /// if request was sent immediately. if it was queued. public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) { - if (CanSend(request)) + + if (CanSend(request, out var reason)) { Send (request); - return true; } - else + + if (reason == ReasonCannotSend.OutstandingRequest) { - _requests.Add (request); - return false; + EvictStaleRequests (request.Terminator); + + // Try again after + if (CanSend (request, out _)) + { + Send (request); + return true; + } } + + _requests.Add (Tuple.Create(request,DateTime.Now)); + return false; + } + + /// + /// Looks to see if the last time we sent + /// is a long time ago. If so we assume that we will never get a response and + /// can proceed with a new request for this terminator (returning ). + /// + /// + /// + private bool EvictStaleRequests (string withTerminator) + { + if (_lastSend.TryGetValue (withTerminator, out var dt)) + { + // TODO: If debugging this can cause problem becuase we stop expecting response but one comes in anyway + // causing parser to ignore and it to fall through to default console iteration which typically crashes. + if (DateTime.Now - dt > _staleTimeout) + { + parser.StopExpecting (withTerminator); + + return true; + } + } + + return false; } private DateTime _lastRun = DateTime.Now; @@ -62,12 +103,12 @@ public bool RunSchedule (bool force = false) return false; } - var opportunity = _requests.FirstOrDefault (CanSend); + var opportunity = _requests.FirstOrDefault (r=>CanSend(r.Item1, out _)); if (opportunity != null) { _requests.Remove (opportunity); - Send (opportunity); + Send (opportunity.Item1); return true; } @@ -82,14 +123,22 @@ private void Send (AnsiEscapeSequenceRequest r) r.Send (); } - public bool CanSend (AnsiEscapeSequenceRequest r) + private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason) { if (ShouldThrottle (r)) { + reason = ReasonCannotSend.TooManyRequests; + return false; + } + + if (parser.IsExpecting (r.Terminator)) + { + reason = ReasonCannotSend.OutstandingRequest; return false; } - return !parser.IsExpecting (r.Terminator); + reason = default; + return true; } private bool ShouldThrottle (AnsiEscapeSequenceRequest r) @@ -102,3 +151,22 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) return false; } } + +internal enum ReasonCannotSend +{ + /// + /// No reason given. + /// + None = 0, + + /// + /// The parser is already waiting for a request to complete with the given terminator. + /// + OutstandingRequest, + + /// + /// There have been too many requests sent recently, new requests will be put into + /// queue to prevent console becoming unresponsive. + /// + TooManyRequests +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index d0c43acc09..ab142226c9 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -224,10 +224,7 @@ protected void DispatchResponse (Action response) ResetState (); } - /// - /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is - /// completed. - /// + /// public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } /// @@ -236,6 +233,12 @@ public bool IsExpecting (string requestTerminator) // 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()); } + + /// + public void StopExpecting (string requestTerminator) + { + expectedResponses.RemoveAll (r => r.terminator == requestTerminator); + } } internal class AnsiResponseParser : AnsiResponseParserBase diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs index 00eac8208c..4a6ee635be 100644 --- a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -1,9 +1,25 @@ #nullable enable namespace Terminal.Gui; +/// +/// When implemented in a derived class, allows watching an input stream of characters +/// (i.e. console input) for ANSI response sequences. +/// public interface IAnsiResponseParser { + /// + /// Current state of the parser based on what sequence of characters it has + /// read from the console input stream. + /// AnsiResponseParserState State { get; } + + /// + /// Notifies the parser that you are expecting a response to come in + /// with the given (i.e. because you have + /// sent an ANSI request out). + /// + /// The terminator you expect to see on response. + /// Callback to invoke when the response is seen in console input. void ExpectResponse (string terminator, Action response); /// @@ -13,4 +29,12 @@ public interface IAnsiResponseParser /// /// bool IsExpecting (string requestTerminator); + + /// + /// Removes callback and expectation that we will get a response for the + /// given . Use to give up on very old + /// requests e.g. if you want to send a different one with the same terminator. + /// + /// + void StopExpecting (string requestTerminator); } From d3ac6e108dc0f7848751a0d456b4b25e8d6f468e Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 04:41:55 +0100 Subject: [PATCH 39/77] Support for late responses collection --- .../ConsoleDrivers/AnsiRequestScheduler.cs | 3 +- .../ConsoleDrivers/AnsiResponseParser.cs | 48 ++++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs index d6a5bd3bbc..a051e6c8eb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.Diagnostics; namespace Terminal.Gui; @@ -73,8 +74,6 @@ private bool EvictStaleRequests (string withTerminator) { if (_lastSend.TryGetValue (withTerminator, out var dt)) { - // TODO: If debugging this can cause problem becuase we stop expecting response but one comes in anyway - // causing parser to ignore and it to fall through to default console iteration which typically crashes. if (DateTime.Now - dt > _staleTimeout) { parser.StopExpecting (withTerminator); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index ab142226c9..f5aaac97b9 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -6,7 +6,16 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { + /// + /// Responses we are expecting to come in. + /// protected readonly List<(string terminator, Action response)> expectedResponses = new (); + + /// + /// Collection of responses that we . + /// + protected readonly List<(string terminator, Action response)> lateResponses = new (); + private AnsiResponseParserState _state = AnsiResponseParserState.Normal; // Current state of the parser @@ -198,17 +207,20 @@ protected bool ShouldReleaseHeldContent () { string cur = HeldToString (); - // Check for expected responses - (string terminator, Action response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator)); - - if (matchingResponse.response != null) + // Look for an expected response for what is accumulated so far (since Esc) + if (MatchResponse (cur, expectedResponses)) { - DispatchResponse (matchingResponse.response); - expectedResponses.Remove (matchingResponse); + return false; + } + // Also try looking for late requests + if (MatchResponse (cur, lateResponses)) + { return false; } + // Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity) + // then we can release it back to input processing stream if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) { // Detected a response that was not expected @@ -218,6 +230,22 @@ protected bool ShouldReleaseHeldContent () return false; // Continue accumulating } + private bool MatchResponse (string cur, List<(string terminator, Action response)> valueTuples) + { + // Check for expected responses + var matchingResponse = valueTuples.FirstOrDefault (r => cur.EndsWith (r.terminator)); + + if (matchingResponse.response != null) + { + DispatchResponse (matchingResponse.response); + expectedResponses.Remove (matchingResponse); + + return true; + } + + return false; + } + protected void DispatchResponse (Action response) { response?.Invoke (HeldToString ()); @@ -237,7 +265,13 @@ public bool IsExpecting (string requestTerminator) /// public void StopExpecting (string requestTerminator) { - expectedResponses.RemoveAll (r => r.terminator == requestTerminator); + var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray (); + + foreach (var r in removed) + { + expectedResponses.Remove (r); + lateResponses.Add (r); + } } } From 232421739713c7d585a01c1bd3bb11b0c4d1ad3b Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Oct 2024 04:57:30 +0100 Subject: [PATCH 40/77] Add tests and fix late queue --- .../ConsoleDrivers/AnsiResponseParser.cs | 25 +++++++------ .../ConsoleDrivers/AnsiResponseParserTests.cs | 36 +++++++++++++++++-- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index f5aaac97b9..d93a900ac3 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -208,13 +208,13 @@ protected bool ShouldReleaseHeldContent () string cur = HeldToString (); // Look for an expected response for what is accumulated so far (since Esc) - if (MatchResponse (cur, expectedResponses)) + if (MatchResponse (cur, expectedResponses, true)) { return false; } - // Also try looking for late requests - if (MatchResponse (cur, lateResponses)) + // 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)) { return false; } @@ -230,15 +230,20 @@ protected bool ShouldReleaseHeldContent () return false; // Continue accumulating } - private bool MatchResponse (string cur, List<(string terminator, Action response)> valueTuples) + private bool MatchResponse (string cur, List<(string terminator, Action response)> collection, bool invokeCallback) { // Check for expected responses - var matchingResponse = valueTuples.FirstOrDefault (r => cur.EndsWith (r.terminator)); + var matchingResponse = collection.FirstOrDefault (r => cur.EndsWith (r.terminator)); if (matchingResponse.response != null) { - DispatchResponse (matchingResponse.response); - expectedResponses.Remove (matchingResponse); + + if (invokeCallback) + { + matchingResponse.response?.Invoke (HeldToString ()); + } + ResetState (); + collection.Remove (matchingResponse); return true; } @@ -246,12 +251,6 @@ private bool MatchResponse (string cur, List<(string terminator, Action return false; } - protected void DispatchResponse (Action response) - { - response?.Invoke (HeldToString ()); - ResetState (); - } - /// public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 71b892b5f5..e1b3a5b24a 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,5 +1,4 @@ -using System.CommandLine.Parsing; -using System.Diagnostics; +using System.Diagnostics; using System.Text; using Xunit.Abstractions; @@ -319,6 +318,39 @@ public void TwoExcapesInARowWithTextBetween () AssertManualReleaseIs ("\u001b", 5); } + [Fact] + public void TestLateResponses () + { + var p = new AnsiResponseParser (); + + string? responseA = null; + string? responseB = null; + + p.ExpectResponse ("z",(r)=>responseA=r); + + // Some time goes by without us seeing a response + p.StopExpecting ("z"); + + // Send our new request + p.ExpectResponse ("z", (r) => responseB = r); + + // 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")); + Assert.Null (responseA); + Assert.Equal ("\u001b[<1;2z", responseB); + + // Oh looks like we got one late after all - swallow it + Assert.Empty (p.ProcessInput ("\u001b[0000z")); + + // Do not expect late responses to be populated back to your variable + Assert.Null (responseA); + Assert.Equal ("\u001b[<1;2z", responseB); + + // We now have no outstanding requests (late or otherwise) so new ansi codes should just fall through + Assert.Equal ("\u001b[111z", p.ProcessInput ("\u001b[111z")); + + } + private Tuple [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From c1f880ef8dabe0c8d2c793c5c6e4002e2c0ddd81 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 25 Oct 2024 20:54:43 +0100 Subject: [PATCH 41/77] WIP: Trying to unpick NetDriver --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 133 +++++++++++------------ UICatalog/Properties/launchSettings.json | 3 +- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 02541c967f..233bbae660 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -256,75 +256,83 @@ private void ProcessInputQueue () return; } - if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) - || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) + // Parse + foreach (var released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { - if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) - { - _cki = EscSeqUtils.ResizeArray ( - new ConsoleKeyInfo ( - (char)KeyCode.Esc, - 0, - false, - false, - false - ), - _cki - ); - } - - _isEscSeq = true; - newConsoleKeyInfo = consoleKeyInfo; - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - - if (Console.KeyAvailable) - { - continue; - } - - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - _isEscSeq = false; - - break; + ProcessInputAfterParsing (released.Item2, ref key, ref mod, ref newConsoleKeyInfo); } + } + } + _inputReady.Set (); + } - if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { }) - { - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - - if (Console.KeyAvailable) - { - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - } - else - { - ProcessMapConsoleKeyInfo (consoleKeyInfo); - } + } + private void ProcessInputAfterParsing(ConsoleKeyInfo consoleKeyInfo, + ref ConsoleKey key, + ref ConsoleModifiers mod, + ref ConsoleKeyInfo newConsoleKeyInfo) + { - break; - } + if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) + || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) + { + if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) + { + _cki = EscSeqUtils.ResizeArray ( + new ConsoleKeyInfo ( + (char)KeyCode.Esc, + 0, + false, + false, + false + ), + _cki + ); + } - ProcessMapConsoleKeyInfo (consoleKeyInfo); + _isEscSeq = true; + newConsoleKeyInfo = consoleKeyInfo; + _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - break; - } + if (Console.KeyAvailable) + { + return; } - _inputReady.Set (); + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); + _cki = null; + _isEscSeq = false; + return; } - void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { }) { - _inputQueue.Enqueue ( - new InputResult - { - EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) - } - ); - _isEscSeq = false; + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); + _cki = null; + + if (Console.KeyAvailable) + { + _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); + } + else + { + ProcessMapConsoleKeyInfo (consoleKeyInfo); + } + return; } + + ProcessMapConsoleKeyInfo (consoleKeyInfo); + } + + void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + _inputQueue.Enqueue ( + new InputResult + { + EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) + } + ); + _isEscSeq = false; } private void CheckWindowSizeChange () @@ -407,7 +415,7 @@ private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight return true; } - public AnsiResponseParser Parser { get; private set; } = new (); + public AnsiResponseParser Parser { get; private set; } = new (); // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) private void ProcessRequestResponse ( @@ -417,15 +425,6 @@ private void ProcessRequestResponse ( ref ConsoleModifiers mod ) { - if (cki != null) - { - // If the response is fully consumed by parser - if(cki.Length > 1 && string.IsNullOrEmpty(Parser.ProcessInput (new string(cki.Select (k=>k.KeyChar).ToArray ())))) - { - // Lets not double process - return; - } - } // isMouse is true if it's CSI<, false otherwise EscSeqUtils.DecodeEscSeq ( diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index 041a5c22dc..f60efb5709 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { "UICatalog": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "-d NetDriver" }, "UICatalog --driver NetDriver": { "commandName": "Project", From eaddbc6c1dbb74f0a20d0232495803b503218420 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 10:21:22 +0100 Subject: [PATCH 42/77] Support for persistent expectations of responses --- .../ConsoleDrivers/AnsiRequestScheduler.cs | 4 +- .../ConsoleDrivers/AnsiResponseExpectation.cs | 10 +++ .../ConsoleDrivers/AnsiResponseParser.cs | 81 ++++++++++++++----- .../ConsoleDrivers/IAnsiResponseParser.cs | 19 +++-- .../ConsoleDrivers/AnsiResponseParserTests.cs | 41 +++++++--- 5 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs index a051e6c8eb..3722a8667a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs @@ -76,7 +76,7 @@ private bool EvictStaleRequests (string withTerminator) { if (DateTime.Now - dt > _staleTimeout) { - parser.StopExpecting (withTerminator); + parser.StopExpecting (withTerminator,false); return true; } @@ -118,7 +118,7 @@ public bool RunSchedule (bool force = false) 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 (); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs new file mode 100644 index 0000000000..d1245adccd --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Terminal.Gui; + +public record AnsiResponseExpectation (string Terminator, Action Response) +{ + public bool Matches (string cur) + { + return cur.EndsWith (Terminator); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index d93a900ac3..9f1e6dcd4c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -4,17 +4,24 @@ namespace Terminal.Gui; + internal abstract class AnsiResponseParserBase : IAnsiResponseParser { /// /// Responses we are expecting to come in. /// - protected readonly List<(string terminator, Action response)> expectedResponses = new (); + protected readonly List expectedResponses = new (); /// /// Collection of responses that we . /// - protected readonly List<(string terminator, Action response)> lateResponses = new (); + protected readonly List lateResponses = new (); + + /// + /// Responses that you want to look out for that will come in continuously e.g. mouse events. + /// Key is the terminator. + /// + protected readonly List persistentExpectations = new (); private AnsiResponseParserState _state = AnsiResponseParserState.Normal; @@ -208,13 +215,28 @@ protected bool ShouldReleaseHeldContent () 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 @@ protected bool ShouldReleaseHeldContent () return false; // Continue accumulating } - private bool MatchResponse (string cur, List<(string terminator, Action response)> collection, bool invokeCallback) + + private bool MatchResponse (string cur, List 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 @@ private bool MatchResponse (string cur, List<(string terminator, Action } /// - public void ExpectResponse (string terminator, Action response) { expectedResponses.Add ((terminator, response)); } + public void ExpectResponse (string terminator, Action response, bool persistent) + { + if (persistent) + { + persistentExpectations.Add (new (terminator, response)); + } + else + { + expectedResponses.Add (new (terminator, response)); + } + } /// - public bool IsExpecting (string requestTerminator) + 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 (requestTerminator).Any()); + return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any()); } /// - public void StopExpecting (string requestTerminator) + public void StopExpecting (string terminator, bool persistent) { - var removed = expectedResponses.Where (r => r.terminator == requestTerminator).ToArray (); - - foreach (var r in removed) + if (persistent) + { + persistentExpectations.RemoveAll (r=>r.Matches (terminator)); + } + else { - expectedResponses.Remove (r); - lateResponses.Add (r); + var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); + + foreach (var r in removed) + { + expectedResponses.Remove (r); + lateResponses.Add (r); + } } } } diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs index 4a6ee635be..d1246c6610 100644 --- a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs @@ -19,16 +19,21 @@ public interface IAnsiResponseParser /// sent an ANSI request out). /// /// The terminator you expect to see on response. + /// if you want this to persist permanently + /// and be raised for every event matching the . /// Callback to invoke when the response is seen in console input. - void ExpectResponse (string terminator, Action response); + /// If trying to register a persistent request for a terminator + /// that already has one. + /// exists. + void ExpectResponse (string terminator, Action response, bool persistent); /// /// Returns true if there is an existing expectation (i.e. we are waiting a response - /// from console) for the given . + /// from console) for the given . /// - /// + /// /// - bool IsExpecting (string requestTerminator); + bool IsExpecting (string terminator); /// /// 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. /// /// - void StopExpecting (string requestTerminator); -} + /// if you want to remove a persistent + /// request listener. + void StopExpecting (string requestTerminator, bool persistent); +} \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index e1b3a5b24a..fa7fac4f79 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -27,8 +27,8 @@ public void TestInputProcessing () 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 void TestInputSequences (string ansiStream, string expectedTerminator, st 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 void TestInputSequencesExact (string caseName, char? terminator, IEnumera 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 void TestLateResponses () 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 void TestLateResponses () } + [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 [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); @@ -395,8 +418,6 @@ private static IEnumerable GenerateBatches (string input, int start, } } - - private void AssertIgnored (string ansiStream,char expected, ref int i) { var c2 = ansiStream [i]; From d9144e505e71d3fe56931d0f24ff823871708072 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 16:16:08 +0100 Subject: [PATCH 43/77] Simplify code --- .../ConsoleDrivers/AnsiResponseParser.cs | 127 ++---------------- 1 file changed, 13 insertions(+), 114 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 9f1e6dcd4c..0e11994d2b 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -41,68 +41,20 @@ protected set /// public DateTime StateChangedAt { get; private set; } = DateTime.Now; - protected readonly HashSet _knownTerminators = new (); - - public AnsiResponseParserBase () + // These all are valid terminators on ansi responses, + // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s + // No - N or O + protected readonly HashSet _knownTerminators = new (new [] { - // These all are valid terminators on ansi responses, - // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s - _knownTerminators.Add ('@'); - _knownTerminators.Add ('A'); - _knownTerminators.Add ('B'); - _knownTerminators.Add ('C'); - _knownTerminators.Add ('D'); - _knownTerminators.Add ('E'); - _knownTerminators.Add ('F'); - _knownTerminators.Add ('G'); - _knownTerminators.Add ('G'); - _knownTerminators.Add ('H'); - _knownTerminators.Add ('I'); - _knownTerminators.Add ('J'); - _knownTerminators.Add ('K'); - _knownTerminators.Add ('L'); - _knownTerminators.Add ('M'); - + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M', // No - N or O - _knownTerminators.Add ('P'); - _knownTerminators.Add ('Q'); - _knownTerminators.Add ('R'); - _knownTerminators.Add ('S'); - _knownTerminators.Add ('T'); - _knownTerminators.Add ('W'); - _knownTerminators.Add ('X'); - _knownTerminators.Add ('Z'); - - _knownTerminators.Add ('^'); - _knownTerminators.Add ('`'); - _knownTerminators.Add ('~'); - - _knownTerminators.Add ('a'); - _knownTerminators.Add ('b'); - _knownTerminators.Add ('c'); - _knownTerminators.Add ('d'); - _knownTerminators.Add ('e'); - _knownTerminators.Add ('f'); - _knownTerminators.Add ('g'); - _knownTerminators.Add ('h'); - _knownTerminators.Add ('i'); - - _knownTerminators.Add ('l'); - _knownTerminators.Add ('m'); - _knownTerminators.Add ('n'); - - _knownTerminators.Add ('p'); - _knownTerminators.Add ('q'); - _knownTerminators.Add ('r'); - _knownTerminators.Add ('s'); - _knownTerminators.Add ('t'); - _knownTerminators.Add ('u'); - _knownTerminators.Add ('v'); - _knownTerminators.Add ('w'); - _knownTerminators.Add ('x'); - _knownTerminators.Add ('y'); - _knownTerminators.Add ('z'); - } + 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z', + '^', '`', '~', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'l', 'm', 'n', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' + }); + protected void ResetState () { @@ -385,57 +337,4 @@ public string Release () protected override IEnumerable HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); } protected override void AddToHeld (object o) { held.Append ((char)o); } -} - - -/// -/// Describes an ongoing ANSI request sent to the console. -/// Use to handle the response -/// when console answers the request. -/// -public class AnsiEscapeSequenceRequest -{ - /// - /// Request to send e.g. see - /// - /// EscSeqUtils.CSI_SendDeviceAttributes.Request - /// - /// - public required string Request { get; init; } - - /// - /// Invoked when the console responds with an ANSI response code that matches the - /// - /// - public Action ResponseReceived; - - /// - /// - /// The terminator that uniquely identifies the type of response as responded - /// by the console. e.g. for - /// - /// EscSeqUtils.CSI_SendDeviceAttributes.Request - /// - /// the terminator is - /// - /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator - /// - /// . - /// - /// - /// After sending a request, the first response with matching terminator will be matched - /// to the oldest outstanding request. - /// - /// - public required string Terminator { get; init; } - - /// - /// Sends the to the raw output stream of the current . - /// Only call this method from the main UI thread. You should use if - /// sending many requests. - /// - public void Send () - { - Application.Driver?.RawWrite (Request); - } -} +} \ No newline at end of file From fdf4953f4f9048417f61a648bb2d94622f9148b2 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 16:16:24 +0100 Subject: [PATCH 44/77] Add missing class (moved from parser) --- .../AnsiEscapeSequenceRequest.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs new file mode 100644 index 0000000000..d57d99796f --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -0,0 +1,54 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes an ongoing ANSI request sent to the console. +/// Use to handle the response +/// when console answers the request. +/// +public class AnsiEscapeSequenceRequest +{ + /// + /// Request to send e.g. see + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// + public required string Request { get; init; } + + /// + /// Invoked when the console responds with an ANSI response code that matches the + /// + /// + public Action ResponseReceived; + + /// + /// + /// The terminator that uniquely identifies the type of response as responded + /// by the console. e.g. for + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// the terminator is + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator + /// + /// . + /// + /// + /// After sending a request, the first response with matching terminator will be matched + /// to the oldest outstanding request. + /// + /// + public required string Terminator { get; init; } + + /// + /// Sends the to the raw output stream of the current . + /// Only call this method from the main UI thread. You should use if + /// sending many requests. + /// + public void Send () + { + Application.Driver?.RawWrite (Request); + } +} From 8c7141219c79ee958aa89fd6fb800725cd488c77 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 16:58:26 +0100 Subject: [PATCH 45/77] Add IHeld --- .../ConsoleDrivers/AnsiResponseParser.cs | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs index 0e11994d2b..3cefbeb35e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs @@ -36,6 +36,8 @@ protected set } } + protected readonly IHeld heldContent; + /// /// When was last changed. /// @@ -55,18 +57,17 @@ protected set '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 HeldToObjects (); - protected abstract void AddToHeld (object o); - /// /// Processes an input collection of objects long. /// You must provide the indexers to return the objects and the action to append @@ -102,7 +103,7 @@ int inputLength { // 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 +118,13 @@ int inputLength { // 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 +136,7 @@ int inputLength break; case AnsiResponseParserState.InResponse: - AddToHeld (currentObj); + heldContent.AddToHeld (currentObj); // Check if the held content should be released if (ShouldReleaseHeldContent ()) @@ -152,19 +153,19 @@ int inputLength private void ReleaseHeld (Action 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, @@ -214,7 +215,7 @@ private bool MatchResponse (string cur, List collection { if (invokeCallback) { - matchingResponse.Response?.Invoke (HeldToString ()); + matchingResponse.Response?.Invoke (heldContent.HeldToString ()); } ResetState (); @@ -269,10 +270,38 @@ public void StopExpecting (string terminator, bool persistent) } } -internal class AnsiResponseParser : AnsiResponseParserBase +internal interface IHeld +{ + void ClearHeld (); + string HeldToString (); + IEnumerable HeldToObjects (); + void AddToHeld (object o); +} + +internal class StringHeld : IHeld +{ + private readonly StringBuilder held = new (); + + public void ClearHeld () => held.Clear (); + public string HeldToString () => held.ToString (); + public IEnumerable HeldToObjects () => held.ToString ().Select (c => (object)c); + public void AddToHeld (object o) => held.Append ((char)o); +} + +internal class GenericHeld : IHeld { private readonly List> held = new (); + public void ClearHeld () => held.Clear (); + public string HeldToString () => new (held.Select (h => h.Item1).ToArray ()); + public IEnumerable HeldToObjects () => held; + public void AddToHeld (object o) => held.Add ((Tuple)o); +} + +internal class AnsiResponseParser : AnsiResponseParserBase +{ + public AnsiResponseParser () : base (new GenericHeld ()) { } + public IEnumerable> ProcessInput (params Tuple [] input) { List> output = new List> (); @@ -288,26 +317,18 @@ public IEnumerable> ProcessInput (params Tuple [] input) public IEnumerable> Release () { - foreach (Tuple h in held.ToArray ()) + foreach (Tuple h in (IEnumerable>)heldContent.HeldToObjects ()) { yield return h; } ResetState (); } - - public override void ClearHeld () { held.Clear (); } - - protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); } - - protected override IEnumerable HeldToObjects () { return held; } - - protected override void AddToHeld (object o) { held.Add ((Tuple)o); } } internal class AnsiResponseParser : AnsiResponseParserBase { - private readonly StringBuilder held = new (); + public AnsiResponseParser () : base (new StringHeld ()) { } public string ProcessInput (string input) { @@ -324,17 +345,9 @@ public string ProcessInput (string input) 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 HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); } - - protected override void AddToHeld (object o) { held.Append ((char)o); } } \ No newline at end of file From 3b53363b74d42cc32274dfecdafc9f382302b3e2 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 17:01:41 +0100 Subject: [PATCH 46/77] Move to seperate folder --- .../AnsiRequestScheduler.cs | 18 +++++++++--------- .../AnsiResponseExpectation.cs | 2 ++ .../AnsiResponseParser.cs | 12 ++++++------ .../IAnsiResponseParser.cs | 0 4 files changed, 17 insertions(+), 15 deletions(-) rename Terminal.Gui/ConsoleDrivers/{ => AnsiResponseParser}/AnsiRequestScheduler.cs (89%) rename Terminal.Gui/ConsoleDrivers/{ => AnsiResponseParser}/AnsiResponseExpectation.cs (92%) rename Terminal.Gui/ConsoleDrivers/{ => AnsiResponseParser}/AnsiResponseParser.cs (97%) rename Terminal.Gui/ConsoleDrivers/{ => AnsiResponseParser}/IAnsiResponseParser.cs (100%) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs similarity index 89% rename from Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs rename to Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 3722a8667a..e4672e5451 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -4,9 +4,9 @@ namespace Terminal.Gui; -public class AnsiRequestScheduler(IAnsiResponseParser parser) +public class AnsiRequestScheduler (IAnsiResponseParser parser) { - private readonly List> _requests = new (); + private readonly List> _requests = new (); /// /// @@ -38,10 +38,10 @@ public class AnsiRequestScheduler(IAnsiResponseParser parser) /// /// /// if request was sent immediately. if it was queued. - 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 +59,7 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request ) } } - _requests.Add (Tuple.Create(request,DateTime.Now)); + _requests.Add (Tuple.Create (request, DateTime.Now)); return false; } @@ -76,7 +76,7 @@ private bool EvictStaleRequests (string withTerminator) { if (DateTime.Now - dt > _staleTimeout) { - parser.StopExpecting (withTerminator,false); + parser.StopExpecting (withTerminator, false); return true; } @@ -102,7 +102,7 @@ public bool RunSchedule (bool force = false) 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 +117,8 @@ public bool RunSchedule (bool force = false) 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 (); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs similarity index 92% rename from Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs rename to Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index d1245adccd..b884f0875c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,4 +1,6 @@ #nullable enable +using Terminal; + namespace Terminal.Gui; public record AnsiResponseExpectation (string Terminator, Action Response) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs similarity index 97% rename from Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs rename to Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 3cefbeb35e..8fcac1e02c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -171,7 +171,7 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse (cur, expectedResponses, invokeCallback: true, - removeExpectation:true)) + removeExpectation: true)) { return false; } @@ -180,7 +180,7 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse (cur, lateResponses, invokeCallback: false, - removeExpectation:true)) + removeExpectation: true)) { return false; } @@ -189,7 +189,7 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse (cur, persistentExpectations, invokeCallback: true, - removeExpectation:false)) + removeExpectation: false)) { return false; } @@ -209,7 +209,7 @@ protected bool ShouldReleaseHeldContent () private bool MatchResponse (string cur, List 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) { @@ -247,7 +247,7 @@ public void ExpectResponse (string terminator, Action response, bool per 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 ()); } /// @@ -255,7 +255,7 @@ public void StopExpecting (string terminator, bool persistent) { if (persistent) { - persistentExpectations.RemoveAll (r=>r.Matches (terminator)); + persistentExpectations.RemoveAll (r => r.Matches (terminator)); } else { diff --git a/Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs similarity index 100% rename from Terminal.Gui/ConsoleDrivers/IAnsiResponseParser.cs rename to Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs From 6e4a6747db68c0f07697e1517326d4e8b10aa742 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 17:05:26 +0100 Subject: [PATCH 47/77] Refactoring and xmldoc --- .../AnsiResponseParser/AnsiResponseParser.cs | 28 ---------------- .../AnsiResponseParser/GenericHeld.cs | 16 +++++++++ .../AnsiResponseParser/IHeld.cs | 33 +++++++++++++++++++ .../AnsiResponseParser/StringHeld.cs | 15 +++++++++ 4 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 8fcac1e02c..02a9bfb5e8 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -270,34 +270,6 @@ public void StopExpecting (string terminator, bool persistent) } } -internal interface IHeld -{ - void ClearHeld (); - string HeldToString (); - IEnumerable HeldToObjects (); - void AddToHeld (object o); -} - -internal class StringHeld : IHeld -{ - private readonly StringBuilder held = new (); - - public void ClearHeld () => held.Clear (); - public string HeldToString () => held.ToString (); - public IEnumerable HeldToObjects () => held.ToString ().Select (c => (object)c); - public void AddToHeld (object o) => held.Append ((char)o); -} - -internal class GenericHeld : IHeld -{ - private readonly List> held = new (); - - public void ClearHeld () => held.Clear (); - public string HeldToString () => new (held.Select (h => h.Item1).ToArray ()); - public IEnumerable HeldToObjects () => held; - public void AddToHeld (object o) => held.Add ((Tuple)o); -} - internal class AnsiResponseParser : AnsiResponseParserBase { public AnsiResponseParser () : base (new GenericHeld ()) { } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs new file mode 100644 index 0000000000..c81cb65652 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Implementation of for +/// +/// +internal class GenericHeld : IHeld +{ + private readonly List> held = new (); + + public void ClearHeld () => held.Clear (); + public string HeldToString () => new (held.Select (h => h.Item1).ToArray ()); + public IEnumerable HeldToObjects () => held; + public void AddToHeld (object o) => held.Add ((Tuple)o); +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs new file mode 100644 index 0000000000..4eb510c16b --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs @@ -0,0 +1,33 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes a sequence of chars (and optionally T metadata) accumulated +/// by an +/// +internal interface IHeld +{ + /// + /// Clears all held objects + /// + void ClearHeld (); + + /// + /// Returns string representation of the held objects + /// + /// + string HeldToString (); + + /// + /// Returns the collection objects directly e.g. + /// or + metadata T + /// + /// + IEnumerable HeldToObjects (); + + /// + /// Adds the given object to the collection. + /// + /// + void AddToHeld (object o); +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs new file mode 100644 index 0000000000..b5d8d390c8 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Implementation of for +/// +internal class StringHeld : IHeld +{ + private readonly StringBuilder held = new (); + + public void ClearHeld () => held.Clear (); + public string HeldToString () => held.ToString (); + public IEnumerable HeldToObjects () => held.ToString ().Select (c => (object)c); + public void AddToHeld (object o) => held.Append ((char)o); +} From 085a0cf32cb6ead461d5a3131941b803701f06e0 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 17:06:19 +0100 Subject: [PATCH 48/77] Remove unused usings --- .../ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs | 1 - .../AnsiResponseParser/AnsiResponseExpectation.cs | 2 -- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 2 -- 3 files changed, 5 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index e4672e5451..b0c0e61bb4 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.Concurrent; -using System.Diagnostics; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index b884f0875c..d1245adccd 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,6 +1,4 @@ #nullable enable -using Terminal; - namespace Terminal.Gui; public record AnsiResponseExpectation (string Terminator, Action Response) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 02a9bfb5e8..1153e9236e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -1,7 +1,5 @@ #nullable enable -using System.Runtime.ConstrainedExecution; - namespace Terminal.Gui; From bdcf36c31422b6645e55b0c32d25ffc69fa6bf8d Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 19:37:32 +0100 Subject: [PATCH 49/77] Support for getting the full T set when using generic AnsiResponseParser --- .../AnsiResponseExpectation.cs | 4 +- .../AnsiResponseParser/AnsiResponseParser.cs | 32 ++++++++++++-- .../ConsoleDrivers/AnsiResponseParserTests.cs | 42 ++++++++++++++++++- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index d1245adccd..75fdb308a8 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,10 +1,10 @@ #nullable enable namespace Terminal.Gui; -public record AnsiResponseExpectation (string Terminator, Action Response) +internal record AnsiResponseExpectation (string Terminator, Action Response) { public bool Matches (string cur) { return cur.EndsWith (Terminator); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 1153e9236e..7366e382ba 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -213,7 +213,7 @@ private bool MatchResponse (string cur, List collection { if (invokeCallback) { - matchingResponse.Response?.Invoke (heldContent.HeldToString ()); + matchingResponse.Response.Invoke (heldContent); } ResetState (); @@ -233,11 +233,11 @@ public void ExpectResponse (string terminator, Action response, bool per { 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 ()))); } } @@ -287,13 +287,37 @@ public IEnumerable> ProcessInput (params Tuple [] input) public IEnumerable> Release () { - foreach (Tuple h in (IEnumerable>)heldContent.HeldToObjects ()) + foreach (Tuple h in HeldToEnumerable()) { yield return h; } ResetState (); } + + private IEnumerable> HeldToEnumerable () + { + return (IEnumerable>)heldContent.HeldToObjects (); + } + + /// + /// '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. + /// + /// + /// + /// + public void ExpectResponseT (string terminator, Action>> 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 diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index fa7fac4f79..99e71fed00 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -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 void TestPersistentResponses () Assert.Equal (4, M); // Expected three `M` responses plus the initial value of 1 } + [Fact] + public void TestPersistentResponses_WithMetadata () + { + var p = new AnsiResponseParser (); + + int m = 0; + + var result = new List> (); + + 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.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 [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From 65cfa49b2855bdf98e0b6d02b602449ab95a3c7d Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 20:01:15 +0100 Subject: [PATCH 50/77] WIP integrating ansi parser --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 233bbae660..aa19dec526 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -149,6 +149,8 @@ internal class NetEvents : IDisposable #endif public EscSeqRequests EscSeqRequests { get; } = new (); + public AnsiResponseParser Parser { get; private set; } = new (); + public NetEvents (ConsoleDriver consoleDriver) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); @@ -157,8 +159,12 @@ public NetEvents (ConsoleDriver consoleDriver) Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); + + Parser.ExpectResponseT ("m",ProcessRequestResponse,true); + Parser.ExpectResponseT ("M", ProcessRequestResponse, true); } + public InputResult? DequeueInput () { while (_inputReadyCancellationTokenSource != null @@ -239,10 +245,6 @@ private void ProcessInputQueue () if (_inputQueue.Count == 0) { - ConsoleKey key = 0; - ConsoleModifiers mod = 0; - ConsoleKeyInfo newConsoleKeyInfo = default; - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) { ConsoleKeyInfo consoleKeyInfo; @@ -257,9 +259,9 @@ private void ProcessInputQueue () } // Parse - foreach (var released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) + foreach (var k in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { - ProcessInputAfterParsing (released.Item2, ref key, ref mod, ref newConsoleKeyInfo); + ProcessMapConsoleKeyInfo (k.Item2); } } } @@ -415,7 +417,15 @@ private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight return true; } - public AnsiResponseParser Parser { get; private set; } = new (); + private void ProcessRequestResponse (IEnumerable> obj) + { + // Added for signature compatibility with existing method, not sure what they are even for. + ConsoleKeyInfo newConsoleKeyInfo = default; + ConsoleKey key = default; + ConsoleModifiers mod = default; + + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, obj.Select (v=>v.Item2).ToArray (),ref mod); + } // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) private void ProcessRequestResponse ( From acdd483cb2c80536bd886621ef513f505b9d7cb7 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 20:49:37 +0100 Subject: [PATCH 51/77] Add UnknownResponseHandler --- .../AnsiResponseParser/AnsiResponseParser.cs | 49 ++++++++++++- .../ConsoleDrivers/AnsiResponseParserTests.cs | 73 +++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 7366e382ba..6c34da6556 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -196,13 +196,28 @@ protected bool ShouldReleaseHeldContent () // then we can release it back to input processing stream if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI)) { - // Detected a response that was not expected - return true; + // We have found a terminator so bail + State = AnsiResponseParserState.Normal; + + // Maybe swallow anyway if user has custom delegate + return ShouldReleaseUnexpectedResponse (); } return false; // Continue accumulating } + /// + /// + /// When overriden in a derived class, indicates whether the unexpected response + /// currently in should be released or swallowed. + /// Use this to enable default event for escape codes. + /// + /// + /// Note this is only called for complete responses. + /// Based on + /// + /// + protected abstract bool ShouldReleaseUnexpectedResponse (); private bool MatchResponse (string cur, List collection, bool invokeCallback, bool removeExpectation) { @@ -272,6 +287,11 @@ internal class AnsiResponseParser : AnsiResponseParserBase { public AnsiResponseParser () : base (new GenericHeld ()) { } + + /// + public Func>, bool> UnknownResponseHandler { get; set; } = (_) => false; + + public IEnumerable> ProcessInput (params Tuple [] input) { List> output = new List> (); @@ -318,10 +338,29 @@ public void ExpectResponseT (string terminator, Action expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); } } + + /// + protected override bool ShouldReleaseUnexpectedResponse () + { + return !UnknownResponseHandler.Invoke (HeldToEnumerable ()); + } } internal class AnsiResponseParser : AnsiResponseParserBase { + /// + /// + /// Delegate for handling unrecognized escape codes. Default behaviour + /// is to return which simply releases the + /// characters back to input stream for downstream processing. + /// + /// + /// Implement a method to handle if you want and return if you want the + /// keystrokes 'swallowed' (i.e. not returned to input stream). + /// + /// + public Func UnknownResponseHandler { get; set; } = (_) => false; + public AnsiResponseParser () : base (new StringHeld ()) { } public string ProcessInput (string input) @@ -344,4 +383,10 @@ public string Release () return output; } + + /// + protected override bool ShouldReleaseUnexpectedResponse () + { + return !UnknownResponseHandler.Invoke (heldContent.ToString () ?? string.Empty); + } } \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 99e71fed00..7f2d3ec52c 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; using Xunit.Abstractions; @@ -414,6 +415,30 @@ public void TestPersistentResponses_WithMetadata () Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content } + [Fact] + public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo () + { + int i = 0; + + // Swallow all unknown escape codes + _parser1.UnknownResponseHandler = _ => true; + _parser2.UnknownResponseHandler = _ => true; + + + AssertReleased ( + "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst", + "Just test", + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 28, + 29); + } + private Tuple [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); @@ -480,6 +505,54 @@ private void AssertConsumed (string ansiStream, ref int i) Assert.Empty (_parser2.ProcessInput (c2.ToString())); } + /// + /// Overload that fully exhausts and asserts + /// that the final released content across whole processing is + /// + /// + /// + /// + private void AssertReleased (string ansiStream, string expectedRelease, params int [] expectedTValues) + { + var sb = new StringBuilder (); + var tValues = new List (); + + int i = 0; + + while (i < ansiStream.Length) + { + var c2 = ansiStream [i]; + var c1 = NextChar (ansiStream, ref i); + + var released1 = _parser1.ProcessInput (c1).ToArray (); + tValues.AddRange(released1.Select (kv => kv.Item2)); + + + var released2 = _parser2.ProcessInput (c2.ToString ()); + + // Both parsers should have same chars so release chars consistently with each other + Assert.Equal (BatchToString(released1),released2); + + sb.Append (released2); + } + + Assert.Equal (expectedRelease, sb.ToString()); + + if (expectedTValues.Length > 0) + { + Assert.True (expectedTValues.SequenceEqual (tValues)); + } + } + + /// + /// Asserts that index of when consumed will release + /// . Results in implicit increment of . + /// Note that this does NOT iteratively consume all the stream, only 1 char at + /// + /// + /// + /// + /// private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int[] expectedTValues) { var c2 = ansiStream [i]; From 7fafa456dadcbc961cd84bb87122641294b7a229 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Oct 2024 21:00:31 +0100 Subject: [PATCH 52/77] Add UnexpectedResponseHandler --- .../AnsiResponseParser/AnsiResponseParser.cs | 24 ++++++++--- .../ConsoleDrivers/AnsiResponseParserTests.cs | 43 ++++++++++++++++++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 6c34da6556..d3b1406be4 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -200,7 +200,17 @@ protected bool ShouldReleaseHeldContent () State = AnsiResponseParserState.Normal; // Maybe swallow anyway if user has custom delegate - return ShouldReleaseUnexpectedResponse (); + var swallow = ShouldSwallowUnexpectedResponse (); + + if (swallow) + { + heldContent.ClearHeld (); + // Do not send back to input stream + return false; + } + + // Do release back to input stream + return true; } return false; // Continue accumulating @@ -217,7 +227,7 @@ protected bool ShouldReleaseHeldContent () /// Based on /// /// - protected abstract bool ShouldReleaseUnexpectedResponse (); + protected abstract bool ShouldSwallowUnexpectedResponse (); private bool MatchResponse (string cur, List collection, bool invokeCallback, bool removeExpectation) { @@ -289,7 +299,7 @@ public AnsiResponseParser () : base (new GenericHeld ()) { } /// - public Func>, bool> UnknownResponseHandler { get; set; } = (_) => false; + public Func>, bool> UnexpectedResponseHandler { get; set; } = (_) => false; public IEnumerable> ProcessInput (params Tuple [] input) @@ -340,9 +350,9 @@ public void ExpectResponseT (string terminator, Action } /// - protected override bool ShouldReleaseUnexpectedResponse () + protected override bool ShouldSwallowUnexpectedResponse () { - return !UnknownResponseHandler.Invoke (HeldToEnumerable ()); + return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); } } @@ -385,8 +395,8 @@ public string Release () } /// - protected override bool ShouldReleaseUnexpectedResponse () + protected override bool ShouldSwallowUnexpectedResponse () { - return !UnknownResponseHandler.Invoke (heldContent.ToString () ?? string.Empty); + return UnknownResponseHandler.Invoke (heldContent.HeldToString ()); } } \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 7f2d3ec52c..a3217cf291 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -421,7 +421,7 @@ public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo () int i = 0; // Swallow all unknown escape codes - _parser1.UnknownResponseHandler = _ => true; + _parser1.UnexpectedResponseHandler = _ => true; _parser2.UnknownResponseHandler = _ => true; @@ -439,6 +439,47 @@ public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo () 29); } + [Fact] + public void UnknownResponses_ParameterShouldMatch () + { + int i = 0; + + // Track unknown responses passed to the UnexpectedResponseHandler + var unknownResponses = new List (); + + // Set up the UnexpectedResponseHandler to log each unknown response + _parser1.UnexpectedResponseHandler = r1 => + { + unknownResponses.Add (BatchToString (r1)); + return true; // Return true to swallow unknown responses + }; + + _parser2.UnknownResponseHandler = r2 => + { + // parsers should be agreeing on what these responses are! + Assert.Equal(unknownResponses.Last(),r2); + return true; // Return true to swallow unknown responses + }; + + // Input with known and unknown responses + AssertReleased ( + "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst", + "Just test"); + + // Expected unknown responses (ANSI sequences that are unknown) + var expectedUnknownResponses = new List + { + "\u001b[<0;0;0M", + "\u001b[3c", + "\u001b[2c", + "\u001b[4c" + }; + + // Assert that the UnexpectedResponseHandler was called with the correct unknown responses + Assert.Equal (expectedUnknownResponses.Count, unknownResponses.Count); + Assert.Equal (expectedUnknownResponses, unknownResponses); + } + private Tuple [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From 512af810d571aa9677d3513b71fdff92dc2c4e89 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 08:32:32 +0000 Subject: [PATCH 53/77] WIP: Trying to get NetDriver to work properly --- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 6 ++++ Terminal.Gui/ConsoleDrivers/NetDriver.cs | 30 +++++++++++++++++--- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 7 +---- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index bc78c8317b..9441622687 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -15,6 +15,12 @@ namespace Terminal.Gui; /// public abstract class ConsoleDriver { + + /// + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// + internal TimeSpan EscTimeout = TimeSpan.FromMilliseconds (50); + // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. internal bool []? _dirtyLines; diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index aa19dec526..a90b30f530 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -160,8 +160,7 @@ public NetEvents (ConsoleDriver consoleDriver) Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); - Parser.ExpectResponseT ("m",ProcessRequestResponse,true); - Parser.ExpectResponseT ("M", ProcessRequestResponse, true); + Parser.UnexpectedResponseHandler = ProcessRequestResponse; } @@ -204,7 +203,7 @@ public NetEvents (ConsoleDriver consoleDriver) return null; } - private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) + private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) { // if there is a key available, return it without waiting // (or dispatching work to the thread queue) @@ -217,6 +216,12 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation { Task.Delay (100, cancellationToken).Wait (cancellationToken); + foreach (var k in ShouldRelease ()) + { + ProcessMapConsoleKeyInfo (k); + _inputReady.Set (); + } + if (Console.KeyAvailable) { return Console.ReadKey (intercept); @@ -228,6 +233,17 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation return default (ConsoleKeyInfo); } + public IEnumerable ShouldRelease () + { + if (Parser.State == AnsiResponseParserState.ExpectingBracket && + DateTime.Now - Parser.StateChangedAt > _consoleDriver.EscTimeout) + { + return Parser.Release ().Select (o => o.Item2); + } + + return []; + } + private void ProcessInputQueue () { while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) @@ -417,7 +433,7 @@ private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight return true; } - private void ProcessRequestResponse (IEnumerable> obj) + private bool ProcessRequestResponse (IEnumerable> obj) { // Added for signature compatibility with existing method, not sure what they are even for. ConsoleKeyInfo newConsoleKeyInfo = default; @@ -425,6 +441,12 @@ private void ProcessRequestResponse (IEnumerable> ob ConsoleModifiers mod = default; ProcessRequestResponse (ref newConsoleKeyInfo, ref key, obj.Select (v=>v.Item2).ToArray (),ref mod); + + // Probably + _inputReady.Set (); + + // Handled + return true; } // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 815a1325e9..f7380f9dca 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1454,10 +1454,6 @@ internal override MainLoop Init () return new MainLoop (_mainLoopDriver); } - /// - /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence - /// - private TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); private AnsiResponseParser _parser = new (); internal void ProcessInput (WindowsConsole.InputRecord inputEvent) @@ -1565,9 +1561,8 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) public IEnumerable ShouldRelease () { - if (_parser.State == AnsiResponseParserState.ExpectingBracket && - DateTime.Now - _parser.StateChangedAt > _escTimeout) + DateTime.Now - _parser.StateChangedAt > EscTimeout) { return _parser.Release ().Select (o => o.Item2); } From 8417387931e8fa4383d730a73fc148158d6e4f4f Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 09:22:18 +0000 Subject: [PATCH 54/77] Change _resultQueue to not hold nulls and use concurrent collection --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index a90b30f530..84c24fa950 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -2,6 +2,7 @@ // NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient. // +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; @@ -1733,7 +1734,7 @@ internal class NetMainLoop : IMainLoopDriver private readonly ManualResetEventSlim _eventReady = new (false); private readonly CancellationTokenSource _inputHandlerTokenSource = new (); - private readonly Queue _resultQueue = new (); + private readonly ConcurrentQueue _resultQueue = new (); private readonly ManualResetEventSlim _waitForProbe = new (false); private readonly CancellationTokenSource _eventReadyTokenSource = new (); private MainLoop _mainLoop; @@ -1799,9 +1800,9 @@ bool IMainLoopDriver.EventsPending () void IMainLoopDriver.Iteration () { - while (_resultQueue.Count > 0) - { - ProcessInput?.Invoke (_resultQueue.Dequeue ().Value); + while (_resultQueue.TryDequeue (out InputResult v)) + { + ProcessInput?.Invoke (v); } } @@ -1852,14 +1853,11 @@ private void NetInputHandler () _inputHandlerTokenSource.Token.ThrowIfCancellationRequested (); - if (_resultQueue.Count == 0) - { - _resultQueue.Enqueue (_netEvents.DequeueInput ()); - } + var input = _netEvents.DequeueInput (); - while (_resultQueue.Count > 0 && _resultQueue.Peek () is null) + if (input.HasValue) { - _resultQueue.Dequeue (); + _resultQueue.Enqueue (input.Value); } if (_resultQueue.Count > 0) From 1ec7aa26700e5e8a2bebaa7305fd798b98c8b827 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 09:50:12 +0000 Subject: [PATCH 55/77] Simplify NetEvents further with BlockingCollection --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 53 ++++-------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 84c24fa950..cb8a26ac5a 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1732,11 +1732,11 @@ internal class NetMainLoop : IMainLoopDriver /// Invoked when a Key is pressed. internal Action ProcessInput; - private readonly ManualResetEventSlim _eventReady = new (false); private readonly CancellationTokenSource _inputHandlerTokenSource = new (); - private readonly ConcurrentQueue _resultQueue = new (); + // Wrap ConcurrentQueue in a BlockingCollection to enable blocking with timeout + private readonly BlockingCollection _resultQueue = new (new ConcurrentQueue ()); + private readonly ManualResetEventSlim _waitForProbe = new (false); - private readonly CancellationTokenSource _eventReadyTokenSource = new (); private MainLoop _mainLoop; /// Initializes the class with the console driver. @@ -1759,49 +1759,24 @@ void IMainLoopDriver.Setup (MainLoop mainLoop) Task.Run (NetInputHandler, _inputHandlerTokenSource.Token); } - void IMainLoopDriver.Wakeup () { _eventReady.Set (); } + void IMainLoopDriver.Wakeup () { } bool IMainLoopDriver.EventsPending () { _waitForProbe.Set (); - if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout)) + if (_mainLoop.CheckTimersAndIdleHandlers (out _)) { return true; } - try - { - if (!_eventReadyTokenSource.IsCancellationRequested) - { - // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there - // are no timers, but there IS an idle handler waiting. - _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); - } - } - catch (OperationCanceledException) - { - return true; - } - finally - { - _eventReady.Reset (); - } - - _eventReadyTokenSource.Token.ThrowIfCancellationRequested (); - - if (!_eventReadyTokenSource.IsCancellationRequested) - { - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); - } - - return true; + return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); } void IMainLoopDriver.Iteration () { - while (_resultQueue.TryDequeue (out InputResult v)) - { + while (_resultQueue.TryTake (out InputResult v)) + { ProcessInput?.Invoke (v); } } @@ -1810,12 +1785,7 @@ void IMainLoopDriver.TearDown () { _inputHandlerTokenSource?.Cancel (); _inputHandlerTokenSource?.Dispose (); - _eventReadyTokenSource?.Cancel (); - _eventReadyTokenSource?.Dispose (); - - _eventReady?.Dispose (); - _resultQueue?.Clear (); _waitForProbe?.Dispose (); _netEvents?.Dispose (); _netEvents = null; @@ -1857,12 +1827,7 @@ private void NetInputHandler () if (input.HasValue) { - _resultQueue.Enqueue (input.Value); - } - - if (_resultQueue.Count > 0) - { - _eventReady.Set (); + _resultQueue.Add (input.Value); } } } From 792170a02981a529c8bba495aacf115ab486d89d Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 17:01:20 +0000 Subject: [PATCH 56/77] Change NetEvents to BlockingCollection --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 52 ++++-------------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index cb8a26ac5a..391deeb743 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -135,13 +135,12 @@ public void Cleanup () internal class NetEvents : IDisposable { - private readonly ManualResetEventSlim _inputReady = new (false); private CancellationTokenSource _inputReadyCancellationTokenSource; private readonly ManualResetEventSlim _waitForStart = new (false); //CancellationTokenSource _waitForStartCancellationTokenSource; private readonly ManualResetEventSlim _winChange = new (false); - private readonly Queue _inputQueue = new (); + private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); private readonly ConsoleDriver _consoleDriver; private ConsoleKeyInfo [] _cki; private bool _isEscSeq; @@ -173,31 +172,9 @@ public NetEvents (ConsoleDriver consoleDriver) _waitForStart.Set (); _winChange.Set (); - try - { - if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) - { - if (_inputQueue.Count == 0) - { - _inputReady.Wait (_inputReadyCancellationTokenSource.Token); - } - } - } - catch (OperationCanceledException) - { - return null; - } - finally - { - _inputReady.Reset (); - } - -#if PROCESS_REQUEST - _neededProcessRequest = false; -#endif - if (_inputQueue.Count > 0) + if (_inputQueue.TryTake (out var item,-1,_inputReadyCancellationTokenSource.Token)) { - return _inputQueue.Dequeue (); + return item; } } @@ -220,7 +197,6 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, foreach (var k in ShouldRelease ()) { ProcessMapConsoleKeyInfo (k); - _inputReady.Set (); } if (Console.KeyAvailable) @@ -282,7 +258,6 @@ private void ProcessInputQueue () } } } - _inputReady.Set (); } } @@ -345,7 +320,7 @@ private void ProcessInputAfterParsing(ConsoleKeyInfo consoleKeyInfo, void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) { - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) @@ -403,8 +378,6 @@ void RequestWindowSize (CancellationToken cancellationToken) { return; } - - _inputReady.Set (); } } @@ -424,7 +397,7 @@ private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight int w = Math.Max (winWidth, 0); int h = Math.Max (winHeight, 0); - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) } @@ -443,9 +416,6 @@ private bool ProcessRequestResponse (IEnumerable> ob ProcessRequestResponse (ref newConsoleKeyInfo, ref key, obj.Select (v=>v.Item2).ToArray (),ref mod); - // Probably - _inputReady.Set (); - // Handled return true; } @@ -647,7 +617,7 @@ private void HandleRequestResponseEvent (string c1Control, string code, string [ var eventType = EventType.WindowPosition; var winPositionEv = new WindowPositionEvent { CursorPosition = point }; - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv } ); } @@ -682,8 +652,6 @@ private void HandleRequestResponseEvent (string c1Control, string code, string [ break; } - - _inputReady.Set (); } private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating) @@ -691,7 +659,7 @@ private void EnqueueRequestResponseEvent (string c1Control, string code, string var eventType = EventType.RequestResponse; var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) }; - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv } ); } @@ -700,11 +668,9 @@ private void HandleMouseEvent (MouseButtonState buttonState, Point pos) { var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState }; - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent } ); - - _inputReady.Set (); } public enum EventType @@ -817,7 +783,7 @@ private void HandleKeyboardEvent (ConsoleKeyInfo cki) { var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki }; - _inputQueue.Enqueue (inputResult); + _inputQueue.Add (inputResult); } public void Dispose () From b30c92bceb7481de62e17d0193acd6cb5aa70cfc Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 17:08:27 +0000 Subject: [PATCH 57/77] Fix MapConsoleKeyInfo not working with Esc properly --- .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 1eb63e34aa..96de432374 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -970,6 +970,18 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) break; case uint n when n > 0 && n <= KeyEsc: + if (n == KeyEsc) + { + key= ConsoleKey.Escape; + + newConsoleKeyInfo = new ConsoleKeyInfo ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + } + else if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r') { key = ConsoleKey.Enter; From edb43a000d560b4286ee4e5aef4ba83e57ed4e31 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 17:35:26 +0000 Subject: [PATCH 58/77] Add lock around access of expected responses for thread safety --- .../AnsiResponseParser/AnsiResponseParser.cs | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index d3b1406be4..f929e80eec 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { + object lockExpectedResponses = new object(); /// /// Responses we are expecting to come in. /// @@ -165,31 +166,37 @@ protected bool ShouldReleaseHeldContent () { 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)) + lock (lockExpectedResponses) { - return false; - } + // Look for an expected response for what is accumulated so far (since Esc) + 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, - invokeCallback: false, - 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, + invokeCallback: false, + removeExpectation: true)) + { + return false; + } - // Look for persistent requests - if (MatchResponse (cur, - persistentExpectations, - invokeCallback: true, - removeExpectation: false)) - { - return false; + // Look for persistent requests + if (MatchResponse ( + cur, + persistentExpectations, + invokeCallback: true, + removeExpectation: false)) + { + return false; + } } // Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity) @@ -256,38 +263,48 @@ private bool MatchResponse (string cur, List collection /// public void ExpectResponse (string terminator, Action response, bool persistent) { - if (persistent) - { - persistentExpectations.Add (new (terminator, (h)=>response.Invoke (h.HeldToString ()))); - } - else + lock (lockExpectedResponses) { - expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ()))); + if (persistent) + { + persistentExpectations.Add (new (terminator, (h) => response.Invoke (h.HeldToString ()))); + } + else + { + expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ()))); + } } } /// 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 ()); + lock (lockExpectedResponses) + { + // 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 ()); + } } /// public void StopExpecting (string terminator, bool persistent) { - if (persistent) - { - persistentExpectations.RemoveAll (r => r.Matches (terminator)); - } - else - { - var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); - foreach (var r in removed) + lock (lockExpectedResponses) + { + if (persistent) + { + persistentExpectations.RemoveAll (r => r.Matches (terminator)); + } + else { - expectedResponses.Remove (r); - lateResponses.Add (r); + var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); + + foreach (var r in removed) + { + expectedResponses.Remove (r); + lateResponses.Add (r); + } } } } From 0adc4cd3ee83eac0b7c79a7d1b1b30be3f55fd89 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Oct 2024 19:19:42 +0000 Subject: [PATCH 59/77] Add lock for answers in AnsiRequestScenario --- UICatalog/Scenarios/AnsiRequestsScenario.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index ba10ab583b..526c5d5b91 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -22,6 +22,8 @@ public class AnsiRequestsScenario : Scenario private ScatterSeries _answeredSeries; private List sends = new (); + + private object lockAnswers = new object (); private Dictionary answers = new (); private Label _lblSummary; @@ -42,9 +44,14 @@ public override void Main () TimeSpan.FromMilliseconds (1000), () => { - UpdateGraph (); + lock (lockAnswers) + { + UpdateGraph (); + + UpdateResponses (); + } + - UpdateResponses (); return true; }); @@ -209,8 +216,9 @@ private void SendDar () private void HandleResponse (string response) { - answers.Add (DateTime.Now,response); + lock (lockAnswers) + { + answers.Add (DateTime.Now,response); + } } - - } \ No newline at end of file From becca1a3a0c08d2487aed55c81ef0f0cc935a8dc Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Oct 2024 08:46:44 +0000 Subject: [PATCH 60/77] WIP: Start simplifying the tokens in NetDriver --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 70 +++++++++++++----------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 391deeb743..4c5cdbc57c 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -135,7 +135,7 @@ public void Cleanup () internal class NetEvents : IDisposable { - private CancellationTokenSource _inputReadyCancellationTokenSource; + private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource (); private readonly ManualResetEventSlim _waitForStart = new (false); //CancellationTokenSource _waitForStartCancellationTokenSource; @@ -154,11 +154,25 @@ internal class NetEvents : IDisposable public NetEvents (ConsoleDriver consoleDriver) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); - _inputReadyCancellationTokenSource = new CancellationTokenSource (); - Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); + Task.Run (() => + { + try + { + ProcessInputQueue (); + } + catch (OperationCanceledException) + { } + }, _netEventsDisposed.Token); - Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); + Task.Run (()=>{ + + try + { + CheckWindowSizeChange(); + } + catch (OperationCanceledException) + { }}, _netEventsDisposed.Token); Parser.UnexpectedResponseHandler = ProcessRequestResponse; } @@ -166,13 +180,12 @@ public NetEvents (ConsoleDriver consoleDriver) public InputResult? DequeueInput () { - while (_inputReadyCancellationTokenSource != null - && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested) + while (!_netEventsDisposed.Token.IsCancellationRequested) { _waitForStart.Set (); _winChange.Set (); - if (_inputQueue.TryTake (out var item,-1,_inputReadyCancellationTokenSource.Token)) + if (_inputQueue.TryTake (out var item,-1,_netEventsDisposed.Token)) { return item; } @@ -181,7 +194,7 @@ public NetEvents (ConsoleDriver consoleDriver) return null; } - private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) + private ConsoleKeyInfo ReadConsoleKeyInfo ( bool intercept = true) { // if there is a key available, return it without waiting // (or dispatching work to the thread queue) @@ -190,9 +203,9 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, return Console.ReadKey (intercept); } - while (!cancellationToken.IsCancellationRequested) + while (!_netEventsDisposed.IsCancellationRequested) { - Task.Delay (100, cancellationToken).Wait (cancellationToken); + Task.Delay (100, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token); foreach (var k in ShouldRelease ()) { @@ -205,7 +218,7 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, } } - cancellationToken.ThrowIfCancellationRequested (); + _netEventsDisposed.Token.ThrowIfCancellationRequested (); return default (ConsoleKeyInfo); } @@ -223,11 +236,11 @@ public IEnumerable ShouldRelease () private void ProcessInputQueue () { - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { try { - _waitForStart.Wait (_inputReadyCancellationTokenSource.Token); + _waitForStart.Wait (_netEventsDisposed.Token); } catch (OperationCanceledException) { @@ -238,18 +251,11 @@ private void ProcessInputQueue () if (_inputQueue.Count == 0) { - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { ConsoleKeyInfo consoleKeyInfo; - try - { - consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - return; - } + consoleKeyInfo = ReadConsoleKeyInfo (); // Parse foreach (var k in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) @@ -259,7 +265,6 @@ private void ProcessInputQueue () } } } - } private void ProcessInputAfterParsing(ConsoleKeyInfo consoleKeyInfo, ref ConsoleKey key, @@ -331,12 +336,12 @@ void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) private void CheckWindowSizeChange () { - void RequestWindowSize (CancellationToken cancellationToken) + void RequestWindowSize () { - while (!cancellationToken.IsCancellationRequested) + while (!_netEventsDisposed.IsCancellationRequested) { // Wait for a while then check if screen has changed sizes - Task.Delay (500, cancellationToken).Wait (cancellationToken); + Task.Delay (500, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token); int buffHeight, buffWidth; @@ -362,17 +367,17 @@ void RequestWindowSize (CancellationToken cancellationToken) } } - cancellationToken.ThrowIfCancellationRequested (); + _netEventsDisposed.Token.ThrowIfCancellationRequested (); } - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { try { - _winChange.Wait (_inputReadyCancellationTokenSource.Token); + _winChange.Wait (_netEventsDisposed.Token); _winChange.Reset (); - RequestWindowSize (_inputReadyCancellationTokenSource.Token); + RequestWindowSize (); } catch (OperationCanceledException) { @@ -788,9 +793,8 @@ private void HandleKeyboardEvent (ConsoleKeyInfo cki) public void Dispose () { - _inputReadyCancellationTokenSource?.Cancel (); - _inputReadyCancellationTokenSource?.Dispose (); - _inputReadyCancellationTokenSource = null; + _netEventsDisposed?.Cancel (); + _netEventsDisposed?.Dispose (); try { From a7468887f73416fbba3bb4ecf011e7cadecec5b8 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Oct 2024 09:01:02 +0000 Subject: [PATCH 61/77] Remove unneeded `_waitForStart` and other now uncalled code --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 76 +----------------------- 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 4c5cdbc57c..986c373217 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -136,17 +136,12 @@ public void Cleanup () internal class NetEvents : IDisposable { private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource (); - private readonly ManualResetEventSlim _waitForStart = new (false); //CancellationTokenSource _waitForStartCancellationTokenSource; private readonly ManualResetEventSlim _winChange = new (false); private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); private readonly ConsoleDriver _consoleDriver; - private ConsoleKeyInfo [] _cki; - private bool _isEscSeq; -#if PROCESS_REQUEST - bool _neededProcessRequest; -#endif + public EscSeqRequests EscSeqRequests { get; } = new (); public AnsiResponseParser Parser { get; private set; } = new (); @@ -182,7 +177,6 @@ public NetEvents (ConsoleDriver consoleDriver) { while (!_netEventsDisposed.Token.IsCancellationRequested) { - _waitForStart.Set (); _winChange.Set (); if (_inputQueue.TryTake (out var item,-1,_netEventsDisposed.Token)) @@ -238,17 +232,6 @@ private void ProcessInputQueue () { while (!_netEventsDisposed.IsCancellationRequested) { - try - { - _waitForStart.Wait (_netEventsDisposed.Token); - } - catch (OperationCanceledException) - { - return; - } - - _waitForStart.Reset (); - if (_inputQueue.Count == 0) { while (!_netEventsDisposed.IsCancellationRequested) @@ -266,62 +249,6 @@ private void ProcessInputQueue () } } } - private void ProcessInputAfterParsing(ConsoleKeyInfo consoleKeyInfo, - ref ConsoleKey key, - ref ConsoleModifiers mod, - ref ConsoleKeyInfo newConsoleKeyInfo) - { - - if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) - || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) - { - if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) - { - _cki = EscSeqUtils.ResizeArray ( - new ConsoleKeyInfo ( - (char)KeyCode.Esc, - 0, - false, - false, - false - ), - _cki - ); - } - - _isEscSeq = true; - newConsoleKeyInfo = consoleKeyInfo; - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - - if (Console.KeyAvailable) - { - return; - } - - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - _isEscSeq = false; - return; - } - - if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { }) - { - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - - if (Console.KeyAvailable) - { - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - } - else - { - ProcessMapConsoleKeyInfo (consoleKeyInfo); - } - return; - } - - ProcessMapConsoleKeyInfo (consoleKeyInfo); - } void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) { @@ -331,7 +258,6 @@ void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) } ); - _isEscSeq = false; } private void CheckWindowSizeChange () From 2fa855fecf4492bb59c130c6f6ec10a953e1dfe4 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Oct 2024 18:28:29 +0000 Subject: [PATCH 62/77] Lock all public members of AnsiResponseParser to ensure consistent states There are 2 lock variables - One for expectations - One for state (i.e. parsing) --- .../AnsiResponseParser/AnsiResponseParser.cs | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index f929e80eec..71f67d6f83 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -5,7 +5,9 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { - object lockExpectedResponses = new object(); + protected object lockExpectedResponses = new object(); + + protected object lockState = new object (); /// /// Responses we are expecting to come in. /// @@ -85,6 +87,19 @@ protected void ProcessInputBase ( Action appendOutput, int inputLength ) + { + lock (lockState) + { + ProcessInputBaseImpl (getCharAtIndex, getObjectAtIndex, appendOutput, inputLength); + } + } + + private void ProcessInputBaseImpl ( + Func getCharAtIndex, + Func getObjectAtIndex, + Action appendOutput, + int inputLength + ) { var index = 0; // Tracks position in the input string @@ -334,12 +349,17 @@ public IEnumerable> ProcessInput (params Tuple [] input) public IEnumerable> Release () { - foreach (Tuple h in HeldToEnumerable()) + // Lock in case Release is called from different Thread from parse + lock (lockState) { - yield return h; + foreach (Tuple h in HeldToEnumerable()) + { + yield return h; + } + + ResetState (); } - ResetState (); } private IEnumerable> HeldToEnumerable () @@ -356,13 +376,16 @@ private IEnumerable> HeldToEnumerable () /// public void ExpectResponseT (string terminator, Action>> response, bool persistent) { - if (persistent) - { - persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); - } - else + lock (lockExpectedResponses) { - expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); + if (persistent) + { + persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); + } + else + { + expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); + } } } @@ -405,10 +428,13 @@ public string ProcessInput (string input) public string Release () { - var output = heldContent.HeldToString (); - ResetState (); + lock (lockState) + { + var output = heldContent.HeldToString (); + ResetState (); - return output; + return output; + } } /// From 2b7d0c7f70a2afbca1bc24bbde761deee256e5a9 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Oct 2024 19:48:01 +0000 Subject: [PATCH 63/77] Add extra try/catch cancellation ex --- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 4f8efa2201..63765c3977 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -179,10 +179,18 @@ public NetEvents (ConsoleDriver consoleDriver) { _winChange.Set (); - if (_inputQueue.TryTake (out var item,-1,_netEventsDisposed.Token)) + try { - return item; + if (_inputQueue.TryTake (out var item, -1, _netEventsDisposed.Token)) + { + return item; + } } + catch (OperationCanceledException) + { + return null; + } + } return null; From fdfd339dd2c8fcb92e8aea183c1d8c994d86c26e Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Oct 2024 20:26:23 +0000 Subject: [PATCH 64/77] Make AnsiRequestScheduler testable by introducing a Func for DateTime.Now --- .../AnsiRequestScheduler.cs | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index b0c0e61bb4..1a6cfab1f3 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -3,8 +3,17 @@ namespace Terminal.Gui; -public class AnsiRequestScheduler (IAnsiResponseParser parser) +/// +/// Manages made to an . +/// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends +/// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from +/// terminal). +/// +internal class AnsiRequestScheduler { + private readonly IAnsiResponseParser _parser; + private readonly Func _now; + private readonly List> _requests = new (); /// @@ -19,8 +28,12 @@ public class AnsiRequestScheduler (IAnsiResponseParser parser) /// queued). /// /// - private ConcurrentDictionary _lastSend = new (); + private readonly ConcurrentDictionary _lastSend = new (); + /// + /// Number of milliseconds after sending a request that we allow + /// another request to go out. + /// private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); @@ -31,6 +44,14 @@ public class AnsiRequestScheduler (IAnsiResponseParser parser) /// private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + + private DateTime _lastRun; + public AnsiRequestScheduler (IAnsiResponseParser parser, Func? now = null) + { + _parser = parser; + _now = now ?? (() => DateTime.Now); + _lastRun = _now (); + } /// /// Sends the immediately or queues it if there is already /// an outstanding request for the given . @@ -50,7 +71,7 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { EvictStaleRequests (request.Terminator); - // Try again after + // Try again after evicting if (CanSend (request, out _)) { Send (request); @@ -58,7 +79,7 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request) } } - _requests.Add (Tuple.Create (request, DateTime.Now)); + _requests.Add (Tuple.Create (request, _now())); return false; } @@ -73,9 +94,9 @@ private bool EvictStaleRequests (string withTerminator) { if (_lastSend.TryGetValue (withTerminator, out var dt)) { - if (DateTime.Now - dt > _staleTimeout) + if (_now() - dt > _staleTimeout) { - parser.StopExpecting (withTerminator, false); + _parser.StopExpecting (withTerminator, false); return true; } @@ -84,7 +105,6 @@ private bool EvictStaleRequests (string withTerminator) return false; } - private DateTime _lastRun = DateTime.Now; /// /// Identifies and runs any that can be sent based on the @@ -96,7 +116,7 @@ private bool EvictStaleRequests (string withTerminator) /// if no outstanding requests or all have existing outstanding requests underway in parser. public bool RunSchedule (bool force = false) { - if (!force && DateTime.Now - _lastRun < _runScheduleThrottle) + if (!force && _now() - _lastRun < _runScheduleThrottle) { return false; } @@ -116,8 +136,8 @@ public bool RunSchedule (bool force = false) 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, _=>_now(), (_, _) => _now()); + _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false); r.Send (); } @@ -129,7 +149,7 @@ private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason) return false; } - if (parser.IsExpecting (r.Terminator)) + if (_parser.IsExpecting (r.Terminator)) { reason = ReasonCannotSend.OutstandingRequest; return false; @@ -143,7 +163,7 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) { if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) { - return DateTime.Now - value < _throttle; + return _now() - value < _throttle; } return false; From fbbda69ba0a03ebf73c646472593a7c5c29333fa Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Oct 2024 20:28:31 +0000 Subject: [PATCH 65/77] Code Cleanup (resharper) --- .../AnsiEscapeSequenceRequest.cs | 11 +- .../AnsiRequestScheduler.cs | 108 ++++++------ .../AnsiResponseExpectation.cs | 7 +- .../AnsiResponseParser/AnsiResponseParser.cs | 155 ++++++++---------- .../AnsiResponseParser/GenericHeld.cs | 13 +- .../AnsiResponseParser/IAnsiResponseParser.cs | 46 +++--- .../AnsiResponseParser/IHeld.cs | 14 +- .../AnsiResponseParser/StringHeld.cs | 13 +- 8 files changed, 186 insertions(+), 181 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs index d57d99796f..af6e7b924b 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -43,12 +43,9 @@ public class AnsiEscapeSequenceRequest public required string Terminator { get; init; } /// - /// Sends the to the raw output stream of the current . - /// Only call this method from the main UI thread. You should use if - /// sending many requests. + /// Sends the to the raw output stream of the current . + /// Only call this method from the main UI thread. You should use if + /// sending many requests. /// - public void Send () - { - Application.Driver?.RawWrite (Request); - } + public void Send () { Application.Driver?.RawWrite (Request); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 1a6cfab1f3..b5c6d47808 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -4,10 +4,10 @@ namespace Terminal.Gui; /// -/// Manages made to an . -/// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends -/// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from -/// terminal). +/// Manages made to an . +/// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends +/// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from +/// terminal). /// internal class AnsiRequestScheduler { @@ -17,53 +17,55 @@ internal class AnsiRequestScheduler private readonly List> _requests = new (); /// - /// - /// Dictionary where key is ansi request terminator and value is when we last sent a request for - /// this terminator. Combined with this prevents hammering the console - /// with too many requests in sequence which can cause console to freeze as there is no space for - /// regular screen drawing / mouse events etc to come in. - /// - /// - /// When user exceeds the throttle, new requests accumulate in (i.e. remain - /// queued). - /// + /// + /// Dictionary where key is ansi request terminator and value is when we last sent a request for + /// this terminator. Combined with this prevents hammering the console + /// with too many requests in sequence which can cause console to freeze as there is no space for + /// regular screen drawing / mouse events etc to come in. + /// + /// + /// When user exceeds the throttle, new requests accumulate in (i.e. remain + /// queued). + /// /// private readonly ConcurrentDictionary _lastSend = new (); /// - /// Number of milliseconds after sending a request that we allow - /// another request to go out. + /// Number of milliseconds after sending a request that we allow + /// another request to go out. /// - private TimeSpan _throttle = TimeSpan.FromMilliseconds (100); - private TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); + private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100); + + private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100); /// - /// If console has not responded to a request after this period of time, we assume that it is never going - /// to respond. Only affects when we try to send a new request with the same terminator - at which point - /// we tell the parser to stop expecting the old request and start expecting the new request. + /// If console has not responded to a request after this period of time, we assume that it is never going + /// to respond. Only affects when we try to send a new request with the same terminator - at which point + /// we tell the parser to stop expecting the old request and start expecting the new request. /// - private TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + private readonly DateTime _lastRun; - private DateTime _lastRun; public AnsiRequestScheduler (IAnsiResponseParser parser, Func? now = null) { _parser = parser; _now = now ?? (() => DateTime.Now); _lastRun = _now (); } + /// - /// Sends the immediately or queues it if there is already - /// an outstanding request for the given . + /// Sends the immediately or queues it if there is already + /// an outstanding request for the given . /// /// /// if request was sent immediately. if it was queued. public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { - - if (CanSend (request, out var reason)) + if (CanSend (request, out ReasonCannotSend reason)) { Send (request); + return true; } @@ -75,26 +77,28 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request) if (CanSend (request, out _)) { Send (request); + return true; } } - _requests.Add (Tuple.Create (request, _now())); + _requests.Add (Tuple.Create (request, _now ())); + return false; } /// - /// Looks to see if the last time we sent - /// is a long time ago. If so we assume that we will never get a response and - /// can proceed with a new request for this terminator (returning ). + /// Looks to see if the last time we sent + /// is a long time ago. If so we assume that we will never get a response and + /// can proceed with a new request for this terminator (returning ). /// /// /// private bool EvictStaleRequests (string withTerminator) { - if (_lastSend.TryGetValue (withTerminator, out var dt)) + if (_lastSend.TryGetValue (withTerminator, out DateTime dt)) { - if (_now() - dt > _staleTimeout) + if (_now () - dt > _staleTimeout) { _parser.StopExpecting (withTerminator, false); @@ -105,23 +109,26 @@ private bool EvictStaleRequests (string withTerminator) return false; } - /// - /// Identifies and runs any that can be sent based on the - /// current outstanding requests of the parser. + /// Identifies and runs any that can be sent based on the + /// current outstanding requests of the parser. /// - /// Repeated requests to run the schedule over short period of time will be ignored. - /// Pass to override this behaviour and force evaluation of outstanding requests. - /// if a request was found and run. - /// if no outstanding requests or all have existing outstanding requests underway in parser. + /// + /// Repeated requests to run the schedule over short period of time will be ignored. + /// Pass to override this behaviour and force evaluation of outstanding requests. + /// + /// + /// if a request was found and run. + /// if no outstanding requests or all have existing outstanding requests underway in parser. + /// public bool RunSchedule (bool force = false) { - if (!force && _now() - _lastRun < _runScheduleThrottle) + if (!force && _now () - _lastRun < _runScheduleThrottle) { return false; } - var opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _)); + Tuple? opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _)); if (opportunity != null) { @@ -136,7 +143,7 @@ public bool RunSchedule (bool force = false) private void Send (AnsiEscapeSequenceRequest r) { - _lastSend.AddOrUpdate (r.Terminator, _=>_now(), (_, _) => _now()); + _lastSend.AddOrUpdate (r.Terminator, _ => _now (), (_, _) => _now ()); _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false); r.Send (); } @@ -146,16 +153,19 @@ private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason) if (ShouldThrottle (r)) { reason = ReasonCannotSend.TooManyRequests; + return false; } if (_parser.IsExpecting (r.Terminator)) { reason = ReasonCannotSend.OutstandingRequest; + return false; } - reason = default; + reason = default (ReasonCannotSend); + return true; } @@ -163,7 +173,7 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) { if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) { - return _now() - value < _throttle; + return _now () - value < _throttle; } return false; @@ -173,18 +183,18 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) internal enum ReasonCannotSend { /// - /// No reason given. + /// No reason given. /// None = 0, /// - /// The parser is already waiting for a request to complete with the given terminator. + /// The parser is already waiting for a request to complete with the given terminator. /// OutstandingRequest, /// - /// There have been too many requests sent recently, new requests will be put into - /// queue to prevent console becoming unresponsive. + /// There have been too many requests sent recently, new requests will be put into + /// queue to prevent console becoming unresponsive. /// TooManyRequests } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index 75fdb308a8..e4c22d7582 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -3,8 +3,5 @@ namespace Terminal.Gui; internal record AnsiResponseExpectation (string Terminator, Action Response) { - public bool Matches (string cur) - { - return cur.EndsWith (Terminator); - } -} \ No newline at end of file + public bool Matches (string cur) { return cur.EndsWith (Terminator); } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 71f67d6f83..71e489b9ef 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -2,25 +2,25 @@ namespace Terminal.Gui; - internal abstract class AnsiResponseParserBase : IAnsiResponseParser { - protected object lockExpectedResponses = new object(); + protected object lockExpectedResponses = new (); + + protected object lockState = new (); - protected object lockState = new object (); /// - /// Responses we are expecting to come in. + /// Responses we are expecting to come in. /// protected readonly List expectedResponses = new (); /// - /// Collection of responses that we . + /// Collection of responses that we . /// protected readonly List lateResponses = new (); /// - /// Responses that you want to look out for that will come in continuously e.g. mouse events. - /// Key is the terminator. + /// Responses that you want to look out for that will come in continuously e.g. mouse events. + /// Key is the terminator. /// protected readonly List persistentExpectations = new (); @@ -47,21 +47,20 @@ protected set // These all are valid terminators on ansi responses, // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s // No - N or O - protected readonly HashSet _knownTerminators = new (new [] - { - '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - // No - N or O - 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z', - '^', '`', '~', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', - 'l', 'm', 'n', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' - }); - - protected AnsiResponseParserBase (IHeld heldContent) - { - this.heldContent = heldContent; - } + protected readonly HashSet _knownTerminators = new ( + new [] + { + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + + // No - N or O + 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z', + '^', '`', '~', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'l', 'm', 'n', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' + }); + + protected AnsiResponseParserBase (IHeld heldContent) { this.heldContent = heldContent; } protected void ResetState () { @@ -187,8 +186,8 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse ( cur, expectedResponses, - invokeCallback: true, - removeExpectation: true)) + true, + true)) { return false; } @@ -197,8 +196,8 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse ( cur, lateResponses, - invokeCallback: false, - removeExpectation: true)) + false, + true)) { return false; } @@ -207,8 +206,8 @@ protected bool ShouldReleaseHeldContent () if (MatchResponse ( cur, persistentExpectations, - invokeCallback: true, - removeExpectation: false)) + true, + false)) { return false; } @@ -222,11 +221,12 @@ protected bool ShouldReleaseHeldContent () State = AnsiResponseParserState.Normal; // Maybe swallow anyway if user has custom delegate - var swallow = ShouldSwallowUnexpectedResponse (); + bool swallow = ShouldSwallowUnexpectedResponse (); if (swallow) { heldContent.ClearHeld (); + // Do not send back to input stream return false; } @@ -239,14 +239,15 @@ protected bool ShouldReleaseHeldContent () } /// - /// - /// When overriden in a derived class, indicates whether the unexpected response - /// currently in should be released or swallowed. - /// Use this to enable default event for escape codes. - /// - /// - /// Note this is only called for complete responses. - /// Based on + /// + /// When overriden in a derived class, indicates whether the unexpected response + /// currently in should be released or swallowed. + /// Use this to enable default event for escape codes. + /// + /// + /// Note this is only called for complete responses. + /// Based on + /// /// /// protected abstract bool ShouldSwallowUnexpectedResponse (); @@ -254,7 +255,7 @@ protected bool ShouldReleaseHeldContent () private bool MatchResponse (string cur, List collection, bool invokeCallback, bool removeExpectation) { // Check for expected responses - var matchingResponse = collection.FirstOrDefault (r => r.Matches (cur)); + AnsiResponseExpectation? matchingResponse = collection.FirstOrDefault (r => r.Matches (cur)); if (matchingResponse?.Response != null) { @@ -262,6 +263,7 @@ private bool MatchResponse (string cur, List collection { matchingResponse.Response.Invoke (heldContent); } + ResetState (); if (removeExpectation) @@ -275,23 +277,23 @@ private bool MatchResponse (string cur, List collection return false; } - /// + /// public void ExpectResponse (string terminator, Action response, bool persistent) { lock (lockExpectedResponses) { if (persistent) { - persistentExpectations.Add (new (terminator, (h) => response.Invoke (h.HeldToString ()))); + persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()))); } else { - expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ()))); + expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()))); } } } - /// + /// public bool IsExpecting (string terminator) { lock (lockExpectedResponses) @@ -301,10 +303,9 @@ public bool IsExpecting (string terminator) } } - /// + /// public void StopExpecting (string terminator, bool persistent) { - lock (lockExpectedResponses) { if (persistent) @@ -313,9 +314,9 @@ public void StopExpecting (string terminator, bool persistent) } else { - var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); + AnsiResponseExpectation [] removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray (); - foreach (var r in removed) + foreach (AnsiResponseExpectation r in removed) { expectedResponses.Remove (r); lateResponses.Add (r); @@ -329,14 +330,12 @@ internal class AnsiResponseParser : AnsiResponseParserBase { public AnsiResponseParser () : base (new GenericHeld ()) { } - /// - public Func>, bool> UnexpectedResponseHandler { get; set; } = (_) => false; - + public Func>, bool> UnexpectedResponseHandler { get; set; } = _ => false; public IEnumerable> ProcessInput (params Tuple [] input) { - List> output = new List> (); + List> output = new (); ProcessInputBase ( i => input [i].Item1, @@ -352,64 +351,57 @@ public IEnumerable> Release () // Lock in case Release is called from different Thread from parse lock (lockState) { - foreach (Tuple h in HeldToEnumerable()) + foreach (Tuple h in HeldToEnumerable ()) { yield return h; } ResetState (); } - } - private IEnumerable> HeldToEnumerable () - { - return (IEnumerable>)heldContent.HeldToObjects (); - } + private IEnumerable> HeldToEnumerable () { return (IEnumerable>)heldContent.HeldToObjects (); } /// - /// '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. + /// '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. /// /// /// /// - public void ExpectResponseT (string terminator, Action>> response, bool persistent) + public void ExpectResponseT (string terminator, Action>> response, bool persistent) { lock (lockExpectedResponses) { if (persistent) { - persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); + persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()))); } else { - expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ()))); + expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()))); } } } - /// - protected override bool ShouldSwallowUnexpectedResponse () - { - return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); - } + /// + protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); } } internal class AnsiResponseParser : AnsiResponseParserBase { /// - /// - /// Delegate for handling unrecognized escape codes. Default behaviour - /// is to return which simply releases the - /// characters back to input stream for downstream processing. - /// - /// - /// Implement a method to handle if you want and return if you want the - /// keystrokes 'swallowed' (i.e. not returned to input stream). - /// + /// + /// Delegate for handling unrecognized escape codes. Default behaviour + /// is to return which simply releases the + /// characters back to input stream for downstream processing. + /// + /// + /// Implement a method to handle if you want and return if you want the + /// keystrokes 'swallowed' (i.e. not returned to input stream). + /// /// - public Func UnknownResponseHandler { get; set; } = (_) => false; + public Func UnknownResponseHandler { get; set; } = _ => false; public AnsiResponseParser () : base (new StringHeld ()) { } @@ -430,16 +422,13 @@ public string Release () { lock (lockState) { - var output = heldContent.HeldToString (); + string output = heldContent.HeldToString (); ResetState (); return output; } } - /// - protected override bool ShouldSwallowUnexpectedResponse () - { - return UnknownResponseHandler.Invoke (heldContent.HeldToString ()); - } -} \ No newline at end of file + /// + protected override bool ShouldSwallowUnexpectedResponse () { return UnknownResponseHandler.Invoke (heldContent.HeldToString ()); } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs index c81cb65652..ffd8d46891 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs @@ -2,15 +2,18 @@ namespace Terminal.Gui; /// -/// Implementation of for +/// Implementation of for /// /// internal class GenericHeld : IHeld { private readonly List> held = new (); - public void ClearHeld () => held.Clear (); - public string HeldToString () => new (held.Select (h => h.Item1).ToArray ()); - public IEnumerable HeldToObjects () => held; - public void AddToHeld (object o) => held.Add ((Tuple)o); + public void ClearHeld () { held.Clear (); } + + public string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); } + + public IEnumerable HeldToObjects () { return held; } + + public void AddToHeld (object o) { held.Add ((Tuple)o); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs index d1246c6610..c26988d127 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs @@ -2,46 +2,52 @@ namespace Terminal.Gui; /// -/// When implemented in a derived class, allows watching an input stream of characters -/// (i.e. console input) for ANSI response sequences. +/// When implemented in a derived class, allows watching an input stream of characters +/// (i.e. console input) for ANSI response sequences. /// public interface IAnsiResponseParser { /// - /// Current state of the parser based on what sequence of characters it has - /// read from the console input stream. + /// Current state of the parser based on what sequence of characters it has + /// read from the console input stream. /// AnsiResponseParserState State { get; } /// - /// Notifies the parser that you are expecting a response to come in - /// with the given (i.e. because you have - /// sent an ANSI request out). + /// Notifies the parser that you are expecting a response to come in + /// with the given (i.e. because you have + /// sent an ANSI request out). /// /// The terminator you expect to see on response. - /// if you want this to persist permanently - /// and be raised for every event matching the . + /// + /// if you want this to persist permanently + /// and be raised for every event matching the . + /// /// Callback to invoke when the response is seen in console input. - /// If trying to register a persistent request for a terminator - /// that already has one. - /// exists. + /// + /// If trying to register a persistent request for a terminator + /// that already has one. + /// exists. + /// void ExpectResponse (string terminator, Action response, bool persistent); /// - /// Returns true if there is an existing expectation (i.e. we are waiting a response - /// from console) for the given . + /// Returns true if there is an existing expectation (i.e. we are waiting a response + /// from console) for the given . /// /// /// bool IsExpecting (string terminator); /// - /// Removes callback and expectation that we will get a response for the - /// given . Use to give up on very old - /// requests e.g. if you want to send a different one with the same terminator. + /// Removes callback and expectation that we will get a response for the + /// given . Use to give up on very old + /// requests e.g. if you want to send a different one with the same terminator. /// /// - /// if you want to remove a persistent - /// request listener. + /// + /// if you want to remove a persistent + /// request listener. + /// void StopExpecting (string requestTerminator, bool persistent); -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs index 4eb510c16b..ab23f477fd 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs @@ -2,31 +2,31 @@ namespace Terminal.Gui; /// -/// Describes a sequence of chars (and optionally T metadata) accumulated -/// by an +/// Describes a sequence of chars (and optionally T metadata) accumulated +/// by an /// internal interface IHeld { /// - /// Clears all held objects + /// Clears all held objects /// void ClearHeld (); /// - /// Returns string representation of the held objects + /// Returns string representation of the held objects /// /// string HeldToString (); /// - /// Returns the collection objects directly e.g. - /// or + metadata T + /// Returns the collection objects directly e.g. + /// or + metadata T /// /// IEnumerable HeldToObjects (); /// - /// Adds the given object to the collection. + /// Adds the given object to the collection. /// /// void AddToHeld (object o); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs index b5d8d390c8..4a03bc3b5e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs @@ -2,14 +2,17 @@ namespace Terminal.Gui; /// -/// Implementation of for +/// Implementation of for /// internal class StringHeld : IHeld { private readonly StringBuilder held = new (); - public void ClearHeld () => held.Clear (); - public string HeldToString () => held.ToString (); - public IEnumerable HeldToObjects () => held.ToString ().Select (c => (object)c); - public void AddToHeld (object o) => held.Append ((char)o); + public void ClearHeld () { held.Clear (); } + + public string HeldToString () { return held.ToString (); } + + public IEnumerable HeldToObjects () { return held.ToString ().Select (c => (object)c); } + + public void AddToHeld (object o) { held.Append ((char)o); } } From 1d8e7563fdc59f5a2610f9b91add7677c5518ef8 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Oct 2024 20:58:31 +0000 Subject: [PATCH 66/77] Start adding tests for AnsiRequestScheduler --- .../AnsiRequestScheduler.cs | 47 +++++++----- .../AnsiRequestSchedulerTests.cs | 74 +++++++++++++++++++ 2 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index b5c6d47808..20e223e215 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -12,9 +12,16 @@ namespace Terminal.Gui; internal class AnsiRequestScheduler { private readonly IAnsiResponseParser _parser; - private readonly Func _now; - private readonly List> _requests = new (); + /// + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. + /// + internal Func Now { get; set; } + + private readonly List> _queuedRequests = new (); + + internal IReadOnlyCollection QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList (); /// /// @@ -24,7 +31,7 @@ internal class AnsiRequestScheduler /// regular screen drawing / mouse events etc to come in. /// /// - /// When user exceeds the throttle, new requests accumulate in (i.e. remain + /// When user exceeds the throttle, new requests accumulate in (i.e. remain /// queued). /// /// @@ -50,8 +57,8 @@ internal class AnsiRequestScheduler public AnsiRequestScheduler (IAnsiResponseParser parser, Func? now = null) { _parser = parser; - _now = now ?? (() => DateTime.Now); - _lastRun = _now (); + Now = now ?? (() => DateTime.Now); + _lastRun = Now (); } /// @@ -71,18 +78,20 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request) if (reason == ReasonCannotSend.OutstandingRequest) { - EvictStaleRequests (request.Terminator); - - // Try again after evicting - if (CanSend (request, out _)) + // If we can evict an old request (no response from terminal after ages) + if (EvictStaleRequests (request.Terminator)) { - Send (request); + // Try again after evicting + if (CanSend (request, out _)) + { + Send (request); - return true; + return true; + } } } - _requests.Add (Tuple.Create (request, _now ())); + _queuedRequests.Add (Tuple.Create (request, Now ())); return false; } @@ -98,7 +107,7 @@ private bool EvictStaleRequests (string withTerminator) { if (_lastSend.TryGetValue (withTerminator, out DateTime dt)) { - if (_now () - dt > _staleTimeout) + if (Now () - dt > _staleTimeout) { _parser.StopExpecting (withTerminator, false); @@ -110,7 +119,7 @@ private bool EvictStaleRequests (string withTerminator) } /// - /// Identifies and runs any that can be sent based on the + /// Identifies and runs any that can be sent based on the /// current outstanding requests of the parser. /// /// @@ -123,16 +132,16 @@ private bool EvictStaleRequests (string withTerminator) /// public bool RunSchedule (bool force = false) { - if (!force && _now () - _lastRun < _runScheduleThrottle) + if (!force && Now () - _lastRun < _runScheduleThrottle) { return false; } - Tuple? opportunity = _requests.FirstOrDefault (r => CanSend (r.Item1, out _)); + Tuple? opportunity = _queuedRequests.FirstOrDefault (r => CanSend (r.Item1, out _)); if (opportunity != null) { - _requests.Remove (opportunity); + _queuedRequests.Remove (opportunity); Send (opportunity.Item1); return true; @@ -143,7 +152,7 @@ public bool RunSchedule (bool force = false) private void Send (AnsiEscapeSequenceRequest r) { - _lastSend.AddOrUpdate (r.Terminator, _ => _now (), (_, _) => _now ()); + _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ()); _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false); r.Send (); } @@ -173,7 +182,7 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) { if (_lastSend.TryGetValue (r.Terminator, out DateTime value)) { - return _now () - value < _throttle; + return Now () - value < _throttle; } return false; diff --git a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs new file mode 100644 index 0000000000..5a2f9d8076 --- /dev/null +++ b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs @@ -0,0 +1,74 @@ +using Moq; + +namespace UnitTests.ConsoleDrivers; + + +public class AnsiRequestSchedulerTests +{ + private readonly Mock _parserMock; + private readonly AnsiRequestScheduler _scheduler; + + private static DateTime _staticNow; // Static value to hold the current time + + public AnsiRequestSchedulerTests () + { + _parserMock = new Mock (MockBehavior.Strict); + _staticNow = DateTime.UtcNow; // Initialize static time + _scheduler = new AnsiRequestScheduler (_parserMock.Object, () => _staticNow); + } + + [Fact] + public void SendOrSchedule_SendsDeviceAttributeRequest_WhenNoOutstandingRequests () + { + // Arrange + var request = new AnsiEscapeSequenceRequest + { + Request = "\u001b[0c", // ESC [ c + Terminator = "c", + ResponseReceived = r => { } + }; + + // we have no outstanding for c already + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable(Times.Once); + + // then we should execute our request + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Once); + + // Act + bool result = _scheduler.SendOrSchedule (request); + + + // Assert + Assert.Empty (_scheduler.QueuedRequests); // We sent it i.e. we did not queue it for later + Assert.True (result); // Should send immediately + _parserMock.Verify (); + } + [Fact] + public void SendOrSchedule_QueuesRequest_WhenOutstandingRequestExists () + { + // Arrange + var request1 = new AnsiEscapeSequenceRequest + { + Request = "\u001b[0c", // ESC [ 0 c + Terminator = "c", + ResponseReceived = r => { } + }; + + // Parser already has an ongoing request for "c" + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once); + + // Act + var result = _scheduler.SendOrSchedule (request1); + + // Assert + Assert.Single (_scheduler.QueuedRequests); // Ensure only one request is in the queue + Assert.False (result); // Should be queued + _parserMock.Verify (); + } + private void SetTime (int milliseconds) + { + // This simulates the passing of time by setting the Now function to return a specific time. + var newNow = _staticNow.AddMilliseconds (milliseconds); + _scheduler.Now = () => newNow; + } +} \ No newline at end of file From 43aef424513df9f58018843f4cb918281b348a5c Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Oct 2024 21:06:30 +0000 Subject: [PATCH 67/77] Add more unit tests for scheduler --- .../AnsiRequestSchedulerTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs index 5a2f9d8076..2526ee534c 100644 --- a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs @@ -65,6 +65,85 @@ public void SendOrSchedule_QueuesRequest_WhenOutstandingRequestExists () Assert.False (result); // Should be queued _parserMock.Verify (); } + + + [Fact] + public void RunSchedule_ThrottleNotExceeded_AllowSend () + { + // Arrange + var request = new AnsiEscapeSequenceRequest + { + Request = "\u001b[0c", // ESC [ 0 c + Terminator = "c", + ResponseReceived = r => { } + }; + + // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable(Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + + _scheduler.SendOrSchedule (request); + + // Simulate time passing beyond throttle + SetTime (101); // Exceed throttle limit + + + // Act + + // Send another request after the throttled time limit + var result = _scheduler.SendOrSchedule (request); + + // Assert + Assert.Empty (_scheduler.QueuedRequests); // Should send and clear the request + Assert.True (result); // Should have found and sent the request + _parserMock.Verify (); + } + + [Fact] + public void RunSchedule_ThrottleExceeded_QueueRequest () + { + // Arrange + var request = new AnsiEscapeSequenceRequest + { + Request = "\u001b[0c", // ESC [ 0 c + Terminator = "c", + ResponseReceived = r => { } + }; + + // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + + _scheduler.SendOrSchedule (request); + + // Simulate time passing + SetTime (55); // Does not exceed throttle limit + + + // Act + + // Send another request after the throttled time limit + var result = _scheduler.SendOrSchedule (request); + + // Assert + Assert.Single (_scheduler.QueuedRequests); // Should have been queued + Assert.False(result); // Should have been queued + + // Throttle still not exceeded + Assert.False(_scheduler.RunSchedule ()); + + SetTime (90); + + // Throttle still not exceeded + Assert.False (_scheduler.RunSchedule ()); + + SetTime (105); + + // Throttle exceeded - so send the request + Assert.True (_scheduler.RunSchedule ()); + + _parserMock.Verify (); + } private void SetTime (int milliseconds) { // This simulates the passing of time by setting the Now function to return a specific time. From f84a58f301628f73c609238417b02075ebd6130b Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Oct 2024 07:50:47 +0000 Subject: [PATCH 68/77] Fix eviction not happening as part of RunSchedule --- .../AnsiRequestScheduler.cs | 24 +++++++--- .../AnsiRequestSchedulerTests.cs | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 20e223e215..3d1f08a1a4 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -19,7 +19,7 @@ internal class AnsiRequestScheduler /// internal Func Now { get; set; } - private readonly List> _queuedRequests = new (); + private readonly HashSet> _queuedRequests = new (); internal IReadOnlyCollection QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList (); @@ -68,6 +68,10 @@ public AnsiRequestScheduler (IAnsiResponseParser parser, Func? now = n /// /// if request was sent immediately. if it was queued. public bool SendOrSchedule (AnsiEscapeSequenceRequest request) + { + return SendOrSchedule (request, true); + } + private bool SendOrSchedule (AnsiEscapeSequenceRequest request,bool addToQueue) { if (CanSend (request, out ReasonCannotSend reason)) { @@ -91,7 +95,10 @@ public bool SendOrSchedule (AnsiEscapeSequenceRequest request) } } - _queuedRequests.Add (Tuple.Create (request, Now ())); + if (addToQueue) + { + _queuedRequests.Add (Tuple.Create (request, Now ())); + } return false; } @@ -137,14 +144,17 @@ public bool RunSchedule (bool force = false) return false; } - Tuple? opportunity = _queuedRequests.FirstOrDefault (r => CanSend (r.Item1, out _)); + // Get oldest request + Tuple? opportunity = _queuedRequests.MinBy (r=>r.Item2); if (opportunity != null) { - _queuedRequests.Remove (opportunity); - Send (opportunity.Item1); - - return true; + // Give it another go + if (SendOrSchedule (opportunity.Item1, false)) + { + _queuedRequests.Remove (opportunity); + return true; + } } return false; diff --git a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs index 2526ee534c..d184c8bae2 100644 --- a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs @@ -144,6 +144,53 @@ public void RunSchedule_ThrottleExceeded_QueueRequest () _parserMock.Verify (); } + + [Fact] + public void EvictStaleRequests_RemovesStaleRequest_AfterTimeout () + { + // Arrange + var request1 = new AnsiEscapeSequenceRequest + { + Request = "\u001b[0c", + Terminator = "c", + ResponseReceived = r => { } + }; + + // Send + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + + Assert.True (_scheduler.SendOrSchedule (request1)); + + // Parser already has an ongoing request for "c" + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Exactly (2)); + + // Cannot send because there is already outstanding request + Assert.False(_scheduler.SendOrSchedule (request1)); + Assert.Single (_scheduler.QueuedRequests); + + // Simulate request going stale + SetTime (5001); // Exceeds stale timeout + + // Parser should be told to give up on this one (evicted) + _parserMock.Setup (p => p.StopExpecting ("c", false)) + .Callback (() => + { + // When we tell parser to evict - it should now tell us it is no longer expecting + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); + }).Verifiable (); + + // When we send again the evicted one should be + var evicted = _scheduler.RunSchedule (); + + Assert.True (evicted); // Stale request should be evicted + Assert.Empty (_scheduler.QueuedRequests); + + // Assert + _parserMock.Verify (); + } + + private void SetTime (int milliseconds) { // This simulates the passing of time by setting the Now function to return a specific time. From 0275f9aed8d55303b96ba48e8cdb123ef3fdf9e2 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 1 Nov 2024 08:29:34 +0000 Subject: [PATCH 69/77] More tests for AnsiRequestScheduler --- .../AnsiRequestSchedulerTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs index d184c8bae2..9d29f9fda8 100644 --- a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs @@ -190,6 +190,42 @@ public void EvictStaleRequests_RemovesStaleRequest_AfterTimeout () _parserMock.Verify (); } + [Fact] + public void RunSchedule_DoesNothing_WhenQueueIsEmpty () + { + // Act + var result = _scheduler.RunSchedule (); + + // Assert + Assert.False (result); // No requests to process + Assert.Empty (_scheduler.QueuedRequests); + } + + [Fact] + public void SendOrSchedule_ManagesIndependentTerminatorsCorrectly () + { + // Arrange + var request1 = new AnsiEscapeSequenceRequest { Request = "\u001b[0c", Terminator = "c", ResponseReceived = r => { } }; + var request2 = new AnsiEscapeSequenceRequest { Request = "\u001b[0x", Terminator = "x", ResponseReceived = r => { } }; + + // Already have a 'c' ongoing + _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once); + + // 'x' is free + _parserMock.Setup (p => p.IsExpecting ("x")).Returns (false).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> (), false)).Verifiable (Times.Once); + + // Act + var a = _scheduler.SendOrSchedule (request1); + var b = _scheduler.SendOrSchedule (request2); + + // Assert + Assert.False (a); + Assert.True (b); + Assert.Equal(request1, Assert.Single (_scheduler.QueuedRequests)); + _parserMock.Verify (); + } + private void SetTime (int milliseconds) { From 395bee419ace18ad83c28c9fc00d078f9a48270b Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 08:38:02 +0000 Subject: [PATCH 70/77] Add support for sending other types of requests --- .../AnsiEscapeSequenceRequest.cs | 11 + .../AnsiRequestScheduler.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 7 +- .../AnsiResponseParser/IAnsiResponseParser.cs | 5 + Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 2 +- .../CursesDriver/CursesDriver.cs | 2 +- .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 9 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 2 +- UICatalog/Scenarios/AnsiRequestsScenario.cs | 256 +++++++++++++++--- 11 files changed, 249 insertions(+), 51 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs index af6e7b924b..d73c8b9bcc 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -48,4 +48,15 @@ public class AnsiEscapeSequenceRequest /// sending many requests. /// public void Send () { Application.Driver?.RawWrite (Request); } + + + /// + /// The value expected in the response e.g. + /// + /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value + /// + /// which will have a 't' as terminator but also other different request may return the same terminator with a + /// different value. + /// + public string? Value { get; init; } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 3d1f08a1a4..24345e1c62 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -50,7 +50,7 @@ internal class AnsiRequestScheduler /// to respond. Only affects when we try to send a new request with the same terminator - at which point /// we tell the parser to stop expecting the old request and start expecting the new request. /// - private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (5); + private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1); private readonly DateTime _lastRun; diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 71e489b9ef..6e7b46f136 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -26,7 +26,10 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser private AnsiResponseParserState _state = AnsiResponseParserState.Normal; - // Current state of the parser + /// + public event EventHandler>? StoppedExpecting; + + /// public AnsiResponseParserState State { get => _state; @@ -306,6 +309,8 @@ public bool IsExpecting (string terminator) /// public void StopExpecting (string terminator, bool persistent) { + StoppedExpecting?.Invoke (this, new (terminator)); + lock (lockExpectedResponses) { if (persistent) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs index c26988d127..b2a421c8a0 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs @@ -7,6 +7,11 @@ namespace Terminal.Gui; /// public interface IAnsiResponseParser { + /// + /// Called when parser is told to a response for a given request + /// + public event EventHandler> StoppedExpecting; + /// /// Current state of the parser based on what sequence of characters it has /// read from the console input stream. diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 9441622687..7c1136b9a8 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -627,7 +627,7 @@ public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) GetRequestScheduler ().SendOrSchedule (request); } - protected abstract IAnsiResponseParser GetParser (); + public abstract IAnsiResponseParser GetParser (); internal AnsiRequestScheduler GetRequestScheduler () { diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 1d83fb5cce..36f8f392a3 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -592,7 +592,7 @@ internal override MainLoop Init () private readonly AnsiResponseParser _parser = new (); /// - protected override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; internal void ProcessInput () { diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 88799c4989..0e97c37066 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1329,7 +1329,7 @@ public enum DECSCUSR_Style /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR) /// https://terminalguide.namepad.de/seq/csi_sn__p-6/ /// - public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n"; + public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" }; /// /// The terminal reply to . ESC [ ? (y) ; (x) R @@ -1354,13 +1354,13 @@ public enum DECSCUSR_Style /// 32 = Text macros /// 42 = ISO Latin-2 character set /// - public static readonly string CSI_SendDeviceAttributes = CSI + "0c"; + public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" }; /// /// ESC [ > 0 c - Send Device Attributes (Secondary DA) /// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220) /// - public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c"; + public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" }; /// /// The terminator indicating a reply to or @@ -1385,7 +1385,8 @@ public enum DECSCUSR_Style /// CSI 1 8 t | yes | yes | yes | report window size in chars /// https://terminalguide.namepad.de/seq/csi_st-18/ /// - public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t"; + public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" }; + /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 537d60adfa..e01e9f0490 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -395,7 +395,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al private AnsiResponseParser _parser = new (); /// - protected override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; /// internal override void RawWrite (string str) { } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 63765c3977..6bac0ed116 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -994,7 +994,7 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out } /// - protected override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; /// internal override void RawWrite (string str) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 5dbf3c5003..e5f3db0599 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1180,7 +1180,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } /// - protected override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; /// internal override void RawWrite (string str) => WinConsole?.WriteANSI (str); diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index 526c5d5b91..d3a0d166df 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -2,42 +2,221 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using ColorHelper; using Terminal.Gui; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace UICatalog.Scenarios; - - -[ScenarioMetadata ("Ansi Requests", "Demonstration of how to send ansi requests.")] -[ScenarioCategory ("Colors")] -[ScenarioCategory ("Drawing")] -public class AnsiRequestsScenario : Scenario +[ScenarioMetadata ("AnsiEscapeSequenceRequest", "Ansi Escape Sequence Request")] +[ScenarioCategory ("Ansi Escape Sequence")] +public sealed class AnsiEscapeSequenceRequests : Scenario { private GraphView _graphView; - private Window _win; private DateTime start = DateTime.Now; private ScatterSeries _sentSeries; private ScatterSeries _answeredSeries; - private List sends = new (); + private List sends = new (); private object lockAnswers = new object (); - private Dictionary answers = new (); + private Dictionary answers = new (); private Label _lblSummary; public override void Main () { + // Init Application.Init (); - _win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; + TabView tv = new TabView + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + Tab single = new Tab (); + single.DisplayText = "Single"; + single.View = BuildSingleTab (); + + Tab bulk = new (); + bulk.DisplayText = "Multi"; + bulk.View = BuildBulkTab (); + + tv.AddTab (single, true); + tv.AddTab (bulk, false); + + // Setup - Create a top-level application window and configure it. + Window appWindow = new () + { + Title = GetQuitKeyAndName (), + }; + + appWindow.Add (tv); + + // Run - Start the application. + Application.Run (appWindow); + bulk.View.Dispose (); + single.View.Dispose (); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); + } + + private View BuildSingleTab () + { + View w = new View () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + w.Padding.Thickness = new (1); + + var scrRequests = new List + { + "CSI_SendDeviceAttributes", + "CSI_ReportTerminalSizeInChars", + "CSI_RequestCursorPositionReport", + "CSI_SendDeviceAttributes2" + }; + + var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) }; + w.Add (cbRequests); + + var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "Request:" }; + var tfRequest = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 20 }; + w.Add (label, tfRequest); + + label = new Label { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "Value:" }; + var tfValue = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6 }; + w.Add (label, tfValue); + + label = new Label { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "Terminator:" }; + var tfTerminator = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4 }; + w.Add (label, tfTerminator); + + cbRequests.SelectedItemChanged += (s, e) => + { + if (cbRequests.SelectedItem == -1) + { + return; + } + + var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem]; + AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null; + + switch (selAnsiEscapeSequenceRequestName) + { + case "CSI_SendDeviceAttributes": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes; + + break; + case "CSI_ReportTerminalSizeInChars": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars; + + break; + case "CSI_RequestCursorPositionReport": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_RequestCursorPositionReport; + + break; + case "CSI_SendDeviceAttributes2": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes2; + + break; + } + + tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : ""; + tfValue.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Value ?? "" : ""; + tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : ""; + }; + + // Forces raise cbRequests.SelectedItemChanged to update TextFields + cbRequests.SelectedItem = 0; + + label = new Label { Y = Pos.Bottom (tfRequest) + 2, Text = "Response:" }; + var tvResponse = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true }; + w.Add (label, tvResponse); + + label = new Label { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "Error:" }; + var tvError = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true }; + w.Add (label, tvError); + + label = new Label { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "Value:" }; + var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true }; + w.Add (label, tvValue); + + label = new Label { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "Terminator:" }; + var tvTerminator = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4, Height = 4, ReadOnly = true }; + w.Add (label, tvTerminator); + + var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "Send Request", IsDefault = true }; + + var lblSuccess = new Label { X = Pos.Center (), Y = Pos.Bottom (btnResponse) + 1 }; + w.Add (lblSuccess); + + Application.Driver.GetParser ().StoppedExpecting += (s,e)=>OnFail(e.CurrentValue,tvResponse,tvError,tvValue,tvTerminator, lblSuccess); + + btnResponse.Accepting += (s, e) => + { + var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest + { + Request = tfRequest.Text, + Terminator = tfTerminator.Text, + Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text + }; + + Application.Driver.QueueAnsiRequest ( + new () + { + Request = ansiEscapeSequenceRequest.Request, + Terminator = ansiEscapeSequenceRequest.Terminator, + ResponseReceived = (s)=>OnSuccess(s, tvResponse, tvError, tvValue, tvTerminator,lblSuccess) + }); + }; + + w.Add (btnResponse); + + w.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "You can send other requests by editing the TextFields." }); + + return w; + } + + private void OnSuccess (string response, TextView tvResponse, TextView tvError, TextView tvValue, TextView tvTerminator,Label lblSuccess) + { + tvResponse.Text = response; + tvError.Text = string.Empty; + tvValue.Text = string.Empty; + tvTerminator.Text = string.Empty; + + lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"]; + lblSuccess.Text = "Successful"; + } + + private void OnFail (string terminator, TextView tvResponse, TextView tvError, TextView tvValue, TextView tvTerminator, Label lblSuccess) + { + tvResponse.Text = string.Empty; + tvError.Text = "No Response"; + tvValue.Text = string.Empty; + tvTerminator.Text = terminator; + + lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"]; + lblSuccess.Text = "Error"; + } + + private View BuildBulkTab () + { + View w = new View () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; var lbl = new Label () { Text = "This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends", Height = 2, - Width = Dim.Fill() + Width = Dim.Fill () }; Application.AddTimeout ( @@ -60,7 +239,7 @@ public override void Main () { Y = Pos.Bottom (lbl), Width = Dim.Percent (50), - Height = Dim.Fill() + Height = Dim.Fill () }; @@ -78,13 +257,13 @@ public override void Main () }; cbDar.ValueChanging += (s, e) => - { - if (e.NewValue < 0 || e.NewValue > 20) - { - e.Cancel = true; - } - }; - _win.Add (cbDar); + { + if (e.NewValue < 0 || e.NewValue > 20) + { + e.Cancel = true; + } + }; + w.Add (cbDar); int lastSendTime = Environment.TickCount; object lockObj = new object (); @@ -116,35 +295,32 @@ public override void Main () { Y = Pos.Bottom (cbDar), X = Pos.Right (tv), - Width = Dim.Fill(), - Height = Dim.Fill(1) + Width = Dim.Fill (), + Height = Dim.Fill (1) }; _lblSummary = new Label () { Y = Pos.Bottom (_graphView), X = Pos.Right (tv), - Width = Dim.Fill() + Width = Dim.Fill () }; SetupGraph (); - _win.Add (lbl); - _win.Add (lblDar); - _win.Add (cbDar); - _win.Add (tv); - _win.Add (_graphView); - _win.Add (_lblSummary); + w.Add (lbl); + w.Add (lblDar); + w.Add (cbDar); + w.Add (tv); + w.Add (_graphView); + w.Add (_lblSummary); - Application.Run (_win); - _win.Dispose (); - Application.Shutdown (); + return w; } - private void UpdateResponses () { _lblSummary.Text = GetSummary (); - _lblSummary.SetNeedsDisplay(); + _lblSummary.SetNeedsDisplay (); } private string GetSummary () @@ -157,8 +333,8 @@ private string GetSummary () var last = answers.Last ().Value; var unique = answers.Values.Distinct ().Count (); - var total = answers.Count; - + var total = answers.Count; + return $"Last:{last} U:{unique} T:{total}"; } @@ -193,7 +369,7 @@ private void UpdateGraph () .Select (g => new PointF (g.Key, g.Count ())) .ToList (); // _graphView.ScrollOffset = new PointF(,0); - _graphView.SetNeedsDisplay(); + _graphView.SetNeedsDisplay (); } @@ -207,7 +383,7 @@ private void SendDar () Application.Driver.QueueAnsiRequest ( new () { - Request = EscSeqUtils.CSI_SendDeviceAttributes, + Request = EscSeqUtils.CSI_SendDeviceAttributes.Request, Terminator = EscSeqUtils.CSI_ReportDeviceAttributes_Terminator, ResponseReceived = HandleResponse }); @@ -218,7 +394,7 @@ private void HandleResponse (string response) { lock (lockAnswers) { - answers.Add (DateTime.Now,response); + answers.Add (DateTime.Now, response); } } -} \ No newline at end of file +} From b104b1bf38439958ba2fd3d42dc2a435cfb66f5e Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 08:52:17 +0000 Subject: [PATCH 71/77] Evict stale requests even just when running schedule --- .../AnsiResponseParser/AnsiRequestScheduler.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 24345e1c62..695d0e92e8 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -103,6 +103,16 @@ private bool SendOrSchedule (AnsiEscapeSequenceRequest request,bool addToQueue) return false; } + private void EvictStaleRequests () + { + foreach (var stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key)) + { + EvictStaleRequests (stale); + } + } + + private bool IsStale (DateTime dt) => Now () - dt > _staleTimeout; + /// /// Looks to see if the last time we sent /// is a long time ago. If so we assume that we will never get a response and @@ -114,7 +124,7 @@ private bool EvictStaleRequests (string withTerminator) { if (_lastSend.TryGetValue (withTerminator, out DateTime dt)) { - if (Now () - dt > _staleTimeout) + if (IsStale (dt)) { _parser.StopExpecting (withTerminator, false); @@ -157,9 +167,12 @@ public bool RunSchedule (bool force = false) } } + EvictStaleRequests (); + return false; } + private void Send (AnsiEscapeSequenceRequest r) { _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ()); From 27f131713fed312f4e8496bb5712324d87ce7c36 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 10:37:30 +0000 Subject: [PATCH 72/77] Sixel detection and added Abandoned event --- .../AnsiEscapeSequenceRequest.cs | 5 + .../AnsiRequestScheduler.cs | 2 +- .../AnsiResponseExpectation.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 22 ++- .../AnsiResponseParser/IAnsiResponseParser.cs | 5 +- .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 3 - Terminal.Gui/Drawing/AssumeSupportDetector.cs | 20 -- Terminal.Gui/Drawing/ISixelSupportDetector.cs | 15 -- Terminal.Gui/Drawing/SixelSupportDetector.cs | 180 ++++++++++-------- UICatalog/Scenarios/Images.cs | 53 ++++-- .../AnsiRequestSchedulerTests.cs | 10 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 27 +-- 12 files changed, 182 insertions(+), 162 deletions(-) delete mode 100644 Terminal.Gui/Drawing/AssumeSupportDetector.cs delete mode 100644 Terminal.Gui/Drawing/ISixelSupportDetector.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs index d73c8b9bcc..3d4c9ebc3e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -22,6 +22,11 @@ public class AnsiEscapeSequenceRequest /// public Action ResponseReceived; + /// + /// Invoked if the console fails to responds to the ANSI response code + /// + public Action? Abandoned; + /// /// /// The terminator that uniquely identifies the type of response as responded diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 695d0e92e8..12a2c270fb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -176,7 +176,7 @@ public bool RunSchedule (bool force = false) private void Send (AnsiEscapeSequenceRequest r) { _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ()); - _parser.ExpectResponse (r.Terminator, r.ResponseReceived, false); + _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false); r.Send (); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index e4c22d7582..00aa1a9512 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,7 +1,7 @@ #nullable enable namespace Terminal.Gui; -internal record AnsiResponseExpectation (string Terminator, Action Response) +internal record AnsiResponseExpectation (string Terminator, Action Response, Action? Abandoned) { public bool Matches (string cur) { return cur.EndsWith (Terminator); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 6e7b46f136..4e5c0f4b02 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -281,17 +281,17 @@ private bool MatchResponse (string cur, List collection } /// - public void ExpectResponse (string terminator, Action response, bool persistent) + public void ExpectResponse (string terminator, Action response,Action? abandoned, bool persistent) { lock (lockExpectedResponses) { if (persistent) { - persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()))); + persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned)); } else { - expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()))); + expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned)); } } } @@ -315,7 +315,13 @@ public void StopExpecting (string terminator, bool persistent) { if (persistent) { - persistentExpectations.RemoveAll (r => r.Matches (terminator)); + AnsiResponseExpectation [] removed = persistentExpectations.Where (r => r.Matches (terminator)).ToArray (); + + foreach (var toRemove in removed) + { + persistentExpectations.Remove (toRemove); + toRemove.Abandoned?.Invoke (); + } } else { @@ -325,6 +331,7 @@ public void StopExpecting (string terminator, bool persistent) { expectedResponses.Remove (r); lateResponses.Add (r); + r.Abandoned?.Invoke (); } } } @@ -373,18 +380,19 @@ public IEnumerable> Release () /// /// /// + /// /// - public void ExpectResponseT (string terminator, Action>> response, bool persistent) + public void ExpectResponseT (string terminator, Action>> response,Action? abandoned, bool persistent) { lock (lockExpectedResponses) { if (persistent) { - persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()))); + persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned)); } else { - expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()))); + expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned)); } } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs index b2a421c8a0..88bee36e94 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs @@ -24,17 +24,18 @@ public interface IAnsiResponseParser /// sent an ANSI request out). /// /// The terminator you expect to see on response. + /// Callback to invoke when the response is seen in console input. + /// /// /// if you want this to persist permanently /// and be raised for every event matching the . /// - /// Callback to invoke when the response is seen in console input. /// /// If trying to register a persistent request for a terminator /// that already has one. /// exists. /// - void ExpectResponse (string terminator, Action response, bool persistent); + void ExpectResponse (string terminator, Action response,Action? abandoned, bool persistent); /// /// Returns true if there is an existing expectation (i.e. we are waiting a response diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 0e97c37066..57460d6dba 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1368,8 +1368,6 @@ public enum DECSCUSR_Style /// public const string CSI_ReportDeviceAttributes_Terminator = "c"; - /* - TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768 /// /// CSI 16 t - Request sixel resolution (width and height in pixels) /// @@ -1379,7 +1377,6 @@ public enum DECSCUSR_Style /// CSI 14 t - Request window size in pixels (width x height) /// public static readonly AnsiEscapeSequenceRequest CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" }; - */ /// /// CSI 1 8 t | yes | yes | yes | report window size in chars diff --git a/Terminal.Gui/Drawing/AssumeSupportDetector.cs b/Terminal.Gui/Drawing/AssumeSupportDetector.cs deleted file mode 100644 index 46081714ac..0000000000 --- a/Terminal.Gui/Drawing/AssumeSupportDetector.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Implementation of that assumes best -/// case scenario (full support including transparency with 10x20 resolution). -/// -public class AssumeSupportDetector : ISixelSupportDetector -{ - /// - public SixelSupportResult Detect () - { - return new() - { - IsSupported = true, - MaxPaletteColors = 256, - Resolution = new (10, 20), - SupportsTransparency = true - }; - } -} diff --git a/Terminal.Gui/Drawing/ISixelSupportDetector.cs b/Terminal.Gui/Drawing/ISixelSupportDetector.cs deleted file mode 100644 index eb0bb9f120..0000000000 --- a/Terminal.Gui/Drawing/ISixelSupportDetector.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Interface for detecting sixel support. Either through -/// ansi requests to terminal or config file etc. -/// -public interface ISixelSupportDetector -{ - /// - /// Gets the supported sixel state e.g. by sending Ansi escape sequences - /// or from a config file etc. - /// - /// Description of sixel support. - public SixelSupportResult Detect (); -} diff --git a/Terminal.Gui/Drawing/SixelSupportDetector.cs b/Terminal.Gui/Drawing/SixelSupportDetector.cs index d6044ff483..ce97bce504 100644 --- a/Terminal.Gui/Drawing/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/SixelSupportDetector.cs @@ -1,12 +1,12 @@ using System.Text.RegularExpressions; namespace Terminal.Gui; -/* TODO : Depends on https://github.com/gui-cs/Terminal.Gui/pull/3768 + /// /// Uses Ansi escape sequences to detect whether sixel is supported /// by the terminal. /// -public class SixelSupportDetector : ISixelSupportDetector +public class SixelSupportDetector { /// /// Sends Ansi escape sequences to the console to determine whether @@ -15,102 +15,130 @@ public class SixelSupportDetector : ISixelSupportDetector /// /// Description of sixel support, may include assumptions where /// expected response codes are not returned by console. - public SixelSupportResult Detect () + public void Detect (Action resultCallback) { var result = new SixelSupportResult (); + result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency (); + IsSixelSupportedByDar (result, resultCallback); + } - result.IsSupported = IsSixelSupportedByDar (); - if (result.IsSupported) - { - if (TryGetResolutionDirectly (out var res)) - { - result.Resolution = res; - } - else if(TryComputeResolution(out res)) - { - result.Resolution = res; - } + private void TryGetResolutionDirectly (SixelSupportResult result, Action resultCallback) + { + // Expect something like: + //[6;20;10t + QueueRequest (EscSeqUtils.CSI_RequestSixelResolution, + (r) => + { + // Terminal supports directly responding with resolution + var match = Regex.Match (r, @"\[\d+;(\d+);(\d+)t$"); + + if (match.Success) + { + if (int.TryParse (match.Groups [1].Value, out var ry) && + int.TryParse (match.Groups [2].Value, out var rx)) + { + result.Resolution = new Size (rx, ry); + } + } + + // Finished + resultCallback.Invoke (result); + + }, + // Request failed, so try to compute instead + ()=>TryComputeResolution (result,resultCallback)); + } - result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency (); - } - return result; + private void TryComputeResolution (SixelSupportResult result, Action resultCallback) + { + string windowSize; + string sizeInChars; + + QueueRequest (EscSeqUtils.CSI_RequestWindowSizeInPixels, + (r1)=> + { + windowSize = r1; + + QueueRequest (EscSeqUtils.CSI_ReportTerminalSizeInChars, + (r2) => + { + sizeInChars = r2; + ComputeResolution (result,windowSize,sizeInChars); + resultCallback (result); + + }, abandoned: () => resultCallback (result)); + },abandoned: ()=>resultCallback(result)); } - - private bool TryGetResolutionDirectly (out Size resolution) + private void ComputeResolution (SixelSupportResult result, string windowSize, string sizeInChars) { - // Expect something like: - //[6;20;10t + // Fallback to window size in pixels and characters + // Example [4;600;1200t + var pixelMatch = Regex.Match (windowSize, @"\[\d+;(\d+);(\d+)t$"); - if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestSixelResolution, out var response)) - { - // Terminal supports directly responding with resolution - var match = Regex.Match (response.Response, @"\[\d+;(\d+);(\d+)t$"); + // Example [8;30;120t + var charMatch = Regex.Match (sizeInChars, @"\[\d+;(\d+);(\d+)t$"); - if (match.Success) + if (pixelMatch.Success && charMatch.Success) + { + // Extract pixel dimensions + if (int.TryParse (pixelMatch.Groups [1].Value, out var pixelHeight) + && int.TryParse (pixelMatch.Groups [2].Value, out var pixelWidth) + && + + // Extract character dimensions + int.TryParse (charMatch.Groups [1].Value, out var charHeight) + && int.TryParse (charMatch.Groups [2].Value, out var charWidth) + && charWidth != 0 + && charHeight != 0) // Avoid divide by zero { - if (int.TryParse (match.Groups [1].Value, out var ry) && - int.TryParse (match.Groups [2].Value, out var rx)) - { - resolution = new Size (rx, ry); + // Calculate the character cell size in pixels + var cellWidth = (int)Math.Round ((double)pixelWidth / charWidth); + var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight); - return true; - } + // Set the resolution based on the character cell size + result.Resolution = new Size (cellWidth, cellHeight); } } - - resolution = default; - return false; } + private void IsSixelSupportedByDar (SixelSupportResult result,Action resultCallback) + { + QueueRequest ( + EscSeqUtils.CSI_SendDeviceAttributes, + (r) => + { + result.IsSupported = ResponseIndicatesSupport (r); + + if (result.IsSupported) + { + TryGetResolutionDirectly (result, resultCallback); + } + else + { + resultCallback (result); + } + },abandoned: () => resultCallback(result)); + } - private bool TryComputeResolution (out Size resolution) + private void QueueRequest (AnsiEscapeSequenceRequest req, Action responseCallback, Action abandoned) { - // Fallback to window size in pixels and characters - if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestWindowSizeInPixels, out var pixelSizeResponse) - && AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_ReportTerminalSizeInChars, out var charSizeResponse)) + var newRequest = new AnsiEscapeSequenceRequest { - // Example [4;600;1200t - var pixelMatch = Regex.Match (pixelSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$"); + Request = req.Request, + Terminator = req.Terminator, + ResponseReceived = responseCallback, + Abandoned = abandoned + }; - // Example [8;30;120t - var charMatch = Regex.Match (charSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$"); - - if (pixelMatch.Success && charMatch.Success) - { - // Extract pixel dimensions - if (int.TryParse (pixelMatch.Groups [1].Value, out var pixelHeight) - && int.TryParse (pixelMatch.Groups [2].Value, out var pixelWidth) - && - - // Extract character dimensions - int.TryParse (charMatch.Groups [1].Value, out var charHeight) - && int.TryParse (charMatch.Groups [2].Value, out var charWidth) - && charWidth != 0 - && charHeight != 0) // Avoid divide by zero - { - // Calculate the character cell size in pixels - var cellWidth = (int)Math.Round ((double)pixelWidth / charWidth); - var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight); - - // Set the resolution based on the character cell size - resolution = new Size (cellWidth, cellHeight); - - return true; - } - } - } - - resolution = default; - return false; + Application.Driver.QueueAnsiRequest (newRequest); } - private bool IsSixelSupportedByDar () + + private bool ResponseIndicatesSupport (string response) { - return AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes, out AnsiEscapeSequenceResponse darResponse) - ? darResponse.Response.Split (';').Contains ("4") - : false; + return response.Split (';').Contains ("4"); } private bool IsWindowsTerminal () @@ -130,4 +158,4 @@ private bool IsXtermWithTransparency () return false; } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs index e1cc7cac25..78c7649b13 100644 --- a/UICatalog/Scenarios/Images.cs +++ b/UICatalog/Scenarios/Images.cs @@ -60,15 +60,15 @@ public class Images : Scenario private RadioGroup _rgDistanceAlgorithm; private NumericUpDown _popularityThreshold; private SixelToRender _sixelImage; - private SixelSupportResult _sixelSupportResult; + + // Start by assuming no support + private SixelSupportResult? _sixelSupportResult = new SixelSupportResult (); + private CheckBox _cbSupportsSixel; public override void Main () { - // TODO: Change to the one that uses Ansi Requests later - var sixelSupportDetector = new AssumeSupportDetector (); - _sixelSupportResult = sixelSupportDetector.Detect (); - Application.Init (); + _win = new () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; @@ -95,8 +95,8 @@ public override void Main () Text = "supports true color " }; _win.Add (cbSupportsTrueColor); - - var cbSupportsSixel = new CheckBox + + _cbSupportsSixel = new CheckBox { X = Pos.Right (lblDriverName) + 2, Y = 1, @@ -108,22 +108,22 @@ public override void Main () { X = Pos.Right (lblDriverName) + 2, - Y = Pos.Bottom (cbSupportsSixel), + Y = Pos.Bottom (_cbSupportsSixel), Text = "(Check if your terminal supports Sixel)" }; -/* CheckedState = _sixelSupportResult.IsSupported - ? CheckState.Checked - : CheckState.UnChecked;*/ - cbSupportsSixel.CheckedStateChanging += (s, e) => - { - _sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked; - SetupSixelSupported (e.NewValue == CheckState.Checked); - ApplyShowTabViewHack (); - }; + /* CheckedState = _sixelSupportResult.IsSupported + ? CheckState.Checked + : CheckState.UnChecked;*/ + _cbSupportsSixel.CheckedStateChanging += (s, e) => + { + _sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked; + SetupSixelSupported (e.NewValue == CheckState.Checked); + ApplyShowTabViewHack (); + }; - _win.Add (cbSupportsSixel); + _win.Add (_cbSupportsSixel); var cbUseTrueColor = new CheckBox { @@ -150,17 +150,32 @@ public override void Main () BuildBasicTab (tabBasic); BuildSixelTab (); - SetupSixelSupported (cbSupportsSixel.CheckedState == CheckState.Checked); + SetupSixelSupported (_cbSupportsSixel.CheckedState == CheckState.Checked); btnOpenImage.Accepting += OpenImage; _win.Add (lblSupportsSixel); _win.Add (_tabView); + + // Start trying to detect sixel support + var sixelSupportDetector = new SixelSupportDetector (); + sixelSupportDetector.Detect (UpdateSixelSupportState); + Application.Run (_win); _win.Dispose (); Application.Shutdown (); } + private void UpdateSixelSupportState (SixelSupportResult newResult) + { + _sixelSupportResult = newResult; + + _cbSupportsSixel.CheckedState = newResult.IsSupported ? CheckState.Checked : CheckState.UnChecked; + _pxX.Value = _sixelSupportResult.Resolution.Width; + _pxY.Value = _sixelSupportResult.Resolution.Height; + + } + private void SetupSixelSupported (bool isSupported) { _tabSixel.View = isSupported ? _sixelSupported : _sixelNotSupported; diff --git a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs index 9d29f9fda8..3dcfaeedd7 100644 --- a/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs @@ -32,7 +32,7 @@ public void SendOrSchedule_SendsDeviceAttributeRequest_WhenNoOutstandingRequests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable(Times.Once); // then we should execute our request - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Once); // Act bool result = _scheduler.SendOrSchedule (request); @@ -80,7 +80,7 @@ public void RunSchedule_ThrottleNotExceeded_AllowSend () // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable(Times.Exactly (2)); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); _scheduler.SendOrSchedule (request); @@ -112,7 +112,7 @@ public void RunSchedule_ThrottleExceeded_QueueRequest () // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); _scheduler.SendOrSchedule (request); @@ -158,7 +158,7 @@ public void EvictStaleRequests_RemovesStaleRequest_AfterTimeout () // Send _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); Assert.True (_scheduler.SendOrSchedule (request1)); @@ -213,7 +213,7 @@ public void SendOrSchedule_ManagesIndependentTerminatorsCorrectly () // 'x' is free _parserMock.Setup (p => p.IsExpecting ("x")).Returns (false).Verifiable (Times.Once); - _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> (), false)).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> (), null, false)).Verifiable (Times.Once); // Act var a = _scheduler.SendOrSchedule (request1); diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index a3217cf291..11c5f252a6 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -30,8 +30,8 @@ public void TestInputProcessing () int i = 0; // Imagine that we are expecting a DAR - _parser1.ExpectResponse ("c",(s)=> response1 = s, false); - _parser2.ExpectResponse ("c", (s) => response2 = s , false); + _parser1.ExpectResponse ("c",(s)=> response1 = s,null, false); + _parser2.ExpectResponse ("c", (s) => response2 = s , null, false); // First char is Escape which we must consume incase what follows is the DAR AssertConsumed (ansiStream, ref i); // Esc @@ -121,8 +121,8 @@ public void TestInputSequences (string ansiStream, string expectedTerminator, st string response2 = string.Empty; // Register the expected response with the given terminator - _parser1.ExpectResponse (expectedTerminator, s => response1 = s, false); - _parser2.ExpectResponse (expectedTerminator, s => response2 = s, false); + _parser1.ExpectResponse (expectedTerminator, s => response1 = s, null, false); + _parser2.ExpectResponse (expectedTerminator, s => response2 = s, null, false); // Process the input StringBuilder actualOutput1 = new StringBuilder (); @@ -228,7 +228,7 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera if (terminator.HasValue) { - parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s, false); + parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s,null, false); } foreach (var state in expectedStates) { @@ -329,13 +329,13 @@ public void TestLateResponses () string? responseA = null; string? responseB = null; - p.ExpectResponse ("z",(r)=>responseA=r, false); + p.ExpectResponse ("z",(r)=>responseA=r, null, false); // Some time goes by without us seeing a response p.StopExpecting ("z", false); // Send our new request - p.ExpectResponse ("z", (r) => responseB = r, false); + p.ExpectResponse ("z", (r) => responseB = r, null, 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")); @@ -362,8 +362,8 @@ public void TestPersistentResponses () int m = 0; int M = 1; - p.ExpectResponse ("m", _ => m++, true); - p.ExpectResponse ("M", _ => M++, true); + p.ExpectResponse ("m", _ => m++, null, true); + p.ExpectResponse ("M", _ => M++, null, true); // Act - Feed input strings containing ANSI sequences p.ProcessInput ("\u001b[<0;10;10m"); // Should match and increment `m` @@ -387,10 +387,11 @@ public void TestPersistentResponses_WithMetadata () var result = new List> (); p.ExpectResponseT ("m", (r) => - { - result = r.ToList (); - m++; - }, true); + { + result = r.ToList (); + m++; + }, + null, true); // Act - Feed input strings containing ANSI sequences p.ProcessInput (StringToBatch("\u001b[<0;10;10m")); // Should match and increment `m` From 5340073ba1ed0e2de59d3cb5d7374d3a4a54d9b1 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 15:30:45 +0000 Subject: [PATCH 73/77] Get rid of StoppedExpecting event and made methods internal again in favor of Abandoned instead --- .../AnsiResponseParser/AnsiResponseParser.cs | 5 ----- .../AnsiResponseParser/IAnsiResponseParser.cs | 5 ----- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 2 +- UICatalog/Scenarios/AnsiRequestsScenario.cs | 9 ++++----- 8 files changed, 9 insertions(+), 20 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 4e5c0f4b02..027dba0552 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -26,9 +26,6 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser private AnsiResponseParserState _state = AnsiResponseParserState.Normal; - /// - public event EventHandler>? StoppedExpecting; - /// public AnsiResponseParserState State { @@ -309,8 +306,6 @@ public bool IsExpecting (string terminator) /// public void StopExpecting (string terminator, bool persistent) { - StoppedExpecting?.Invoke (this, new (terminator)); - lock (lockExpectedResponses) { if (persistent) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs index 88bee36e94..bc70142276 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs @@ -7,11 +7,6 @@ namespace Terminal.Gui; /// public interface IAnsiResponseParser { - /// - /// Called when parser is told to a response for a given request - /// - public event EventHandler> StoppedExpecting; - /// /// Current state of the parser based on what sequence of characters it has /// read from the console input stream. diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 7c1136b9a8..a6c94e4786 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -627,7 +627,7 @@ public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) GetRequestScheduler ().SendOrSchedule (request); } - public abstract IAnsiResponseParser GetParser (); + internal abstract IAnsiResponseParser GetParser (); internal AnsiRequestScheduler GetRequestScheduler () { diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 36f8f392a3..7be9c79d13 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -592,7 +592,7 @@ internal override MainLoop Init () private readonly AnsiResponseParser _parser = new (); /// - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; internal void ProcessInput () { diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index e01e9f0490..bb115b9fb8 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -395,7 +395,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al private AnsiResponseParser _parser = new (); /// - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; /// internal override void RawWrite (string str) { } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 6bac0ed116..3d16ddd594 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -994,7 +994,7 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out } /// - public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; /// internal override void RawWrite (string str) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index e5f3db0599..8782667eac 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1180,7 +1180,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } /// - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; /// internal override void RawWrite (string str) => WinConsole?.WriteANSI (str); diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index d3a0d166df..4898a9077d 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -155,8 +155,6 @@ private View BuildSingleTab () var lblSuccess = new Label { X = Pos.Center (), Y = Pos.Bottom (btnResponse) + 1 }; w.Add (lblSuccess); - Application.Driver.GetParser ().StoppedExpecting += (s,e)=>OnFail(e.CurrentValue,tvResponse,tvError,tvValue,tvTerminator, lblSuccess); - btnResponse.Accepting += (s, e) => { var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest @@ -171,7 +169,8 @@ private View BuildSingleTab () { Request = ansiEscapeSequenceRequest.Request, Terminator = ansiEscapeSequenceRequest.Terminator, - ResponseReceived = (s)=>OnSuccess(s, tvResponse, tvError, tvValue, tvTerminator,lblSuccess) + ResponseReceived = (s)=>OnSuccess(s, tvResponse, tvError, tvValue, tvTerminator,lblSuccess), + Abandoned =()=> OnFail (tvResponse, tvError, tvValue, tvTerminator, lblSuccess) }); }; @@ -193,12 +192,12 @@ private void OnSuccess (string response, TextView tvResponse, TextView tvError, lblSuccess.Text = "Successful"; } - private void OnFail (string terminator, TextView tvResponse, TextView tvError, TextView tvValue, TextView tvTerminator, Label lblSuccess) + private void OnFail (TextView tvResponse, TextView tvError, TextView tvValue, TextView tvTerminator, Label lblSuccess) { tvResponse.Text = string.Empty; tvError.Text = "No Response"; tvValue.Text = string.Empty; - tvTerminator.Text = terminator; + tvTerminator.Text = string.Empty; lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"]; lblSuccess.Text = "Error"; From 5cbb2306ef2708449427dd1757f3a536da57620c Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 18:31:53 +0000 Subject: [PATCH 74/77] Fix to have CanFocus true --- UICatalog/Scenarios/AnsiRequestsScenario.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index 4898a9077d..7c5051a2a1 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -31,7 +31,8 @@ public override void Main () TabView tv = new TabView { Width = Dim.Fill (), - Height = Dim.Fill () + Height = Dim.Fill (), + CanFocus = true }; Tab single = new Tab (); @@ -68,7 +69,8 @@ private View BuildSingleTab () View w = new View () { Width = Dim.Fill (), - Height = Dim.Fill () + Height = Dim.Fill (), + CanFocus = true }; w.Padding.Thickness = new (1); From fa7fb656cb592248b26c81f63539cb07de27a7eb Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 18:34:05 +0000 Subject: [PATCH 75/77] Revert changes to launchSettings.json --- UICatalog/Properties/launchSettings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index f60efb5709..041a5c22dc 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -1,8 +1,7 @@ { "profiles": { "UICatalog": { - "commandName": "Project", - "commandLineArgs": "-d NetDriver" + "commandName": "Project" }, "UICatalog --driver NetDriver": { "commandName": "Project", From 4d232e4cf9a16d24c78901702b79fc5957fa2456 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Nov 2024 21:25:39 +0000 Subject: [PATCH 76/77] Add CanFocus true --- UICatalog/Scenarios/AnsiRequestsScenario.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/AnsiRequestsScenario.cs b/UICatalog/Scenarios/AnsiRequestsScenario.cs index 7c5051a2a1..5175a6c52e 100644 --- a/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -210,7 +210,8 @@ private View BuildBulkTab () View w = new View () { Width = Dim.Fill (), - Height = Dim.Fill () + Height = Dim.Fill (), + CanFocus = true }; var lbl = new Label () From 9d86336bff3f75214eb77239e8e6c7af61c0c63e Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 3 Nov 2024 08:55:16 +0000 Subject: [PATCH 77/77] tidyup --- UICatalog/Scenarios/Images.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs index 78c7649b13..85e6e495ca 100644 --- a/UICatalog/Scenarios/Images.cs +++ b/UICatalog/Scenarios/Images.cs @@ -62,7 +62,7 @@ public class Images : Scenario private SixelToRender _sixelImage; // Start by assuming no support - private SixelSupportResult? _sixelSupportResult = new SixelSupportResult (); + private SixelSupportResult _sixelSupportResult = new SixelSupportResult (); private CheckBox _cbSupportsSixel; public override void Main () diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 11c5f252a6..d3019e8d42 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -1,7 +1,4 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Text; using Xunit.Abstractions; @@ -24,8 +21,8 @@ public void TestInputProcessing () "\u001b[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR) - string? response1 = null; - string? response2 = null; + string response1 = null; + string response2 = null; int i = 0; @@ -224,7 +221,7 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera output.WriteLine ("Running test case:" + caseName); var parser = new AnsiResponseParser (); - string? response = null; + string response = null; if (terminator.HasValue) { @@ -326,8 +323,8 @@ public void TestLateResponses () { var p = new AnsiResponseParser (); - string? responseA = null; - string? responseB = null; + string responseA = null; + string responseB = null; p.ExpectResponse ("z",(r)=>responseA=r, null, false);