123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- #nullable enable
- namespace Terminal.Gui;
- internal abstract class AnsiResponseParserBase : IAnsiResponseParser
- {
- /// <summary>
- /// Responses we are expecting to come in.
- /// </summary>
- protected readonly List<AnsiResponseExpectation> expectedResponses = new ();
- /// <summary>
- /// Collection of responses that we <see cref="StopExpecting"/>.
- /// </summary>
- protected readonly List<AnsiResponseExpectation> lateResponses = new ();
- /// <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 = 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;
- /// <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 (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 ();
- }
- /// <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<char,T>)</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
- )
- {
- 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 ()
- {
- string cur = heldContent.HeldToString ();
- // Look for an expected response for what is accumulated so far (since Esc)
- if (MatchResponse (cur,
- expectedResponses,
- invokeCallback: true,
- removeExpectation: true))
- {
- 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))
- {
- // Detected a response that was not expected
- return true;
- }
- return false; // Continue accumulating
- }
- private bool MatchResponse (string cur, List<AnsiResponseExpectation> 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;
- }
- /// <inheritdoc />
- public void ExpectResponse (string terminator, Action<string> response, bool persistent)
- {
- if (persistent)
- {
- persistentExpectations.Add (new (terminator, (h)=>response.Invoke (h.HeldToString ())));
- }
- else
- {
- expectedResponses.Add (new (terminator, (h) => response.Invoke (h.HeldToString ())));
- }
- }
- /// <inheritdoc />
- public bool IsExpecting (string terminator)
- {
- // If any of the new terminator matches any existing terminators characters it's a collision so true.
- return expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any ());
- }
- /// <inheritdoc />
- public void StopExpecting (string terminator, bool persistent)
- {
- if (persistent)
- {
- persistentExpectations.RemoveAll (r => r.Matches (terminator));
- }
- else
- {
- var removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
- foreach (var r in removed)
- {
- expectedResponses.Remove (r);
- lateResponses.Add (r);
- }
- }
- }
- }
- internal class AnsiResponseParser<T> : AnsiResponseParserBase
- {
- public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
- public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
- {
- List<Tuple<char, T>> output = new List<Tuple<char, T>> ();
- ProcessInputBase (
- i => input [i].Item1,
- i => input [i],
- c => output.Add ((Tuple<char, T>)c),
- input.Length);
- return output;
- }
- public IEnumerable<Tuple<char, T>> Release ()
- {
- foreach (Tuple<char, T> h in HeldToEnumerable())
- {
- yield return h;
- }
- ResetState ();
- }
- 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="persistent"></param>
- public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char,T>>> response, bool persistent)
- {
- if (persistent)
- {
- persistentExpectations.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
- }
- else
- {
- expectedResponses.Add (new (terminator, (h) => response.Invoke (HeldToEnumerable ())));
- }
- }
- }
- internal class AnsiResponseParser : AnsiResponseParserBase
- {
- 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 ()
- {
- var output = heldContent.HeldToString ();
- ResetState ();
- return output;
- }
- }
|