#nullable enable
namespace Terminal.Gui;
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
protected object lockExpectedResponses = new object();
protected object lockState = new object ();
///
/// Responses we are expecting to come in.
///
protected readonly List expectedResponses = new ();
///
/// Collection of responses that we .
///
protected readonly List lateResponses = new ();
///
/// Responses that you want to look out for that will come in continuously e.g. mouse events.
/// Key is the terminator.
///
protected readonly List persistentExpectations = new ();
private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
// Current state of the parser
public AnsiResponseParserState State
{
get => _state;
protected set
{
StateChangedAt = DateTime.Now;
_state = value;
}
}
protected readonly IHeld heldContent;
///
/// When was last changed.
///
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 _knownTerminators = new (new []
{
'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
// No - N or O
'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z',
'^', '`', '~',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'l', 'm', 'n',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
});
protected AnsiResponseParserBase (IHeld heldContent)
{
this.heldContent = heldContent;
}
protected void ResetState ()
{
State = AnsiResponseParserState.Normal;
heldContent.ClearHeld ();
}
///
/// Processes an input collection of objects long.
/// You must provide the indexers to return the objects and the action to append
/// to output stream.
///
/// The character representation of element i of your input collection
/// The actual element in the collection (e.g. char or Tuple<char,T>)
///
/// Action to invoke when parser confirms an element of the current collection or a previous
/// call's collection should be appended to the current output (i.e. append to your output List/StringBuilder).
///
/// The total number of elements in your collection
protected void ProcessInputBase (
Func getCharAtIndex,
Func getObjectAtIndex,
Action appendOutput,
int inputLength
)
{
lock (lockState)
{
ProcessInputBaseImpl (getCharAtIndex, getObjectAtIndex, appendOutput, inputLength);
}
}
private void ProcessInputBaseImpl (
Func getCharAtIndex,
Func getObjectAtIndex,
Action appendOutput,
int inputLength
)
{
var index = 0; // Tracks position in the input string
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 appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
{
foreach (object o in heldContent.HeldToObjects ())
{
appendOutput (o);
}
State = newState;
heldContent.ClearHeld ();
}
// Common response handler logic
protected bool ShouldReleaseHeldContent ()
{
string cur = heldContent.HeldToString ();
lock (lockExpectedResponses)
{
// Look for an expected response for what is accumulated so far (since Esc)
if (MatchResponse (
cur,
expectedResponses,
invokeCallback: true,
removeExpectation: true))
{
return false;
}
// Also try looking for late requests - in which case we do not invoke but still swallow content to avoid corrupting downstream
if (MatchResponse (
cur,
lateResponses,
invokeCallback: false,
removeExpectation: true))
{
return false;
}
// Look for persistent requests
if (MatchResponse (
cur,
persistentExpectations,
invokeCallback: true,
removeExpectation: false))
{
return false;
}
}
// Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity)
// 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
var swallow = ShouldSwallowUnexpectedResponse ();
if (swallow)
{
heldContent.ClearHeld ();
// Do not send back to input stream
return false;
}
// Do release back to input stream
return true;
}
return false; // Continue accumulating
}
///
///
/// When overriden in a derived class, indicates whether the unexpected response
/// currently in should be released or swallowed.
/// Use this to enable default event for escape codes.
///
///
/// Note this is only called for complete responses.
/// Based on
///
///
protected abstract bool ShouldSwallowUnexpectedResponse ();
private bool MatchResponse (string cur, List collection, bool invokeCallback, bool removeExpectation)
{
// Check for expected responses
var 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;
}
///
public void ExpectResponse (string terminator, Action response, bool persistent)
{
lock (lockExpectedResponses)
{
if (persistent)
{
persistentExpectations.Add (new (terminator, (h) => response.Invoke (h.HeldToString ())));
}
else
{
expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ())));
}
}
}
///
public bool IsExpecting (string terminator)
{
lock (lockExpectedResponses)
{
// If any of the new terminator matches any existing terminators characters it's a collision so true.
return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any ());
}
}
///
public void StopExpecting (string terminator, bool persistent)
{
lock (lockExpectedResponses)
{
if (persistent)
{
persistentExpectations.RemoveAll (r => r.Matches (terminator));
}
else
{
var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
foreach (var r in removed)
{
expectedResponses.Remove (r);
lateResponses.Add (r);
}
}
}
}
}
internal class AnsiResponseParser : AnsiResponseParserBase
{
public AnsiResponseParser () : base (new GenericHeld ()) { }
///
public Func>, bool> UnexpectedResponseHandler { get; set; } = (_) => false;
public IEnumerable> ProcessInput (params Tuple [] input)
{
List> output = new List> ();
ProcessInputBase (
i => input [i].Item1,
i => input [i],
c => output.Add ((Tuple)c),
input.Length);
return output;
}
public IEnumerable> Release ()
{
// Lock in case Release is called from different Thread from parse
lock (lockState)
{
foreach (Tuple h in HeldToEnumerable())
{
yield return h;
}
ResetState ();
}
}
private IEnumerable> HeldToEnumerable ()
{
return (IEnumerable>)heldContent.HeldToObjects ();
}
///
/// 'Overload' for specifying an expectation that requires the metadata as well as characters. Has
/// a unique name because otherwise most lamdas will give ambiguous overload errors.
///
///
///
///
public void ExpectResponseT (string terminator, Action>> response, bool persistent)
{
lock (lockExpectedResponses)
{
if (persistent)
{
persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
}
else
{
expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
}
}
}
///
protected override bool ShouldSwallowUnexpectedResponse ()
{
return UnexpectedResponseHandler.Invoke (HeldToEnumerable ());
}
}
internal class AnsiResponseParser : AnsiResponseParserBase
{
///
///
/// Delegate for handling unrecognized escape codes. Default behaviour
/// is to return which simply releases the
/// characters back to input stream for downstream processing.
///
///
/// Implement a method to handle if you want and return if you want the
/// keystrokes 'swallowed' (i.e. not returned to input stream).
///
///
public Func UnknownResponseHandler { get; set; } = (_) => false;
public AnsiResponseParser () : base (new StringHeld ()) { }
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)
{
var output = heldContent.HeldToString ();
ResetState ();
return output;
}
}
///
protected override bool ShouldSwallowUnexpectedResponse ()
{
return UnknownResponseHandler.Invoke (heldContent.HeldToString ());
}
}