Code review comments and cleanup

This commit is contained in:
Tig
2024-11-27 08:58:16 -07:00
parent 1480f1316b
commit 97558c2dbf
43 changed files with 304 additions and 250 deletions

View File

@@ -16,6 +16,7 @@ public class AnsiEscapeSequenceRequest
/// </summary>
public required string Request { get; init; }
// BUGBUG: Nullable issue
/// <summary>
/// Invoked when the console responds with an ANSI response code that matches the
/// <see cref="Terminator"/>

View File

@@ -14,8 +14,8 @@ public class AnsiRequestScheduler
private readonly IAnsiResponseParser _parser;
/// <summary>
/// Function for returning the current time. Use in unit tests to
/// ensure repeatable tests.
/// Function for returning the current time. Use in unit tests to
/// ensure repeatable tests.
/// </summary>
internal Func<DateTime> Now { get; set; }
@@ -54,6 +54,11 @@ public class AnsiRequestScheduler
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;
@@ -67,11 +72,9 @@ public class AnsiRequestScheduler
/// </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)
public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { return SendOrSchedule (request, true); }
private bool SendOrSchedule (AnsiEscapeSequenceRequest request, bool addToQueue)
{
if (CanSend (request, out ReasonCannotSend reason))
{
@@ -105,13 +108,13 @@ public class AnsiRequestScheduler
private void EvictStaleRequests ()
{
foreach (var stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
foreach (string stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
{
EvictStaleRequests (stale);
}
}
private bool IsStale (DateTime dt) => Now () - dt > _staleTimeout;
private bool IsStale (DateTime dt) { return Now () - dt > _staleTimeout; }
/// <summary>
/// Looks to see if the last time we sent <paramref name="withTerminator"/>
@@ -155,7 +158,7 @@ public class AnsiRequestScheduler
}
// Get oldest request
Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.MinBy (r=>r.Item2);
Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.MinBy (r => r.Item2);
if (opportunity != null)
{
@@ -163,6 +166,7 @@ public class AnsiRequestScheduler
if (SendOrSchedule (opportunity.Item1, false))
{
_queuedRequests.Remove (opportunity);
return true;
}
}
@@ -172,7 +176,6 @@ public class AnsiRequestScheduler
return false;
}
private void Send (AnsiEscapeSequenceRequest r)
{
_lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ());
@@ -210,23 +213,4 @@ public class AnsiRequestScheduler
return false;
}
}
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
}
}

View File

