Browse Source

Merge pull request #3791 from tznind/ansi-parser

Fixes #3767 - Adds Ansi parser and scheduler.
Tig 7 months ago
parent
commit
62641c8f26
58 changed files with 2838 additions and 593 deletions
  1. 7 0
      Terminal.Gui/Application/MainLoop.cs
  2. 49 0
      Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence.cs
  3. 30 0
      Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs
  4. 216 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
  5. 7 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs
  6. 449 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
  7. 24 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs
  8. 19 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
  9. 54 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
  10. 33 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
  11. 21 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/ReasonCannotSend.cs
  12. 18 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
  13. 33 7
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  14. 36 7
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  15. 1 0
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  16. 10 14
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
  17. 10 2
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  18. 55 26
      Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
  19. 30 27
      Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
  20. 175 255
      Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
  21. 1 4
      Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
  22. 80 59
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
  23. 5 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
  24. 0 20
      Terminal.Gui/Drawing/AssumeSupportDetector.cs
  25. 0 0
      Terminal.Gui/Drawing/Color/AnsiColorCode.cs
  26. 0 0
      Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs
  27. 0 0
      Terminal.Gui/Drawing/Color/Color.ColorName.cs
  28. 0 0
      Terminal.Gui/Drawing/Color/Color.ColorParseException.cs
  29. 0 0
      Terminal.Gui/Drawing/Color/Color.Formatting.cs
  30. 0 0
      Terminal.Gui/Drawing/Color/Color.Operators.cs
  31. 0 0
      Terminal.Gui/Drawing/Color/Color.cs
  32. 0 0
      Terminal.Gui/Drawing/Color/ColorEventArgs.cs
  33. 0 0
      Terminal.Gui/Drawing/Color/ColorModel.cs
  34. 0 0
      Terminal.Gui/Drawing/Color/ColorQuantizer.cs
  35. 0 0
      Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs
  36. 0 0
      Terminal.Gui/Drawing/Color/ColorScheme.cs
  37. 0 0
      Terminal.Gui/Drawing/Color/ColorStrings.cs
  38. 0 0
      Terminal.Gui/Drawing/Color/IColorDistance.cs
  39. 0 0
      Terminal.Gui/Drawing/Color/IColorNameResolver.cs
  40. 0 0
      Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs
  41. 0 0
      Terminal.Gui/Drawing/Color/W3CColors.cs
  42. 0 15
      Terminal.Gui/Drawing/ISixelSupportDetector.cs
  43. 0 0
      Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs
  44. 0 0
      Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs
  45. 0 0
      Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs
  46. 0 0
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  47. 0 0
      Terminal.Gui/Drawing/LineCanvas/LineStyle.cs
  48. 0 0
      Terminal.Gui/Drawing/LineCanvas/StraightLine.cs
  49. 0 0
      Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs
  50. 4 4
      Terminal.Gui/Drawing/Sixel/SixelEncoder.cs
  51. 165 0
      Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs
  52. 1 1
      Terminal.Gui/Drawing/Sixel/SixelSupportResult.cs
  53. 0 0
      Terminal.Gui/Drawing/Sixel/SixelToRender.cs
  54. 0 133
      Terminal.Gui/Drawing/SixelSupportDetector.cs
  55. 400 0
      UICatalog/Scenarios/AnsiRequestsScenario.cs
  56. 32 19
      UICatalog/Scenarios/Images.cs
  57. 236 0
      UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs
  58. 637 0
      UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

+ 7 - 0
Terminal.Gui/Application/MainLoop.cs

@@ -268,6 +268,8 @@ public class MainLoop : IDisposable
             }
         }
 
+        RunAnsiScheduler ();
+
         MainLoopDriver?.Iteration ();
 
         bool runIdle;
@@ -283,6 +285,11 @@ public class MainLoop : IDisposable
         }
     }
 
+    private void RunAnsiScheduler ()
+    {
+        Application.Driver?.GetRequestScheduler ().RunSchedule ();
+    }
+
     /// <summary>Stops the main loop driver and calls <see cref="IMainLoopDriver.Wakeup"/>. Used only for unit tests.</summary>
     internal void Stop ()
     {

+ 49 - 0
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence.cs

@@ -0,0 +1,49 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+/// Describes an Ansi escape sequence. This is a 'blueprint'. If you
+/// want to send the sequence you should instead use <see cref="AnsiEscapeSequenceRequest"/>
+/// </summary>
+public class AnsiEscapeSequence
+{
+    /// <summary>
+    ///     Request to send e.g. see
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///     </see>
+    /// </summary>
+    public required string Request { get; init; }
+
+    /// <summary>
+    ///     <para>
+    ///         The terminator that uniquely identifies the type of response as responded
+    ///         by the console. e.g. for
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
+    ///         </see>
+    ///         the terminator is
+    ///         <see>
+    ///             <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
+    ///         </see>
+    ///         .
+    ///     </para>
+    ///     <para>
+    ///         After sending a request, the first response with matching terminator will be matched
+    ///         to the oldest outstanding request.
+    ///     </para>
+    /// </summary>
+    public required string Terminator { get; init; }
+
+
+
+    /// <summary>
+    ///     The value expected in the response e.g.
+    ///     <see>
+    ///         <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
+    ///     </see>
+    ///     which will have a 't' as terminator but also other different request may return the same terminator with a
+    ///     different value.
+    /// </summary>
+    public string? Value { get; init; }
+}

+ 30 - 0
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs

@@ -0,0 +1,30 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes an ongoing ANSI request sent to the console.
+///     Use <see cref="ResponseReceived"/> to handle the response
+///     when console answers the request.
+/// </summary>
+public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
+{
+    /// <summary>
+    ///     Invoked when the console responds with an ANSI response code that matches the
+    ///     <see cref="AnsiEscapeSequence.Terminator"/>
+    /// </summary>
+    public required Action<string> ResponseReceived { get; init; }
+
+    /// <summary>
+    ///     Invoked if the console fails to responds to the ANSI response code
+    /// </summary>
+    public Action? Abandoned { get; init; }
+
+
+    /// <summary>
+    ///     Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
+    ///     Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
+    ///     sending many requests.
+    /// </summary>
+    public void Send () { Application.Driver?.WriteRaw (Request); }
+
+}

+ 216 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs

@@ -0,0 +1,216 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Manages <see cref="AnsiEscapeSequenceRequest"/> made to an <see cref="IAnsiResponseParser"/>.
+///     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).
+/// </summary>
+public class AnsiRequestScheduler
+{
+    private readonly IAnsiResponseParser _parser;
+
+    /// <summary>
+    ///     Function for returning the current time. Use in unit tests to
+    ///     ensure repeatable tests.
+    /// </summary>
+    internal Func<DateTime> Now { get; set; }
+
+    private readonly HashSet<Tuple<AnsiEscapeSequenceRequest, DateTime>> _queuedRequests = new ();
+
+    internal IReadOnlyCollection<AnsiEscapeSequenceRequest> QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList ();
+
+    /// <summary>
+    ///     <para>
+    ///         Dictionary where key is ansi request terminator and value is when we last sent a request for
+    ///         this terminator. Combined with <see cref="_throttle"/> 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.
+    ///     </para>
+    ///     <para>
+    ///         When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain
+    ///         queued).
+    ///     </para>
+    /// </summary>
+    private readonly ConcurrentDictionary<string, DateTime> _lastSend = new ();
+
+    /// <summary>
+    ///     Number of milliseconds after sending a request that we allow
+    ///     another request to go out.
+    /// </summary>
+    private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
+
+    private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1);
+
+    private readonly DateTime _lastRun;
+
+    /// <summary>
+    ///     Creates a new instance.
+    /// </summary>
+    /// <param name="parser"></param>
+    /// <param name="now"></param>
+    public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null)
+    {
+        _parser = parser;
+        Now = now ?? (() => DateTime.Now);
+        _lastRun = Now ();
+    }
+
+    /// <summary>
+    ///     Sends the <paramref name="request"/> immediately or queues it if there is already
+    ///     an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
+    /// </summary>
+    /// <param name="request"></param>
+    /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
+    public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { return SendOrSchedule (request, true); }
+
+    private bool SendOrSchedule (AnsiEscapeSequenceRequest request, bool addToQueue)
+    {
+        if (CanSend (request, out ReasonCannotSend reason))
+        {
+            Send (request);
+
+            return true;
+        }
+
+        if (reason == ReasonCannotSend.OutstandingRequest)
+        {
+            // If we can evict an old request (no response from terminal after ages)
+            if (EvictStaleRequests (request.Terminator))
+            {
+                // Try again after evicting
+                if (CanSend (request, out _))
+                {
+                    Send (request);
+
+                    return true;
+                }
+            }
+        }
+
+        if (addToQueue)
+        {
+            _queuedRequests.Add (Tuple.Create (request, Now ()));
+        }
+
+        return false;
+    }
+
+    private void EvictStaleRequests ()
+    {
+        foreach (string stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
+        {
+            EvictStaleRequests (stale);
+        }
+    }
+
+    private bool IsStale (DateTime dt) { return Now () - dt > _staleTimeout; }
+
+    /// <summary>
+    ///     Looks to see if the last time we sent <paramref name="withTerminator"/>
+    ///     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 <see langword="true"/>).
+    /// </summary>
+    /// <param name="withTerminator"></param>
+    /// <returns></returns>
+    private bool EvictStaleRequests (string withTerminator)
+    {
+        if (_lastSend.TryGetValue (withTerminator, out DateTime dt))
+        {
+            if (IsStale (dt))
+            {
+                _parser.StopExpecting (withTerminator, false);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the
+    ///     current outstanding requests of the parser.
+    /// </summary>
+    /// <param name="force">
+    ///     Repeated requests to run the schedule over short period of time will be ignored.
+    ///     Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.
+    /// </param>
+    /// <returns>
+    ///     <see langword="true"/> if a request was found and run. <see langword="false"/>
+    ///     if no outstanding requests or all have existing outstanding requests underway in parser.
+    /// </returns>
+    public bool RunSchedule (bool force = false)
+    {
+        if (!force && Now () - _lastRun < _runScheduleThrottle)
+        {
+            return false;
+        }
+
+        // Get oldest request
+        Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.MinBy (r => r.Item2);
+
+        if (opportunity != null)
+        {
+            // Give it another go
+            if (SendOrSchedule (opportunity.Item1, false))
+            {
+                _queuedRequests.Remove (opportunity);
+
+                return true;
+            }
+        }
+
+        EvictStaleRequests ();
+
+        return false;
+    }
+
+    private void Send (AnsiEscapeSequenceRequest r)
+    {
+        _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ());
+        _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false);
+        r.Send ();
+    }
+
+    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 (ReasonCannotSend);
+
+        return true;
+    }
+
+    private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
+    {
+        if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
+        {
+            return Now () - value < _throttle;
+        }
+
+        return false;
+    }
+}

+ 7 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs

@@ -0,0 +1,7 @@
+#nullable enable
+namespace Terminal.Gui;
+
+internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned)
+{
+    public bool Matches (string cur) { return cur.EndsWith (Terminator); }
+}

+ 449 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs

