|
@@ -2,45 +2,31 @@
|
|
|
|
|
|
namespace Terminal.Gui;
|
|
|
|
|
|
- // Enum to manage the parser's state
|
|
|
- public enum ParserState
|
|
|
- {
|
|
|
- Normal,
|
|
|
- ExpectingBracket,
|
|
|
- InResponse
|
|
|
- }
|
|
|
-
|
|
|
- public interface IAnsiResponseParser
|
|
|
- {
|
|
|
- void ExpectResponse (string terminator, Action<string> response);
|
|
|
- }
|
|
|
+internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
|
|
+{
|
|
|
+ protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
|
|
|
+ private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
|
|
|
|
|
|
- internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
|
|
+ // Current state of the parser
|
|
|
+ public AnsiResponseParserState State
|
|
|
{
|
|
|
- protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
|
|
|
- private ParserState _state = ParserState.Normal;
|
|
|
-
|
|
|
- // Current state of the parser
|
|
|
- public ParserState State
|
|
|
+ get => _state;
|
|
|
+ protected set
|
|
|
{
|
|
|
- get => _state;
|
|
|
- protected set
|
|
|
- {
|
|
|
- StateChangedAt = DateTime.Now;
|
|
|
- _state = value;
|
|
|
- }
|
|
|
+ StateChangedAt = DateTime.Now;
|
|
|
+ _state = value;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// When <see cref="State"/> was last changed.
|
|
|
- /// </summary>
|
|
|
- public DateTime StateChangedAt { get; private set; } = DateTime.Now;
|
|
|
-
|
|
|
- protected readonly HashSet<char> _knownTerminators = new ();
|
|
|
+ /// <summary>
|
|
|
+ /// When <see cref="State"/> was last changed.
|
|
|
+ /// </summary>
|
|
|
+ public DateTime StateChangedAt { get; private set; } = DateTime.Now;
|
|
|
|
|
|
- public AnsiResponseParserBase ()
|
|
|
- {
|
|
|
+ protected readonly HashSet<char> _knownTerminators = new ();
|
|
|
|
|
|
+ public AnsiResponseParserBase ()
|
|
|
+ {
|
|
|
// These all are valid terminators on ansi responses,
|
|
|
// see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
|
|
|
_knownTerminators.Add ('@');
|
|
@@ -98,206 +84,217 @@ namespace Terminal.Gui;
|
|
|
_knownTerminators.Add ('x');
|
|
|
_knownTerminators.Add ('y');
|
|
|
_knownTerminators.Add ('z');
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- protected void ResetState ()
|
|
|
- {
|
|
|
- State = ParserState.Normal;
|
|
|
- ClearHeld ();
|
|
|
- }
|
|
|
+ protected void ResetState ()
|
|
|
+ {
|
|
|
+ State = AnsiResponseParserState.Normal;
|
|
|
+ ClearHeld ();
|
|
|
+ }
|
|
|
|
|
|
- public abstract void ClearHeld ();
|
|
|
- protected abstract string HeldToString ();
|
|
|
- protected abstract IEnumerable<object> HeldToObjects ();
|
|
|
- protected abstract void AddToHeld (object o);
|
|
|
-
|
|
|
- /// <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
|
|
|
+ public abstract void ClearHeld ();
|
|
|
+ protected abstract string HeldToString ();
|
|
|
+ protected abstract IEnumerable<object> HeldToObjects ();
|
|
|
+ protected abstract void AddToHeld (object o);
|
|
|
+
|
|
|
+ /// <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)
|
|
|
- {
|
|
|
- var currentChar = getCharAtIndex (index);
|
|
|
- var currentObj = getObjectAtIndex (index);
|
|
|
-
|
|
|
- bool isEscape = currentChar == '\x1B';
|
|
|
-
|
|
|
- switch (State)
|
|
|
- {
|
|
|
- case ParserState.Normal:
|
|
|
- if (isEscape)
|
|
|
- {
|
|
|
- // Escape character detected, move to ExpectingBracket state
|
|
|
- State = ParserState.ExpectingBracket;
|
|
|
- AddToHeld (currentObj); // Hold the escape character
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Normal character, append to output
|
|
|
- appendOutput (currentObj);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case ParserState.ExpectingBracket:
|
|
|
- if (isEscape)
|
|
|
- {
|
|
|
- // Second escape so we must release first
|
|
|
- ReleaseHeld (appendOutput, ParserState.ExpectingBracket);
|
|
|
- AddToHeld (currentObj); // Hold the new escape
|
|
|
- }
|
|
|
- else
|
|
|
- if (currentChar == '[')
|
|
|
- {
|
|
|
- // Detected '[', transition to InResponse state
|
|
|
- State = ParserState.InResponse;
|
|
|
- AddToHeld (currentObj); // Hold the '['
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Invalid sequence, release held characters and reset to Normal
|
|
|
- ReleaseHeld (appendOutput);
|
|
|
- appendOutput (currentObj); // Add current character
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case ParserState.InResponse:
|
|
|
- AddToHeld (currentObj);
|
|
|
-
|
|
|
- // Check if the held content should be released
|
|
|
- if (ShouldReleaseHeldContent ())
|
|
|
- {
|
|
|
- ReleaseHeld (appendOutput);
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- index++;
|
|
|
- }
|
|
|
- }
|
|
|
+ while (index < inputLength)
|
|
|
+ {
|
|
|
+ char currentChar = getCharAtIndex (index);
|
|
|
+ object currentObj = getObjectAtIndex (index);
|
|
|
|
|
|
+ bool isEscape = currentChar == '\x1B';
|
|
|
|
|
|
- private void ReleaseHeld (Action<object> appendOutput, ParserState newState = ParserState.Normal)
|
|
|
- {
|
|
|
- foreach (var o in HeldToObjects ())
|
|
|
+ switch (State)
|
|
|
{
|
|
|
- appendOutput (o);
|
|
|
+ case AnsiResponseParserState.Normal:
|
|
|
+ if (isEscape)
|
|
|
+ {
|
|
|
+ // Escape character detected, move to ExpectingBracket state
|
|
|
+ State = AnsiResponseParserState.ExpectingBracket;
|
|
|
+ AddToHeld (currentObj); // Hold the escape character
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Normal character, append to output
|
|
|
+ appendOutput (currentObj);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case AnsiResponseParserState.ExpectingBracket:
|
|
|
+ if (isEscape)
|
|
|
+ {
|
|
|
+ // Second escape so we must release first
|
|
|
+ ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket);
|
|
|
+ AddToHeld (currentObj); // Hold the new escape
|
|
|
+ }
|
|
|
+ else if (currentChar == '[')
|
|
|
+ {
|
|
|
+ // Detected '[', transition to InResponse state
|
|
|
+ State = AnsiResponseParserState.InResponse;
|
|
|
+ AddToHeld (currentObj); // Hold the '['
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Invalid sequence, release held characters and reset to Normal
|
|
|
+ ReleaseHeld (appendOutput);
|
|
|
+ appendOutput (currentObj); // Add current character
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case AnsiResponseParserState.InResponse:
|
|
|
+ AddToHeld (currentObj);
|
|
|
+
|
|
|
+ // Check if the held content should be released
|
|
|
+ if (ShouldReleaseHeldContent ())
|
|
|
+ {
|
|
|
+ ReleaseHeld (appendOutput);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
}
|
|
|
|
|
|
- State = newState;
|
|
|
- ClearHeld ();
|
|
|
+ index++;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // Common response handler logic
|
|
|
- protected bool ShouldReleaseHeldContent ()
|
|
|
+ private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
|
|
|
+ {
|
|
|
+ foreach (object o in HeldToObjects ())
|
|
|
{
|
|
|
- string cur = HeldToString ();
|
|
|
+ appendOutput (o);
|
|
|
+ }
|
|
|
|
|
|
- // Check for expected responses
|
|
|
- (string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
|
|
|
+ State = newState;
|
|
|
+ ClearHeld ();
|
|
|
+ }
|
|
|
|
|
|
- if (matchingResponse.response != null)
|
|
|
- {
|
|
|
- DispatchResponse (matchingResponse.response);
|
|
|
- expectedResponses.Remove (matchingResponse);
|
|
|
- return false;
|
|
|
- }
|
|
|
+ // Common response handler logic
|
|
|
+ protected bool ShouldReleaseHeldContent ()
|
|
|
+ {
|
|
|
+ string cur = HeldToString ();
|
|
|
|
|
|
- if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
|
|
|
- {
|
|
|
- // Detected a response that was not expected
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // Check for expected responses
|
|
|
+ (string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
|
|
|
|
|
|
- return false; // Continue accumulating
|
|
|
- }
|
|
|
+ if (matchingResponse.response != null)
|
|
|
+ {
|
|
|
+ DispatchResponse (matchingResponse.response);
|
|
|
+ expectedResponses.Remove (matchingResponse);
|
|
|
|
|
|
+ return false;
|
|
|
+ }
|
|
|
|
|
|
- protected void DispatchResponse (Action<string> response)
|
|
|
+ if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
|
|
|
{
|
|
|
- response?.Invoke (HeldToString ());
|
|
|
- ResetState ();
|
|
|
+ // Detected a response that was not expected
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is completed.
|
|
|
- /// </summary>
|
|
|
- public void ExpectResponse (string terminator, Action<string> response) => expectedResponses.Add ((terminator, response));
|
|
|
+ return false; // Continue accumulating
|
|
|
}
|
|
|
|
|
|
- internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
|
|
+ protected void DispatchResponse (Action<string> response)
|
|
|
{
|
|
|
- private readonly List<Tuple<char, T>> held = new ();
|
|
|
+ response?.Invoke (HeldToString ());
|
|
|
+ ResetState ();
|
|
|
+ }
|
|
|
|
|
|
- public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
|
|
|
- {
|
|
|
- var 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;
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Registers a new expected ANSI response with a specific terminator and a callback for when the response is
|
|
|
+ /// completed.
|
|
|
+ /// </summary>
|
|
|
+ public void ExpectResponse (string terminator, Action<string> response) { expectedResponses.Add ((terminator, response)); }
|
|
|
+}
|
|
|
|
|
|
- public IEnumerable<Tuple<char, T>> Release ()
|
|
|
+internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
|
|
+{
|
|
|
+ private readonly List<Tuple<char, T>> held = new ();
|
|
|
+
|
|
|
+ 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 held.ToArray ())
|
|
|
{
|
|
|
- foreach (var h in held.ToArray ())
|
|
|
- {
|
|
|
- yield return h;
|
|
|
- }
|
|
|
- ResetState ();
|
|
|
+ yield return h;
|
|
|
}
|
|
|
|
|
|
- public override void ClearHeld () => held.Clear ();
|
|
|
-
|
|
|
- protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ());
|
|
|
+ ResetState ();
|
|
|
+ }
|
|
|
|
|
|
- protected override IEnumerable<object> HeldToObjects () => held;
|
|
|
+ public override void ClearHeld () { held.Clear (); }
|
|
|
|
|
|
- protected override void AddToHeld (object o) => held.Add ((Tuple<char, T>)o);
|
|
|
+ protected override string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
|
|
|
|
|
|
+ protected override IEnumerable<object> HeldToObjects () { return held; }
|
|
|
|
|
|
+ protected override void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
|
|
|
}
|
|
|
|
|
|
- internal class AnsiResponseParser : AnsiResponseParserBase
|
|
|
+internal class AnsiResponseParser : AnsiResponseParserBase
|
|
|
+{
|
|
|
+ private readonly StringBuilder held = new ();
|
|
|
+
|
|
|
+ public string ProcessInput (string input)
|
|
|
{
|
|
|
- private readonly StringBuilder held = new ();
|
|
|
+ var output = new StringBuilder ();
|
|
|
|
|
|
- public string ProcessInput (string input)
|
|
|
- {
|
|
|
- var output = new StringBuilder ();
|
|
|
- ProcessInputBase (
|
|
|
- i => input [i],
|
|
|
- i => input [i], // For string there is no T so object is same as char
|
|
|
- c => output.Append ((char)c),
|
|
|
- input.Length);
|
|
|
- return output.ToString ();
|
|
|
- }
|
|
|
- public string Release ()
|
|
|
- {
|
|
|
- var output = held.ToString ();
|
|
|
- ResetState ();
|
|
|
+ ProcessInputBase (
|
|
|
+ i => input [i],
|
|
|
+ i => input [i], // For string there is no T so object is same as char
|
|
|
+ c => output.Append ((char)c),
|
|
|
+ input.Length);
|
|
|
|
|
|
- return output;
|
|
|
- }
|
|
|
- public override void ClearHeld () => held.Clear ();
|
|
|
+ return output.ToString ();
|
|
|
+ }
|
|
|
+
|
|
|
+ public string Release ()
|
|
|
+ {
|
|
|
+ var output = held.ToString ();
|
|
|
+ ResetState ();
|
|
|
+
|
|
|
+ return output;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void ClearHeld () { held.Clear (); }
|
|
|
|
|
|
- protected override string HeldToString () => held.ToString ();
|
|
|
+ protected override string HeldToString () { return held.ToString (); }
|
|
|
|
|
|
- protected override IEnumerable<object> HeldToObjects () => held.ToString().Select(c => (object) c).ToArray ();
|
|
|
- protected override void AddToHeld (object o) => held.Append ((char)o);
|
|
|
- }
|
|
|
+ protected override IEnumerable<object> HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); }
|
|
|
+
|
|
|
+ protected override void AddToHeld (object o) { held.Append ((char)o); }
|
|
|
+}
|