@@ -4,29 +4,29 @@ namespace Terminal.Gui;
internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
protected object lockExpectedResponses = new ();
protected object _lockExpectedResponses = new ();
protected object lockState = new ();
protected object _lockState = new ();
/// <summary>
/// Responses we are expecting to come in.
/// </summary>
protected readonly List<AnsiResponseExpectation> expectedResponses = new ();
protected readonly List<AnsiResponseExpectation> _expectedResponses = [];
/// <summary>
/// Collection of responses that we <see cref="StopExpecting"/>.
/// </summary>
protected readonly List<AnsiResponseExpectation> lateResponses = new ();
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 = new ();
protected readonly List<AnsiResponseExpectation> _persistentExpectations = [];
private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
/// <inheritdoc />
/// <inheritdoc/>
public AnsiResponseParserState State
{
get => _state;
@@ -37,7 +37,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
}
}
protected readonly IHeld heldContent;
protected readonly IHeld _heldContent;
/// <summary>
/// When <see cref="State"/> was last changed.
@@ -48,8 +48,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
// 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
@@ -58,14 +57,18 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
'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 AnsiResponseParserBase (IHeld heldContent) { _heldContent = heldContent; }
protected void ResetState ()
{
State = AnsiResponseParserState.Normal;
heldContent.ClearHeld ();
lock (_lockState)
{
_heldContent.ClearHeld ();
}
}
/// <summary>
@@ -87,7 +90,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
int inputLength
)
{
lock (lockState)
lock (_lockState)
{
ProcessInputBaseImpl (getCharAtIndex, getObjectAtIndex, appendOutput, inputLength);
}
@@ -116,7 +119,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
// Escape character detected, move to ExpectingBracket state
State = AnsiResponseParserState.ExpectingBracket;
heldContent.AddToHeld (currentObj); // Hold the escape character
_heldContent.AddToHeld (currentObj); // Hold the escape character
}
else
{
@@ -131,13 +134,13 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
// Second escape so we must release first
ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket);
heldContent.AddToHeld (currentObj); // Hold the new escape
_heldContent.AddToHeld (currentObj); // Hold the new escape
}
else if (currentChar == '[')
{
// Detected '[', transition to InResponse state
State = AnsiResponseParserState.InResponse;
heldContent.AddToHeld (currentObj); // Hold the '['
_heldContent.AddToHeld (currentObj); // Hold the '['
}
else
{
@@ -149,7 +152,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
break;
case AnsiResponseParserState.InResponse:
heldContent.AddToHeld (currentObj);
_heldContent.AddToHeld (currentObj);
// Check if the held content should be released
if (ShouldReleaseHeldContent ())
@@ -166,73 +169,76 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
{
foreach (object o in heldContent.HeldToObjects ())
foreach (object o in _heldContent.HeldToObjects ())
{
appendOutput (o);
}
State = newState;
heldContent.ClearHeld ();
_heldContent.ClearHeld ();
}
// Common response handler logic
protected bool ShouldReleaseHeldContent ()
{
string cur = heldContent.HeldToString ();
lock (lockExpectedResponses)
lock (_lockState)
{
// Look for an expected response for what is accumulated so far (since Esc)
if (MatchResponse (
cur,
expectedResponses,
true,
true))
string cur = _heldContent.HeldToString ();
lock (_lockExpectedResponses)
{
return false;
// 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;
}
}
// 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))
// 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))
{
return false;
// 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;
}
// 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
@@ -241,7 +247,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
/// <summary>
/// <para>
/// When overriden in a derived class, indicates whether the unexpected response
/// currently in <see cref="heldContent"/> should be released or swallowed.
/// currently in <see cref="_heldContent"/> should be released or swallowed.
/// Use this to enable default event for escape codes.
/// </para>
/// <remarks>
@@ -261,7 +267,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
{
if (invokeCallback)
{
matchingResponse.Response.Invoke (heldContent);
matchingResponse.Response.Invoke (_heldContent);
}
ResetState ();
@@ -278,17 +284,17 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
}
/// <inheritdoc/>
public void ExpectResponse (string terminator, Action<string> response,Action? abandoned, bool persistent)
public void ExpectResponse (string terminator, Action<string> response, Action? abandoned, bool persistent)
{
lock (lockExpectedResponses)
lock (_lockExpectedResponses)
{
if (persistent)
{
persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
_persistentExpectations.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
}
else
{
expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
_expectedResponses.Add (new (terminator, h => response.Invoke (h.HeldToString ()), abandoned));
}
}
}
@@ -296,36 +302,36 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
/// <inheritdoc/>
public bool IsExpecting (string terminator)
{
lock (lockExpectedResponses)
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 ());
return _expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any ());
}
}
/// <inheritdoc/>
public void StopExpecting (string terminator, bool persistent)
{
lock (lockExpectedResponses)
lock (_lockExpectedResponses)
{
if (persistent)
{
AnsiResponseExpectation [] removed = persistentExpectations.Where (r => r.Matches (terminator)).ToArray ();
AnsiResponseExpectation [] removed = _persistentExpectations.Where (r => r.Matches (terminator)).ToArray ();
foreach (var toRemove in removed)
foreach (AnsiResponseExpectation toRemove in removed)
{
persistentExpectations.Remove (toRemove);
_persistentExpectations.Remove (toRemove);
toRemove.Abandoned?.Invoke ();
}
}
else
{
AnsiResponseExpectation [] removed = expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
AnsiResponseExpectation [] removed = _expectedResponses.Where (r => r.Terminator == terminator).ToArray ();
foreach (AnsiResponseExpectation r in removed)
{
expectedResponses.Remove (r);
lateResponses.Add (r);
_expectedResponses.Remove (r);
_lateResponses.Add (r);
r.Abandoned?.Invoke ();
}
}
@@ -333,10 +339,8 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
}
}
internal class AnsiResponseParser<T> : AnsiResponseParserBase
internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHeld<T> ())
{
public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
/// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
@@ -353,10 +357,10 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
return output;
}
public Tuple<char, T>[] Release ()
public Tuple<char, T> [] Release ()
{
// Lock in case Release is called from different Thread from parse
lock (lockState)
lock (_lockState)
{
Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
@@ -366,7 +370,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
}
}
private IEnumerable<Tuple<char, T>> HeldToEnumerable () { return (IEnumerable<Tuple<char, T>>)heldContent.HeldToObjects (); }
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
@@ -376,17 +380,17 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
/// <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)
public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
{
lock (lockExpectedResponses)
lock (_lockExpectedResponses)
{
if (persistent)
{
persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
_persistentExpectations.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
}
else
{
expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
_expectedResponses.Add (new (terminator, h => response.Invoke (HeldToEnumerable ()), abandoned));
}
}
}
@@ -395,7 +399,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); }
}
internal class AnsiResponseParser : AnsiResponseParserBase
internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ())
{
/// <summary>
/// <para>
@@ -410,8 +414,6 @@ internal class AnsiResponseParser : AnsiResponseParserBase
/// </summary>
public Func<string, bool> UnknownResponseHandler { get; set; } = _ => false;
public AnsiResponseParser () : base (new StringHeld ()) { }
public string ProcessInput (string input)
{
var output = new StringBuilder ();
@@ -427,9 +429,9 @@ internal class AnsiResponseParser : AnsiResponseParserBase
public string Release ()
{
lock (lockState)
lock (_lockState)
{
string output = heldContent.HeldToString ();
string output = _heldContent.HeldToString ();
ResetState ();
return output;
@@ -437,5 +439,11 @@ internal class AnsiResponseParser : AnsiResponseParserBase
}
/// <inheritdoc/>
protected override bool ShouldSwallowUnexpectedResponse () { return UnknownResponseHandler.Invoke (heldContent.HeldToString ()); }
protected override bool ShouldSwallowUnexpectedResponse ()
{
lock (_lockState)
{
return UnknownResponseHandler.Invoke (_heldContent.HeldToString ());
}
}
}

View File

@@ -30,7 +30,7 @@ public interface IAnsiResponseParser
/// that already has one.
/// exists.
/// </exception>
void ExpectResponse (string terminator, Action<string> response,Action? abandoned, bool persistent);
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

View File

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

View File

@@ -6,13 +6,13 @@ namespace Terminal.Gui;
/// </summary>
internal class StringHeld : IHeld
{
private readonly StringBuilder held = new ();
private readonly StringBuilder _held = new ();
public void ClearHeld () { held.Clear (); }
public void ClearHeld () { _held.Clear (); }
public string HeldToString () { return held.ToString (); }
public string HeldToString () { return _held.ToString (); }
public IEnumerable<object> HeldToObjects () { return held.ToString ().Select (c => (object)c); }
public IEnumerable<object> HeldToObjects () { return _held.ToString ().Select (c => (object)c); }
public void AddToHeld (object o) { held.Append ((char)o); }
public void AddToHeld (object o) { _held.Append ((char)o); }
}

View File

@@ -710,6 +710,10 @@ public abstract class ConsoleDriver : IConsoleDriver
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

View File

@@ -581,6 +581,7 @@ internal class CursesDriver : ConsoleDriver
private Curses.Window? _window;
private UnixMainLoop? _mainLoopDriver;
// BUGBUG: Fix this nullable issue.
private object _processInputToken;
public override MainLoop Init ()
@@ -730,6 +731,7 @@ internal class CursesDriver : ConsoleDriver
while (wch2 == Curses.KeyMouse)
{
// BUGBUG: Fix this nullable issue.
Key kea = null;
ConsoleKeyInfo [] cki =
@@ -739,6 +741,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);
}
@@ -796,6 +799,7 @@ internal class CursesDriver : ConsoleDriver
k = KeyCode.AltMask | MapCursesKey (wch);
}
// BUGBUG: Fix this nullable issue.
Key key = null;
if (code == 0)
@@ -826,6 +830,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;
@@ -954,6 +959,7 @@ internal class CursesDriver : ConsoleDriver
if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
{
// BUGBUG: Fix this nullable issue.
EscSeqUtils.DecodeEscSeq (
ref consoleKeyInfo,
ref ck,
@@ -977,6 +983,7 @@ internal class CursesDriver : ConsoleDriver
OnMouseEvent (new () { Flags = mf, Position = pos });
}
// BUGBUG: Fix this nullable issue.
cki = null;
if (wch2 == 27)

View File

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

View File

@@ -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,12 +200,14 @@ public interface IConsoleDriver
/// </summary>
event EventHandler<EventArgs>? ClearedContents;
// BUGBUG: This is not referenced. Can it be removed?
/// <summary>
/// Sets <see cref="ConsoleDriver.Contents"/> as dirty for situations where views
/// don't need layout and redrawing, but just refresh the screen.
/// 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 ();
// BUGBUG: This is not referenced. Can it be removed?
/// <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 ();
@@ -224,7 +241,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 +283,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 +293,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>
@@ -294,15 +318,19 @@ public interface IConsoleDriver
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
/// 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
/// 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 ();
}