@@ -0,0 +1,449 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+internal abstract class AnsiResponseParserBase : IAnsiResponseParser
+{
+    protected object _lockExpectedResponses = new ();
+
+    protected object _lockState = new ();
+
+    /// <summary>
+    ///     Responses we are expecting to come in.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _expectedResponses = [];
+
+    /// <summary>
+    ///     Collection of responses that we <see cref="StopExpecting"/>.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _lateResponses = [];
+
+    /// <summary>
+    ///     Responses that you want to look out for that will come in continuously e.g. mouse events.
+    ///     Key is the terminator.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _persistentExpectations = [];
+
+    private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
+
+    /// <inheritdoc/>
+    public AnsiResponseParserState State
+    {
+        get => _state;
+        protected set
+        {
+            StateChangedAt = DateTime.Now;
+            _state = value;
+        }
+    }
+
+    protected readonly IHeld _heldContent;
+
+    /// <summary>
+    ///     When <see cref="State"/> was last changed.
+    /// </summary>
+    public DateTime StateChangedAt { get; private set; } = DateTime.Now;
+
+    // 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<char> _knownTerminators = 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) { _heldContent = heldContent; }
+
+    protected void ResetState ()
+    {
+        State = AnsiResponseParserState.Normal;
+
+        lock (_lockState)
+        {
+            _heldContent.ClearHeld ();
+        }
+    }
+
+    /// <summary>
+    ///     Processes an input collection of objects <paramref name="inputLength"/> long.
+    ///     You must provide the indexers to return the objects and the action to append
+    ///     to output stream.
+    /// </summary>
+    /// <param name="getCharAtIndex">The character representation of element i of your input collection</param>
+    /// <param name="getObjectAtIndex">The actual element in the collection (e.g. char or Tuple&lt;char,T&gt;)</param>
+    /// <param name="appendOutput">
+    ///     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).
+    /// </param>
+    /// <param name="inputLength">The total number of elements in your collection</param>
+    protected void ProcessInputBase (
+        Func<int, char> getCharAtIndex,
+        Func<int, object> getObjectAtIndex,
+        Action<object> appendOutput,
+        int inputLength
+    )
+    {
+        lock (_lockState)
+        {
+            ProcessInputBaseImpl (getCharAtIndex, getObjectAtIndex, appendOutput, inputLength);
+        }
+    }
+
+    private void ProcessInputBaseImpl (
+        Func<int, char> getCharAtIndex,
+        Func<int, object> getObjectAtIndex,
+        Action<object> appendOutput,
+        int inputLength
+    )
+    {
+        var index = 0; // Tracks position in the input string
+
+        while (index < inputLength)
+        {
+            char currentChar = getCharAtIndex (index);
+            object currentObj = getObjectAtIndex (index);
+
+            bool isEscape = currentChar == '\x1B';
+
+            switch (State)
+            {
+                case AnsiResponseParserState.Normal:
+                    if (isEscape)
+                    {
+                        // Escape character detected, move to ExpectingBracket state
+                        State = AnsiResponseParserState.ExpectingBracket;
+                        _heldContent.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);
+                        _heldContent.AddToHeld (currentObj); // Hold the new escape
+                    }
+                    else if (currentChar == '[')
+                    {
+                        // Detected '[', transition to InResponse state
+                        State = AnsiResponseParserState.InResponse;
+                        _heldContent.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:
+                    _heldContent.AddToHeld (currentObj);
+
+                    // Check if the held content should be released
+                    if (ShouldReleaseHeldContent ())
+                    {
+                        ReleaseHeld (appendOutput);
+                    }
+
+                    break;
+            }
+
+            index++;
+        }
+    }
+
+    private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
+    {
+        foreach (object o in _heldContent.HeldToObjects ())
+        {
+            appendOutput (o);
+        }
+
+        State = newState;
+        _heldContent.ClearHeld ();
+    }
+
+    // Common response handler logic
+    protected bool ShouldReleaseHeldContent ()
+    {
+        lock (_lockState)
+        {
+            string cur = _heldContent.HeldToString ();
+
+            lock (_lockExpectedResponses)
+            {
+                // Look for an expected response for what is accumulated so far (since Esc)
+                if (MatchResponse (
+                                   cur,
+                                   _expectedResponses,
+                                   true,
+                                   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,
+                                   true))
+                {
+                    return false;
+                }
+
+                // Look for persistent requests
+                if (MatchResponse (
+                                   cur,
+                                   _persistentExpectations,
+                                   true,
+                                   false))
+                {
+                    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))
+            {
+                // We have found a terminator so bail
+                State = AnsiResponseParserState.Normal;
+
+                // Maybe swallow anyway if user has custom delegate
+                bool 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
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         When overriden in a derived class, indicates whether the unexpected response
+    ///         currently in <see cref="_heldContent"/> should be released or swallowed.
+    ///         Use this to enable default event for escape codes.
+    ///     </para>
+    ///     <remarks>
+    ///         Note this is only called for complete responses.
+    ///         Based on <see cref="_knownTerminators"/>
+    ///     </remarks>
+    /// </summary>
+    /// <returns></returns>
+    protected abstract bool ShouldSwallowUnexpectedResponse ();
+
+    private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
+    {
+        // Check for expected responses
+        AnsiResponseExpectation? matchingResponse = collection.FirstOrDefault (r => r.Matches (cur));
+
+        if (matchingResponse?.Response != null)
+        {
+            if (invokeCallback)
+            {
+                matchingResponse.Response.Invoke (_heldContent);
+            }
+
+            ResetState ();
+
+            if (removeExpectation)
+            {
+                collection.Remove (matchingResponse);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public void ExpectResponse (string terminator, Action<string> response, Action? abandoned, bool persistent)
+    {
+        lock (_lockExpectedResponses)
+        {
+            if (persistent)
+            {
+                _persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
+            }
+            else
+            {
+                _expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public bool IsExpecting (string terminator)
+    {
+        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 ());
+        }
+    }
+
+    /// <inheritdoc/>
+    public void StopExpecting (string terminator, bool persistent)
+    {
+        lock (_lockExpectedResponses)
+        {
+            if (persistent)
+            {
+                AnsiResponseExpectation [] removed = _persistentExpectations.Where (r => r.Matches (terminator)).ToArray ();
+
+                foreach (AnsiResponseExpectation toRemove in removed)
+                {
+                    _persistentExpectations.Remove (toRemove);
+                    toRemove.Abandoned?.Invoke ();
+                }
+            }
+            else
+            {
+                AnsiResponseExpectation [] removed = _expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
+
+                foreach (AnsiResponseExpectation r in removed)
+                {
+                    _expectedResponses.Remove (r);
+                    _lateResponses.Add (r);
+                    r.Abandoned?.Invoke ();
+                }
+            }
+        }
+    }
+}
+
+internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHeld<T> ())
+{
+    /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
+    public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
+
+    public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
+    {
+        List<Tuple<char, T>> output = new ();
+
+        ProcessInputBase (
+                          i => input [i].Item1,
+                          i => input [i],
+                          c => output.Add ((Tuple<char, T>)c),
+                          input.Length);
+
+        return output;
+    }
+
+    public Tuple<char, T> [] Release ()
+    {
+        // Lock in case Release is called from different Thread from parse
+        lock (_lockState)
+        {
+            Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
+
+            ResetState ();
+
+            return result;
+        }
+    }
+
+    private IEnumerable<Tuple<char, T>> HeldToEnumerable () { return (IEnumerable<Tuple<char, T>>)_heldContent.HeldToObjects (); }
+
+    /// <summary>
+    ///     'Overload' for specifying an expectation that requires the metadata as well as characters. Has
+    ///     a unique name because otherwise most lamdas will give ambiguous overload errors.
+    /// </summary>
+    /// <param name="terminator"></param>
+    /// <param name="response"></param>
+    /// <param name="abandoned"></param>
+    /// <param name="persistent"></param>
+    public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
+    {
+        lock (_lockExpectedResponses)
+        {
+            if (persistent)
+            {
+                _persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
+            }
+            else
+            {
+                _expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); }
+}
+
+internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ())
+{
+    /// <summary>
+    ///     <para>
+    ///         Delegate for handling unrecognized escape codes. Default behaviour
+    ///         is to return <see langword="false"/> which simply releases the
+    ///         characters back to input stream for downstream processing.
+    ///     </para>
+    ///     <para>
+    ///         Implement a method to handle if you want and return <see langword="true"/> if you want the
+    ///         keystrokes 'swallowed' (i.e. not returned to input stream).
+    ///     </para>
+    /// </summary>
+    public Func<string, bool> UnknownResponseHandler { get; set; } = _ => false;
+
+    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 ()
+    {
+        lock (_lockState)
+        {
+            string output = _heldContent.HeldToString ();
+            ResetState ();
+
+            return output;
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override bool ShouldSwallowUnexpectedResponse ()
+    {
+        lock (_lockState)
+        {
+            return UnknownResponseHandler.Invoke (_heldContent.HeldToString ());
+        }
+    }
+}

+ 24 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs

@@ -0,0 +1,24 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes the current state of an <see cref="IAnsiResponseParser"/>
+/// </summary>
+public enum AnsiResponseParserState
+{
+    /// <summary>
+    ///     Parser is reading normal input e.g. keys typed by user.
+    /// </summary>
+    Normal,
+
+    /// <summary>
+    ///     Parser has encountered an Esc and is waiting to see if next
+    ///     key(s) continue to form an Ansi escape sequence
+    /// </summary>
+    ExpectingBracket,
+
+    /// <summary>
+    ///     Parser has encountered Esc[ and considers that it is in the process
+    ///     of reading an ANSI sequence.
+    /// </summary>
+    InResponse
+}

+ 19 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs

@@ -0,0 +1,19 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{T}"/>
+/// </summary>
+/// <typeparam name="T"></typeparam>
+internal class GenericHeld<T> : IHeld
+{
+    private readonly List<Tuple<char, T>> held = new ();
+
+    public void ClearHeld () { held.Clear (); }
+
+    public string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
+
+    public IEnumerable<object> HeldToObjects () { return held; }
+
+    public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
+}

+ 54 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs

@@ -0,0 +1,54 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     When implemented in a derived class, allows watching an input stream of characters
+///     (i.e. console input) for ANSI response sequences.
+/// </summary>
+public interface IAnsiResponseParser
+{
+    /// <summary>
+    ///     Current state of the parser based on what sequence of characters it has
+    ///     read from the console input stream.
+    /// </summary>
+    AnsiResponseParserState State { get; }
+
+    /// <summary>
+    ///     Notifies the parser that you are expecting a response to come in
+    ///     with the given <paramref name="terminator"/> (i.e. because you have
+    ///     sent an ANSI request out).
+    /// </summary>
+    /// <param name="terminator">The terminator you expect to see on response.</param>
+    /// <param name="response">Callback to invoke when the response is seen in console input.</param>
+    /// <param name="abandoned"></param>
+    /// <param name="persistent">
+    ///     <see langword="true"/> if you want this to persist permanently
+    ///     and be raised for every event matching the <paramref name="terminator"/>.
+    /// </param>
+    /// <exception cref="ArgumentException">
+    ///     If trying to register a persistent request for a terminator
+    ///     that already has one.
+    ///     exists.
+    /// </exception>
+    void ExpectResponse (string terminator, Action<string> response, Action? abandoned, bool persistent);
+
+    /// <summary>
+    ///     Returns true if there is an existing expectation (i.e. we are waiting a response
+    ///     from console) for the given <paramref name="terminator"/>.
+    /// </summary>
+    /// <param name="terminator"></param>
+    /// <returns></returns>
+    bool IsExpecting (string terminator);
+
+    /// <summary>
+    ///     Removes callback and expectation that we will get a response for the
+    ///     given <pararef name="requestTerminator"/>. Use to give up on very old
+    ///     requests e.g. if you want to send a different one with the same terminator.
+    /// </summary>
+    /// <param name="requestTerminator"></param>
+    /// <param name="persistent">
+    ///     <see langword="true"/> if you want to remove a persistent
+    ///     request listener.
+    /// </param>
+    void StopExpecting (string requestTerminator, bool persistent);
+}

+ 33 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs

@@ -0,0 +1,33 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes a sequence of chars (and optionally T metadata) accumulated
+///     by an <see cref="IAnsiResponseParser"/>
+/// </summary>
+internal interface IHeld
+{
+    /// <summary>
+    ///     Clears all held objects
+    /// </summary>
+    void ClearHeld ();
+
+    /// <summary>
+    ///     Returns string representation of the held objects
+    /// </summary>
+    /// <returns></returns>
+    string HeldToString ();
+
+    /// <summary>
+    ///     Returns the collection objects directly e.g. <see langword="char"/>
+    ///     or <see cref="Tuple"/> <see langword="char"/> + metadata T
+    /// </summary>
+    /// <returns></returns>
+    IEnumerable<object> HeldToObjects ();
+
+    /// <summary>
+    ///     Adds the given object to the collection.
+    /// </summary>
+    /// <param name="o"></param>
+    void AddToHeld (object o);
+}

+ 21 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/ReasonCannotSend.cs

@@ -0,0 +1,21 @@
+#nullable enable
+namespace Terminal.Gui;
+
+internal enum ReasonCannotSend
+{
+    /// <summary>
+    ///     No reason given.
+    /// </summary>
+    None = 0,
+
+    /// <summary>
+    ///     The parser is already waiting for a request to complete with the given terminator.
+    /// </summary>
+    OutstandingRequest,
+
+    /// <summary>
+    ///     There have been too many requests sent recently, new requests will be put into
+    ///     queue to prevent console becoming unresponsive.
+    /// </summary>
+    TooManyRequests
+}

+ 18 - 0
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs

@@ -0,0 +1,18 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser"/>
+/// </summary>
+internal class StringHeld : IHeld
+{
+    private readonly StringBuilder _held = new ();
+
+    public void ClearHeld () { _held.Clear (); }
+
+    public string HeldToString () { return _held.ToString (); }
+
+    public IEnumerable<object> HeldToObjects () { return _held.ToString ().Select (c => (object)c); }
+
+    public void AddToHeld (object o) { _held.Append ((char)o); }
+}

+ 33 - 7
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -44,6 +44,12 @@ public abstract class ConsoleDriver : IConsoleDriver
 
     #region Screen and Contents
 
+
+    /// <summary>
+    /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
+    /// </summary>
+    public TimeSpan EscTimeout { get; } = 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;
@@ -446,7 +452,7 @@ public abstract class ConsoleDriver : IConsoleDriver
     /// Sets <see cref="Contents"/> as dirty for situations where views
     /// don't need layout and redrawing, but just refresh the screen.
     /// </summary>
-    public void SetContentsAsDirty ()
+    protected void SetContentsAsDirty ()
     {
         lock (Contents!)
         {
@@ -474,10 +480,6 @@ public abstract class ConsoleDriver : IConsoleDriver
 
     #region Cursor Handling
 
-    /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
-    /// <returns><see langword="true"/> upon success</returns>
-    public abstract bool EnsureCursorVisibility ();
-
     /// <summary>Gets the terminal cursor visibility.</summary>
     /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
     /// <returns><see langword="true"/> upon success</returns>
@@ -689,5 +691,29 @@ public abstract class ConsoleDriver : IConsoleDriver
     /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
     public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
 
-    #endregion Keyboard Handling
-}
+    #endregion
+
+    private AnsiRequestScheduler? _scheduler;
+
+    /// <summary>
+    /// Queues the given <paramref name="request"/> for execution
+    /// </summary>
+    /// <param name="request"></param>
+    public void QueueAnsiRequest (AnsiEscapeSequenceRequest request)
+    {
+        GetRequestScheduler ().SendOrSchedule (request);
+    }
+
+    internal abstract IAnsiResponseParser GetParser ();
+
+    /// <summary>
+    ///     Gets the <see cref="AnsiRequestScheduler"/> for this <see cref="ConsoleDriver"/>.
+    /// </summary>
+    /// <returns></returns>
+    public AnsiRequestScheduler GetRequestScheduler ()
+    {
+        // Lazy initialization because GetParser is virtual
+        return _scheduler ??= new (GetParser ());
+    }
+
+}

+ 36 - 7
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -116,6 +116,7 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
+
     public override void Suspend ()
     {
         StopReportingMouseMoves ();
@@ -506,8 +507,8 @@ internal class CursesDriver : ConsoleDriver
     private CursorVisibility? _currentCursorVisibility;
     private CursorVisibility? _initialCursorVisibility;
 
-    /// <inheritdoc/>
-    public override bool EnsureCursorVisibility ()
+
+    private void EnsureCursorVisibility ()
     {
         if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
         {
@@ -515,12 +516,10 @@ internal class CursesDriver : ConsoleDriver
             _currentCursorVisibility = cursorVisibility;
             SetCursorVisibility (CursorVisibility.Invisible);
 
-            return false;
+            return;
         }
 
         SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
-
-        return _currentCursorVisibility == CursorVisibility.Default;
     }
 
     /// <inheritdoc/>
@@ -580,7 +579,7 @@ internal class CursesDriver : ConsoleDriver
 
     private Curses.Window? _window;
     private UnixMainLoop? _mainLoopDriver;
-    private object _processInputToken;
+    private object? _processInputToken;
 
     public override MainLoop Init ()
     {
@@ -693,6 +692,10 @@ internal class CursesDriver : ConsoleDriver
         return new (_mainLoopDriver);
     }
 
+    private readonly AnsiResponseParser _parser = new ();
+    /// <inheritdoc />
+    internal override IAnsiResponseParser GetParser () => _parser;
+
     internal void ProcessInput ()
     {
         int wch;
@@ -725,6 +728,7 @@ internal class CursesDriver : ConsoleDriver
 
                 while (wch2 == Curses.KeyMouse)
                 {
+                    // BUGBUG: Fix this nullable issue.
                     Key kea = null;
 
                     ConsoleKeyInfo [] cki =
@@ -734,6 +738,7 @@ internal class CursesDriver : ConsoleDriver
                         new ('<', 0, false, false, false)
                     };
                     code = 0;
+                    // BUGBUG: Fix this nullable issue.
                     HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
                 }
 
@@ -791,6 +796,7 @@ internal class CursesDriver : ConsoleDriver
                 k = KeyCode.AltMask | MapCursesKey (wch);
             }
 
+            // BUGBUG: Fix this nullable issue.
             Key key = null;
 
             if (code == 0)
@@ -821,6 +827,7 @@ internal class CursesDriver : ConsoleDriver
                     [
                         new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
                     ];
+                    // BUGBUG: Fix this nullable issue.
                     HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
 
                     return;
@@ -930,6 +937,15 @@ internal class CursesDriver : ConsoleDriver
             OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
         }
     }
+    static string ConvertToString (ConsoleKeyInfo [] keyInfos)
+    {
+        char [] chars = new char [keyInfos.Length];
+        for (int i = 0; i < keyInfos.Length; i++)
+        {
+            chars [i] = keyInfos [i].KeyChar;
+        }
+        return new string (chars);
+    }
 
     private void HandleEscSeqResponse (
         ref int code,
@@ -949,6 +965,18 @@ internal class CursesDriver : ConsoleDriver
 
             if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
             {
+                // Give ansi parser a chance to deal with the escape sequence
+                if (cki != null && string.IsNullOrEmpty(_parser.ProcessInput (ConvertToString(cki))))
+                {
+                    // Parser fully consumed all keys meaning keys are processed - job done
+                    return;
+                }
+
+                // Ansi parser could not deal with it either because it is not expecting
+                // the given terminator (e.g. mouse) or did not understand format somehow.
+                // Carry on with the older code for processing curses escape codes
+
+                // BUGBUG: Fix this nullable issue.
                 EscSeqUtils.DecodeEscSeq (
                                           ref consoleKeyInfo,
                                           ref ck,
@@ -972,6 +1000,7 @@ internal class CursesDriver : ConsoleDriver
                         OnMouseEvent (new () { Flags = mf, Position = pos });
                     }
 
+                    // BUGBUG: Fix this nullable issue.
                     cki = null;
 
                     if (wch2 == 27)
@@ -1091,7 +1120,7 @@ internal class CursesDriver : ConsoleDriver
         StopReportingMouseMoves ();
         SetCursorVisibility (CursorVisibility.Default);
 
-        if (_mainLoopDriver is { })
+        if (_mainLoopDriver is { } && _processInputToken != null)
         {
             _mainLoopDriver.RemoveWatch (_processInputToken);
         }

+ 1 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -247,6 +247,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
     private class Watch
     {
+        // BUGBUG: Fix this nullable issue.
         public Func<MainLoop, bool> Callback;
         public Condition Condition;
         public int File;

+ 10 - 14
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -1799,12 +1799,12 @@ public static class EscSeqUtils
     ///     https://terminalguide.namepad.de/seq/csi_sn__p-6/
     ///     The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) ; 1 R
     /// </summary>
-    public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
+    public static readonly AnsiEscapeSequence CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
 
     /// <summary>
     ///     The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
     /// </summary>
-    public static readonly string CSI_RequestCursorPositionReport_Terminator = "R";
+    public const string CSI_RequestCursorPositionReport_Terminator = "R";
 
     /// <summary>
     ///     ESC [ 0 c - Send Device Attributes (Primary DA)
@@ -1826,46 +1826,42 @@ public static class EscSeqUtils
     ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
     ///     <see cref="CSI_SendDeviceAttributes2"/>
     /// </summary>
-    public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
+    public static readonly AnsiEscapeSequence CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
 
     /// <summary>
     ///     ESC [ > 0 c - Send Device Attributes (Secondary DA)
     ///     Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
-    ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
-    ///     <see cref="CSI_SendDeviceAttributes2"/>
     /// </summary>
-    public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
+    public static readonly AnsiEscapeSequence CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
 
-    /*
-     TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
     /// <summary>
     ///     CSI 16 t - Request sixel resolution (width and height in pixels)
     /// </summary>
-    public static readonly AnsiEscapeSequenceRequest CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
+    public static readonly AnsiEscapeSequence CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
 
     /// <summary>
     ///     CSI 14 t - Request window size in pixels (width x height)
     /// </summary>
-    public static readonly AnsiEscapeSequenceRequest CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
-    */
+    public static readonly AnsiEscapeSequence CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
 
     /// <summary>
     ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
     ///     https://terminalguide.namepad.de/seq/csi_st-18/
     ///     The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
     /// </summary>
-    public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
+    public static readonly AnsiEscapeSequence CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
+
 
     /// <summary>
     ///     The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
     /// </summary>
-    public static readonly string CSI_ReportTerminalSizeInChars_Terminator = "t";
+    public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
 
     /// <summary>
     ///     The value of the response to <see cref="CSI_ReportTerminalSizeInChars"/> indicating value 1 and 2 are the terminal
     ///     size in chars.
     /// </summary>
-    public static readonly string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
+    public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
 
     #endregion
 }

+ 10 - 2
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -40,6 +40,12 @@ public class FakeDriver : ConsoleDriver
     public static Behaviors FakeBehaviors = new ();
     public override bool SupportsTrueColor => false;
 
+    /// <inheritdoc />
+    public override void WriteRaw (string ansi)
+    {
+
+    }
+
     public FakeDriver ()
     {
         Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
@@ -371,7 +377,7 @@ public class FakeDriver : ConsoleDriver
     }
 
     /// <inheritdoc/>
-    public override bool EnsureCursorVisibility ()
+    private bool EnsureCursorVisibility ()
     {
         if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
         {
@@ -392,8 +398,10 @@ public class FakeDriver : ConsoleDriver
         MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
     }
 
+    private AnsiResponseParser _parser = new ();
+
     /// <inheritdoc />
-    public override void WriteRaw (string ansi) { throw new NotImplementedException (); }
+    internal override IAnsiResponseParser GetParser () => _parser;
 
     public void SetBufferSize (int width, int height)
     {

+ 55 - 26
Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs

@@ -31,8 +31,9 @@ public interface IConsoleDriver
     /// <summary>The number of columns visible in the terminal.</summary>
     int Cols { get; set; }
 
+    // BUGBUG: This should not be publicly settable.
     /// <summary>
-    ///     The contents of the application output. The driver outputs this buffer to the terminal when
+    ///     Gets or sets the contents of the application output. The driver outputs this buffer to the terminal when
     ///     <see cref="UpdateScreen"/> is called.
     ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
     /// </summary>
@@ -92,11 +93,13 @@ public interface IConsoleDriver
     /// </returns>
     bool IsRuneSupported (Rune rune);
 
+    // BUGBUG: This is not referenced. Can it be removed?
     /// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
     /// <param name="col">The column.</param>
     /// <param name="row">The row.</param>
     /// <returns>
-    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>.
+    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
+    ///     <see cref="ConsoleDriver.Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
     bool IsValidLocation (int col, int row);
@@ -106,19 +109,23 @@ public interface IConsoleDriver
     /// <param name="col">The column.</param>
     /// <param name="row">The row.</param>
     /// <returns>
-    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>.
+    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
+    ///     <see cref="ConsoleDriver.Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
     bool IsValidLocation (Rune rune, int col, int row);
 
     /// <summary>
-    ///     Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in <see cref="ConsoleDriver.Contents"/>.
-    ///     Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine where to add content.
+    ///     Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in
+    ///     <see cref="ConsoleDriver.Contents"/>.
+    ///     Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine
+    ///     where to add content.
     /// </summary>
     /// <remarks>
     ///     <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
     ///     <para>
-    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="ConsoleDriver.Cols"/> and
+    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="ConsoleDriver.Cols"/>
+    ///         and
     ///         <see cref="ConsoleDriver.Rows"/>, the method still sets those properties.
     ///     </para>
     /// </remarks>
@@ -130,12 +137,15 @@ public interface IConsoleDriver
     /// <remarks>
     ///     <para>
     ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
-    ///         <paramref name="rune"/> required, even if the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen
+    ///         <paramref name="rune"/> required, even if the new column value is outside of the
+    ///         <see cref="ConsoleDriver.Clip"/> or screen
     ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
     ///     </para>
     ///     <para>
-    ///         If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number of columns
-    ///         needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number
+    ///         of columns
+    ///         needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement
+    ///         character (U+FFFD)
     ///         will be added instead.
     ///     </para>
     /// </remarks>
@@ -144,7 +154,8 @@ public interface IConsoleDriver
 
     /// <summary>
     ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
-    ///     convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> constructor.
+    ///     convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
+    ///     constructor.
     /// </summary>
     /// <param name="c">Character to add.</param>
     void AddRune (char c);
@@ -153,7 +164,8 @@ public interface IConsoleDriver
     /// <remarks>
     ///     <para>
     ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
-    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/>
+    ///         or screen
     ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
     ///     </para>
     ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
@@ -161,9 +173,12 @@ public interface IConsoleDriver
     /// <param name="str">String.</param>
     void AddStr (string str);
 
-    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary>
+    /// <summary>
+    ///     Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
+    /// </summary>
     /// <remarks>
-    ///     The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
+    ///     The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
+    ///     drawn.
     /// </remarks>
     /// <param name="rect">The Screen-relative rectangle.</param>
     /// <param name="rune">The Rune used to fill the rectangle</param>
@@ -185,16 +200,6 @@ public interface IConsoleDriver
     /// </summary>
     event EventHandler<EventArgs>? ClearedContents;
 
-    /// <summary>
-    /// Sets <see cref="ConsoleDriver.Contents"/> as dirty for situations where views
-    /// don't need layout and redrawing, but just refresh the screen.
-    /// </summary>
-    void SetContentsAsDirty ();
-
-    /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
-    /// <returns><see langword="true"/> upon success</returns>
-    bool EnsureCursorVisibility ();
-
     /// <summary>Gets the terminal cursor visibility.</summary>
     /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
     /// <returns><see langword="true"/> upon success</returns>
@@ -224,7 +229,10 @@ public interface IConsoleDriver
     /// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks>
     void Suspend ();
 
-    /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary>
+    /// <summary>
+    ///     Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
+    ///     <see cref="ConsoleDriver.Row"/>.
+    /// </summary>
     void UpdateCursor ();
 
     /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
@@ -263,6 +271,7 @@ public interface IConsoleDriver
     /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
     event EventHandler<Key>? KeyDown;
 
+    // BUGBUG: This is not referenced. Can it be removed?
     /// <summary>
     ///     Called when a key is pressed down. Fires the <see cref="ConsoleDriver.KeyDown"/> event. This is a precursor to
     ///     <see cref="ConsoleDriver.OnKeyUp"/>.
@@ -272,14 +281,17 @@ public interface IConsoleDriver
 
     /// <summary>Event fired when a key is released.</summary>
     /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/> processing is
+    ///     Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/>
+    ///     processing is
     ///     complete.
     /// </remarks>
     event EventHandler<Key>? KeyUp;
 
+    // BUGBUG: This is not referenced. Can it be removed?
     /// <summary>Called when a key is released. Fires the <see cref="ConsoleDriver.KeyUp"/> event.</summary>
     /// <remarks>
-    ///     Drivers that do not support key release events will call this method after <see cref="ConsoleDriver.OnKeyDown"/> processing
+    ///     Drivers that do not support key release events will call this method after <see cref="ConsoleDriver.OnKeyDown"/>
+    ///     processing
     ///     is complete.
     /// </remarks>
     /// <param name="a"></param>
@@ -292,4 +304,21 @@ public interface IConsoleDriver
     /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
     /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
     void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
+
+    /// <summary>
+    ///     How long after Esc has been pressed before we give up on getting an Ansi escape sequence
+    /// </summary>
+    public TimeSpan EscTimeout { get; }
+
+    /// <summary>
+    ///     Queues the given <paramref name="request"/> for execution
+    /// </summary>
+    /// <param name="request"></param>
+    public void QueueAnsiRequest (AnsiEscapeSequenceRequest request);
+
+    /// <summary>
+    ///     Gets the <see cref="AnsiRequestScheduler"/> for the driver
+    /// </summary>
+    /// <returns></returns>
+    public AnsiRequestScheduler GetRequestScheduler ();
 }

+ 30 - 27
Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs

@@ -3,7 +3,9 @@
 // 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;
 using static Terminal.Gui.NetEvents;
 
@@ -11,9 +13,11 @@ namespace Terminal.Gui;
 
 internal class NetDriver : ConsoleDriver
 {
+
     public bool IsWinPlatform { get; private set; }
     public NetWinVTConsole? NetWinConsole { get; private set; }
 
+
     public override void Suspend ()
     {
         if (Environment.OSVersion.Platform != PlatformID.Unix)
@@ -132,33 +136,30 @@ internal class NetDriver : ConsoleDriver
                         {
                             output.Append (
                                            EscSeqUtils.CSI_SetGraphicsRendition (
-                                                                                                    MapColors (
-                                                                                                         (ConsoleColor)attr.Background
-                                                                                                             .GetClosestNamedColor16 (),
-                                                                                                         false
-                                                                                                        ),
-                                                                                                    MapColors (
-                                                                                                     (ConsoleColor)attr.Foreground
-                                                                                                         .GetClosestNamedColor16 ())
-                                                                                                   )
+                                                                                 MapColors (
+                                                                                            (ConsoleColor)attr.Background.GetClosestNamedColor16 (),
+                                                                                            false
+                                                                                           ),
+                                                                                 MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ())
+                                                                                )
                                           );
                         }
                         else
                         {
                             output.Append (
                                            EscSeqUtils.CSI_SetForegroundColorRGB (
-                                                                                                     attr.Foreground.R,
-                                                                                                     attr.Foreground.G,
-                                                                                                     attr.Foreground.B
-                                                                                                    )
+                                                                                  attr.Foreground.R,
+                                                                                  attr.Foreground.G,
+                                                                                  attr.Foreground.B
+                                                                                 )
                                           );
 
                             output.Append (
                                            EscSeqUtils.CSI_SetBackgroundColorRGB (
-                                                                                                     attr.Background.R,
-                                                                                                     attr.Background.G,
-                                                                                                     attr.Background.B
-                                                                                                    )
+                                                                                  attr.Background.R,
+                                                                                  attr.Background.G,
+                                                                                  attr.Background.B
+                                                                                 )
                                           );
                         }
                     }
@@ -221,11 +222,14 @@ internal class NetDriver : ConsoleDriver
 
         return updated;
     }
-
     #region Init/End/MainLoop
 
+    // BUGBUG: Fix this nullable issue.
+    /// <inheritdoc />
+    internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser;
     internal NetMainLoop? _mainLoopDriver;
 
+    /// <inheritdoc />
     public override MainLoop Init ()
     {
         PlatformID p = Environment.OSVersion.Platform;
@@ -236,7 +240,7 @@ internal class NetDriver : ConsoleDriver
 
             try
             {
-                NetWinConsole = new ();
+                NetWinConsole = new NetWinVTConsole ();
             }
             catch (ApplicationException)
             {
@@ -323,7 +327,6 @@ internal class NetDriver : ConsoleDriver
                 break;
             case EventType.Mouse:
                 MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
-
                 //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
                 OnMouseEvent (me);
 
@@ -334,7 +337,7 @@ internal class NetDriver : ConsoleDriver
                 Left = 0;
                 Cols = inputEvent.WindowSizeEvent.Size.Width;
                 Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
-
+                ;
                 ResizeScreen ();
                 ClearContents ();
                 _winSizeChanging = false;
@@ -349,7 +352,6 @@ internal class NetDriver : ConsoleDriver
                 throw new ArgumentOutOfRangeException ();
         }
     }
-
     public override void End ()
     {
         if (IsWinPlatform)
@@ -374,6 +376,9 @@ internal class NetDriver : ConsoleDriver
 
     #endregion Init/End/MainLoop
 
+    
+
+
     #region Color Handling
 
     public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
@@ -491,7 +496,7 @@ internal class NetDriver : ConsoleDriver
         return visibility == CursorVisibility.Default;
     }
 
-    public override bool EnsureCursorVisibility ()
+    private void EnsureCursorVisibility ()
     {
         if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
         {
@@ -499,12 +504,10 @@ internal class NetDriver : ConsoleDriver
             _cachedCursorVisibility = cursorVisibility;
             SetCursorVisibility (CursorVisibility.Invisible);
 
-            return false;
+            return;
         }
 
         SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
-
-        return _cachedCursorVisibility == CursorVisibility.Default;
     }
 
     #endregion
@@ -787,4 +790,4 @@ internal class NetDriver : ConsoleDriver
     }
 
     #endregion Low-Level DotNet tuff
-}
+}

+ 175 - 255
Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs

@@ -1,238 +1,150 @@
 #nullable enable
+using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Terminal.Gui;
 
 internal class NetEvents : IDisposable
 {
-    private readonly ManualResetEventSlim _inputReady = new (false);
-    private CancellationTokenSource? _inputReadyCancellationTokenSource;
-    private readonly Queue<InputResult> _inputQueue = new ();
+    private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource ();
+
+    //CancellationTokenSource _waitForStartCancellationTokenSource;
+    private readonly ManualResetEventSlim _winChange = new (false);
+    private readonly BlockingCollection<InputResult?> _inputQueue = new (new ConcurrentQueue<InputResult?> ());
     private readonly IConsoleDriver _consoleDriver;
-    private ConsoleKeyInfo []? _cki;
-    private bool _isEscSeq;
-#if PROCESS_REQUEST
-    bool _neededProcessRequest;
-#endif
+
+    public AnsiResponseParser<ConsoleKeyInfo> Parser { get; private set; } = new ();
+
     public NetEvents (IConsoleDriver consoleDriver)
     {
         _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
-        _inputReadyCancellationTokenSource = new ();
 
-        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;
     }
 
+
     public InputResult? DequeueInput ()
     {
-        while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false })
+        while (!_netEventsDisposed.Token.IsCancellationRequested)
         {
+            _winChange.Set ();
+
             try
             {
-                if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+                if (_inputQueue.TryTake (out var item, -1, _netEventsDisposed.Token))
                 {
-                    if (_inputQueue.Count == 0)
-                    {
-                        _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
-                    }
-                }
-
-                if (_inputQueue.Count > 0)
-                {
-                    return _inputQueue.Dequeue ();
+                    return item;
                 }
             }
             catch (OperationCanceledException)
             {
                 return null;
             }
-            finally
-            {
-                if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
-                {
-                    _inputReady.Reset ();
-                }
-            }
 
-#if PROCESS_REQUEST
-            _neededProcessRequest = false;
-#endif
         }
 
         return null;
     }
 
-    private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
+    private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true)
     {
-        while (!cancellationToken.IsCancellationRequested)
+        // if there is a key available, return it without waiting
+        //  (or dispatching work to the thread queue)
+        if (Console.KeyAvailable)
+        {
+            return Console.ReadKey (intercept);
+        }
+
+        while (!_netEventsDisposed.IsCancellationRequested)
         {
-            // if there is a key available, return it without waiting
-            //  (or dispatching work to the thread queue)
+            Task.Delay (100, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token);
+
+            foreach (var k in ShouldReleaseParserHeldKeys ())
+            {
+                ProcessMapConsoleKeyInfo (k);
+            }
+
             if (Console.KeyAvailable)
             {
                 return Console.ReadKey (intercept);
             }
-
-            // The delay must be here because it may have a request response after a while
-            // In WSL it takes longer for keys to be available.
-            Task.Delay (100, cancellationToken).Wait (cancellationToken);
         }
 
-        cancellationToken.ThrowIfCancellationRequested ();
+        _netEventsDisposed.Token.ThrowIfCancellationRequested ();
 
         return default (ConsoleKeyInfo);
     }
 
