tidyup and streamline naming

This commit is contained in:
tznind
2024-10-20 15:37:47 +01:00
parent f6cb3bb854
commit e0415e6584
5 changed files with 259 additions and 230 deletions

View File

@@ -2,45 +2,31 @@
namespace Terminal.Gui;
// Enum to manage the parser's state
public enum ParserState
{
Normal,
ExpectingBracket,
InResponse
}
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
protected readonly List<(string terminator, Action<string> response)> expectedResponses = new ();
private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
public interface IAnsiResponseParser
// Current state of the parser
public AnsiResponseParserState State
{
void ExpectResponse (string terminator, Action<string> response);
}
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
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;
/// <summary>
/// When <see cref="State"/> was last changed.
/// </summary>
public DateTime StateChangedAt { get; private set; } = DateTime.Now;
protected readonly HashSet<char> _knownTerminators = new ();
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 ();
}
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&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)
{
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++;
}
}
private void ReleaseHeld (Action<object> appendOutput, ParserState newState = ParserState.Normal)
{
foreach (var o in HeldToObjects ())
{
appendOutput (o);
}
State = newState;
ClearHeld ();
}
// Common response handler logic
protected bool ShouldReleaseHeldContent ()
{
string cur = HeldToString ();
// Check for expected responses
(string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
if (matchingResponse.response != null)
{
DispatchResponse (matchingResponse.response);
expectedResponses.Remove (matchingResponse);
return false;
}
if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
{
// Detected a response that was not expected
return true;
}
return false; // Continue accumulating
}
protected void DispatchResponse (Action<string> response)
{
response?.Invoke (HeldToString ());
ResetState ();
}
/// <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));
}
internal class AnsiResponseParser<T> : AnsiResponseParserBase
protected void ResetState ()
{
private readonly List<Tuple<char, T>> held = new ();
State = AnsiResponseParserState.Normal;
ClearHeld ();
}
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;
}
public abstract void ClearHeld ();
protected abstract string HeldToString ();
protected abstract IEnumerable<object> HeldToObjects ();
protected abstract void AddToHeld (object o);
public IEnumerable<Tuple<char, T>> Release ()
/// <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
)
{
var index = 0; // Tracks position in the input string
while (index < inputLength)
{
foreach (var h in held.ToArray ())
char currentChar = getCharAtIndex (index);
object currentObj = getObjectAtIndex (index);
bool isEscape = currentChar == '\x1B';
switch (State)
{
yield return h;
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;
}
ResetState ();
index++;
}
}
private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
{
foreach (object o in HeldToObjects ())
{
appendOutput (o);
}
public override void ClearHeld () => held.Clear ();
State = newState;
ClearHeld ();
}
protected override string HeldToString () => new string (held.Select (h => h.Item1).ToArray ());
// Common response handler logic
protected bool ShouldReleaseHeldContent ()
{
string cur = HeldToString ();
protected override IEnumerable<object> HeldToObjects () => held;
// Check for expected responses
(string terminator, Action<string> response) matchingResponse = expectedResponses.FirstOrDefault (r => cur.EndsWith (r.terminator));
protected override void AddToHeld (object o) => held.Add ((Tuple<char, T>)o);
if (matchingResponse.response != null)
{
DispatchResponse (matchingResponse.response);
expectedResponses.Remove (matchingResponse);
return false;
}
if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
{
// Detected a response that was not expected
return true;
}
return false; // Continue accumulating
}
protected void DispatchResponse (Action<string> response)
{
response?.Invoke (HeldToString ());
ResetState ();
}
/// <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)); }
}
internal class AnsiResponseParser : AnsiResponseParserBase
internal class AnsiResponseParser<T> : AnsiResponseParserBase
{
private readonly List<Tuple<char, T>> held = new ();
public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
{
private readonly StringBuilder held = new ();
List<Tuple<char, T>> output = new List<Tuple<char, T>> ();
public string ProcessInput (string input)
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 ())
{
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 ();
yield return h;
}
public string Release ()
{
var output = held.ToString ();
ResetState ();
return output;
}
public override void ClearHeld () => held.Clear ();
ResetState ();
}
protected override string HeldToString () => held.ToString ();
public override void ClearHeld () { held.Clear (); }
protected override IEnumerable<object> HeldToObjects () => held.ToString().Select(c => (object) c).ToArray ();
protected override void AddToHeld (object o) => held.Append ((char)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
{
private readonly StringBuilder held = new ();
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 ();
return output;
}
public override void ClearHeld () { held.Clear (); }
protected override string HeldToString () { return held.ToString (); }
protected override IEnumerable<object> HeldToObjects () { return held.ToString ().Select (c => (object)c).ToArray (); }
protected override void AddToHeld (object o) { held.Append ((char)o); }
}