View File

@@ -223,6 +223,8 @@ 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;

View File

@@ -41,6 +41,7 @@ internal class WindowsDriver : ConsoleDriver
private Point _pointMove;
private bool _processButtonClick;
// BUGBUG: Fix this nullable issue.
public WindowsDriver ()
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)

View File

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

View File

@@ -9,12 +9,14 @@ namespace Terminal.Gui;
public class SixelSupportDetector
{
/// <summary>
/// Sends Ansi escape sequences to the console to determine whether
/// sixel is supported (and <see cref="SixelSupportResult.Resolution"/>
/// etc).
/// 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>
/// <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 ();
@@ -22,75 +24,76 @@ public class SixelSupportDetector
IsSixelSupportedByDar (result, resultCallback);
}
private void TryGetResolutionDirectly (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
{
// Expect something like:
//<esc>[6;20;10t
QueueRequest (EscSeqUtils.CSI_RequestSixelResolution,
(r) =>
QueueRequest (
EscSeqUtils.CSI_RequestSixelResolution,
r =>
{
// Terminal supports directly responding with resolution
var match = Regex.Match (r, @"\[\d+;(\d+);(\d+)t$");
Match match = Regex.Match (r, @"\[\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))
if (int.TryParse (match.Groups [1].Value, out int ry) && int.TryParse (match.Groups [2].Value, out int rx))
{
result.Resolution = new Size (rx, ry);
result.Resolution = new (rx, ry);
}
}
// Finished
resultCallback.Invoke (result);
},
// Request failed, so try to compute instead
()=>TryComputeResolution (result,resultCallback));
}
// 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)=>
QueueRequest (
EscSeqUtils.CSI_RequestWindowSizeInPixels,
r1 =>
{
windowSize = r1;
QueueRequest (EscSeqUtils.CSI_ReportTerminalSizeInChars,
(r2) =>
QueueRequest (
EscSeqUtils.CSI_ReportTerminalSizeInChars,
r2 =>
{
sizeInChars = r2;
ComputeResolution (result,windowSize,sizeInChars);
ComputeResolution (result, windowSize, sizeInChars);
resultCallback (result);
}, abandoned: () => resultCallback (result));
},abandoned: ()=>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
var pixelMatch = Regex.Match (windowSize, @"\[\d+;(\d+);(\d+)t$");
Match pixelMatch = Regex.Match (windowSize, @"\[\d+;(\d+);(\d+)t$");
// Example [8;30;120t
var charMatch = Regex.Match (sizeInChars, @"\[\d+;(\d+);(\d+)t$");
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 var pixelHeight)
&& int.TryParse (pixelMatch.Groups [2].Value, out var pixelWidth)
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 var charHeight)
&& int.TryParse (charMatch.Groups [2].Value, out var charWidth)
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
{
@@ -99,31 +102,32 @@ public class SixelSupportDetector
var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight);
// Set the resolution based on the character cell size
result.Resolution = new Size (cellWidth, cellHeight);
result.Resolution = new (cellWidth, cellHeight);
}
}
}
private void IsSixelSupportedByDar (SixelSupportResult result,Action<SixelSupportResult> resultCallback)
private void IsSixelSupportedByDar (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
{
QueueRequest (
EscSeqUtils.CSI_SendDeviceAttributes,
(r) =>
{
result.IsSupported = ResponseIndicatesSupport (r);
EscSeqUtils.CSI_SendDeviceAttributes,
r =>
{
result.IsSupported = ResponseIndicatesSupport (r);
if (result.IsSupported)
{
TryGetResolutionDirectly (result, resultCallback);
}
else
{
resultCallback (result);
}
},abandoned: () => resultCallback(result));
if (result.IsSupported)
{
TryGetResolutionDirectly (result, resultCallback);
}
else
{
resultCallback (result);
}
},
() => resultCallback (result));
}
private void QueueRequest (AnsiEscapeSequenceRequest req, Action<string> responseCallback, Action abandoned)
private static void QueueRequest (AnsiEscapeSequenceRequest req, Action<string> responseCallback, Action abandoned)
{
var newRequest = new AnsiEscapeSequenceRequest
{
@@ -133,29 +137,29 @@ public class SixelSupportDetector
Abandoned = abandoned
};
Application.Driver.QueueAnsiRequest (newRequest);
Application.Driver?.QueueAnsiRequest (newRequest);
}
private bool ResponseIndicatesSupport (string response)
private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); }
private static bool IsWindowsTerminal ()
{
return response.Split (';').Contains ("4");
return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION"));
;
}
private bool IsWindowsTerminal ()
{
return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable ("WT_SESSION"));;
}
private bool IsXtermWithTransparency ()
private static bool IsXtermWithTransparency ()
{
// Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
var xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
string 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)
if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out int xtermVersion) && xtermVersion >= 370)
{
return true;
}
return false;
}
}
}