+    public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys ()
+    {
+        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 })
+        while (!_netEventsDisposed.IsCancellationRequested)
         {
-            try
+            if (_inputQueue.Count == 0)
             {
-                ConsoleKey key = 0;
-                ConsoleModifiers mod = 0;
-                ConsoleKeyInfo newConsoleKeyInfo = default;
-
-                while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+                while (!_netEventsDisposed.IsCancellationRequested)
                 {
                     ConsoleKeyInfo consoleKeyInfo;
 
-                    try
-                    {
-                        consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
-                    }
-                    catch (OperationCanceledException)
-                    {
-                        return;
-                    }
-
-                    var ckiAlreadyResized = false;
+                    consoleKeyInfo = ReadConsoleKeyInfo ();
 
-                    if (EscSeqUtils.IncompleteCkInfos is { })
+                    // Parse
+                    foreach (var k in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
                     {
-                        ckiAlreadyResized = true;
-
-                        _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
-                        _cki = EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
-                        EscSeqUtils.IncompleteCkInfos = null;
-
-                        if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B')
-                        {
-                            _isEscSeq = true;
-                        }
-                    }
-
-                    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 (
-                                                                                    (char)KeyCode.Esc,
-                                                                                    0,
-                                                                                    false,
-                                                                                    false,
-                                                                                    false
-                                                                                   ),
-                                                                               _cki
-                                                                              );
-                        }
-
-                        _isEscSeq = true;
-
-                        if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
-                            || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
-                            || (_cki is { }
-                                && char.IsLetter (_cki [^1].KeyChar)
-                                && char.IsLower (consoleKeyInfo.KeyChar)
-                                && char.IsLetter (consoleKeyInfo.KeyChar))
-                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetterOrDigit (consoleKeyInfo.KeyChar))
-                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar))
-                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar)))
-                        {
-                            ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
-                            _cki = null;
-                            _isEscSeq = false;
-
-                            ProcessMapConsoleKeyInfo (consoleKeyInfo);
-                        }
-                        else
-                        {
-                            newConsoleKeyInfo = consoleKeyInfo;
-
-                            if (!ckiAlreadyResized)
-                            {
-                                _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
-                            }
-
-                            if (Console.KeyAvailable)
-                            {
-                                continue;
-                            }
-
-                            ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki!, ref mod);
-                            _cki = null;
-                            _isEscSeq = false;
-                        }
-
-                        break;
+                        ProcessMapConsoleKeyInfo (k.Item2);
                     }