View File

@@ -0,0 +1,8 @@
#nullable enable
namespace Terminal.Gui;
public interface IAnsiResponseParser
{
AnsiResponseParserState State { get; }
void ExpectResponse (string terminator, Action<string> response);
}

View File

@@ -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
}

View File

@@ -1566,7 +1566,7 @@ internal class WindowsDriver : ConsoleDriver
public IEnumerable<WindowsConsole.InputRecord> ShouldRelease ()
{
if (Parser.State == ParserState.ExpectingBracket &&
if (Parser.State == AnsiResponseParserState.ExpectingBracket &&
DateTime.Now - Parser.StateChangedAt > _escTimeout)
{
return Parser.Release ().Select (o => o.Item2);

View File

@@ -154,7 +154,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
null,
new []
{
new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty)
new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty)
}
];
@@ -164,13 +164,13 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
'c',
new []
{
new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty),
new StepExpectation ('H',ParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars
new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty),
new StepExpectation ('[',ParserState.InResponse,string.Empty),
new StepExpectation ('0',ParserState.InResponse,string.Empty),
new StepExpectation ('c',ParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response
new StepExpectation ('\u001b',ParserState.ExpectingBracket,string.Empty),
new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
new StepExpectation ('H',AnsiResponseParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars
new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
new StepExpectation ('[',AnsiResponseParserState.InResponse,string.Empty),
new StepExpectation ('0',AnsiResponseParserState.InResponse,string.Empty),
new StepExpectation ('c',AnsiResponseParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response
new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
}
];
}
@@ -186,7 +186,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
/// What should the state of the parser be after the <see cref="Input"/>
/// is fed in.
/// </summary>
public ParserState ExpectedStateAfterOperation { get; }
public AnsiResponseParserState ExpectedStateAfterOperation { get; }
/// <summary>
/// If this step should release one or more characters, put them here.
@@ -201,7 +201,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
public StepExpectation (
char input,
ParserState expectedStateAfterOperation,
AnsiResponseParserState expectedStateAfterOperation,
string expectedRelease = "",
string expectedAnsiResponse = "") : this ()
{
@@ -261,8 +261,8 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
AssertConsumed (input,ref i);
// We should know when the state changed
Assert.Equal (ParserState.ExpectingBracket, _parser1.State);
Assert.Equal (ParserState.ExpectingBracket, _parser2.State);
Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser1.State);
Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State);
Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date);
Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date);
@@ -300,14 +300,14 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
// First Esc gets grabbed
AssertConsumed (input, ref i); // Esc
Assert.Equal (ParserState.ExpectingBracket,_parser1.State);
Assert.Equal (ParserState.ExpectingBracket, _parser2.State);
Assert.Equal (AnsiResponseParserState.ExpectingBracket,_parser1.State);
Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State);
// Because next char is 'f' we do not see a bracket so release both
AssertReleased (input, ref i, "\u001bf", 0,1); // f
Assert.Equal (ParserState.Normal, _parser1.State);
Assert.Equal (ParserState.Normal, _parser2.State);
Assert.Equal (AnsiResponseParserState.Normal, _parser1.State);
Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
AssertReleased (input, ref i,"i",2);
AssertReleased (input, ref i, "s", 3);
@@ -428,7 +428,7 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
Assert.Equal (expectedRelease, _parser2.Release ());
Assert.Equal (ParserState.Normal, _parser1.State);
Assert.Equal (ParserState.Normal, _parser2.State);
Assert.Equal (AnsiResponseParserState.Normal, _parser1.State);
Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
}
}