View File

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

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using Terminal.Gui;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace UICatalog.Scenarios;
@@ -13,14 +12,13 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
{
private GraphView _graphView;
private DateTime start = DateTime.Now;
private ScatterSeries _sentSeries;
private ScatterSeries _answeredSeries;
private List<DateTime> sends = new ();
private readonly List<DateTime> _sends = new ();
private object lockAnswers = new object ();
private Dictionary<DateTime, string> answers = new ();
private readonly object _lockAnswers = new object ();
private readonly Dictionary<DateTime, string> _answers = new ();
private Label _lblSummary;
public override void Main ()
@@ -82,7 +80,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
"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);
@@ -225,7 +223,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
TimeSpan.FromMilliseconds (1000),
() =>
{
lock (lockAnswers)
lock (_lockAnswers)
{
UpdateGraph ();
@@ -327,15 +325,15 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
private string GetSummary ()
{
if (answers.Count == 0)
if (_answers.Count == 0)
{
return "No requests sent yet";
}
var last = answers.Last ().Value;
var last = _answers.Last ().Value;
var unique = answers.Values.Distinct ().Count ();
var total = answers.Count;
var unique = _answers.Values.Distinct ().Count ();
var total = _answers.Count;
return $"Last:{last} U:{unique} T:{total}";
}
@@ -361,12 +359,12 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
private void UpdateGraph ()
{
_sentSeries.Points = sends
_sentSeries.Points = _sends
.GroupBy (ToSeconds)
.Select (g => new PointF (g.Key, g.Count ()))
.ToList ();
_answeredSeries.Points = answers.Keys
_answeredSeries.Points = _answers.Keys
.GroupBy (ToSeconds)
.Select (g => new PointF (g.Key, g.Count ()))
.ToList ();
@@ -389,14 +387,14 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
Terminator = EscSeqUtils.CSI_SendDeviceAttributes.Terminator,
ResponseReceived = HandleResponse
});
sends.Add (DateTime.Now);
_sends.Add (DateTime.Now);
}
private void HandleResponse (string response)
{
lock (lockAnswers)
lock (_lockAnswers)
{
answers.Add (DateTime.Now, response);
_answers.Add (DateTime.Now, response);
}
}
}