-
-                    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);
-                        }
-
-                        break;
-                    }
-
-                    ProcessMapConsoleKeyInfo (consoleKeyInfo);
-
-                    break;
-                }
-
-                if (_inputQueue.Count > 0)
-                {
-                    _inputReady.Set ();
                 }
             }
-            catch (OperationCanceledException)
-            {
-                return;
-            }
         }
+    }
 
-        void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-        {
-            _inputQueue.Enqueue (
-                                 new ()
-                                 {
-                                     EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
-                                 }
-                                );
-            _isEscSeq = false;
-        }
+    void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+    {
+        _inputQueue.Add (
+                             new InputResult
+                             {
+                                 EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (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;
 
@@ -258,19 +170,17 @@ internal class NetEvents : IDisposable
                 }
             }
 
-            cancellationToken.ThrowIfCancellationRequested ();
+            _netEventsDisposed.Token.ThrowIfCancellationRequested ();
         }
 
-        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+        while (!_netEventsDisposed.IsCancellationRequested)
         {
             try
             {
-                RequestWindowSize (_inputReadyCancellationTokenSource.Token);
+                _winChange.Wait (_netEventsDisposed.Token);
+                _winChange.Reset ();
 
-                if (_inputQueue.Count > 0)
-                {
-                    _inputReady.Set ();
-                }
+                RequestWindowSize ();
             }
             catch (OperationCanceledException)
             {
@@ -295,16 +205,29 @@ internal class NetEvents : IDisposable
         int w = Math.Max (winWidth, 0);
         int h = Math.Max (winHeight, 0);
 
-        _inputQueue.Enqueue (
-                             new ()
+        _inputQueue.Add (
+                             new InputResult
                              {
-                                 EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (w, h) }
+                                 EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) }
                              }
                             );
 
         return true;
     }
 
