mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-31 02:08:03 +01:00
Merge pull request #203 from tig/BDisp-v2_3767_ansi-escape-sequence-all-drivers
Code Review of #3768
This commit is contained in:
@@ -4,35 +4,38 @@ namespace Terminal.Gui;
|
||||
/// <summary>
|
||||
/// Describes an ongoing ANSI request sent to the console.
|
||||
/// Use <see cref="ResponseReceived"/> to handle the response
|
||||
/// when console answers the request.
|
||||
/// when the console answers the request.
|
||||
/// </summary>
|
||||
public class AnsiEscapeSequenceRequest
|
||||
{
|
||||
internal readonly object _responseLock = new (); // Per-instance lock
|
||||
|
||||
/// <summary>
|
||||
/// Request to send e.g. see
|
||||
/// Gets the request string to send e.g. see
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
|
||||
/// </see>
|
||||
/// </summary>
|
||||
public required string Request { get; init; }
|
||||
|
||||
// QUESTION: Could the type of this propperty be AnsiEscapeSequenceResponse? This would remove the
|
||||
// QUESTION: removal of the redundant Rresponse, Terminator, and ExpectedRespnseValue properties from this class?
|
||||
// QUESTION: Does string.Empty indicate no response recevied? If not, perhaps make this property nullable?
|
||||
/// <summary>
|
||||
/// Response received from the request.
|
||||
/// Gets the response received from the request.
|
||||
/// </summary>
|
||||
public string Response { get; internal set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the console responds with an ANSI response code that matches the
|
||||
/// Raised when the console responds with an ANSI response code that matches the
|
||||
/// <see cref="Terminator"/>
|
||||
/// </summary>
|
||||
public event EventHandler<AnsiEscapeSequenceResponse>? ResponseReceived;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// The terminator that uniquely identifies the type of response as responded
|
||||
/// by the console. e.g. for
|
||||
/// Gets the terminator that uniquely identifies the response received from
|
||||
/// the console. e.g. for
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
|
||||
/// </see>
|
||||
@@ -50,15 +53,15 @@ public class AnsiEscapeSequenceRequest
|
||||
public required string Terminator { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Execute an ANSI escape sequence escape which may return a response or error.
|
||||
/// Attempt an ANSI escape sequence request which may return a response or error.
|
||||
/// </summary>
|
||||
/// <param name="ansiRequest">The ANSI escape sequence to request.</param>
|
||||
/// <param name="result">
|
||||
/// When this method returns <see langword="true"/>, an object containing the response with an empty
|
||||
/// error.
|
||||
/// When this method returns <see langword="true"/>, the response. <see cref="AnsiEscapeSequenceResponse.Error"/> will
|
||||
/// be <see cref="string.Empty"/>.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator and value.</returns>
|
||||
public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
|
||||
/// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator, and value.</returns>
|
||||
public static bool TryRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
|
||||
{
|
||||
var error = new StringBuilder ();
|
||||
var values = new string? [] { null };
|
||||
@@ -72,7 +75,7 @@ public class AnsiEscapeSequenceRequest
|
||||
|
||||
if (!string.IsNullOrEmpty (ansiRequest.Response) && !ansiRequest.Response.StartsWith (EscSeqUtils.KeyEsc))
|
||||
{
|
||||
throw new InvalidOperationException ("Invalid escape character!");
|
||||
throw new InvalidOperationException ($"Invalid Response: {ansiRequest.Response}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty (ansiRequest.Terminator))
|
||||
@@ -102,7 +105,7 @@ public class AnsiEscapeSequenceRequest
|
||||
AnsiEscapeSequenceResponse ansiResponse = new ()
|
||||
{
|
||||
Response = ansiRequest.Response, Error = error.ToString (),
|
||||
Terminator = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response [^1].ToString (), Value = values [0]
|
||||
Terminator = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response [^1].ToString (), ExpectedResponseValue = values [0]
|
||||
};
|
||||
|
||||
// Invoke the event if it's subscribed
|
||||
@@ -114,16 +117,17 @@ public class AnsiEscapeSequenceRequest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value expected in the response e.g.
|
||||
/// The value expected in the response after the CSI e.g.
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
|
||||
/// </see>
|
||||
/// which will have a 't' as terminator but also other different request may return the same terminator with a
|
||||
/// different value.
|
||||
/// should result in a response of the form <c>ESC [ 8 ; height ; width t</c>. In this case, <see cref="ExpectedResponseValue"/>
|
||||
/// will be <c>"8"</c>.
|
||||
/// </summary>
|
||||
public string? Value { get; init; }
|
||||
public string? ExpectedResponseValue { get; init; }
|
||||
|
||||
internal void RaiseResponseFromInput (AnsiEscapeSequenceRequest ansiRequest, string response) { ResponseFromInput?.Invoke (ansiRequest, response); }
|
||||
|
||||
// QUESTION: What is this for? Please provide a descriptive comment.
|
||||
internal event EventHandler<string>? ResponseFromInput;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a finished ANSI received from the console.
|
||||
/// Describes a response received from the console as a result of a request being sent via <see cref="AnsiEscapeSequenceRequest"/>.
|
||||
/// </summary>
|
||||
public class AnsiEscapeSequenceResponse
|
||||
{
|
||||
// QUESTION: Should this be nullable to indicate there was no error, or is string.Empty sufficient?
|
||||
/// <summary>
|
||||
/// Error received from e.g. see
|
||||
/// Gets the error string received from e.g. see
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
|
||||
/// </see>
|
||||
/// .
|
||||
/// </summary>
|
||||
public required string Error { get; init; }
|
||||
|
||||
// QUESTION: Does string.Empty indicate no response recevied? If not, perhaps make this property nullable?
|
||||
/// <summary>
|
||||
/// Response received from e.g. see
|
||||
/// Gets the Response string received from e.g. see
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
|
||||
/// </see>
|
||||
@@ -23,10 +26,11 @@ public class AnsiEscapeSequenceResponse
|
||||
/// </summary>
|
||||
public required string Response { get; init; }
|
||||
|
||||
// QUESTION: Does string.Empty indicate no terminator expected? If not, perhaps make this property nullable?
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// The terminator that uniquely identifies the type of response as responded
|
||||
/// by the console. e.g. for
|
||||
/// Gets the terminator that uniquely identifies the response received from
|
||||
/// the console. e.g. for
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
|
||||
/// </see>
|
||||
@@ -34,20 +38,23 @@ public class AnsiEscapeSequenceResponse
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
|
||||
/// </see>
|
||||
/// .
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The received terminator must match to the terminator sent by the request.
|
||||
/// After sending a request, the first response with matching terminator will be matched
|
||||
/// to the oldest outstanding request.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public required string Terminator { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value expected in the response e.g.
|
||||
/// The value expected in the response after the CSI e.g.
|
||||
/// <see>
|
||||
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
|
||||
/// </see>
|
||||
/// which will have a 't' as terminator but also other different request may return the same terminator with a
|
||||
/// different value.
|
||||
/// should result in a response of the form <c>ESC [ 8 ; height ; width t</c>. In this case, <see cref="ExpectedResponseValue"/>
|
||||
/// will be <c>"8"</c>.
|
||||
/// </summary>
|
||||
public string? Value { get; init; }
|
||||
|
||||
public string? ExpectedResponseValue { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#nullable enable
|
||||
//
|
||||
// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
|
||||
//
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -15,6 +12,53 @@ namespace Terminal.Gui;
|
||||
/// </remarks>
|
||||
public abstract class ConsoleDriver
|
||||
{
|
||||
/// <summary>
|
||||
/// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
|
||||
/// <code>
|
||||
/// public ColorTests ()
|
||||
/// {
|
||||
/// ConsoleDriver.RunningUnitTests = true;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
internal static bool RunningUnitTests { get; set; }
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
public IClipboard? Clipboard { get; internal set; }
|
||||
|
||||
/// <summary>Returns the name of the driver and relevant library version information.</summary>
|
||||
/// <returns></returns>
|
||||
public virtual string GetVersionInfo () { return GetType ().Name; }
|
||||
|
||||
/// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
|
||||
/// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks>
|
||||
public abstract void Suspend ();
|
||||
|
||||
#region ANSI Esc Sequence Handling
|
||||
|
||||
// QUESTION: Should this be virtual with a default implementation that does the common stuff?
|
||||
// QUESTION: Looking at the implementations of this method, there is TONs of duplicated code.
|
||||
// QUESTION: We should figure out how to find just the things that are unique to each driver and
|
||||
// QUESTION: create more fine-grained APIs to handle those.
|
||||
/// <summary>
|
||||
/// Provide handling for the terminal write ANSI escape sequence request.
|
||||
/// </summary>
|
||||
/// <param name="ansiRequest">The <see cref="AnsiEscapeSequenceRequest"/> object.</param>
|
||||
/// <returns>The request response.</returns>
|
||||
public abstract string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest);
|
||||
|
||||
// QUESTION: This appears to be an API to help in debugging. It's only implemented in CursesDriver and WindowsDriver.
|
||||
// QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
|
||||
/// <summary>
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="ansi"></param>
|
||||
public abstract void WriteRaw (string ansi);
|
||||
|
||||
#endregion ANSI Esc Sequence Handling
|
||||
|
||||
#region Screen and Contents
|
||||
|
||||
// As performance is a concern, we keep track of the dirty lines and only refresh those.
|
||||
// This is in addition to the dirty flag on each cell.
|
||||
internal bool []? _dirtyLines;
|
||||
@@ -23,30 +67,18 @@ public abstract class ConsoleDriver
|
||||
/// <summary>Gets the location and size of the terminal screen.</summary>
|
||||
internal Rectangle Screen => new (0, 0, Cols, Rows);
|
||||
|
||||
private Rectangle _clip;
|
||||
/// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
|
||||
public abstract void UpdateScreen ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
|
||||
/// to.
|
||||
/// </summary>
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Rectangle Clip
|
||||
{
|
||||
get => _clip;
|
||||
set
|
||||
{
|
||||
if (_clip == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
/// <summary>Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.</summary>
|
||||
/// <param name="args"></param>
|
||||
public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
|
||||
|
||||
// Don't ever let Clip be bigger than Screen
|
||||
_clip = Rectangle.Intersect (Screen, value);
|
||||
}
|
||||
}
|
||||
/// <summary>The event fired when the terminal is resized.</summary>
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
public IClipboard? Clipboard { get; internal set; }
|
||||
/// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
|
||||
public abstract void Refresh ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
@@ -75,6 +107,43 @@ public abstract class ConsoleDriver
|
||||
/// <summary>The leftmost column in the terminal.</summary>
|
||||
public virtual int Left { get; internal set; } = 0;
|
||||
|
||||
/// <summary>Tests if the specified rune is supported by the driver.</summary>
|
||||
/// <param name="rune"></param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
|
||||
/// support displaying this rune.
|
||||
/// </returns>
|
||||
public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
|
||||
|
||||
/// <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="Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
||||
/// Used by <see cref="AddRune(Rune)"/> and <see cref="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="Cols"/> and
|
||||
/// <see cref="Rows"/>, the method still sets those properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
public virtual void Move (int col, int row)
|
||||
{
|
||||
//Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
|
||||
Col = col;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
|
||||
/// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
@@ -95,16 +164,27 @@ public abstract class ConsoleDriver
|
||||
/// <summary>The topmost row in the terminal.</summary>
|
||||
public virtual int Top { get; internal set; } = 0;
|
||||
|
||||
private Rectangle _clip;
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
|
||||
/// <code>
|
||||
/// public ColorTests ()
|
||||
/// {
|
||||
/// ConsoleDriver.RunningUnitTests = true;
|
||||
/// }
|
||||
/// </code>
|
||||
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
|
||||
/// to.
|
||||
/// </summary>
|
||||
internal static bool RunningUnitTests { get; set; }
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
public Rectangle Clip
|
||||
{
|
||||
get => _clip;
|
||||
set
|
||||
{
|
||||
if (_clip == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't ever let Clip be bigger than Screen
|
||||
_clip = Rectangle.Intersect (Screen, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified rune to the display at the current cursor position.</summary>
|
||||
/// <remarks>
|
||||
@@ -310,10 +390,38 @@ public abstract class ConsoleDriver
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
|
||||
/// <remarks>
|
||||
/// The value of <see cref="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>
|
||||
public void FillRect (Rectangle rect, Rune rune = default)
|
||||
{
|
||||
rect = Rectangle.Intersect (rect, Clip);
|
||||
|
||||
lock (Contents!)
|
||||
{
|
||||
for (int r = rect.Y; r < rect.Y + rect.Height; r++)
|
||||
{
|
||||
for (int c = rect.X; c < rect.X + rect.Width; c++)
|
||||
{
|
||||
Contents [r, c] = new ()
|
||||
{
|
||||
Rune = rune != default (Rune) ? rune : (Rune)' ',
|
||||
Attribute = CurrentAttribute, IsDirty = true
|
||||
};
|
||||
_dirtyLines! [r] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
|
||||
public void ClearContents ()
|
||||
{
|
||||
Contents = new Cell [Rows, Cols];
|
||||
|
||||
//CONCURRENCY: Unsynchronized access to Clip isn't safe.
|
||||
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
|
||||
Clip = Screen;
|
||||
@@ -325,21 +433,22 @@ public abstract class ConsoleDriver
|
||||
{
|
||||
for (var c = 0; c < Cols; c++)
|
||||
{
|
||||
Contents [row, c] = new Cell
|
||||
Contents [row, c] = new ()
|
||||
{
|
||||
Rune = (Rune)' ',
|
||||
Attribute = new Attribute (Color.White, Color.Black),
|
||||
IsDirty = true
|
||||
};
|
||||
}
|
||||
|
||||
_dirtyLines [row] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="Contents"/> as dirty for situations where views
|
||||
/// don't need layout and redrawing, but just refresh the screen.
|
||||
/// Sets <see cref="Contents"/> as dirty for situations where views
|
||||
/// don't need layout and redrawing, but just refresh the screen.
|
||||
/// </summary>
|
||||
public void SetContentsAsDirty ()
|
||||
{
|
||||
@@ -351,41 +460,12 @@ public abstract class ConsoleDriver
|
||||
{
|
||||
Contents [row, c].IsDirty = true;
|
||||
}
|
||||
|
||||
_dirtyLines! [row] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool EnsureCursorVisibility ();
|
||||
|
||||
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
|
||||
/// <remarks>
|
||||
/// The value of <see cref="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>
|
||||
public void FillRect (Rectangle rect, Rune rune = default)
|
||||
{
|
||||
rect = Rectangle.Intersect (rect, Clip);
|
||||
lock (Contents!)
|
||||
{
|
||||
for (int r = rect.Y; r < rect.Y + rect.Height; r++)
|
||||
{
|
||||
for (int c = rect.X; c < rect.X + rect.Width; c++)
|
||||
{
|
||||
Contents [r, c] = new Cell
|
||||
{
|
||||
Rune = rune != default ? rune : (Rune)' ',
|
||||
Attribute = CurrentAttribute, IsDirty = true
|
||||
};
|
||||
_dirtyLines! [r] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="FillRect(Rectangle, Rune)"/>.
|
||||
@@ -394,79 +474,28 @@ public abstract class ConsoleDriver
|
||||
/// <param name="c"></param>
|
||||
public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
|
||||
|
||||
#endregion Screen and Contents
|
||||
|
||||
#region Cursor Handling
|
||||
|
||||
/// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool EnsureCursorVisibility ();
|
||||
|
||||
/// <summary>Gets the terminal cursor visibility.</summary>
|
||||
/// <param name="visibility">The current <see cref="CursorVisibility"/></param>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool GetCursorVisibility (out CursorVisibility visibility);
|
||||
|
||||
/// <summary>Returns the name of the driver and relevant library version information.</summary>
|
||||
/// <returns></returns>
|
||||
public virtual string GetVersionInfo () { return GetType ().Name; }
|
||||
|
||||
/// <summary>Tests if the specified rune is supported by the driver.</summary>
|
||||
/// <param name="rune"></param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
|
||||
/// support displaying this rune.
|
||||
/// </returns>
|
||||
public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
|
||||
|
||||
/// <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="Clip"/>.
|
||||
/// <see langword="true"/> otherwise.
|
||||
/// </returns>
|
||||
public bool IsValidLocation (int col, int row)
|
||||
{
|
||||
return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
||||
/// Used by <see cref="AddRune(Rune)"/> and <see cref="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="Cols"/> and
|
||||
/// <see cref="Rows"/>, the method still sets those properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
public virtual void Move (int col, int row)
|
||||
{
|
||||
//Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
|
||||
Col = col;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
/// <summary>Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.</summary>
|
||||
/// <param name="args"></param>
|
||||
public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
|
||||
|
||||
/// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
|
||||
public abstract void Refresh ();
|
||||
/// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
|
||||
public abstract void UpdateCursor ();
|
||||
|
||||
/// <summary>Sets the terminal cursor visibility.</summary>
|
||||
/// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
|
||||
/// <returns><see langword="true"/> upon success</returns>
|
||||
public abstract bool SetCursorVisibility (CursorVisibility visibility);
|
||||
|
||||
/// <summary>The event fired when the terminal is resized.</summary>
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
|
||||
|
||||
/// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
|
||||
/// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks>
|
||||
public abstract void Suspend ();
|
||||
|
||||
/// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
|
||||
public abstract void UpdateCursor ();
|
||||
|
||||
/// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
|
||||
public abstract void UpdateScreen ();
|
||||
#endregion Cursor Handling
|
||||
|
||||
#region Setup & Teardown
|
||||
|
||||
@@ -518,7 +547,7 @@ public abstract class ConsoleDriver
|
||||
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
|
||||
if (Application.Driver is { })
|
||||
{
|
||||
_currentAttribute = new Attribute (value.Foreground, value.Background);
|
||||
_currentAttribute = new (value.Foreground, value.Background);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -551,16 +580,33 @@ public abstract class ConsoleDriver
|
||||
public virtual Attribute MakeColor (in Color foreground, in Color background)
|
||||
{
|
||||
// Encode the colors into the int value.
|
||||
return new Attribute (
|
||||
-1, // only used by cursesdriver!
|
||||
foreground,
|
||||
background
|
||||
);
|
||||
return new (
|
||||
-1, // only used by cursesdriver!
|
||||
foreground,
|
||||
background
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion Color Handling
|
||||
|
||||
#region Mouse and Keyboard
|
||||
#region Mouse Handling
|
||||
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnMouseEvent (MouseEventArgs a)
|
||||
{
|
||||
// Ensure ScreenPosition is set
|
||||
a.ScreenPosition = a.Position;
|
||||
|
||||
MouseEvent?.Invoke (this, a);
|
||||
}
|
||||
|
||||
#endregion Mouse Handling
|
||||
|
||||
#region Keyboard Handling
|
||||
|
||||
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
|
||||
public event EventHandler<Key>? KeyDown;
|
||||
@@ -587,19 +633,8 @@ public abstract class ConsoleDriver
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
|
||||
|
||||
/// <summary>Event fired when a mouse event occurs.</summary>
|
||||
public event EventHandler<MouseEventArgs>? MouseEvent;
|
||||
|
||||
/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnMouseEvent (MouseEventArgs a)
|
||||
{
|
||||
// Ensure ScreenPosition is set
|
||||
a.ScreenPosition = a.Position;
|
||||
|
||||
MouseEvent?.Invoke (this, a);
|
||||
}
|
||||
|
||||
// TODO: Remove this API - it was needed when we didn't have a reliable way to simulate key presses.
|
||||
// TODO: We now do: Applicaiton.RaiseKeyDown and Application.RaiseKeyUp
|
||||
/// <summary>Simulates a key press.</summary>
|
||||
/// <param name="keyChar">The key character.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
@@ -608,337 +643,5 @@ public abstract class ConsoleDriver
|
||||
/// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
|
||||
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
|
||||
|
||||
/// <summary>
|
||||
/// Provide handling for the terminal write ANSI escape sequence request.
|
||||
/// </summary>
|
||||
/// <param name="ansiRequest">The <see cref="AnsiEscapeSequenceRequest"/> object.</param>
|
||||
/// <returns>The request response.</returns>
|
||||
public abstract string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest);
|
||||
|
||||
/// <summary>
|
||||
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
|
||||
/// </summary>
|
||||
/// <param name="ansi"></param>
|
||||
public abstract void WriteRaw (string ansi);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a
|
||||
/// consistent way for application code to specify keys and receive key events.
|
||||
/// <para>
|
||||
/// The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for
|
||||
/// common operations. For example, <see cref="Key.IsAlt"/> and <see cref="Key.IsCtrl"/> provide a convenient way
|
||||
/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a
|
||||
/// keyboard. Enum values are provided for these (e.g. <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.).
|
||||
/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent
|
||||
/// *lowercase*, un-shifted characters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. <see cref="KeyCode.D0"/>,
|
||||
/// <see cref="KeyCode.D1"/>, etc.).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The shift modifiers (<see cref="KeyCode.ShiftMask"/>, <see cref="KeyCode.CtrlMask"/>, and
|
||||
/// <see cref="KeyCode.AltMask"/>) can be combined (with logical or) with the other key codes to represent shifted
|
||||
/// keys. For example, the <see cref="KeyCode.A"/> enum value represents the un-shifted 'a' key, while
|
||||
/// <see cref="KeyCode.ShiftMask"/> | <see cref="KeyCode.A"/> represents the 'A' key (shifted 'a' key). Likewise,
|
||||
/// <see cref="KeyCode.AltMask"/> | <see cref="KeyCode.A"/> represents the 'Alt+A' key combination.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All other keys that produce a printable character are encoded as the Unicode value of the character. For
|
||||
/// example, the <see cref="KeyCode"/> for the '!' character is 33, which is the Unicode value for '!'. Likewise,
|
||||
/// `â` is 226, `Â` is 194, etc.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
|
||||
/// the one of the lower bits (as extracted by <see cref="CharMask"/>).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
public enum KeyCode : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Mask that indicates that the key is a unicode codepoint. Values outside this range indicate the key has shift
|
||||
/// modifiers or is a special key like function keys, arrows keys and so on.
|
||||
/// </summary>
|
||||
CharMask = 0x_f_ffff,
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
|
||||
/// in the lower bits (as extracted by <see cref="CharMask"/>).
|
||||
/// </summary>
|
||||
SpecialMask = 0x_fff0_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by
|
||||
/// removing the ShiftMask.
|
||||
/// </summary>
|
||||
ShiftMask = 0x_1000_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by
|
||||
/// removing the AltMask.
|
||||
/// </summary>
|
||||
AltMask = 0x_8000_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by
|
||||
/// removing the CtrlMask.
|
||||
/// </summary>
|
||||
CtrlMask = 0x_4000_0000,
|
||||
|
||||
/// <summary>The key code representing an invalid or empty key.</summary>
|
||||
Null = 0,
|
||||
|
||||
/// <summary>Backspace key.</summary>
|
||||
Backspace = 8,
|
||||
|
||||
/// <summary>The key code for the tab key (forwards tab key).</summary>
|
||||
Tab = 9,
|
||||
|
||||
/// <summary>The key code for the return key.</summary>
|
||||
Enter = ConsoleKey.Enter,
|
||||
|
||||
/// <summary>The key code for the clear key.</summary>
|
||||
Clear = 12,
|
||||
|
||||
/// <summary>The key code for the escape key.</summary>
|
||||
Esc = 27,
|
||||
|
||||
/// <summary>The key code for the space bar key.</summary>
|
||||
Space = 32,
|
||||
|
||||
/// <summary>Digit 0.</summary>
|
||||
D0 = 48,
|
||||
|
||||
/// <summary>Digit 1.</summary>
|
||||
D1,
|
||||
|
||||
/// <summary>Digit 2.</summary>
|
||||
D2,
|
||||
|
||||
/// <summary>Digit 3.</summary>
|
||||
D3,
|
||||
|
||||
/// <summary>Digit 4.</summary>
|
||||
D4,
|
||||
|
||||
/// <summary>Digit 5.</summary>
|
||||
D5,
|
||||
|
||||
/// <summary>Digit 6.</summary>
|
||||
D6,
|
||||
|
||||
/// <summary>Digit 7.</summary>
|
||||
D7,
|
||||
|
||||
/// <summary>Digit 8.</summary>
|
||||
D8,
|
||||
|
||||
/// <summary>Digit 9.</summary>
|
||||
D9,
|
||||
|
||||
/// <summary>The key code for the A key</summary>
|
||||
A = 65,
|
||||
|
||||
/// <summary>The key code for the B key</summary>
|
||||
B,
|
||||
|
||||
/// <summary>The key code for the C key</summary>
|
||||
C,
|
||||
|
||||
/// <summary>The key code for the D key</summary>
|
||||
D,
|
||||
|
||||
/// <summary>The key code for the E key</summary>
|
||||
E,
|
||||
|
||||
/// <summary>The key code for the F key</summary>
|
||||
F,
|
||||
|
||||
/// <summary>The key code for the G key</summary>
|
||||
G,
|
||||
|
||||
/// <summary>The key code for the H key</summary>
|
||||
H,
|
||||
|
||||
/// <summary>The key code for the I key</summary>
|
||||
I,
|
||||
|
||||
/// <summary>The key code for the J key</summary>
|
||||
J,
|
||||
|
||||
/// <summary>The key code for the K key</summary>
|
||||
K,
|
||||
|
||||
/// <summary>The key code for the L key</summary>
|
||||
L,
|
||||
|
||||
/// <summary>The key code for the M key</summary>
|
||||
M,
|
||||
|
||||
/// <summary>The key code for the N key</summary>
|
||||
N,
|
||||
|
||||
/// <summary>The key code for the O key</summary>
|
||||
O,
|
||||
|
||||
/// <summary>The key code for the P key</summary>
|
||||
P,
|
||||
|
||||
/// <summary>The key code for the Q key</summary>
|
||||
Q,
|
||||
|
||||
/// <summary>The key code for the R key</summary>
|
||||
R,
|
||||
|
||||
/// <summary>The key code for the S key</summary>
|
||||
S,
|
||||
|
||||
/// <summary>The key code for the T key</summary>
|
||||
T,
|
||||
|
||||
/// <summary>The key code for the U key</summary>
|
||||
U,
|
||||
|
||||
/// <summary>The key code for the V key</summary>
|
||||
V,
|
||||
|
||||
/// <summary>The key code for the W key</summary>
|
||||
W,
|
||||
|
||||
/// <summary>The key code for the X key</summary>
|
||||
X,
|
||||
|
||||
/// <summary>The key code for the Y key</summary>
|
||||
Y,
|
||||
|
||||
/// <summary>The key code for the Z key</summary>
|
||||
Z,
|
||||
|
||||
///// <summary>
|
||||
///// The key code for the Delete key.
|
||||
///// </summary>
|
||||
//Delete = 127,
|
||||
|
||||
// --- Special keys ---
|
||||
// The values below are common non-alphanum keys. Their values are
|
||||
// based on the .NET ConsoleKey values, which, in-turn are based on the
|
||||
// VK_ values from the Windows API.
|
||||
// We add MaxCodePoint to avoid conflicts with the Unicode values.
|
||||
|
||||
/// <summary>The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys.</summary>
|
||||
MaxCodePoint = 0x10FFFF,
|
||||
|
||||
/// <summary>Cursor up key</summary>
|
||||
CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
|
||||
|
||||
/// <summary>Cursor down key.</summary>
|
||||
CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
|
||||
|
||||
/// <summary>Cursor left key.</summary>
|
||||
CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
|
||||
|
||||
/// <summary>Cursor right key.</summary>
|
||||
CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
|
||||
|
||||
/// <summary>Page Up key.</summary>
|
||||
PageUp = MaxCodePoint + ConsoleKey.PageUp,
|
||||
|
||||
/// <summary>Page Down key.</summary>
|
||||
PageDown = MaxCodePoint + ConsoleKey.PageDown,
|
||||
|
||||
/// <summary>Home key.</summary>
|
||||
Home = MaxCodePoint + ConsoleKey.Home,
|
||||
|
||||
/// <summary>End key.</summary>
|
||||
End = MaxCodePoint + ConsoleKey.End,
|
||||
|
||||
/// <summary>Insert (INS) key.</summary>
|
||||
Insert = MaxCodePoint + ConsoleKey.Insert,
|
||||
|
||||
/// <summary>Delete (DEL) key.</summary>
|
||||
Delete = MaxCodePoint + ConsoleKey.Delete,
|
||||
|
||||
/// <summary>Print screen character key.</summary>
|
||||
PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
|
||||
|
||||
/// <summary>F1 key.</summary>
|
||||
F1 = MaxCodePoint + ConsoleKey.F1,
|
||||
|
||||
/// <summary>F2 key.</summary>
|
||||
F2 = MaxCodePoint + ConsoleKey.F2,
|
||||
|
||||
/// <summary>F3 key.</summary>
|
||||
F3 = MaxCodePoint + ConsoleKey.F3,
|
||||
|
||||
/// <summary>F4 key.</summary>
|
||||
F4 = MaxCodePoint + ConsoleKey.F4,
|
||||
|
||||
/// <summary>F5 key.</summary>
|
||||
F5 = MaxCodePoint + ConsoleKey.F5,
|
||||
|
||||
/// <summary>F6 key.</summary>
|
||||
F6 = MaxCodePoint + ConsoleKey.F6,
|
||||
|
||||
/// <summary>F7 key.</summary>
|
||||
F7 = MaxCodePoint + ConsoleKey.F7,
|
||||
|
||||
/// <summary>F8 key.</summary>
|
||||
F8 = MaxCodePoint + ConsoleKey.F8,
|
||||
|
||||
/// <summary>F9 key.</summary>
|
||||
F9 = MaxCodePoint + ConsoleKey.F9,
|
||||
|
||||
/// <summary>F10 key.</summary>
|
||||
F10 = MaxCodePoint + ConsoleKey.F10,
|
||||
|
||||
/// <summary>F11 key.</summary>
|
||||
F11 = MaxCodePoint + ConsoleKey.F11,
|
||||
|
||||
/// <summary>F12 key.</summary>
|
||||
F12 = MaxCodePoint + ConsoleKey.F12,
|
||||
|
||||
/// <summary>F13 key.</summary>
|
||||
F13 = MaxCodePoint + ConsoleKey.F13,
|
||||
|
||||
/// <summary>F14 key.</summary>
|
||||
F14 = MaxCodePoint + ConsoleKey.F14,
|
||||
|
||||
/// <summary>F15 key.</summary>
|
||||
F15 = MaxCodePoint + ConsoleKey.F15,
|
||||
|
||||
/// <summary>F16 key.</summary>
|
||||
F16 = MaxCodePoint + ConsoleKey.F16,
|
||||
|
||||
/// <summary>F17 key.</summary>
|
||||
F17 = MaxCodePoint + ConsoleKey.F17,
|
||||
|
||||
/// <summary>F18 key.</summary>
|
||||
F18 = MaxCodePoint + ConsoleKey.F18,
|
||||
|
||||
/// <summary>F19 key.</summary>
|
||||
F19 = MaxCodePoint + ConsoleKey.F19,
|
||||
|
||||
/// <summary>F20 key.</summary>
|
||||
F20 = MaxCodePoint + ConsoleKey.F20,
|
||||
|
||||
/// <summary>F21 key.</summary>
|
||||
F21 = MaxCodePoint + ConsoleKey.F21,
|
||||
|
||||
/// <summary>F22 key.</summary>
|
||||
F22 = MaxCodePoint + ConsoleKey.F22,
|
||||
|
||||
/// <summary>F23 key.</summary>
|
||||
F23 = MaxCodePoint + ConsoleKey.F23,
|
||||
|
||||
/// <summary>F24 key.</summary>
|
||||
F24 = MaxCodePoint + ConsoleKey.F24
|
||||
#endregion Keyboard Handling
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui.ConsoleDrivers;
|
||||
|
||||
// QUESTION: This class combines Windows specific code with cross-platform code. Should this be split into two classes?
|
||||
/// <summary>Helper class to handle the scan code and virtual key from a <see cref="ConsoleKey"/>.</summary>
|
||||
public static class ConsoleKeyMapping
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
#include <stdio.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
// This function is used to get the value of the TIOCGWINSZ variable,
|
||||
// Used to get the value of the TIOCGWINSZ variable,
|
||||
// which may have different values on different Unix operating systems.
|
||||
// In Linux=0x005413, in Darwin and OpenBSD=0x40087468,
|
||||
// In Solaris=0x005468
|
||||
// The best solution is having a function that get the real value of the current OS
|
||||
// Linux=0x005413
|
||||
// Darwin and OpenBSD=0x40087468,
|
||||
// Solaris=0x005468
|
||||
// See https://stackoverflow.com/questions/16237137/what-is-termios-tiocgwinsz
|
||||
int get_tiocgwinsz_value() {
|
||||
return TIOCGWINSZ;
|
||||
}
|
||||
@@ -4,20 +4,7 @@ using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of an ANSI escape sequence request made to the terminal using
|
||||
/// <see cref="EscSeqRequests"/>.
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public class EscSeqReqStatus
|
||||
{
|
||||
/// <summary>Creates a new state of escape sequence request.</summary>
|
||||
/// <param name="ansiRequest">The <see cref="AnsiEscapeSequenceRequest"/> object.</param>
|
||||
public EscSeqReqStatus (AnsiEscapeSequenceRequest ansiRequest) { AnsiRequest = ansiRequest; }
|
||||
|
||||
/// <summary>Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).</summary>
|
||||
public AnsiEscapeSequenceRequest AnsiRequest { get; }
|
||||
}
|
||||
// QUESTION: Can this class be moved/refactored/combined with the new AnsiEscapeSequenceRequest/Response class?
|
||||
|
||||
// TODO: This class is a singleton. It should use the singleton pattern.
|
||||
/// <summary>
|
||||
|
||||
17
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs
Normal file
17
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of an ANSI escape sequence request made to the terminal using
|
||||
/// <see cref="EscSeqRequests"/>.
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public class EscSeqReqStatus
|
||||
{
|
||||
/// <summary>Creates a new state of escape sequence request.</summary>
|
||||
/// <param name="ansiRequest">The <see cref="AnsiEscapeSequenceRequest"/> object.</param>
|
||||
public EscSeqReqStatus (AnsiEscapeSequenceRequest ansiRequest) { AnsiRequest = ansiRequest; }
|
||||
|
||||
/// <summary>Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).</summary>
|
||||
public AnsiEscapeSequenceRequest AnsiRequest { get; }
|
||||
}
|
||||
@@ -3,6 +3,13 @@ using Terminal.Gui.ConsoleDrivers;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
// QUESTION: Should this class be refactored into separate classes for:
|
||||
// QUESTION: CSI definitions
|
||||
// QUESTION: Primitives like DecodeEsqReq
|
||||
// QUESTION: Screen/Color/Cursor handling
|
||||
// QUESTION: Mouse handling
|
||||
// QUESTION: Keyboard handling
|
||||
|
||||
/// <summary>
|
||||
/// Provides a platform-independent API for managing ANSI escape sequences.
|
||||
/// </summary>
|
||||
@@ -14,6 +21,7 @@ namespace Terminal.Gui;
|
||||
/// </remarks>
|
||||
public static class EscSeqUtils
|
||||
{
|
||||
// TODO: One type per file - Move this enum to a separate file.
|
||||
/// <summary>
|
||||
/// Options for ANSI ESC "[xJ" - Clears part of the screen.
|
||||
/// </summary>
|
||||
@@ -40,6 +48,9 @@ public static class EscSeqUtils
|
||||
EntireScreenAndScrollbackBuffer = 3
|
||||
}
|
||||
|
||||
// QUESTION: I wonder if EscSeqUtils.CSI_... should be more strongly typed such that this (and Terminator could be
|
||||
// QUESTION: public required CSIRequests Request { get; init; }
|
||||
// QUESTION: public required CSITerminators Terminator { get; init; }
|
||||
/// <summary>
|
||||
/// Escape key code (ASCII 27/0x1B).
|
||||
/// </summary>
|
||||
@@ -419,6 +430,7 @@ public static class EscSeqUtils
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ConsoleKey"/> depending on terminating and value.
|
||||
/// </summary>
|
||||
@@ -1721,7 +1733,7 @@ public static class EscSeqUtils
|
||||
/// https://terminalguide.namepad.de/seq/csi_st-18/
|
||||
/// The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
|
||||
/// </summary>
|
||||
public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
|
||||
public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", ExpectedResponseValue = "8" };
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
321
Terminal.Gui/ConsoleDrivers/KeyCode.cs
Normal file
321
Terminal.Gui/ConsoleDrivers/KeyCode.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a
|
||||
/// consistent way for application code to specify keys and receive key events.
|
||||
/// <para>
|
||||
/// The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for
|
||||
/// common operations. For example, <see cref="Key.IsAlt"/> and <see cref="Key.IsCtrl"/> provide a convenient way
|
||||
/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a
|
||||
/// keyboard. Enum values are provided for these (e.g. <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.).
|
||||
/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent
|
||||
/// *lowercase*, un-shifted characters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. <see cref="KeyCode.D0"/>,
|
||||
/// <see cref="KeyCode.D1"/>, etc.).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The shift modifiers (<see cref="KeyCode.ShiftMask"/>, <see cref="KeyCode.CtrlMask"/>, and
|
||||
/// <see cref="KeyCode.AltMask"/>) can be combined (with logical or) with the other key codes to represent shifted
|
||||
/// keys. For example, the <see cref="KeyCode.A"/> enum value represents the un-shifted 'a' key, while
|
||||
/// <see cref="KeyCode.ShiftMask"/> | <see cref="KeyCode.A"/> represents the 'A' key (shifted 'a' key). Likewise,
|
||||
/// <see cref="KeyCode.AltMask"/> | <see cref="KeyCode.A"/> represents the 'Alt+A' key combination.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All other keys that produce a printable character are encoded as the Unicode value of the character. For
|
||||
/// example, the <see cref="KeyCode"/> for the '!' character is 33, which is the Unicode value for '!'. Likewise,
|
||||
/// `â` is 226, `Â` is 194, etc.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
|
||||
/// the one of the lower bits (as extracted by <see cref="CharMask"/>).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
public enum KeyCode : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Mask that indicates that the key is a unicode codepoint. Values outside this range indicate the key has shift
|
||||
/// modifiers or is a special key like function keys, arrows keys and so on.
|
||||
/// </summary>
|
||||
CharMask = 0x_f_ffff,
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
|
||||
/// in the lower bits (as extracted by <see cref="CharMask"/>).
|
||||
/// </summary>
|
||||
SpecialMask = 0x_fff0_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by
|
||||
/// removing the ShiftMask.
|
||||
/// </summary>
|
||||
ShiftMask = 0x_1000_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by
|
||||
/// removing the AltMask.
|
||||
/// </summary>
|
||||
AltMask = 0x_8000_0000,
|
||||
|
||||
/// <summary>
|
||||
/// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by
|
||||
/// removing the CtrlMask.
|
||||
/// </summary>
|
||||
CtrlMask = 0x_4000_0000,
|
||||
|
||||
/// <summary>The key code representing an invalid or empty key.</summary>
|
||||
Null = 0,
|
||||
|
||||
/// <summary>Backspace key.</summary>
|
||||
Backspace = 8,
|
||||
|
||||
/// <summary>The key code for the tab key (forwards tab key).</summary>
|
||||
Tab = 9,
|
||||
|
||||
/// <summary>The key code for the return key.</summary>
|
||||
Enter = ConsoleKey.Enter,
|
||||
|
||||
/// <summary>The key code for the clear key.</summary>
|
||||
Clear = 12,
|
||||
|
||||
/// <summary>The key code for the escape key.</summary>
|
||||
Esc = 27,
|
||||
|
||||
/// <summary>The key code for the space bar key.</summary>
|
||||
Space = 32,
|
||||
|
||||
/// <summary>Digit 0.</summary>
|
||||
D0 = 48,
|
||||
|
||||
/// <summary>Digit 1.</summary>
|
||||
D1,
|
||||
|
||||
/// <summary>Digit 2.</summary>
|
||||
D2,
|
||||
|
||||
/// <summary>Digit 3.</summary>
|
||||
D3,
|
||||
|
||||
/// <summary>Digit 4.</summary>
|
||||
D4,
|
||||
|
||||
/// <summary>Digit 5.</summary>
|
||||
D5,
|
||||
|
||||
/// <summary>Digit 6.</summary>
|
||||
D6,
|
||||
|
||||
/// <summary>Digit 7.</summary>
|
||||
D7,
|
||||
|
||||
/// <summary>Digit 8.</summary>
|
||||
D8,
|
||||
|
||||
/// <summary>Digit 9.</summary>
|
||||
D9,
|
||||
|
||||
/// <summary>The key code for the A key</summary>
|
||||
A = 65,
|
||||
|
||||
/// <summary>The key code for the B key</summary>
|
||||
B,
|
||||
|
||||
/// <summary>The key code for the C key</summary>
|
||||
C,
|
||||
|
||||
/// <summary>The key code for the D key</summary>
|
||||
D,
|
||||
|
||||
/// <summary>The key code for the E key</summary>
|
||||
E,
|
||||
|
||||
/// <summary>The key code for the F key</summary>
|
||||
F,
|
||||
|
||||
/// <summary>The key code for the G key</summary>
|
||||
G,
|
||||
|
||||
/// <summary>The key code for the H key</summary>
|
||||
H,
|
||||
|
||||
/// <summary>The key code for the I key</summary>
|
||||
I,
|
||||
|
||||
/// <summary>The key code for the J key</summary>
|
||||
J,
|
||||
|
||||
/// <summary>The key code for the K key</summary>
|
||||
K,
|
||||
|
||||
/// <summary>The key code for the L key</summary>
|
||||
L,
|
||||
|
||||
/// <summary>The key code for the M key</summary>
|
||||
M,
|
||||
|
||||
/// <summary>The key code for the N key</summary>
|
||||
N,
|
||||
|
||||
/// <summary>The key code for the O key</summary>
|
||||
O,
|
||||
|
||||
/// <summary>The key code for the P key</summary>
|
||||
P,
|
||||
|
||||
/// <summary>The key code for the Q key</summary>
|
||||
Q,
|
||||
|
||||
/// <summary>The key code for the R key</summary>
|
||||
R,
|
||||
|
||||
/// <summary>The key code for the S key</summary>
|
||||
S,
|
||||
|
||||
/// <summary>The key code for the T key</summary>
|
||||
T,
|
||||
|
||||
/// <summary>The key code for the U key</summary>
|
||||
U,
|
||||
|
||||
/// <summary>The key code for the V key</summary>
|
||||
V,
|
||||
|
||||
/// <summary>The key code for the W key</summary>
|
||||
W,
|
||||
|
||||
/// <summary>The key code for the X key</summary>
|
||||
X,
|
||||
|
||||
/// <summary>The key code for the Y key</summary>
|
||||
Y,
|
||||
|
||||
/// <summary>The key code for the Z key</summary>
|
||||
Z,
|
||||
|
||||
///// <summary>
|
||||
///// The key code for the Delete key.
|
||||
///// </summary>
|
||||
//Delete = 127,
|
||||
|
||||
// --- Special keys ---
|
||||
// The values below are common non-alphanum keys. Their values are
|
||||
// based on the .NET ConsoleKey values, which, in-turn are based on the
|
||||
// VK_ values from the Windows API.
|
||||
// We add MaxCodePoint to avoid conflicts with the Unicode values.
|
||||
|
||||
/// <summary>The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys.</summary>
|
||||
MaxCodePoint = 0x10FFFF,
|
||||
|
||||
/// <summary>Cursor up key</summary>
|
||||
CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
|
||||
|
||||
/// <summary>Cursor down key.</summary>
|
||||
CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
|
||||
|
||||
/// <summary>Cursor left key.</summary>
|
||||
CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
|
||||
|
||||
/// <summary>Cursor right key.</summary>
|
||||
CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
|
||||
|
||||
/// <summary>Page Up key.</summary>
|
||||
PageUp = MaxCodePoint + ConsoleKey.PageUp,
|
||||
|
||||
/// <summary>Page Down key.</summary>
|
||||
PageDown = MaxCodePoint + ConsoleKey.PageDown,
|
||||
|
||||
/// <summary>Home key.</summary>
|
||||
Home = MaxCodePoint + ConsoleKey.Home,
|
||||
|
||||
/// <summary>End key.</summary>
|
||||
End = MaxCodePoint + ConsoleKey.End,
|
||||
|
||||
/// <summary>Insert (INS) key.</summary>
|
||||
Insert = MaxCodePoint + ConsoleKey.Insert,
|
||||
|
||||
/// <summary>Delete (DEL) key.</summary>
|
||||
Delete = MaxCodePoint + ConsoleKey.Delete,
|
||||
|
||||
/// <summary>Print screen character key.</summary>
|
||||
PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
|
||||
|
||||
/// <summary>F1 key.</summary>
|
||||
F1 = MaxCodePoint + ConsoleKey.F1,
|
||||
|
||||
/// <summary>F2 key.</summary>
|
||||
F2 = MaxCodePoint + ConsoleKey.F2,
|
||||
|
||||
/// <summary>F3 key.</summary>
|
||||
F3 = MaxCodePoint + ConsoleKey.F3,
|
||||
|
||||
/// <summary>F4 key.</summary>
|
||||
F4 = MaxCodePoint + ConsoleKey.F4,
|
||||
|
||||
/// <summary>F5 key.</summary>
|
||||
F5 = MaxCodePoint + ConsoleKey.F5,
|
||||
|
||||
/// <summary>F6 key.</summary>
|
||||
F6 = MaxCodePoint + ConsoleKey.F6,
|
||||
|
||||
/// <summary>F7 key.</summary>
|
||||
F7 = MaxCodePoint + ConsoleKey.F7,
|
||||
|
||||
/// <summary>F8 key.</summary>
|
||||
F8 = MaxCodePoint + ConsoleKey.F8,
|
||||
|
||||
/// <summary>F9 key.</summary>
|
||||
F9 = MaxCodePoint + ConsoleKey.F9,
|
||||
|
||||
/// <summary>F10 key.</summary>
|
||||
F10 = MaxCodePoint + ConsoleKey.F10,
|
||||
|
||||
/// <summary>F11 key.</summary>
|
||||
F11 = MaxCodePoint + ConsoleKey.F11,
|
||||
|
||||
/// <summary>F12 key.</summary>
|
||||
F12 = MaxCodePoint + ConsoleKey.F12,
|
||||
|
||||
/// <summary>F13 key.</summary>
|
||||
F13 = MaxCodePoint + ConsoleKey.F13,
|
||||
|
||||
/// <summary>F14 key.</summary>
|
||||
F14 = MaxCodePoint + ConsoleKey.F14,
|
||||
|
||||
/// <summary>F15 key.</summary>
|
||||
F15 = MaxCodePoint + ConsoleKey.F15,
|
||||
|
||||
/// <summary>F16 key.</summary>
|
||||
F16 = MaxCodePoint + ConsoleKey.F16,
|
||||
|
||||
/// <summary>F17 key.</summary>
|
||||
F17 = MaxCodePoint + ConsoleKey.F17,
|
||||
|
||||
/// <summary>F18 key.</summary>
|
||||
F18 = MaxCodePoint + ConsoleKey.F18,
|
||||
|
||||
/// <summary>F19 key.</summary>
|
||||
F19 = MaxCodePoint + ConsoleKey.F19,
|
||||
|
||||
/// <summary>F20 key.</summary>
|
||||
F20 = MaxCodePoint + ConsoleKey.F20,
|
||||
|
||||
/// <summary>F21 key.</summary>
|
||||
F21 = MaxCodePoint + ConsoleKey.F21,
|
||||
|
||||
/// <summary>F22 key.</summary>
|
||||
F22 = MaxCodePoint + ConsoleKey.F22,
|
||||
|
||||
/// <summary>F23 key.</summary>
|
||||
F23 = MaxCodePoint + ConsoleKey.F23,
|
||||
|
||||
/// <summary>F24 key.</summary>
|
||||
F24 = MaxCodePoint + ConsoleKey.F24
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
965
Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
Normal file
965
Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
Normal file
@@ -0,0 +1,965 @@
|
||||
// TODO: #nullable enable
|
||||
//
|
||||
// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
|
||||
//
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
|
||||
using static Terminal.Gui.NetEvents;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class NetDriver : ConsoleDriver
|
||||
{
|
||||
public bool IsWinPlatform { get; private set; }
|
||||
public NetWinVTConsole NetWinConsole { get; private set; }
|
||||
|
||||
public override void Refresh ()
|
||||
{
|
||||
UpdateScreen ();
|
||||
UpdateCursor ();
|
||||
}
|
||||
|
||||
public override void Suspend ()
|
||||
{
|
||||
if (Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopReportingMouseMoves ();
|
||||
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Console.ResetColor ();
|
||||
Console.Clear ();
|
||||
|
||||
//Disable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
|
||||
|
||||
//Set cursor key to cursor.
|
||||
Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
|
||||
|
||||
Platform.Suspend ();
|
||||
|
||||
//Enable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
|
||||
|
||||
SetContentsAsDirty ();
|
||||
Refresh ();
|
||||
}
|
||||
|
||||
StartReportingMouseMoves ();
|
||||
}
|
||||
|
||||
#region Screen and Contents
|
||||
|
||||
public override void UpdateScreen ()
|
||||
{
|
||||
if (RunningUnitTests
|
||||
|| _winSizeChanging
|
||||
|| Console.WindowHeight < 1
|
||||
|| Contents.Length != Rows * Cols
|
||||
|| Rows != Console.WindowHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
int rows = Rows;
|
||||
int cols = Cols;
|
||||
var output = new StringBuilder ();
|
||||
Attribute? redrawAttr = null;
|
||||
int lastCol = -1;
|
||||
|
||||
CursorVisibility? savedVisibility = _cachedCursorVisibility;
|
||||
SetCursorVisibility (CursorVisibility.Invisible);
|
||||
|
||||
for (int row = top; row < rows; row++)
|
||||
{
|
||||
if (Console.WindowHeight < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_dirtyLines [row])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!SetCursorPosition (0, row))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dirtyLines [row] = false;
|
||||
output.Clear ();
|
||||
|
||||
for (int col = left; col < cols; col++)
|
||||
{
|
||||
lastCol = -1;
|
||||
var outputWidth = 0;
|
||||
|
||||
for (; col < cols; col++)
|
||||
{
|
||||
if (!Contents [row, col].IsDirty)
|
||||
{
|
||||
if (output.Length > 0)
|
||||
{
|
||||
WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
||||
}
|
||||
else if (lastCol == -1)
|
||||
{
|
||||
lastCol = col;
|
||||
}
|
||||
|
||||
if (lastCol + 1 < cols)
|
||||
{
|
||||
lastCol++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastCol == -1)
|
||||
{
|
||||
lastCol = col;
|
||||
}
|
||||
|
||||
Attribute attr = Contents [row, col].Attribute.Value;
|
||||
|
||||
// Performance: Only send the escape sequence if the attribute has changed.
|
||||
if (attr != redrawAttr)
|
||||
{
|
||||
redrawAttr = attr;
|
||||
|
||||
if (Force16Colors)
|
||||
{
|
||||
output.Append (
|
||||
EscSeqUtils.CSI_SetGraphicsRendition (
|
||||
MapColors (
|
||||
(ConsoleColor)attr.Background.GetClosestNamedColor16 (),
|
||||
false
|
||||
),
|
||||
MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ())
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append (
|
||||
EscSeqUtils.CSI_SetForegroundColorRGB (
|
||||
attr.Foreground.R,
|
||||
attr.Foreground.G,
|
||||
attr.Foreground.B
|
||||
)
|
||||
);
|
||||
|
||||
output.Append (
|
||||
EscSeqUtils.CSI_SetBackgroundColorRGB (
|
||||
attr.Background.R,
|
||||
attr.Background.G,
|
||||
attr.Background.B
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
outputWidth++;
|
||||
Rune rune = Contents [row, col].Rune;
|
||||
output.Append (rune);
|
||||
|
||||
if (Contents [row, col].CombiningMarks.Count > 0)
|
||||
{
|
||||
// AtlasEngine does not support NON-NORMALIZED combining marks in a way
|
||||
// compatible with the driver architecture. Any CMs (except in the first col)
|
||||
// are correctly combined with the base char, but are ALSO treated as 1 column
|
||||
// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
|
||||
//
|
||||
// For now, we just ignore the list of CMs.
|
||||
//foreach (var combMark in Contents [row, col].CombiningMarks) {
|
||||
// output.Append (combMark);
|
||||
//}
|
||||
// WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
||||
}
|
||||
else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
|
||||
{
|
||||
WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
||||
SetCursorPosition (col - 1, row);
|
||||
}
|
||||
|
||||
Contents [row, col].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
SetCursorPosition (lastCol, row);
|
||||
Console.Write (output);
|
||||
}
|
||||
|
||||
foreach (SixelToRender s in Application.Sixel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace (s.SixelData))
|
||||
{
|
||||
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
|
||||
Console.Write (s.SixelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetCursorPosition (0, 0);
|
||||
|
||||
_cachedCursorVisibility = savedVisibility;
|
||||
|
||||
void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
|
||||
{
|
||||
SetCursorPosition (lastCol, row);
|
||||
Console.Write (output);
|
||||
output.Clear ();
|
||||
lastCol += outputWidth;
|
||||
outputWidth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Screen and Contents
|
||||
|
||||
#region Init/End/MainLoop
|
||||
|
||||
internal NetMainLoop _mainLoopDriver;
|
||||
|
||||
internal override MainLoop Init ()
|
||||
{
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
IsWinPlatform = true;
|
||||
|
||||
try
|
||||
{
|
||||
NetWinConsole = new ();
|
||||
}
|
||||
catch (ApplicationException)
|
||||
{
|
||||
// Likely running as a unit test, or in a non-interactive session.
|
||||
}
|
||||
}
|
||||
|
||||
if (IsWinPlatform)
|
||||
{
|
||||
Clipboard = new WindowsClipboard ();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
|
||||
{
|
||||
Clipboard = new MacOSXClipboard ();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CursesDriver.Is_WSL_Platform ())
|
||||
{
|
||||
Clipboard = new WSLClipboard ();
|
||||
}
|
||||
else
|
||||
{
|
||||
Clipboard = new CursesClipboard ();
|
||||
}
|
||||
}
|
||||
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Console.TreatControlCAsInput = true;
|
||||
|
||||
Cols = Console.WindowWidth;
|
||||
Rows = Console.WindowHeight;
|
||||
|
||||
//Enable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
|
||||
|
||||
//Set cursor key to application.
|
||||
Console.Out.Write (EscSeqUtils.CSI_HideCursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are being run in an environment that does not support a console
|
||||
// such as a unit test, or a pipe.
|
||||
Cols = 80;
|
||||
Rows = 24;
|
||||
}
|
||||
|
||||
ResizeScreen ();
|
||||
ClearContents ();
|
||||
CurrentAttribute = new (Color.White, Color.Black);
|
||||
|
||||
StartReportingMouseMoves ();
|
||||
|
||||
_mainLoopDriver = new (this);
|
||||
_mainLoopDriver.ProcessInput = ProcessInput;
|
||||
|
||||
return new (_mainLoopDriver);
|
||||
}
|
||||
|
||||
private void ProcessInput (InputResult inputEvent)
|
||||
{
|
||||
switch (inputEvent.EventType)
|
||||
{
|
||||
case EventType.Key:
|
||||
ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
|
||||
|
||||
//if (consoleKeyInfo.Key == ConsoleKey.Packet) {
|
||||
// consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
|
||||
//}
|
||||
|
||||
//Debug.WriteLine ($"event: {inputEvent}");
|
||||
|
||||
KeyCode map = MapKey (consoleKeyInfo);
|
||||
|
||||
if (map == KeyCode.Null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
OnKeyDown (new (map));
|
||||
OnKeyUp (new (map));
|
||||
|
||||
break;
|
||||
case EventType.Mouse:
|
||||
MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
|
||||
|
||||
//Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
|
||||
OnMouseEvent (me);
|
||||
|
||||
break;
|
||||
case EventType.WindowSize:
|
||||
_winSizeChanging = true;
|
||||
Top = 0;
|
||||
Left = 0;
|
||||
Cols = inputEvent.WindowSizeEvent.Size.Width;
|
||||
Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
|
||||
;
|
||||
ResizeScreen ();
|
||||
ClearContents ();
|
||||
_winSizeChanging = false;
|
||||
OnSizeChanged (new (new (Cols, Rows)));
|
||||
|
||||
break;
|
||||
case EventType.RequestResponse:
|
||||
break;
|
||||
case EventType.WindowPosition:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void End ()
|
||||
{
|
||||
if (IsWinPlatform)
|
||||
{
|
||||
NetWinConsole?.Cleanup ();
|
||||
}
|
||||
|
||||
StopReportingMouseMoves ();
|
||||
|
||||
_ansiResponseTokenSource?.Cancel ();
|
||||
_ansiResponseTokenSource?.Dispose ();
|
||||
|
||||
_waitAnsiResponse?.Dispose ();
|
||||
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Console.ResetColor ();
|
||||
|
||||
//Disable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
|
||||
|
||||
//Set cursor key to cursor.
|
||||
Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
|
||||
Console.Out.Close ();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Init/End/MainLoop
|
||||
|
||||
#region Color Handling
|
||||
|
||||
public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
|
||||
|| (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
|
||||
|
||||
private const int COLOR_BLACK = 30;
|
||||
private const int COLOR_BLUE = 34;
|
||||
private const int COLOR_BRIGHT_BLACK = 90;
|
||||
private const int COLOR_BRIGHT_BLUE = 94;
|
||||
private const int COLOR_BRIGHT_CYAN = 96;
|
||||
private const int COLOR_BRIGHT_GREEN = 92;
|
||||
private const int COLOR_BRIGHT_MAGENTA = 95;
|
||||
private const int COLOR_BRIGHT_RED = 91;
|
||||
private const int COLOR_BRIGHT_WHITE = 97;
|
||||
private const int COLOR_BRIGHT_YELLOW = 93;
|
||||
private const int COLOR_CYAN = 36;
|
||||
private const int COLOR_GREEN = 32;
|
||||
private const int COLOR_MAGENTA = 35;
|
||||
private const int COLOR_RED = 31;
|
||||
private const int COLOR_WHITE = 37;
|
||||
private const int COLOR_YELLOW = 33;
|
||||
|
||||
// Cache the list of ConsoleColor values.
|
||||
[UnconditionalSuppressMessage (
|
||||
"AOT",
|
||||
"IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
|
||||
Justification = "<Pending>")]
|
||||
private static readonly HashSet<int> ConsoleColorValues = new (
|
||||
Enum.GetValues (typeof (ConsoleColor))
|
||||
.OfType<ConsoleColor> ()
|
||||
.Select (c => (int)c)
|
||||
);
|
||||
|
||||
// Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
|
||||
private static readonly Dictionary<ConsoleColor, int> colorMap = new ()
|
||||
{
|
||||
{ ConsoleColor.Black, COLOR_BLACK },
|
||||
{ ConsoleColor.DarkBlue, COLOR_BLUE },
|
||||
{ ConsoleColor.DarkGreen, COLOR_GREEN },
|
||||
{ ConsoleColor.DarkCyan, COLOR_CYAN },
|
||||
{ ConsoleColor.DarkRed, COLOR_RED },
|
||||
{ ConsoleColor.DarkMagenta, COLOR_MAGENTA },
|
||||
{ ConsoleColor.DarkYellow, COLOR_YELLOW },
|
||||
{ ConsoleColor.Gray, COLOR_WHITE },
|
||||
{ ConsoleColor.DarkGray, COLOR_BRIGHT_BLACK },
|
||||
{ ConsoleColor.Blue, COLOR_BRIGHT_BLUE },
|
||||
{ ConsoleColor.Green, COLOR_BRIGHT_GREEN },
|
||||
{ ConsoleColor.Cyan, COLOR_BRIGHT_CYAN },
|
||||
{ ConsoleColor.Red, COLOR_BRIGHT_RED },
|
||||
{ ConsoleColor.Magenta, COLOR_BRIGHT_MAGENTA },
|
||||
{ ConsoleColor.Yellow, COLOR_BRIGHT_YELLOW },
|
||||
{ ConsoleColor.White, COLOR_BRIGHT_WHITE }
|
||||
};
|
||||
|
||||
// Map a ConsoleColor to a platform dependent value.
|
||||
private int MapColors (ConsoleColor color, bool isForeground = true)
|
||||
{
|
||||
return colorMap.TryGetValue (color, out int colorValue) ? colorValue + (isForeground ? 0 : 10) : 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cursor Handling
|
||||
|
||||
private bool SetCursorPosition (int col, int row)
|
||||
{
|
||||
if (IsWinPlatform)
|
||||
{
|
||||
// Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
|
||||
try
|
||||
{
|
||||
Console.SetCursorPosition (col, row);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// + 1 is needed because non-Windows is based on 1 instead of 0 and
|
||||
// Console.CursorTop/CursorLeft isn't reliable.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private CursorVisibility? _cachedCursorVisibility;
|
||||
|
||||
public override void UpdateCursor ()
|
||||
{
|
||||
EnsureCursorVisibility ();
|
||||
|
||||
if (Col >= 0 && Col < Cols && Row >= 0 && Row <= Rows)
|
||||
{
|
||||
SetCursorPosition (Col, Row);
|
||||
SetWindowPosition (0, Row);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GetCursorVisibility (out CursorVisibility visibility)
|
||||
{
|
||||
visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
|
||||
|
||||
return visibility == CursorVisibility.Default;
|
||||
}
|
||||
|
||||
public override bool SetCursorVisibility (CursorVisibility visibility)
|
||||
{
|
||||
_cachedCursorVisibility = visibility;
|
||||
|
||||
Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
|
||||
|
||||
return visibility == CursorVisibility.Default;
|
||||
}
|
||||
|
||||
public override bool EnsureCursorVisibility ()
|
||||
{
|
||||
if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
|
||||
{
|
||||
GetCursorVisibility (out CursorVisibility cursorVisibility);
|
||||
_cachedCursorVisibility = cursorVisibility;
|
||||
SetCursorVisibility (CursorVisibility.Invisible);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
|
||||
|
||||
return _cachedCursorVisibility == CursorVisibility.Default;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse Handling
|
||||
|
||||
public void StartReportingMouseMoves ()
|
||||
{
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopReportingMouseMoves ()
|
||||
{
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private MouseEventArgs ToDriverMouse (MouseEvent me)
|
||||
{
|
||||
//System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
|
||||
|
||||
MouseFlags mouseFlag = 0;
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button1Pressed) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button1Pressed;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button1Released) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button1Released;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button1Clicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button1DoubleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button1TripleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button2Pressed;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button2Released) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button2Released;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button2Clicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button2DoubleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button2TripleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button3Pressed;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button3Released) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button3Released;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button3Clicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button3DoubleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button3TripleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.WheeledUp;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.WheeledDown;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.WheeledLeft;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.WheeledRight;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button4Pressed;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button4Released) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button4Released;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button4Clicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button4DoubleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.Button4TripleClicked;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.ReportMousePosition;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonShift) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.ButtonShift;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.ButtonCtrl;
|
||||
}
|
||||
|
||||
if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0)
|
||||
{
|
||||
mouseFlag |= MouseFlags.ButtonAlt;
|
||||
}
|
||||
|
||||
return new() { Position = me.Position, Flags = mouseFlag };
|
||||
}
|
||||
|
||||
#endregion Mouse Handling
|
||||
|
||||
#region Keyboard Handling
|
||||
|
||||
public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
|
||||
{
|
||||
var input = new InputResult
|
||||
{
|
||||
EventType = EventType.Key, ConsoleKeyInfo = new (keyChar, key, shift, alt, control)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ProcessInput (input);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{ }
|
||||
}
|
||||
|
||||
private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
|
||||
{
|
||||
if (consoleKeyInfo.Key != ConsoleKey.Packet)
|
||||
{
|
||||
return consoleKeyInfo;
|
||||
}
|
||||
|
||||
ConsoleModifiers mod = consoleKeyInfo.Modifiers;
|
||||
bool shift = (mod & ConsoleModifiers.Shift) != 0;
|
||||
bool alt = (mod & ConsoleModifiers.Alt) != 0;
|
||||
bool control = (mod & ConsoleModifiers.Control) != 0;
|
||||
|
||||
ConsoleKeyInfo cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
|
||||
|
||||
return new (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
|
||||
}
|
||||
|
||||
private KeyCode MapKey (ConsoleKeyInfo keyInfo)
|
||||
{
|
||||
switch (keyInfo.Key)
|
||||
{
|
||||
case ConsoleKey.OemPeriod:
|
||||
case ConsoleKey.OemComma:
|
||||
case ConsoleKey.OemPlus:
|
||||
case ConsoleKey.OemMinus:
|
||||
case ConsoleKey.Packet:
|
||||
case ConsoleKey.Oem1:
|
||||
case ConsoleKey.Oem2:
|
||||
case ConsoleKey.Oem3:
|
||||
case ConsoleKey.Oem4:
|
||||
case ConsoleKey.Oem5:
|
||||
case ConsoleKey.Oem6:
|
||||
case ConsoleKey.Oem7:
|
||||
case ConsoleKey.Oem8:
|
||||
case ConsoleKey.Oem102:
|
||||
if (keyInfo.KeyChar == 0)
|
||||
{
|
||||
// If the keyChar is 0, keyInfo.Key value is not a printable character.
|
||||
|
||||
return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
|
||||
}
|
||||
|
||||
if (keyInfo.Modifiers != ConsoleModifiers.Shift)
|
||||
{
|
||||
// If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
|
||||
}
|
||||
|
||||
// Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
|
||||
// and passing on Shift would be redundant.
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
|
||||
}
|
||||
|
||||
// Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
|
||||
if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
|
||||
{
|
||||
if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
|
||||
{
|
||||
return KeyCode.Tab;
|
||||
}
|
||||
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
|
||||
}
|
||||
|
||||
// Handle control keys (e.g. CursorUp)
|
||||
if (keyInfo.Key != ConsoleKey.None
|
||||
&& Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
|
||||
{
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
|
||||
}
|
||||
|
||||
if ((ConsoleKey)keyInfo.KeyChar is >= ConsoleKey.A and <= ConsoleKey.Z)
|
||||
{
|
||||
// Shifted
|
||||
keyInfo = new (
|
||||
keyInfo.KeyChar,
|
||||
(ConsoleKey)keyInfo.KeyChar,
|
||||
true,
|
||||
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
|
||||
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
|
||||
}
|
||||
|
||||
if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z)
|
||||
{
|
||||
// Unshifted
|
||||
keyInfo = new (
|
||||
keyInfo.KeyChar,
|
||||
(ConsoleKey)(keyInfo.KeyChar - 32),
|
||||
false,
|
||||
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
|
||||
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
|
||||
}
|
||||
|
||||
if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
|
||||
{
|
||||
if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
|
||||
|| keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
|
||||
{
|
||||
// NetDriver doesn't support Shift-Ctrl/Shift-Alt combos
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key);
|
||||
}
|
||||
|
||||
if (keyInfo.Modifiers == ConsoleModifiers.Shift)
|
||||
{
|
||||
// If ShiftMask is on add the ShiftMask
|
||||
if (char.IsUpper (keyInfo.KeyChar))
|
||||
{
|
||||
return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
|
||||
}
|
||||
}
|
||||
|
||||
return (KeyCode)keyInfo.Key;
|
||||
}
|
||||
|
||||
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
|
||||
}
|
||||
|
||||
#endregion Keyboard Handling
|
||||
|
||||
#region Low-Level DotNet tuff
|
||||
|
||||
private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
|
||||
private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
|
||||
{
|
||||
if (_mainLoopDriver is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (ansiRequest._responseLock)
|
||||
{
|
||||
ansiRequest.ResponseFromInput += (s, e) =>
|
||||
{
|
||||
Debug.Assert (s == ansiRequest);
|
||||
Debug.Assert (e == ansiRequest.Response);
|
||||
|
||||
_waitAnsiResponse.Set ();
|
||||
};
|
||||
|
||||
_mainLoopDriver._netEvents.EscSeqRequests.Add (ansiRequest);
|
||||
|
||||
_mainLoopDriver._netEvents._forceRead = true;
|
||||
}
|
||||
|
||||
if (!_ansiResponseTokenSource.IsCancellationRequested)
|
||||
{
|
||||
_mainLoopDriver._netEvents._waitForStart.Set ();
|
||||
|
||||
if (!_mainLoopDriver._waitForProbe.IsSet)
|
||||
{
|
||||
_mainLoopDriver._waitForProbe.Set ();
|
||||
}
|
||||
|
||||
_waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
lock (ansiRequest._responseLock)
|
||||
{
|
||||
_mainLoopDriver._netEvents._forceRead = false;
|
||||
|
||||
if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
|
||||
{
|
||||
if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.Count > 0
|
||||
&& string.IsNullOrEmpty (request.AnsiRequest.Response))
|
||||
{
|
||||
lock (request!.AnsiRequest._responseLock)
|
||||
{
|
||||
// Bad request or no response at all
|
||||
_mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryDequeue (out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_waitAnsiResponse.Reset ();
|
||||
|
||||
return ansiRequest.Response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void WriteRaw (string ansi) { throw new NotImplementedException (); }
|
||||
|
||||
private volatile bool _winSizeChanging;
|
||||
|
||||
private void SetWindowPosition (int col, int row)
|
||||
{
|
||||
if (!RunningUnitTests)
|
||||
{
|
||||
Top = Console.WindowTop;
|
||||
Left = Console.WindowLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
Top = row;
|
||||
Left = col;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResizeScreen ()
|
||||
{
|
||||
// Not supported on Unix.
|
||||
if (IsWinPlatform)
|
||||
{
|
||||
// Can raise an exception while is still resizing.
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1416
|
||||
if (Console.WindowHeight > 0)
|
||||
{
|
||||
Console.CursorTop = 0;
|
||||
Console.CursorLeft = 0;
|
||||
Console.WindowTop = 0;
|
||||
Console.WindowLeft = 0;
|
||||
|
||||
if (Console.WindowHeight > Rows)
|
||||
{
|
||||
Console.SetWindowSize (Cols, Rows);
|
||||
}
|
||||
|
||||
Console.SetBufferSize (Cols, Rows);
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
}
|
||||
|
||||
// INTENT: Why are these eating the exceptions?
|
||||
// Comments would be good here.
|
||||
catch (IOException)
|
||||
{
|
||||
// CONCURRENCY: Unsynchronized access to Clip is not safe.
|
||||
Clip = new (0, 0, Cols, Rows);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// CONCURRENCY: Unsynchronized access to Clip is not safe.
|
||||
Clip = new (0, 0, Cols, Rows);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
|
||||
}
|
||||
|
||||
// CONCURRENCY: Unsynchronized access to Clip is not safe.
|
||||
Clip = new (0, 0, Cols, Rows);
|
||||
}
|
||||
|
||||
#endregion Low-Level DotNet tuff
|
||||
}
|
||||
753
Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
Normal file
753
Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
Normal file
@@ -0,0 +1,753 @@
|
||||
// TODO: #nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class NetEvents : IDisposable
|
||||
{
|
||||
private readonly ManualResetEventSlim _inputReady = new (false);
|
||||
private CancellationTokenSource _inputReadyCancellationTokenSource;
|
||||
internal readonly ManualResetEventSlim _waitForStart = new (false);
|
||||
|
||||
//CancellationTokenSource _waitForStartCancellationTokenSource;
|
||||
private readonly ManualResetEventSlim _winChange = new (false);
|
||||
private readonly ConcurrentQueue<InputResult?> _inputQueue = new ();
|
||||
private readonly ConsoleDriver _consoleDriver;
|
||||
private ConsoleKeyInfo [] _cki;
|
||||
private bool _isEscSeq;
|
||||
#if PROCESS_REQUEST
|
||||
bool _neededProcessRequest;
|
||||
#endif
|
||||
public EscSeqRequests EscSeqRequests { get; } = new ();
|
||||
|
||||
public NetEvents (ConsoleDriver consoleDriver)
|
||||
{
|
||||
_consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
|
||||
_inputReadyCancellationTokenSource = new CancellationTokenSource ();
|
||||
|
||||
Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
|
||||
|
||||
Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public InputResult? DequeueInput ()
|
||||
{
|
||||
while (_inputReadyCancellationTokenSource != null
|
||||
&& !_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
_waitForStart.Set ();
|
||||
_winChange.Set ();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
if (_inputQueue.Count == 0)
|
||||
{
|
||||
_inputReady.Wait (_inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inputReady.Reset ();
|
||||
}
|
||||
|
||||
#if PROCESS_REQUEST
|
||||
_neededProcessRequest = false;
|
||||
#endif
|
||||
if (_inputQueue.Count > 0)
|
||||
{
|
||||
if (_inputQueue.TryDequeue (out InputResult? result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// if there is a key available, return it without waiting
|
||||
// (or dispatching work to the thread queue)
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
return Console.ReadKey (intercept);
|
||||
}
|
||||
|
||||
if (EscSeqUtils.IncompleteCkInfos is null && EscSeqRequests is { Statuses.Count: > 0 })
|
||||
{
|
||||
if (_retries > 1)
|
||||
{
|
||||
if (EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response))
|
||||
{
|
||||
lock (seqReqStatus!.AnsiRequest._responseLock)
|
||||
{
|
||||
EscSeqRequests.Statuses.TryDequeue (out _);
|
||||
|
||||
seqReqStatus.AnsiRequest.Response = string.Empty;
|
||||
seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
_retries = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_retries++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_retries = 0;
|
||||
}
|
||||
|
||||
if (!_forceRead)
|
||||
{
|
||||
Task.Delay (100, cancellationToken).Wait (cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested ();
|
||||
|
||||
return default (ConsoleKeyInfo);
|
||||
}
|
||||
|
||||
internal bool _forceRead;
|
||||
private int _retries;
|
||||
|
||||
private void ProcessInputQueue ()
|
||||
{
|
||||
while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_forceRead)
|
||||
{
|
||||
_waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_waitForStart.Reset ();
|
||||
|
||||
if (_inputQueue.Count == 0 || _forceRead)
|
||||
{
|
||||
ConsoleKey key = 0;
|
||||
ConsoleModifiers mod = 0;
|
||||
ConsoleKeyInfo newConsoleKeyInfo = default;
|
||||
|
||||
while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
|
||||
{
|
||||
ConsoleKeyInfo consoleKeyInfo;
|
||||
|
||||
try
|
||||
{
|
||||
consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EscSeqUtils.IncompleteCkInfos is { })
|
||||
{
|
||||
EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
|
||||
}
|
||||
|
||||
if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
|
||||
|| (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
|
||||
{
|
||||
if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
|
||||
{
|
||||
_cki = EscSeqUtils.ResizeArray (
|
||||
new ConsoleKeyInfo (
|
||||
(char)KeyCode.Esc,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
),
|
||||
_cki
|
||||
);
|
||||
}
|
||||
|
||||
_isEscSeq = true;
|
||||
|
||||
if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
|
||||
|| (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
|
||||
|| (_cki is { } && char.IsLetter (_cki [^1].KeyChar) && char.IsLower (consoleKeyInfo.KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar))
|
||||
|| (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar)))
|
||||
{
|
||||
ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
|
||||
_cki = null;
|
||||
_isEscSeq = false;
|
||||
|
||||
ProcessMapConsoleKeyInfo (consoleKeyInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
newConsoleKeyInfo = consoleKeyInfo;
|
||||
_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
|
||||
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
|
||||
_cki = null;
|
||||
_isEscSeq = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
|
||||
{
|
||||
ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
|
||||
_cki = null;
|
||||
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessMapConsoleKeyInfo (consoleKeyInfo);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ProcessMapConsoleKeyInfo (consoleKeyInfo);
|
||||
|
||||
if (_retries > 0)
|
||||
{
|
||||
_retries = 0;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_inputReady.Set ();
|
||||
}
|
||||
|
||||
void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
|
||||
{
|
||||
_inputQueue.Enqueue (
|
||||
new InputResult
|
||||
{
|
||||
EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
|
||||
}
|
||||
);
|
||||
_isEscSeq = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckWindowSizeChange ()
|
||||
{
|
||||
void RequestWindowSize (CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Wait for a while then check if screen has changed sizes
|
||||
Task.Delay (500, cancellationToken).Wait (cancellationToken);
|
||||
|
||||
int buffHeight, buffWidth;
|
||||
|
||||
if (((NetDriver)_consoleDriver).IsWinPlatform)
|
||||
{
|
||||
buffHeight = Math.Max (Console.BufferHeight, 0);
|
||||
buffWidth = Math.Max (Console.BufferWidth, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
buffHeight = _consoleDriver.Rows;
|
||||
buffWidth = _consoleDriver.Cols;
|
||||
}
|
||||
|
||||
if (EnqueueWindowSizeEvent (
|
||||
Math.Max (Console.WindowHeight, 0),
|
||||
Math.Max (Console.WindowWidth, 0),
|
||||
buffHeight,
|
||||
buffWidth
|
||||
))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested ();
|
||||
}
|
||||
|
||||
while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
_winChange.Wait (_inputReadyCancellationTokenSource.Token);
|
||||
_winChange.Reset ();
|
||||
|
||||
RequestWindowSize (_inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inputReady.Set ();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enqueue a window size event if the window size has changed.</summary>
|
||||
/// <param name="winHeight"></param>
|
||||
/// <param name="winWidth"></param>
|
||||
/// <param name="buffHeight"></param>
|
||||
/// <param name="buffWidth"></param>
|
||||
/// <returns></returns>
|
||||
private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
|
||||
{
|
||||
if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int w = Math.Max (winWidth, 0);
|
||||
int h = Math.Max (winHeight, 0);
|
||||
|
||||
_inputQueue.Enqueue (
|
||||
new InputResult
|
||||
{
|
||||
EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) }
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
|
||||
private void ProcessRequestResponse (
|
||||
ref ConsoleKeyInfo newConsoleKeyInfo,
|
||||
ref ConsoleKey key,
|
||||
ConsoleKeyInfo [] cki,
|
||||
ref ConsoleModifiers mod
|
||||
)
|
||||
{
|
||||
// isMouse is true if it's CSI<, false otherwise
|
||||
EscSeqUtils.DecodeEscSeq (
|
||||
EscSeqRequests,
|
||||
ref newConsoleKeyInfo,
|
||||
ref key,
|
||||
cki,
|
||||
ref mod,
|
||||
out string c1Control,
|
||||
out string code,
|
||||
out string [] values,
|
||||
out string terminating,
|
||||
out bool isMouse,
|
||||
out List<MouseFlags> mouseFlags,
|
||||
out Point pos,
|
||||
out EscSeqReqStatus seqReqStatus,
|
||||
(f, p) => HandleMouseEvent (MapMouseFlags (f), p)
|
||||
);
|
||||
|
||||
if (isMouse)
|
||||
{
|
||||
foreach (MouseFlags mf in mouseFlags)
|
||||
{
|
||||
HandleMouseEvent (MapMouseFlags (mf), pos);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (seqReqStatus is { })
|
||||
{
|
||||
//HandleRequestResponseEvent (c1Control, code, values, terminating);
|
||||
|
||||
var ckiString = EscSeqUtils.ToString (cki);
|
||||
|
||||
lock (seqReqStatus.AnsiRequest._responseLock)
|
||||
{
|
||||
seqReqStatus.AnsiRequest.Response = ckiString;
|
||||
seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, ckiString);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty (EscSeqUtils.InvalidRequestTerminator))
|
||||
{
|
||||
if (EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus result))
|
||||
{
|
||||
lock (result.AnsiRequest._responseLock)
|
||||
{
|
||||
result.AnsiRequest.Response = EscSeqUtils.InvalidRequestTerminator;
|
||||
result.AnsiRequest.RaiseResponseFromInput (result.AnsiRequest, EscSeqUtils.InvalidRequestTerminator);
|
||||
|
||||
EscSeqUtils.InvalidRequestTerminator = null;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newConsoleKeyInfo != default)
|
||||
{
|
||||
HandleKeyboardEvent (newConsoleKeyInfo);
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
|
||||
private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
|
||||
{
|
||||
MouseButtonState mbs = default;
|
||||
|
||||
foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
|
||||
{
|
||||
if (mouseFlags.HasFlag ((MouseFlags)flag))
|
||||
{
|
||||
switch (flag)
|
||||
{
|
||||
case MouseFlags.Button1Pressed:
|
||||
mbs |= MouseButtonState.Button1Pressed;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button1Released:
|
||||
mbs |= MouseButtonState.Button1Released;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button1Clicked:
|
||||
mbs |= MouseButtonState.Button1Clicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button1DoubleClicked:
|
||||
mbs |= MouseButtonState.Button1DoubleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button1TripleClicked:
|
||||
mbs |= MouseButtonState.Button1TripleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button2Pressed:
|
||||
mbs |= MouseButtonState.Button2Pressed;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button2Released:
|
||||
mbs |= MouseButtonState.Button2Released;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button2Clicked:
|
||||
mbs |= MouseButtonState.Button2Clicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button2DoubleClicked:
|
||||
mbs |= MouseButtonState.Button2DoubleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button2TripleClicked:
|
||||
mbs |= MouseButtonState.Button2TripleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button3Pressed:
|
||||
mbs |= MouseButtonState.Button3Pressed;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button3Released:
|
||||
mbs |= MouseButtonState.Button3Released;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button3Clicked:
|
||||
mbs |= MouseButtonState.Button3Clicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button3DoubleClicked:
|
||||
mbs |= MouseButtonState.Button3DoubleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button3TripleClicked:
|
||||
mbs |= MouseButtonState.Button3TripleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.WheeledUp:
|
||||
mbs |= MouseButtonState.ButtonWheeledUp;
|
||||
|
||||
break;
|
||||
case MouseFlags.WheeledDown:
|
||||
mbs |= MouseButtonState.ButtonWheeledDown;
|
||||
|
||||
break;
|
||||
case MouseFlags.WheeledLeft:
|
||||
mbs |= MouseButtonState.ButtonWheeledLeft;
|
||||
|
||||
break;
|
||||
case MouseFlags.WheeledRight:
|
||||
mbs |= MouseButtonState.ButtonWheeledRight;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button4Pressed:
|
||||
mbs |= MouseButtonState.Button4Pressed;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button4Released:
|
||||
mbs |= MouseButtonState.Button4Released;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button4Clicked:
|
||||
mbs |= MouseButtonState.Button4Clicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button4DoubleClicked:
|
||||
mbs |= MouseButtonState.Button4DoubleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.Button4TripleClicked:
|
||||
mbs |= MouseButtonState.Button4TripleClicked;
|
||||
|
||||
break;
|
||||
case MouseFlags.ButtonShift:
|
||||
mbs |= MouseButtonState.ButtonShift;
|
||||
|
||||
break;
|
||||
case MouseFlags.ButtonCtrl:
|
||||
mbs |= MouseButtonState.ButtonCtrl;
|
||||
|
||||
break;
|
||||
case MouseFlags.ButtonAlt:
|
||||
mbs |= MouseButtonState.ButtonAlt;
|
||||
|
||||
break;
|
||||
case MouseFlags.ReportMousePosition:
|
||||
mbs |= MouseButtonState.ReportMousePosition;
|
||||
|
||||
break;
|
||||
case MouseFlags.AllEvents:
|
||||
mbs |= MouseButtonState.AllEvents;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mbs;
|
||||
}
|
||||
|
||||
private Point _lastCursorPosition;
|
||||
|
||||
//private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
|
||||
//{
|
||||
// if (terminating ==
|
||||
|
||||
// // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
|
||||
// // The observation is correct because the response isn't immediate and this is useless
|
||||
// EscSeqUtils.CSI_RequestCursorPositionReport.Terminator)
|
||||
// {
|
||||
// var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
|
||||
|
||||
// if (_lastCursorPosition.Y != point.Y)
|
||||
// {
|
||||
// _lastCursorPosition = point;
|
||||
// var eventType = EventType.WindowPosition;
|
||||
// var winPositionEv = new WindowPositionEvent { CursorPosition = point };
|
||||
|
||||
// _inputQueue.Enqueue (
|
||||
// new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
|
||||
// );
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator)
|
||||
// {
|
||||
// if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value)
|
||||
// {
|
||||
// EnqueueWindowSizeEvent (
|
||||
// Math.Max (int.Parse (values [1]), 0),
|
||||
// Math.Max (int.Parse (values [2]), 0),
|
||||
// Math.Max (int.Parse (values [1]), 0),
|
||||
// Math.Max (int.Parse (values [2]), 0)
|
||||
// );
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// EnqueueRequestResponseEvent (c1Control, code, values, terminating);
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// EnqueueRequestResponseEvent (c1Control, code, values, terminating);
|
||||
// }
|
||||
|
||||
// _inputReady.Set ();
|
||||
//}
|
||||
|
||||
//private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
|
||||
//{
|
||||
// var eventType = EventType.RequestResponse;
|
||||
// var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
|
||||
|
||||
// _inputQueue.Enqueue (
|
||||
// new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
|
||||
// );
|
||||
//}
|
||||
|
||||
private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
|
||||
{
|
||||
var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
|
||||
|
||||
_inputQueue.Enqueue (
|
||||
new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
|
||||
);
|
||||
|
||||
_inputReady.Set ();
|
||||
}
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
Key = 1,
|
||||
Mouse = 2,
|
||||
WindowSize = 3,
|
||||
WindowPosition = 4,
|
||||
RequestResponse = 5
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum MouseButtonState
|
||||
{
|
||||
Button1Pressed = 0x1,
|
||||
Button1Released = 0x2,
|
||||
Button1Clicked = 0x4,
|
||||
Button1DoubleClicked = 0x8,
|
||||
Button1TripleClicked = 0x10,
|
||||
Button2Pressed = 0x20,
|
||||
Button2Released = 0x40,
|
||||
Button2Clicked = 0x80,
|
||||
Button2DoubleClicked = 0x100,
|
||||
Button2TripleClicked = 0x200,
|
||||
Button3Pressed = 0x400,
|
||||
Button3Released = 0x800,
|
||||
Button3Clicked = 0x1000,
|
||||
Button3DoubleClicked = 0x2000,
|
||||
Button3TripleClicked = 0x4000,
|
||||
ButtonWheeledUp = 0x8000,
|
||||
ButtonWheeledDown = 0x10000,
|
||||
ButtonWheeledLeft = 0x20000,
|
||||
ButtonWheeledRight = 0x40000,
|
||||
Button4Pressed = 0x80000,
|
||||
Button4Released = 0x100000,
|
||||
Button4Clicked = 0x200000,
|
||||
Button4DoubleClicked = 0x400000,
|
||||
Button4TripleClicked = 0x800000,
|
||||
ButtonShift = 0x1000000,
|
||||
ButtonCtrl = 0x2000000,
|
||||
ButtonAlt = 0x4000000,
|
||||
ReportMousePosition = 0x8000000,
|
||||
AllEvents = -1
|
||||
}
|
||||
|
||||
public struct MouseEvent
|
||||
{
|
||||
public Point Position;
|
||||
public MouseButtonState ButtonState;
|
||||
}
|
||||
|
||||
public struct WindowSizeEvent
|
||||
{
|
||||
public Size Size;
|
||||
}
|
||||
|
||||
public struct WindowPositionEvent
|
||||
{
|
||||
public int Top;
|
||||
public int Left;
|
||||
public Point CursorPosition;
|
||||
}
|
||||
|
||||
public struct RequestResponseEvent
|
||||
{
|
||||
public (string c1Control, string code, string [] values, string terminating) ResultTuple;
|
||||
}
|
||||
|
||||
public struct InputResult
|
||||
{
|
||||
public EventType EventType;
|
||||
public ConsoleKeyInfo ConsoleKeyInfo;
|
||||
public MouseEvent MouseEvent;
|
||||
public WindowSizeEvent WindowSizeEvent;
|
||||
public WindowPositionEvent WindowPositionEvent;
|
||||
public RequestResponseEvent RequestResponseEvent;
|
||||
|
||||
public readonly override string ToString ()
|
||||
{
|
||||
return EventType switch
|
||||
{
|
||||
EventType.Key => ToString (ConsoleKeyInfo),
|
||||
EventType.Mouse => MouseEvent.ToString (),
|
||||
|
||||
//EventType.WindowSize => WindowSize.ToString (),
|
||||
//EventType.RequestResponse => RequestResponse.ToString (),
|
||||
_ => "Unknown event type: " + EventType
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Prints a ConsoleKeyInfoEx structure</summary>
|
||||
/// <param name="cki"></param>
|
||||
/// <returns></returns>
|
||||
public readonly string ToString (ConsoleKeyInfo cki)
|
||||
{
|
||||
var ke = new Key ((KeyCode)cki.KeyChar);
|
||||
var sb = new StringBuilder ();
|
||||
sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
|
||||
sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
|
||||
sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
|
||||
sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
|
||||
sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
|
||||
string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
|
||||
|
||||
return $"[ConsoleKeyInfo({s})]";
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleKeyboardEvent (ConsoleKeyInfo cki)
|
||||
{
|
||||
var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
|
||||
|
||||
_inputQueue.Enqueue (inputResult);
|
||||
}
|
||||
|
||||
public void Dispose ()
|
||||
{
|
||||
_inputReadyCancellationTokenSource?.Cancel ();
|
||||
_inputReadyCancellationTokenSource?.Dispose ();
|
||||
_inputReadyCancellationTokenSource = null;
|
||||
|
||||
try
|
||||
{
|
||||
// throws away any typeahead that has been typed by
|
||||
// the user and has not yet been read by the program.
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
Console.ReadKey (true);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Ignore - Console input has already been closed
|
||||
}
|
||||
}
|
||||
}
|
||||
173
Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
Normal file
173
Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Mainloop intended to be used with the .NET System.Console API, and can be used on Windows and Unix, it is
|
||||
/// cross-platform but lacks things like file descriptor monitoring.
|
||||
/// </summary>
|
||||
/// <remarks>This implementation is used for NetDriver.</remarks>
|
||||
internal class NetMainLoop : IMainLoopDriver
|
||||
{
|
||||
internal NetEvents _netEvents;
|
||||
|
||||
/// <summary>Invoked when a Key is pressed.</summary>
|
||||
internal Action<NetEvents.InputResult> ProcessInput;
|
||||
|
||||
private readonly ManualResetEventSlim _eventReady = new (false);
|
||||
private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
|
||||
private readonly ConcurrentQueue<NetEvents.InputResult?> _resultQueue = new ();
|
||||
internal readonly ManualResetEventSlim _waitForProbe = new (false);
|
||||
private readonly CancellationTokenSource _eventReadyTokenSource = new ();
|
||||
private MainLoop _mainLoop;
|
||||
|
||||
/// <summary>Initializes the class with the console driver.</summary>
|
||||
/// <remarks>Passing a consoleDriver is provided to capture windows resizing.</remarks>
|
||||
/// <param name="consoleDriver">The console driver used by this Net main loop.</param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public NetMainLoop (ConsoleDriver consoleDriver = null)
|
||||
{
|
||||
if (consoleDriver is null)
|
||||
{
|
||||
throw new ArgumentNullException (nameof (consoleDriver));
|
||||
}
|
||||
|
||||
_netEvents = new NetEvents (consoleDriver);
|
||||
}
|
||||
|
||||
void IMainLoopDriver.Setup (MainLoop mainLoop)
|
||||
{
|
||||
_mainLoop = mainLoop;
|
||||
|
||||
if (ConsoleDriver.RunningUnitTests)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
|
||||
}
|
||||
|
||||
void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
|
||||
|
||||
bool IMainLoopDriver.EventsPending ()
|
||||
{
|
||||
_waitForProbe.Set ();
|
||||
|
||||
if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_eventReadyTokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
|
||||
// are no timers, but there IS an idle handler waiting.
|
||||
_eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_eventReady.Reset ();
|
||||
}
|
||||
|
||||
_eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
|
||||
|
||||
if (!_eventReadyTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IMainLoopDriver.Iteration ()
|
||||
{
|
||||
while (_resultQueue.Count > 0)
|
||||
{
|
||||
// Always dequeue even if it's null and invoke if isn't null
|
||||
if (_resultQueue.TryDequeue (out NetEvents.InputResult? dequeueResult))
|
||||
{
|
||||
if (dequeueResult is { })
|
||||
{
|
||||
ProcessInput?.Invoke (dequeueResult.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IMainLoopDriver.TearDown ()
|
||||
{
|
||||
_inputHandlerTokenSource?.Cancel ();
|
||||
_inputHandlerTokenSource?.Dispose ();
|
||||
_eventReadyTokenSource?.Cancel ();
|
||||
_eventReadyTokenSource?.Dispose ();
|
||||
|
||||
_eventReady?.Dispose ();
|
||||
|
||||
_resultQueue?.Clear ();
|
||||
_waitForProbe?.Dispose ();
|
||||
_netEvents?.Dispose ();
|
||||
_netEvents = null;
|
||||
|
||||
_mainLoop = null;
|
||||
}
|
||||
|
||||
private void NetInputHandler ()
|
||||
{
|
||||
while (_mainLoop is { })
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_netEvents._forceRead && !_inputHandlerTokenSource.IsCancellationRequested)
|
||||
{
|
||||
_waitForProbe.Wait (_inputHandlerTokenSource.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_waitForProbe.IsSet)
|
||||
{
|
||||
_waitForProbe.Reset ();
|
||||
}
|
||||
}
|
||||
|
||||
if (_inputHandlerTokenSource.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inputHandlerTokenSource.Token.ThrowIfCancellationRequested ();
|
||||
|
||||
if (_resultQueue.Count == 0)
|
||||
{
|
||||
_resultQueue.Enqueue (_netEvents.DequeueInput ());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (_resultQueue.Count > 0 && _resultQueue.TryPeek (out NetEvents.InputResult? result) && result is null)
|
||||
{
|
||||
// Dequeue null values
|
||||
_resultQueue.TryDequeue (out _);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException) // Peek can raise an exception
|
||||
{ }
|
||||
|
||||
if (_resultQueue.Count > 0)
|
||||
{
|
||||
_eventReady.Set ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
Normal file
125
Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class NetWinVTConsole
|
||||
{
|
||||
private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
|
||||
private const uint ENABLE_ECHO_INPUT = 4;
|
||||
private const uint ENABLE_EXTENDED_FLAGS = 128;
|
||||
private const uint ENABLE_INSERT_MODE = 32;
|
||||
private const uint ENABLE_LINE_INPUT = 2;
|
||||
private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
|
||||
private const uint ENABLE_MOUSE_INPUT = 16;
|
||||
|
||||
// Input modes.
|
||||
private const uint ENABLE_PROCESSED_INPUT = 1;
|
||||
|
||||
// Output modes.
|
||||
private const uint ENABLE_PROCESSED_OUTPUT = 1;
|
||||
private const uint ENABLE_QUICK_EDIT_MODE = 64;
|
||||
private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
|
||||
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
|
||||
private const uint ENABLE_WINDOW_INPUT = 8;
|
||||
private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const int STD_INPUT_HANDLE = -10;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
|
||||
private readonly nint _errorHandle;
|
||||
private readonly nint _inputHandle;
|
||||
private readonly uint _originalErrorConsoleMode;
|
||||
private readonly uint _originalInputConsoleMode;
|
||||
private readonly uint _originalOutputConsoleMode;
|
||||
private readonly nint _outputHandle;
|
||||
|
||||
public NetWinVTConsole ()
|
||||
{
|
||||
_inputHandle = GetStdHandle (STD_INPUT_HANDLE);
|
||||
|
||||
if (!GetConsoleMode (_inputHandle, out uint mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
|
||||
_originalInputConsoleMode = mode;
|
||||
|
||||
if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
|
||||
{
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
|
||||
|
||||
if (!SetConsoleMode (_inputHandle, mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
}
|
||||
|
||||
_outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
|
||||
|
||||
if (!GetConsoleMode (_outputHandle, out mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
|
||||
_originalOutputConsoleMode = mode;
|
||||
|
||||
if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
|
||||
{
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
|
||||
|
||||
if (!SetConsoleMode (_outputHandle, mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
}
|
||||
|
||||
_errorHandle = GetStdHandle (STD_ERROR_HANDLE);
|
||||
|
||||
if (!GetConsoleMode (_errorHandle, out mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
|
||||
_originalErrorConsoleMode = mode;
|
||||
|
||||
if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
|
||||
{
|
||||
mode |= DISABLE_NEWLINE_AUTO_RETURN;
|
||||
|
||||
if (!SetConsoleMode (_errorHandle, mode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Cleanup ()
|
||||
{
|
||||
if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
|
||||
if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
|
||||
if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode))
|
||||
{
|
||||
throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport ("kernel32.dll")]
|
||||
private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
|
||||
|
||||
[DllImport ("kernel32.dll")]
|
||||
private static extern uint GetLastError ();
|
||||
|
||||
[DllImport ("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint GetStdHandle (int nStdHandle);
|
||||
|
||||
[DllImport ("kernel32.dll")]
|
||||
private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
|
||||
}
|
||||
1109
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
Normal file
1109
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -164,8 +164,7 @@
|
||||
<Message Text="Found packages: @(NuGetPackages)" Importance="high" />
|
||||
|
||||
<!-- Copy files only if found -->
|
||||
<Copy SourceFiles="@(NuGetPackages)" DestinationFolder="$(LocalPackagesPath)" SkipUnchangedFiles="false"
|
||||
Condition="@(NuGetPackages) != ''" />
|
||||
<Copy SourceFiles="@(NuGetPackages)" DestinationFolder="$(LocalPackagesPath)" SkipUnchangedFiles="false" Condition="@(NuGetPackages) != ''" />
|
||||
|
||||
<!-- Log success -->
|
||||
<Message Text="Copy completed successfully." Importance="high" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
@@ -12,14 +11,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 ();
|
||||
private readonly Dictionary<DateTime, string> _answers = new ();
|
||||
private Label _lblSummary;
|
||||
|
||||
public override void Main ()
|
||||
@@ -27,18 +25,18 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
// Init
|
||||
Application.Init ();
|
||||
|
||||
TabView tv = new TabView
|
||||
var tv = new TabView
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
Tab single = new Tab ();
|
||||
single.DisplayText = "Single";
|
||||
var single = new Tab ();
|
||||
single.DisplayText = "_Single";
|
||||
single.View = BuildSingleTab ();
|
||||
|
||||
Tab bulk = new ();
|
||||
bulk.DisplayText = "Multi";
|
||||
bulk.DisplayText = "_Multi";
|
||||
bulk.View = BuildBulkTab ();
|
||||
|
||||
tv.AddTab (single, true);
|
||||
@@ -47,7 +45,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
// Setup - Create a top-level application window and configure it.
|
||||
Window appWindow = new ()
|
||||
{
|
||||
Title = GetQuitKeyAndName (),
|
||||
Title = GetQuitKeyAndName ()
|
||||
};
|
||||
|
||||
appWindow.Add (tv);
|
||||
@@ -61,18 +59,20 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
// Shutdown - Calling Application.Shutdown is required.
|
||||
Application.Shutdown ();
|
||||
}
|
||||
|
||||
private View BuildSingleTab ()
|
||||
{
|
||||
View w = new View ()
|
||||
var w = new View
|
||||
{
|
||||
Width = Dim.Fill(),
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill (),
|
||||
CanFocus = true
|
||||
};
|
||||
|
||||
w.Padding.Thickness = new (1);
|
||||
|
||||
var scrRequests = new List<string>
|
||||
// TODO: This hackery is why I think the EscSeqUtils class should be refactored and the CSI's made type safe.
|
||||
List<string> scrRequests = new ()
|
||||
{
|
||||
"CSI_SendDeviceAttributes",
|
||||
"CSI_ReportTerminalSizeInChars",
|
||||
@@ -80,18 +80,19 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
"CSI_SendDeviceAttributes2"
|
||||
};
|
||||
|
||||
var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper<string> (new (scrRequests)) };
|
||||
var cbRequests = new ComboBox { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper<string> (new (scrRequests)) };
|
||||
w.Add (cbRequests);
|
||||
|
||||
var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "Request:" };
|
||||
// TODO: Use Pos.Align and Dim.Func so these hardcoded widths aren't needed.
|
||||
var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "_Request:" };
|
||||
var tfRequest = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 20 };
|
||||
w.Add (label, tfRequest);
|
||||
|
||||
label = new Label { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "Value:" };
|
||||
label = new () { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "E_xpectedResponseValue:" };
|
||||
var tfValue = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6 };
|
||||
w.Add (label, tfValue);
|
||||
|
||||
label = new Label { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "Terminator:" };
|
||||
label = new () { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "_Terminator:" };
|
||||
var tfTerminator = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4 };
|
||||
w.Add (label, tfTerminator);
|
||||
|
||||
@@ -102,101 +103,111 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
return;
|
||||
}
|
||||
|
||||
var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
|
||||
string selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
|
||||
AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null;
|
||||
|
||||
switch (selAnsiEscapeSequenceRequestName)
|
||||
{
|
||||
case "CSI_SendDeviceAttributes":
|
||||
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes;
|
||||
|
||||
break;
|
||||
case "CSI_ReportTerminalSizeInChars":
|
||||
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars;
|
||||
|
||||
break;
|
||||
case "CSI_RequestCursorPositionReport":
|
||||
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_RequestCursorPositionReport;
|
||||
|
||||
break;
|
||||
case "CSI_SendDeviceAttributes2":
|
||||
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes2;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : "";
|
||||
tfValue.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Value ?? "" : "";
|
||||
|
||||
tfValue.Text = selAnsiEscapeSequenceRequest is { }
|
||||
? selAnsiEscapeSequenceRequest.ExpectedResponseValue ?? ""
|
||||
: "";
|
||||
tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : "";
|
||||
};
|
||||
|
||||
// Forces raise cbRequests.SelectedItemChanged to update TextFields
|
||||
cbRequests.SelectedItem = 0;
|
||||
|
||||
label = new Label { Y = Pos.Bottom (tfRequest) + 2, Text = "Response:" };
|
||||
label = new () { Y = Pos.Bottom (tfRequest) + 2, Text = "_Response:" };
|
||||
var tvResponse = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
|
||||
w.Add (label, tvResponse);
|
||||
|
||||
label = new Label { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "Error:" };
|
||||
label = new () { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "_Error:" };
|
||||
var tvError = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
|
||||
w.Add (label, tvError);
|
||||
|
||||
label = new Label { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "Value:" };
|
||||
label = new () { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "E_xpectedResponseValue:" };
|
||||
var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true };
|
||||
w.Add (label, tvValue);
|
||||
|
||||
label = new Label { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "Terminator:" };
|
||||
label = new () { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "_Terminator:" };
|
||||
var tvTerminator = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4, Height = 4, ReadOnly = true };
|
||||
w.Add (label, tvTerminator);
|
||||
|
||||
var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "Send Request", IsDefault = true };
|
||||
var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "_Send Request", IsDefault = true };
|
||||
|
||||
var lblSuccess = new Label { X = Pos.Center (), Y = Pos.Bottom (btnResponse) + 1 };
|
||||
w.Add (lblSuccess);
|
||||
|
||||
btnResponse.Accepting += (s, e) =>
|
||||
{
|
||||
var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest
|
||||
{
|
||||
Request = tfRequest.Text,
|
||||
Terminator = tfTerminator.Text,
|
||||
Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
|
||||
};
|
||||
{
|
||||
var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest
|
||||
{
|
||||
Request = tfRequest.Text,
|
||||
Terminator = tfTerminator.Text,
|
||||
ExpectedResponseValue = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
|
||||
};
|
||||
|
||||
var success = AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (
|
||||
ansiEscapeSequenceRequest,
|
||||
out AnsiEscapeSequenceResponse ansiEscapeSequenceResponse
|
||||
);
|
||||
bool success = AnsiEscapeSequenceRequest.TryRequest (
|
||||
ansiEscapeSequenceRequest,
|
||||
out AnsiEscapeSequenceResponse ansiEscapeSequenceResponse
|
||||
);
|
||||
|
||||
tvResponse.Text = ansiEscapeSequenceResponse.Response;
|
||||
tvError.Text = ansiEscapeSequenceResponse.Error;
|
||||
tvValue.Text = ansiEscapeSequenceResponse.Value ?? "";
|
||||
tvTerminator.Text = ansiEscapeSequenceResponse.Terminator;
|
||||
tvResponse.Text = ansiEscapeSequenceResponse.Response;
|
||||
tvError.Text = ansiEscapeSequenceResponse.Error;
|
||||
tvValue.Text = ansiEscapeSequenceResponse.ExpectedResponseValue ?? "";
|
||||
tvTerminator.Text = ansiEscapeSequenceResponse.Terminator;
|
||||
|
||||
if (success)
|
||||
{
|
||||
lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"];
|
||||
lblSuccess.Text = "Successful";
|
||||
}
|
||||
else
|
||||
{
|
||||
lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
|
||||
lblSuccess.Text = "Error";
|
||||
}
|
||||
};
|
||||
if (success)
|
||||
{
|
||||
lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"];
|
||||
lblSuccess.Text = "Success";
|
||||
}
|
||||
else
|
||||
{
|
||||
lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
|
||||
lblSuccess.Text = "Error";
|
||||
}
|
||||
};
|
||||
w.Add (btnResponse);
|
||||
|
||||
w.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "You can send other requests by editing the TextFields." });
|
||||
w.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "Send other requests by editing the TextFields." });
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
private View BuildBulkTab ()
|
||||
{
|
||||
View w = new View ()
|
||||
var w = new View
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill (),
|
||||
CanFocus = true
|
||||
};
|
||||
|
||||
var lbl = new Label ()
|
||||
var lbl = new Label
|
||||
{
|
||||
Text = "This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends. Responses are in red, queued messages are in green.",
|
||||
Text =
|
||||
"_This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends. Responses are in red, queued messages are in green.",
|
||||
Height = 2,
|
||||
Width = Dim.Fill ()
|
||||
};
|
||||
@@ -205,50 +216,49 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
TimeSpan.FromMilliseconds (1000),
|
||||
() =>
|
||||
{
|
||||
lock (lockAnswers)
|
||||
lock (_lockAnswers)
|
||||
{
|
||||
UpdateGraph ();
|
||||
|
||||
UpdateResponses ();
|
||||
}
|
||||
|
||||
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
var tv = new TextView ()
|
||||
var tv = new TextView
|
||||
{
|
||||
Y = Pos.Bottom (lbl),
|
||||
Width = Dim.Percent (50),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
|
||||
var lblDar = new Label ()
|
||||
var lblDar = new Label
|
||||
{
|
||||
Y = Pos.Bottom (lbl),
|
||||
X = Pos.Right (tv) + 1,
|
||||
Text = "DAR per second",
|
||||
Text = "_DAR per second: "
|
||||
};
|
||||
var cbDar = new NumericUpDown ()
|
||||
|
||||
var cbDar = new NumericUpDown
|
||||
{
|
||||
X = Pos.Right (lblDar),
|
||||
Y = Pos.Bottom (lbl),
|
||||
Value = 0,
|
||||
Value = 0
|
||||
};
|
||||
|
||||
cbDar.ValueChanging += (s, e) =>
|
||||
{
|
||||
if (e.NewValue < 0 || e.NewValue > 20)
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
};
|
||||
{
|
||||
if (e.NewValue < 0 || e.NewValue > 20)
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
};
|
||||
w.Add (cbDar);
|
||||
|
||||
int lastSendTime = Environment.TickCount;
|
||||
object lockObj = new object ();
|
||||
var lockObj = new object ();
|
||||
|
||||
Application.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (50),
|
||||
() =>
|
||||
@@ -272,8 +282,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
_graphView = new GraphView ()
|
||||
_graphView = new ()
|
||||
{
|
||||
Y = Pos.Bottom (cbDar),
|
||||
X = Pos.Right (tv),
|
||||
@@ -281,7 +290,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
Height = Dim.Fill (1)
|
||||
};
|
||||
|
||||
_lblSummary = new Label ()
|
||||
_lblSummary = new ()
|
||||
{
|
||||
Y = Pos.Bottom (_graphView),
|
||||
X = Pos.Right (tv),
|
||||
@@ -299,6 +308,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
private void UpdateResponses ()
|
||||
{
|
||||
_lblSummary.Text = GetSummary ();
|
||||
@@ -307,32 +317,31 @@ 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;
|
||||
string last = _answers.Last ().Value;
|
||||
|
||||
var unique = answers.Values.Distinct ().Count ();
|
||||
var total = answers.Count;
|
||||
int unique = _answers.Values.Distinct ().Count ();
|
||||
int total = _answers.Count;
|
||||
|
||||
return $"Last:{last} U:{unique} T:{total}";
|
||||
}
|
||||
|
||||
private void SetupGraph ()
|
||||
{
|
||||
_graphView.Series.Add (_sentSeries = new ());
|
||||
_graphView.Series.Add (_answeredSeries = new ());
|
||||
|
||||
_graphView.Series.Add (_sentSeries = new ScatterSeries ());
|
||||
_graphView.Series.Add (_answeredSeries = new ScatterSeries ());
|
||||
|
||||
_sentSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightGreen, ColorName16.Black));
|
||||
_answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightRed, ColorName16.Black));
|
||||
_sentSeries.Fill = new (new ('.'), new (ColorName16.BrightGreen, ColorName16.Black));
|
||||
_answeredSeries.Fill = new (new ('.'), new (ColorName16.BrightRed, ColorName16.Black));
|
||||
|
||||
// Todo:
|
||||
// _graphView.Annotations.Add (_sentSeries new PathAnnotation {});
|
||||
|
||||
_graphView.CellSize = new PointF (1, 1);
|
||||
_graphView.CellSize = new (1, 1);
|
||||
_graphView.MarginBottom = 2;
|
||||
_graphView.AxisX.Increment = 1;
|
||||
_graphView.AxisX.Text = "Seconds";
|
||||
@@ -341,40 +350,37 @@ 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 ();
|
||||
|
||||
// _graphView.ScrollOffset = new PointF(,0);
|
||||
if (_sentSeries.Points.Count > 0 || _answeredSeries.Points.Count > 0)
|
||||
{
|
||||
_graphView.SetNeedsDisplay ();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private int ToSeconds (DateTime t)
|
||||
{
|
||||
return (int)(DateTime.Now - t).TotalSeconds;
|
||||
}
|
||||
private int ToSeconds (DateTime t) { return (int)(DateTime.Now - t).TotalSeconds; }
|
||||
|
||||
private void SendDar ()
|
||||
{
|
||||
sends.Add (DateTime.Now);
|
||||
var result = Application.Driver.WriteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes);
|
||||
_sends.Add (DateTime.Now);
|
||||
string result = Application.Driver.WriteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes);
|
||||
HandleResponse (result);
|
||||
}
|
||||
|
||||
private void HandleResponse (string response)
|
||||
{
|
||||
lock (lockAnswers)
|
||||
lock (_lockAnswers)
|
||||
{
|
||||
answers.Add (DateTime.Now, response);
|
||||
_answers.Add (DateTime.Now, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user