View File

@@ -62,7 +62,7 @@ public class Images : Scenario
private SixelToRender _sixelImage;
// Start by assuming no support
private SixelSupportResult _sixelSupportResult = new SixelSupportResult ();
private SixelSupportResult _sixelSupportResult = new ();
private CheckBox _cbSupportsSixel;
public override void Main ()
@@ -95,8 +95,8 @@ public class Images : Scenario
Text = "supports true color "
};
_win.Add (cbSupportsTrueColor);
_cbSupportsSixel = new CheckBox
_cbSupportsSixel = new()
{
X = Pos.Right (lblDriverName) + 2,
Y = 1,
@@ -104,26 +104,24 @@ public class Images : Scenario
Text = "Supports Sixel"
};
var lblSupportsSixel = new Label ()
var lblSupportsSixel = new Label
{
X = Pos.Right (lblDriverName) + 2,
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 ();
};
{
_sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked;
SetupSixelSupported (e.NewValue == CheckState.Checked);
ApplyShowTabViewHack ();
};
_win.Add (_cbSupportsSixel);
var cbUseTrueColor = new CheckBox
@@ -174,7 +172,6 @@ public class Images : Scenario
_cbSupportsSixel.CheckedState = newResult.IsSupported ? CheckState.Checked : CheckState.UnChecked;
_pxX.Value = _sixelSupportResult.Resolution.Width;
_pxY.Value = _sixelSupportResult.Resolution.Height;
}
private void SetupSixelSupported (bool isSupported)
@@ -311,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;
}

View File

@@ -416,8 +416,6 @@ public class AnsiResponseParserTests (ITestOutputHelper output)
[Fact]
public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo ()
{
int i = 0;
// Swallow all unknown escape codes
_parser1.UnexpectedResponseHandler = _ => true;
_parser2.UnknownResponseHandler = _ => true;