+    private bool ProcessRequestResponse (IEnumerable<Tuple<char, ConsoleKeyInfo>> 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);
+
+        // Handled
+        return true;
+    }
+
     // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
     private void ProcessRequestResponse (
         ref ConsoleKeyInfo newConsoleKeyInfo,
@@ -313,22 +236,23 @@ internal class NetEvents : IDisposable
         ref ConsoleModifiers mod
     )
     {
+
         // isMouse is true if it's CSI<, false otherwise
         EscSeqUtils.DecodeEscSeq (
-                                                     ref newConsoleKeyInfo,
-                                                     ref key,
-                                                     cki,
-                                                     ref mod,
-                                                     out string c1Control,
-                                                     out string code,
-                                                     out string [] values,
-                                                     out string terminating,
-                                                     out bool isMouse,
-                                                     out List<MouseFlags> mouseFlags,
-                                                     out Point pos,
-                                                     out bool isReq,
-                                                     (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
-                                                    );
+                                  ref newConsoleKeyInfo,
+                                  ref key,
+                                  cki,
+                                  ref mod,
+                                  out string c1Control,
+                                  out string code,
+                                  out string [] values,
+                                  out string terminating,
+                                  out bool isMouse,
+                                  out List<MouseFlags> mouseFlags,
+                                  out Point pos,
+                                  out bool isReq,
+                                  (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
+                                 );
 
         if (isMouse)
         {
@@ -347,10 +271,7 @@ internal class NetEvents : IDisposable
             return;
         }
 
-        if (newConsoleKeyInfo != default)
-        {
-            HandleKeyboardEvent (newConsoleKeyInfo);
-        }
+        HandleKeyboardEvent (newConsoleKeyInfo);
     }
 
     [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
@@ -491,51 +412,53 @@ internal class NetEvents : IDisposable
 
     private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
     {
-        if (terminating ==
-
-            // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
-            // The observation is correct because the response isn't immediate and this is useless
-            EscSeqUtils.CSI_RequestCursorPositionReport_Terminator)
+        switch (terminating)
         {
-            var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
+            // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
+            case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
+                var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
 
-            if (_lastCursorPosition.Y != point.Y)
-            {
-                _lastCursorPosition = point;
-                var eventType = EventType.WindowPosition;
-                var winPositionEv = new WindowPositionEvent { CursorPosition = point };
+                if (_lastCursorPosition.Y != point.Y)
+                {
+                    _lastCursorPosition = point;
+                    var eventType = EventType.WindowPosition;
+                    var winPositionEv = new WindowPositionEvent { CursorPosition = point };
 
-                _inputQueue.Enqueue (
-                                     new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
-                                    );
-            }
-            else
-            {
-                return;
-            }
-        }
-        else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator)
-        {
-            if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue)
-            {
-                EnqueueWindowSizeEvent (
-                                        Math.Max (int.Parse (values [1]), 0),
-                                        Math.Max (int.Parse (values [2]), 0),
-                                        Math.Max (int.Parse (values [1]), 0),
-                                        Math.Max (int.Parse (values [2]), 0)
-                                       );
-            }
-            else
-            {
+                    _inputQueue.Add (
+                                         new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
+                                        );
+                }
+                else
+                {
+                    return;
+                }
+
+                break;
+
+            case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
+                switch (values [0])
+                {
+                    case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
+                        EnqueueWindowSizeEvent (
+                                                Math.Max (int.Parse (values [1]), 0),
+                                                Math.Max (int.Parse (values [2]), 0),
+                                                Math.Max (int.Parse (values [1]), 0),
+                                                Math.Max (int.Parse (values [2]), 0)
+                                               );
+
+                        break;
+                    default:
+                        EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+
+                        break;
+                }
+
+                break;
+            default:
                 EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-            }
-        }
-        else
-        {
-            EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-        }
 
-        _inputReady.Set ();
+                break;
+        }
     }
 
     private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
@@ -543,7 +466,7 @@ internal class NetEvents : IDisposable
         var eventType = EventType.RequestResponse;
         var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
 
-        _inputQueue.Enqueue (
+        _inputQueue.Add (
                              new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
                             );
     }
@@ -552,8 +475,8 @@ internal class NetEvents : IDisposable
     {
         var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
 
-        _inputQueue.Enqueue (
-                             new () { EventType = EventType.Mouse, MouseEvent = mouseEvent }
+        _inputQueue.Add (
+                             new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
                             );
     }
 
@@ -634,15 +557,15 @@ internal class NetEvents : IDisposable
 
         public readonly override string ToString ()
         {
-            return (EventType switch
-                    {
-                        EventType.Key => ToString (ConsoleKeyInfo),
-                        EventType.Mouse => MouseEvent.ToString (),
+            return EventType switch
+            {
+                EventType.Key => ToString (ConsoleKeyInfo),
+                EventType.Mouse => MouseEvent.ToString (),
 
-                        //EventType.WindowSize => WindowSize.ToString (),
-                        //EventType.RequestResponse => RequestResponse.ToString (),
-                        _ => "Unknown event type: " + EventType
-                    })!;
+                //EventType.WindowSize => WindowSize.ToString (),
+                //EventType.RequestResponse => RequestResponse.ToString (),
+                _ => "Unknown event type: " + EventType
+            };
         }
 
         /// <summary>Prints a ConsoleKeyInfoEx structure</summary>
@@ -667,16 +590,13 @@ internal class NetEvents : IDisposable
     {
         var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
 
-        _inputQueue.Enqueue (inputResult);
+        _inputQueue.Add (inputResult);
     }
 
     public void Dispose ()
     {
-        _inputReadyCancellationTokenSource?.Cancel ();
-        _inputReadyCancellationTokenSource?.Dispose ();
-        _inputReadyCancellationTokenSource = null;
-
-        _inputReady.Dispose ();
+        _netEventsDisposed?.Cancel ();
+        _netEventsDisposed?.Dispose ();
 
         try
         {
@@ -692,4 +612,4 @@ internal class NetEvents : IDisposable
             // Ignore - Console input has already been closed
         }
     }
-}
+}

+ 1 - 4
Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs

@@ -31,10 +31,7 @@ internal class NetMainLoop : IMainLoopDriver
     {
         ArgumentNullException.ThrowIfNull (consoleDriver);
 
-        if (!ConsoleDriver.RunningUnitTests)
-        {
-            _netEvents = new (consoleDriver);
-        }
+        _netEvents = new (consoleDriver);
     }
 
     void IMainLoopDriver.Setup (MainLoop mainLoop)

+ 80 - 59
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -20,6 +20,7 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.Runtime.InteropServices;
 using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
+using static Terminal.Gui.SpinnerStyle;
 
 namespace Terminal.Gui;
 
@@ -35,7 +36,7 @@ internal class WindowsDriver : ConsoleDriver
 
     private WindowsConsole.ButtonState? _lastMouseButtonPressed;
     private WindowsMainLoop? _mainLoopDriver;
-    private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
+    private WindowsConsole.ExtendedCharInfo [] _outputBuffer = new WindowsConsole.ExtendedCharInfo [0 * 0];
     private Point? _point;
     private Point _pointMove;
     private bool _processButtonClick;
@@ -187,7 +188,14 @@ internal class WindowsDriver : ConsoleDriver
         }
     }
 
-    public override void WriteRaw (string ansi) { WinConsole?.WriteANSI (ansi); }
+    /// <inheritdoc />
+    internal override IAnsiResponseParser GetParser () => _parser;
+
+
+    public override void WriteRaw (string str)
+    {
+        WinConsole?.WriteANSI (str);
+    }
 
     #region Not Implemented
 
@@ -284,35 +292,6 @@ internal class WindowsDriver : ConsoleDriver
             return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
         }
     }
-
-    /// <inheritdoc/>
-    public override bool EnsureCursorVisibility ()
-    {
-        if (Force16Colors)
-        {
-            return WinConsole is null || WinConsole.EnsureCursorVisibility ();
-        }
-        else
-        {
-            var sb = new StringBuilder ();
-            sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-            return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
-        }
-
-        //if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
-        //{
-        //    GetCursorVisibility (out CursorVisibility cursorVisibility);
-        //    _cachedCursorVisibility = cursorVisibility;
-        //    SetCursorVisibility (CursorVisibility.Invisible);
-
-        //    return false;
-        //}
-
-        //SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
-
-        //return _cachedCursorVisibility == CursorVisibility.Default;
-    }
-
     #endregion Cursor Handling
 
     public override bool UpdateScreen ()
@@ -480,14 +459,25 @@ internal class WindowsDriver : ConsoleDriver
 
         if (!RunningUnitTests)
         {
-            WinConsole?.SetInitialCursorVisibility ();
+        WinConsole?.SetInitialCursorVisibility ();
         }
 
         return new MainLoop (_mainLoopDriver);
     }
 
+    private AnsiResponseParser<WindowsConsole.InputRecord> _parser = new ();
+
     internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
     {
+        foreach (var e in Parse (inputEvent))
+        {
+            ProcessInputAfterParsing (e);
+        }
+    }
+
+    internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent)
+    {
+
         switch (inputEvent.EventType)
         {
             case WindowsConsole.EventType.Key:
@@ -510,22 +500,16 @@ internal class WindowsDriver : ConsoleDriver
                     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;
 
             case WindowsConsole.EventType.Mouse:
                 MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
 
-                if (me.Flags == MouseFlags.None)
+                if (me is null || me.Flags == MouseFlags.None)
                 {
                     break;
                 }
@@ -563,6 +547,43 @@ internal class WindowsDriver : ConsoleDriver
         }
     }
 
+    private IEnumerable<WindowsConsole.InputRecord> Parse (WindowsConsole.InputRecord inputEvent)
+    {
+        if (inputEvent.EventType != WindowsConsole.EventType.Key)
+        {
+            yield return inputEvent;
+            yield break;
+        }
+
+        // Swallow key up events - they are unreliable
+        if (!inputEvent.KeyEvent.bKeyDown)
+        {
+            yield break;
+        }
+
+        foreach (var i in ShouldReleaseParserHeldKeys ())
+        {
+            yield return i;
+        }
+
+        foreach (Tuple<char, WindowsConsole.InputRecord> output in
+                 _parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent)))
+        {
+            yield return output.Item2;
+        }
+    }
+
+    public IEnumerable<WindowsConsole.InputRecord> ShouldReleaseParserHeldKeys ()
+    {
+        if (_parser.State == AnsiResponseParserState.ExpectingBracket &&
+            DateTime.Now - _parser.StateChangedAt > EscTimeout)
+        {
+            return _parser.Release ().Select (o => o.Item2);
+        }
+
+        return [];
+    }
+
 #if HACK_CHECK_WINCHANGED
     private void ChangeWin (object s, SizeChangedEventArgs e)
     {
@@ -661,13 +682,13 @@ internal class WindowsDriver : ConsoleDriver
 
                 if (keyInfo.KeyChar == 0)
                 {
-                    // If the keyChar is 0, keyInfo.Key value is not a printable character.
+                    // If the keyChar is 0, keyInfo.Key value is not a printable character. 
 
-                    // Dead keys (diacritics) are indicated by setting the top bit of the return value.
+                    // Dead keys (diacritics) are indicated by setting the top bit of the return value. 
                     if ((mapResult & 0x80000000) != 0)
                     {
                         // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
-                        // Option 1: Throw it out.
+                        // Option 1: Throw it out. 
                         //    - Apps will never see the dead keys
                         //    - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
                         //      - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
@@ -688,7 +709,7 @@ internal class WindowsDriver : ConsoleDriver
                     if (keyInfo.Modifiers != 0)
                     {
                         // These Oem keys have well-defined chars. We ensure the representative char is used.
-                        // If we don't do this, then on some keyboard layouts the wrong char is
+                        // If we don't do this, then on some keyboard layouts the wrong char is 
                         // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
                         // for key persistence ("Ctrl++" vs. "Ctrl+=").
                         mappedChar = keyInfo.Key switch
@@ -754,11 +775,11 @@ internal class WindowsDriver : ConsoleDriver
                     if (keyInfo.KeyChar <= 'Z')
                     {
                         return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
-                    }
+                }
 
                     // Always return the KeyChar because it may be an Á, À with Oem1, etc
                     return (KeyCode)keyInfo.KeyChar;
-                }
+            }
             }
 
             if (keyInfo.KeyChar <= 'z')
@@ -947,12 +968,12 @@ internal class WindowsDriver : ConsoleDriver
         {
             // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
             Application.MainLoop!.AddIdle (
-                                           () =>
-                                           {
-                                               Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
+                                          () =>
+                                          {
+                                              Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
 
-                                               return false;
-                                           });
+                                              return false;
+                                          });
         }
 
         // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
@@ -1019,12 +1040,12 @@ internal class WindowsDriver : ConsoleDriver
             {
                 // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
                 Application.MainLoop!.AddIdle (
-                                               () =>
-                                               {
-                                                   Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
+                                              () =>
+                                              {
+                                                  Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
 
-                                                   return false;
-                                               });
+                                                  return false;
+                                              });
             }
         }
         else if (_lastMouseButtonPressed != null
@@ -1187,4 +1208,4 @@ internal class WindowsDriver : ConsoleDriver
             Flags = mouseFlag
         };
     }
-}
+}

+ 5 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs

@@ -112,6 +112,11 @@ internal class WindowsMainLoop : IMainLoopDriver
 
     void IMainLoopDriver.Iteration ()
     {
+        foreach (var i in ((WindowsDriver)_consoleDriver).ShouldReleaseParserHeldKeys ())
+        {
+            ((WindowsDriver)_consoleDriver).ProcessInputAfterParsing (i);
+        }
+
         while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out WindowsConsole.InputRecord inputRecords))
         {
             ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords);

+ 0 - 20
Terminal.Gui/Drawing/AssumeSupportDetector.cs

@@ -1,20 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     Implementation of <see cref="ISixelSupportDetector"/> that assumes best
-///     case scenario (full support including transparency with 10x20 resolution).
-/// </summary>
-public class AssumeSupportDetector : ISixelSupportDetector
-{
-    /// <inheritdoc/>
-    public SixelSupportResult Detect ()
-    {
-        return new()
-        {
-            IsSupported = true,
-            MaxPaletteColors = 256,
-            Resolution = new (10, 20),
-            SupportsTransparency = true
-        };
-    }
-}

+ 0 - 0
Terminal.Gui/Drawing/AnsiColorCode.cs → Terminal.Gui/Drawing/Color/AnsiColorCode.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.ColorExtensions.cs → Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.ColorName.cs → Terminal.Gui/Drawing/Color/Color.ColorName.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.ColorParseException.cs → Terminal.Gui/Drawing/Color/Color.ColorParseException.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.Formatting.cs → Terminal.Gui/Drawing/Color/Color.Formatting.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.Operators.cs → Terminal.Gui/Drawing/Color/Color.Operators.cs


+ 0 - 0
Terminal.Gui/Drawing/Color.cs → Terminal.Gui/Drawing/Color/Color.cs


+ 0 - 0
Terminal.Gui/Drawing/ColorEventArgs.cs → Terminal.Gui/Drawing/Color/ColorEventArgs.cs


+ 0 - 0
Terminal.Gui/Drawing/ColorModel.cs → Terminal.Gui/Drawing/Color/ColorModel.cs


+ 0 - 0
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs → Terminal.Gui/Drawing/Color/ColorQuantizer.cs


+ 0 - 0
Terminal.Gui/Drawing/ColorScheme.Colors.cs → Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs


+ 0 - 0
Terminal.Gui/Drawing/ColorScheme.cs → Terminal.Gui/Drawing/Color/ColorScheme.cs


+ 0 - 0
Terminal.Gui/Drawing/ColorStrings.cs → Terminal.Gui/Drawing/Color/ColorStrings.cs


+ 0 - 0
Terminal.Gui/Drawing/Quant/IColorDistance.cs → Terminal.Gui/Drawing/Color/IColorDistance.cs


+ 0 - 0
Terminal.Gui/Drawing/IColorNameResolver.cs → Terminal.Gui/Drawing/Color/IColorNameResolver.cs


+ 0 - 0
Terminal.Gui/Drawing/ICustomColorFormatter.cs → Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs


+ 0 - 0
Terminal.Gui/Drawing/W3CColors.cs → Terminal.Gui/Drawing/Color/W3CColors.cs


+ 0 - 15
Terminal.Gui/Drawing/ISixelSupportDetector.cs

@@ -1,15 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     Interface for detecting sixel support. Either through
-///     ansi requests to terminal or config file etc.
-/// </summary>
-public interface ISixelSupportDetector
-{
-    /// <summary>
-    ///     Gets the supported sixel state e.g. by sending Ansi escape sequences
-    ///     or from a config file etc.
-    /// </summary>
-    /// <returns>Description of sixel support.</returns>
-    public SixelSupportResult Detect ();
-}

+ 0 - 0
Terminal.Gui/Drawing/IntersectionDefinition.cs → Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs


+ 0 - 0
Terminal.Gui/Drawing/IntersectionRuneType.cs → Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs


+ 0 - 0
Terminal.Gui/Drawing/IntersectionType.cs → Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs


+ 0 - 0
Terminal.Gui/Drawing/LineCanvas.cs → Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs


+ 0 - 0
Terminal.Gui/Drawing/LineStyle.cs → Terminal.Gui/Drawing/LineCanvas/LineStyle.cs


+ 0 - 0
Terminal.Gui/Drawing/StraightLine.cs → Terminal.Gui/Drawing/LineCanvas/StraightLine.cs


+ 0 - 0
Terminal.Gui/Drawing/StraightLineExtensions.cs → Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs


+ 4 - 4
Terminal.Gui/Drawing/SixelEncoder.cs → Terminal.Gui/Drawing/Sixel/SixelEncoder.cs

@@ -47,11 +47,11 @@ public class SixelEncoder
     /// <returns></returns>
     public string EncodeSixel (Color [,] pixels)
     {
-        const string start = "\u001bP"; // Start sixel sequence
+        const string START = "\u001bP"; // Start sixel sequence
 
         string defaultRatios = AnyHasAlphaOfZero (pixels) ? "0;1;0" : "0;0;0"; // Defaults for aspect ratio and grid size
-        const string completeStartSequence = "q"; // Signals beginning of sixel image data
-        const string noScaling = "\"1;1;"; // no scaling factors (1x1);
+        const string COMPLETE_START_SEQUENCE = "q"; // Signals beginning of sixel image data
+        const string NO_SCALING = "\"1;1;"; // no scaling factors (1x1);
 
         string fillArea = GetFillArea (pixels);
 
@@ -61,7 +61,7 @@ public class SixelEncoder
 
         const string terminator = "\u001b\\"; // End sixel sequence
 
-        return start + defaultRatios + completeStartSequence + noScaling + fillArea + pallette + pixelData + terminator;
+        return START + defaultRatios + COMPLETE_START_SEQUENCE + NO_SCALING + fillArea + pallette + pixelData + terminator;
     }
 
     private string WriteSixel (Color [,] pixels)

+ 165 - 0
Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs

@@ -0,0 +1,165 @@
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Uses Ansi escape sequences to detect whether sixel is supported
+///     by the terminal.
+/// </summary>
+public class SixelSupportDetector
+{
+    /// <summary>
+    ///     Sends Ansi escape sequences to the console to determine whether
+    ///     sixel is supported (and <see cref="SixelSupportResult.Resolution"/>
+    ///     etc).
+    /// </summary>
+    /// <returns>
+    ///     Description of sixel support, may include assumptions where
+    ///     expected response codes are not returned by console.
+    /// </returns>
+    public void Detect (Action<SixelSupportResult> resultCallback)
+    {
+        var result = new SixelSupportResult ();
+        result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
+        IsSixelSupportedByDar (result, resultCallback);
+    }
+
+    private void TryGetResolutionDirectly (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
+    {
+        // Expect something like:
+        //<esc>[6;20;10t
+        QueueRequest (
+                      EscSeqUtils.CSI_RequestSixelResolution,
+                      r =>
+                      {
+                          // Terminal supports directly responding with resolution
+                          Match match = Regex.Match (r, @"\[\d+;(\d+);(\d+)t$");
+
+                          if (match.Success)
+                          {
+                              if (int.TryParse (match.Groups [1].Value, out int ry) && int.TryParse (match.Groups [2].Value, out int rx))
+                              {
+                                  result.Resolution = new (rx, ry);
+                              }
+                          }
+
+                          // Finished
+                          resultCallback.Invoke (result);
+                      },
+
+                      // Request failed, so try to compute instead
+                      () => TryComputeResolution (result, resultCallback));
+    }
+
+    private void TryComputeResolution (SixelSupportResult result, Action<SixelSupportResult> 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);
+                                        },
+                                        () => resultCallback (result));
+                      },
+                      () => resultCallback (result));
+    }
+
+    private void ComputeResolution (SixelSupportResult result, string windowSize, string sizeInChars)
+    {
+        // Fallback to window size in pixels and characters
+        // Example [4;600;1200t
+        Match pixelMatch = Regex.Match (windowSize, @"\[\d+;(\d+);(\d+)t$");
+
+        // Example [8;30;120t
+        Match charMatch = Regex.Match (sizeInChars, @"\[\d+;(\d+);(\d+)t$");
+
+        if (pixelMatch.Success && charMatch.Success)
+        {
+            // Extract pixel dimensions
+            if (int.TryParse (pixelMatch.Groups [1].Value, out int pixelHeight)
+                && int.TryParse (pixelMatch.Groups [2].Value, out int pixelWidth)
+                &&
+
+                // Extract character dimensions
+                int.TryParse (charMatch.Groups [1].Value, out int charHeight)
+                && int.TryParse (charMatch.Groups [2].Value, out int 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
+                result.Resolution = new (cellWidth, cellHeight);
+            }
+        }
+    }
+
+    private void IsSixelSupportedByDar (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
+    {
+        QueueRequest (
+                      EscSeqUtils.CSI_SendDeviceAttributes,
+                      r =>
+                      {
+                          result.IsSupported = ResponseIndicatesSupport (r);
+
+                          if (result.IsSupported)
+                          {
+                              TryGetResolutionDirectly (result, resultCallback);
+                          }
+                          else
+                          {
+                              resultCallback (result);
+                          }
+                      },
+                      () => resultCallback (result));
+    }
+
+    private static void QueueRequest (AnsiEscapeSequence req, Action<string> responseCallback, Action abandoned)
+    {
+        var newRequest = new AnsiEscapeSequenceRequest
+        {
+            Request = req.Request,
+            Terminator = req.Terminator,
+            ResponseReceived = responseCallback,
+            Abandoned = abandoned
+        };
+
+        Application.Driver?.QueueAnsiRequest (newRequest);
+    }
+
+    private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); }
+
+    private static bool IsWindowsTerminal ()
+    {
+        return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION"));
+
+        ;
+    }
+
+    private static bool IsXtermWithTransparency ()
+    {
+        // Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
+        string xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
+
+        // If XTERM_VERSION exists, we are in a real xterm
+        if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out int xtermVersion) && xtermVersion >= 370)
+        {
+            return true;
+        }
+
+        return false;
+    }
+}

+ 1 - 1
Terminal.Gui/Drawing/SixelSupportResult.cs → Terminal.Gui/Drawing/Sixel/SixelSupportResult.cs

@@ -2,7 +2,7 @@
 
 /// <summary>
 ///     Describes the discovered state of sixel support and ancillary information
-///     e.g. <see cref="Resolution"/>. You can use any <see cref="ISixelSupportDetector"/>
+///     e.g. <see cref="Resolution"/>. You can use any <see cref="SixelSupportDetector"/>
 ///     to discover this information.
 /// </summary>
 public class SixelSupportResult

+ 0 - 0
Terminal.Gui/Drawing/SixelToRender.cs → Terminal.Gui/Drawing/Sixel/SixelToRender.cs


+ 0 - 133
Terminal.Gui/Drawing/SixelSupportDetector.cs

@@ -1,133 +0,0 @@
-using System.Text.RegularExpressions;
-
-namespace Terminal.Gui;
-/* TODO : Depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
-/// <summary>
-///     Uses Ansi escape sequences to detect whether sixel is supported
-///     by the terminal.
-/// </summary>
-public class SixelSupportDetector : ISixelSupportDetector
-{
-    /// <summary>
-    /// Sends Ansi escape sequences to the console to determine whether
-    /// sixel is supported (and <see cref="SixelSupportResult.Resolution"/>
-    /// etc).
-    /// </summary>
-    /// <returns>Description of sixel support, may include assumptions where
-    /// expected response codes are not returned by console.</returns>
-    public SixelSupportResult Detect ()
-    {
-        var result = new SixelSupportResult ();
-
-        result.IsSupported = IsSixelSupportedByDar ();
-
-        if (result.IsSupported)
-        {
-            if (TryGetResolutionDirectly (out var res))
-            {
-                result.Resolution = res;
-            }
-            else if(TryComputeResolution(out res))
-            {
-                result.Resolution = res;
-            }
-
-            result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
-        }
-
-        return result;
-    }
-
-
-    private bool TryGetResolutionDirectly (out Size resolution)
-    {
-        // Expect something like:
-        //<esc>[6;20;10t
-
-        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$");
-
-            if (match.Success)
-            {
-                if (int.TryParse (match.Groups [1].Value, out var ry) &&
-                    int.TryParse (match.Groups [2].Value, out var rx))
-                {
-                    resolution = new Size (rx, ry);
-
-                    return true;
-                }
-            }
-        }
-
-        resolution = default;
-        return false;
-    }
-
-
-    private bool TryComputeResolution (out Size resolution)
-    {
-        // 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))
-        {
-            // Example [4;600;1200t
-            var pixelMatch = Regex.Match (pixelSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$");
-
-            // 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;
-    }
-    private bool IsSixelSupportedByDar ()
-    {
-        return AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes, out AnsiEscapeSequenceResponse darResponse)
-            ? darResponse.Response.Split (';').Contains ("4")
-            : false;
-    }
-
-    private bool IsWindowsTerminal ()
-    {
-        return  !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable ("WT_SESSION"));;
-    }
-    private bool IsXtermWithTransparency ()
-    {
-        // Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
-        var xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
-
-        // If XTERM_VERSION exists, we are in a real xterm
-        if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out var xtermVersion) && xtermVersion >= 370)
-        {
-            return true;
-        }
-
-        return false;
-    }
-}*/

+ 400 - 0
UICatalog/Scenarios/AnsiRequestsScenario.cs

@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("AnsiEscapeSequenceRequest", "Ansi Escape Sequence Request")]
+[ScenarioCategory ("Ansi Escape Sequence")]
+public sealed class AnsiEscapeSequenceRequests : Scenario
+{
+    private GraphView _graphView;
+
+    private ScatterSeries _sentSeries;
+    private ScatterSeries _answeredSeries;
+
+    private readonly List<DateTime> _sends = new ();
+
+    private readonly object _lockAnswers = new object ();
+    private readonly Dictionary<DateTime, string> _answers = new ();
+    private Label _lblSummary;
+
+    public override void Main ()
+    {
+        // Init
+        Application.Init ();
+
+        TabView tv = new TabView
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            CanFocus = true
+        };
+
+        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 (),
+            CanFocus = true
+        };
+
+        w.Padding.Thickness = new (1);
+
+        var scrRequests = new List<string>
+        {
+            "CSI_SendDeviceAttributes",
+            "CSI_ReportTerminalSizeInChars",
+            "CSI_RequestCursorPositionReport",
+            "CSI_SendDeviceAttributes2"
+        };
+        // TODO: This UI would be cleaner/less rigid if Pos.Align were used
+        var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper<string> (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];
+                                              AnsiEscapeSequence 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);
+
+        btnResponse.Accepting += (s, e) =>
+                                 {
+                                     var ansiEscapeSequenceRequest = new AnsiEscapeSequence
+                                     {
+                                         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),
+                                                                              Abandoned =()=> OnFail (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 (TextView tvResponse, TextView tvError, TextView tvValue, TextView tvTerminator, Label lblSuccess)
+    {
+        tvResponse.Text = string.Empty;
+        tvError.Text = "No Response";
+        tvValue.Text = string.Empty;
+        tvTerminator.Text = string.Empty;
+
+        lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
+        lblSuccess.Text = "Error";
+    }
+
+    private View BuildBulkTab ()
+    {
+        View w = new View ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            CanFocus = true
+        };
+
+        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 ()
+        };
+
+        Application.AddTimeout (
+                                TimeSpan.FromMilliseconds (1000),
+                                () =>
+                                {
+                                    lock (_lockAnswers)
+                                    {
+                                        UpdateGraph ();
+
+                                        UpdateResponses ();
+                                    }
+
+
+
+                                    return true;
+                                });
+
+        var tv = new TextView ()
+        {
+            Y = Pos.Bottom (lbl),
+            Width = Dim.Percent (50),
+            Height = Dim.Fill ()
+        };
+
+
+        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;
+            }
+        };
+        w.Add (cbDar);
+
+        int lastSendTime = Environment.TickCount;
+        object lockObj = new object ();
+        Application.AddTimeout (
+                                TimeSpan.FromMilliseconds (50),
+                                () =>
+                                {
+                                    lock (lockObj)
+                                    {
+                                        if (cbDar.Value > 0)
+                                        {
+                                            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
+                                            }
+                                        }
+                                    }
+
+                                    return true;
+                                });
+
+
+        _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 ();
+
+        w.Add (lbl);
+        w.Add (lblDar);
+        w.Add (cbDar);
+        w.Add (tv);
+        w.Add (_graphView);
+        w.Add (_lblSummary);
+
+        return w;
+    }
+    private void UpdateResponses ()
+    {
+        _lblSummary.Text = GetSummary ();
+        _lblSummary.SetNeedsDraw();
+    }
+
+    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.BrightRed, 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.SetNeedsDraw();
+
+    }
+
+    private int ToSeconds (DateTime t)
+    {
+        return (int)(DateTime.Now - t).TotalSeconds;
+    }
+
+    private void SendDar ()
+    {
+        Application.Driver.QueueAnsiRequest (
+                                             new ()
+                                             {
+                                                 Request = EscSeqUtils.CSI_SendDeviceAttributes.Request,
+                                                 Terminator = EscSeqUtils.CSI_SendDeviceAttributes.Terminator,
+                                                 ResponseReceived = HandleResponse
+                                             });
+        _sends.Add (DateTime.Now);
+    }
+
+    private void HandleResponse (string response)
+    {
+        lock (_lockAnswers)
+        {
+            _answers.Add (DateTime.Now, response);
+        }
+    }
+}

+ 32 - 19
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 ();
+    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;
@@ -96,7 +96,7 @@ public class Images : Scenario
         };
         _win.Add (cbSupportsTrueColor);
 
-        var cbSupportsSixel = new CheckBox
+        _cbSupportsSixel = new()
         {
             X = Pos.Right (lblDriverName) + 2,
             Y = 1,
@@ -104,26 +104,25 @@ public class Images : Scenario
             Text = "Supports Sixel"
         };
 
-        var lblSupportsSixel = new Label ()
+        var lblSupportsSixel = new Label
         {
-
             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 ();
-                                                };
 
-        _win.Add (cbSupportsSixel);
+        _cbSupportsSixel.CheckedStateChanging += (s, e) =>
+                                                 {
+                                                     _sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked;
+                                                     SetupSixelSupported (e.NewValue == CheckState.Checked);
+                                                     ApplyShowTabViewHack ();
+                                                 };
+
+        _win.Add (_cbSupportsSixel);
 
         var cbUseTrueColor = new CheckBox
         {
@@ -150,17 +149,31 @@ public class Images : Scenario
         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;
@@ -295,7 +308,7 @@ public class Images : Scenario
     {
         // TODO HACK: This hack seems to be required to make tabview actually refresh itself
         _tabView.SetNeedsDraw ();
-        var orig = _tabView.SelectedTab;
+        Tab orig = _tabView.SelectedTab;
         _tabView.SelectedTab = _tabView.Tabs.Except (new [] { orig }).ElementAt (0);
         _tabView.SelectedTab = orig;
     }

+ 236 - 0
UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs

@@ -0,0 +1,236 @@
+using Moq;
+
+namespace UnitTests.ConsoleDrivers;
+
+
+public class AnsiRequestSchedulerTests
+{
+    private readonly Mock<IAnsiResponseParser> _parserMock;
+    private readonly AnsiRequestScheduler _scheduler;
+
+    private static DateTime _staticNow; // Static value to hold the current time
+
+    public AnsiRequestSchedulerTests ()
+    {
+        _parserMock = new Mock<IAnsiResponseParser> (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<Action<string>> (), null, 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 ();
+    }
+
+
+    [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<Action<string>> (), null, 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<Action<string>> (), null, 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 ();
+    }
+
+    [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<Action<string>> (), null, 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 ();
+    }
+
+    [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<Action<string>> (), null, 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)
+    {
+        // This simulates the passing of time by setting the Now function to return a specific time.
+        var newNow = _staticNow.AddMilliseconds (milliseconds);
+        _scheduler.Now = () => newNow;
+    }
+}

+ 637 - 0
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs

@@ -0,0 +1,637 @@
+using System.Diagnostics;
+using System.Text;
+using Xunit.Abstractions;
+
+namespace UnitTests.ConsoleDrivers;
+public class AnsiResponseParserTests (ITestOutputHelper output)
+{
+    AnsiResponseParser<int> _parser1 = new AnsiResponseParser<int> ();
+    AnsiResponseParser _parser2 = new AnsiResponseParser ();
+
+    /// <summary>
+    /// Used for the T value in batches that are passed to the  AnsiResponseParser&lt;int&gt;  (parser1)
+    /// </summary>
+    private int tIndex = 0;
+
+    [Fact]
+    public void TestInputProcessing ()
+    {
+        string ansiStream = "\u001b[<0;10;20M" +   // ANSI escape for mouse move at (10, 20)
+                            "Hello" +             // User types "Hello"
+                            "\u001b[0c";            // Device Attributes response (e.g., terminal identification i.e. DAR)
+
+
+        string response1 = null;
+        string response2 = null;
+
+        int i = 0;
+
+        // Imagine that we are expecting a DAR
+        _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
+
+        for (int c = 0; c < "[<0;10;20".Length; c++)
+        {
+            AssertConsumed (ansiStream, ref i);
+        }
+
+        // We see the M terminator
+        AssertReleased (ansiStream, ref i, "\u001b[<0;10;20M");
+
+        // Regular user typing
+        for (int c = 0; c < "Hello".Length; c++)
+        {
+            AssertIgnored (ansiStream,"Hello"[c], ref i);
+        }
+
+        // Now we have entered the actual DAR we should be consuming these
+        for (int c = 0; c < "\u001b[0".Length; c++)
+        {
+            AssertConsumed (ansiStream, ref i);
+        }
+
+        // Consume the terminator 'c' and expect this to call the above event
+        Assert.Null (response1);
+        Assert.Null (response1);
+        AssertConsumed (ansiStream, ref i);
+        Assert.NotNull (response2);
+        Assert.Equal ("\u001b[0c", response2);
+        Assert.NotNull (response2);
+        Assert.Equal ("\u001b[0c", response2);
+    }
+
+    [Theory]
+    [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 ("\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 ("\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 ("\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\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 ("\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)
+    {
+        var swGenBatches = Stopwatch.StartNew ();
+        int tests = 0;
+
+        var permutations = GetBatchPermutations (ansiStream,5).ToArray ();
+
+        swGenBatches.Stop ();
+        var swRunTest = Stopwatch.StartNew ();
+
+        foreach (var batchSet in permutations)
+        {
+            tIndex = 0;
+            string response1 = string.Empty;
+            string response2 = string.Empty;
+
+            // Register the expected response with the given terminator
+            _parser1.ExpectResponse (expectedTerminator, s => response1 = s, null, false);
+            _parser2.ExpectResponse (expectedTerminator, s => response2 = s, null, false);
+
+            // Process the input
+            StringBuilder actualOutput1 = new StringBuilder ();
+            StringBuilder actualOutput2 = new StringBuilder ();
+
+            foreach (var batch in batchSet)
+            {
+                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, actualOutput1.ToString());
+            Assert.Equal (expectedResponse, response1);
+            Assert.Equal (expectedOutput, actualOutput2.ToString ());
+            Assert.Equal (expectedResponse, response2);
+            tests++;
+        }
+
+        output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)" );
+    }
+
+    public static IEnumerable<object []> TestInputSequencesExact_Cases ()
+    {
+        yield return
+        [
+            "Esc Only",
+            null,
+            new []
+            {
+                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty)
+            }
+        ];
+
+        yield return
+        [
+            "Esc Hi with intermediate",
+            'c',
+            new []
+            {
+                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),
+            }
+        ];
+    }
+
+    public class StepExpectation ()
+    {
+        /// <summary>
+        /// The input character to feed into the parser at this step of the test
+        /// </summary>
+        public char Input { get; }
+
+        /// <summary>
+        /// What should the state of the parser be after the <see cref="Input"/>
+        /// is fed in.
+        /// </summary>
+        public AnsiResponseParserState ExpectedStateAfterOperation { get; }
+
+        /// <summary>
+        /// If this step should release one or more characters, put them here.
+        /// </summary>
+        public string ExpectedRelease { get; } = string.Empty;
+
+        /// <summary>
+        /// If this step should result in a completing of detection of ANSI response
+        /// then put the expected full response sequence here.
+        /// </summary>
+        public string ExpectedAnsiResponse { get; } = string.Empty;
+
+        public StepExpectation (
+            char input,
+            AnsiResponseParserState 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<StepExpectation> 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,null, false);
+        }
+        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 ()
+    {
+        string input = "\u001b";
+        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 (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);
+
+        AssertManualReleaseIs (input);
+    }
+
+
+    [Fact]
+    public void TwoExcapesInARow ()
+    {
+        // Example user presses Esc key then a DAR comes in
+        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, "\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 ("\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 (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 (AnsiResponseParserState.Normal, _parser1.State);
+        Assert.Equal (AnsiResponseParserState.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);
+    }
+
+    [Fact]
+    public void TestLateResponses ()
+    {
+        var p = new AnsiResponseParser ();
+
+        string responseA = null;
+        string responseB = null;
+
+        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, 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"));
+        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"));
+
+    }
+
+    [Fact]
+    public void TestPersistentResponses ()
+    {
+        var p = new AnsiResponseParser ();
+
+        int m = 0;
+        int M = 1;
+
+        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`
+        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
+    }
+
+    [Fact]
+    public void TestPersistentResponses_WithMetadata ()
+    {
+        var p = new AnsiResponseParser<int> ();
+
+        int m = 0;
+
+        var result = new List<Tuple<char,int>> ();
+
+        p.ExpectResponseT ("m", (r) =>
+                                {
+                                    result = r.ToList ();
+                                    m++;
+                                },
+                           null, true);
+
+        // Act - Feed input strings containing ANSI sequences
+        p.ProcessInput (StringToBatch("\u001b[<0;10;10m"));  // Should match and increment `m`
+
+        // Prepare expected result: 
+        var expected = new List<Tuple<char, int>>
+        {
+            Tuple.Create('\u001b', 0), // Escape character
+            Tuple.Create('[', 1),
+            Tuple.Create('<', 2),
+            Tuple.Create('0', 3),
+            Tuple.Create(';', 4),
+            Tuple.Create('1', 5),
+            Tuple.Create('0', 6),
+            Tuple.Create(';', 7),
+            Tuple.Create('1', 8),
+            Tuple.Create('0', 9),
+            Tuple.Create('m', 10)
+        };
+
+        Assert.Equal (expected.Count, result.Count); // Ensure the count is as expected
+        Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
+    }
+
+    [Fact]
+    public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo ()
+    {
+        // Swallow all unknown escape codes
+        _parser1.UnexpectedResponseHandler = _ => 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);
+    }
+
+    [Fact]
+    public void UnknownResponses_ParameterShouldMatch ()
+    {
+        int i = 0;
+
+        // Track unknown responses passed to the UnexpectedResponseHandler
+        var unknownResponses = new List<string> ();
+
+        // 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<string>
+        {
+            "\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<char, int> [] StringToBatch (string batch)
+    {
+        return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
+    }
+
+    public static IEnumerable<string []> GetBatchPermutations (string input, int maxDepth = 3)
+    {
+        // Call the recursive method to generate batches with an initial depth of 0
+        return GenerateBatches (input, 0, maxDepth, 0);
+    }
+
+    private static IEnumerable<string []> 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)
+        {
+            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, 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];
+                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 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 ( 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 c2 = ansiStream [i];
+        var c1 = NextChar (ansiStream, ref i);
+
+        Assert.Empty (_parser1.ProcessInput(c1));
+        Assert.Empty (_parser2.ProcessInput (c2.ToString()));
+    }
+
+    /// <summary>
+    /// Overload that fully exhausts <paramref name="ansiStream"/> and asserts
+    /// that the final released content across whole processing is <paramref name="expectedRelease"/>
+    /// </summary>
+    /// <param name="ansiStream"></param>
+    /// <param name="expectedRelease"></param>
+    /// <param name="expectedTValues"></param>
+    private void AssertReleased (string ansiStream, string expectedRelease, params int [] expectedTValues)
+    {
+        var sb = new StringBuilder ();
+        var tValues = new List<int> ();
+
+        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));
+        }
+    }
+
+    /// <summary>
+    /// Asserts that <paramref name="i"/> index of <see cref="ansiStream"/> when consumed will release
+    /// <paramref name="expectedRelease"/>. Results in implicit increment of <paramref name="i"/>.
+    /// <remarks>Note that this does NOT iteratively consume all the stream, only 1 char at <paramref name="i"/></remarks>
+    /// </summary>
+    /// <param name="ansiStream"></param>
+    /// <param name="i"></param>
+    /// <param name="expectedRelease"></param>
+    /// <param name="expectedTValues"></param>
+    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
+        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 ()));
+    }
+
+    private string BatchToString (IEnumerable<Tuple<char, int>> processInput)
+    {
+        return new string(processInput.Select (a=>a.Item1).ToArray ());
+    }
+
+    private Tuple<char,int>[] 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 (AnsiResponseParserState.Normal, _parser1.State);
+        Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
+    }
+}